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变得更加容易!

相关推荐
巫山老妖4 小时前
Android 应用架构演进:MVC / MVP / MVVM / MVI (2026)
android·android jetpack
木头左8 小时前
【架构设计】教育类 App 服务端化改造:AI 上移、题库分层与个人题库归属
android·架构设计·系统设计·后端开发·数据库设计
墨狂之逸才9 小时前
USB 的 HOST 和 DEVICE 到底有什么区别?ADB 调试应该选哪个?
android
奶油话梅糖10 小时前
苹果的内核跑上了安卓手机:2,631 Star 的 HakiOS 让 XNU 在 Adreno GPU 上画出 118 帧
android·macos·智能手机
恋猫de小郭10 小时前
Flutter THLC 的 WebView 滚动终于从 70 FPS 提升到 120 FPS
android·前端·flutter
好的佩奇21 小时前
Dart 之扩展类型
android·flutter·dart
alexhilton1 天前
2027 Android 开发者路线图:学习、面试与 AI
android·kotlin·android jetpack
灿飞屋1 天前
Kotlin与Java虚拟机三:控制流
android·java·开发语言·后端·python·kotlin·安卓
疯华整猫1 天前
我用 Trae 把 Android 内存账重算了一遍
android·性能优化·源码
TimeFine1 天前
AI 编程工作流的设计:从人工判断到 AI裁判
android·ai编程