Android Compose如何构建阶梯式抽屉

背景

近期有个需求,需要做一个三段式抽屉组件,有3个关键状态:折叠、半展开、展开,交互效果类似:

官方Bottom Sheet组件(View版)

m3.material.io/components/... 中提到的两种BottomSheet组件,效果如下:

Standard bottom sheets Modal bottom sheets

半展开状态:SheetBehavior#setHalfExpandedRatio

详情参考官方Demo(view版本):github.com/material-co...

可以满足需求,但项目基本都迁移到Compose了,所以暂时不考虑View版本实现。

官方Bottom Sheet组件(Compose版)

Demo源码: androidx.compose.material3.samples.SimpleBottomSheetScaffoldSample

Standard bottom sheets Modal bottom sheets

Compose版本支持3种状态:

状态 描述
Hidden sheet不可见
Expanded sheet完全展开
PartiallyExpanded sheet半展开,展示高度对应下图sheetPeekHeight

折叠状态下可见高度为0,看起来并不满足我们的需求。

破局之路

控制Hidden状态显示高度的代码是私有的,代码如下:

kotlin 复制代码
@Composable
@ExperimentalMaterial3Api
fun BottomSheetScaffold(
    sheetContent: @Composable ColumnScope.() -> Unit,
    modifier: Modifier = Modifier,
    scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(),
    sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight,
    ...
) {
    val peekHeightPx = with(LocalDensity.current) {
        sheetPeekHeight.roundToPx()
    }
    BottomSheetScaffoldLayout(
        ...,
        bottomSheet = { layoutHeight ->
            StandardBottomSheet(
                ...,
                calculateAnchors = { sheetSize ->
                    val sheetHeight = sheetSize.height
                    // 关键代码
                    // 1. layoutHeight: sheet content的总高度;
                    // 2. peekHeightPx: 半展开状态的展示高度
                    DraggableAnchors {
                        if (!scaffoldState.bottomSheetState.skipPartiallyExpanded) {
                            PartiallyExpanded at (layoutHeight - peekHeightPx).toFloat()
                        }
                        if (sheetHeight != peekHeightPx) {
                            Expanded at maxOf(layoutHeight - sheetHeight, 0).toFloat()
                        }
                        if (!scaffoldState.bottomSheetState.skipHiddenState) {
                            // Hidden下偏移量等于layoutHeight,完全不可见
                            Hidden at layoutHeight.toFloat()
                        }
                    }
                },
                ...
            )
        }
    )
}

BottomSheetScaffold源码中用到了一个类DraggableAnchors,我们可以基于此自定义BottomSheet

参考官方文档:developer.android.com/develop/ui/...

自定义抽屉效果

代码如下:

kotlin 复制代码
@Composable
@Preview
fun MyBottomSheet(
    modifier: Modifier = Modifier,
) {
    val anchoredDraggableState = rememberAnchoredDraggableState()
    Box(
        modifier
            .offset {
                IntOffset(
                    x = 0,
                    y = anchoredDraggableState
                        .requireOffset()
                        .roundToInt(),
                )
            }
            .onSizeChanged {
                val anchors = DraggableAnchors {
                    DragValue.Expanded at 0.dpToPx
                    DragValue.PartiallyExpanded at it.height * 0.33f // 修改
                    DragValue.Collapsed at it.height - 200.dpToPx // 修改
                }
                anchoredDraggableState.updateAnchors(anchors)
            },
    ) {
        Column(
            modifier = Modifier
                .anchoredDraggable(
                    state = anchoredDraggableState,
                    orientation = Orientation.Vertical,
                )
                .fillMaxSize()
                .background(Color.Gray),
        ) {
            Text(text = "Hello")
        }
    }
}

private enum class DragValue { // 3阶抽屉状态,可以根据实际场景定义更多
    Collapsed,
    Expanded,
    PartiallyExpanded,
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun rememberAnchoredDraggableState(
    initialState: DragValue = DragValue.PartiallyExpanded,
): AnchoredDraggableState<DragValue> = remember(initialState) {
    AnchoredDraggableState(
        initialValue = DragValue.PartiallyExpanded,
        animationSpec = SpringSpec(),
        positionalThreshold = { 56.dpToPx },
        velocityThreshold = { 56.dpToPx },
    )
}

总结

本文从需求开始探索Compose版本的阶梯式抽屉效果实现,最后从BottomSheetScaffold中找到关键突破口,基于AnchoredDraggable定制出所需的抽屉效果,可以发现,在Compose体系下,定制UI变得更加容易!

相关推荐
JMchen1233 分钟前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
crmscs44 分钟前
剪映永久解锁版/电脑版永久会员VIP/安卓SVIP手机永久版下载
android·智能手机·电脑
localbob1 小时前
杀戮尖塔 v6 MOD整合版(Slay the Spire)安卓+PC端免安装中文版分享 卡牌肉鸽神作!杀戮尖塔中文版,电脑和手机都能玩!杀戮尖塔.exe 杀戮尖塔.apk
android·杀戮尖塔apk·杀戮尖塔exe·游戏分享
机建狂魔1 小时前
手机秒变电影机:Blackmagic Camera + LUT滤镜包的专业级视频解决方案
android·拍照·摄影·lut滤镜·拍摄·摄像·录像
hudawei9961 小时前
flutter和Android动画的对比
android·flutter·动画
lxysbly3 小时前
md模拟器安卓版带金手指2026
android
儿歌八万首3 小时前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-19436 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
Jinkxs6 小时前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin
&有梦想的咸鱼&6 小时前
Kotlin委托机制的底层实现深度解析(74)
android·开发语言·kotlin