背景
近期有个需求,需要做一个三段式抽屉组件,有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变得更加容易!