1. ModalBottomSheet
ModalBottomSheet
是Compose
官方的底部对话框,类似传统View中的BottomSheetDialog
,可以实现从底部弹出,并支持滑动关闭的效果。
需要注意的是,这里使用的是material3
,然后ModalBottomSheet
这个组件目前还是实验性的,未来可能会改变或删除。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyModalBottomSheetTest1() {
val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
var showBottomSheet by remember {
mutableStateOf(false) }
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
},
sheetState = sheetState
) {
// Sheet content
Column(Modifier.height(300.dp)) {
Button(onClick = {
scope.launch {
sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
showBottomSheet = false
}
}
}) {
Text("Hide bottom sheet")
}
}
}
}
Button(onClick = {
showBottomSheet = true
}, Modifier.padding(50.dp)) {
Text("show")
}
}
效果如下
2. 自定义圆角大小
通过定义ModalBottomSheet
的shape
可以修改圆角的大小
shape = RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)
完整代码如下
//修改圆角
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyModalBottomSheetTest2() {
val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
var showBottomSheet by remember {
mutableStateOf(false) }
if (showBottomSheet) {
ModalBottomSheet(
shape = RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp),
onDismissRequest = {
showBottomSheet = false
},
sheetState = sheetState
) {
// Sheet content
Column(Modifier.height(300.dp)) {
Button(onClick = {
scope.launch {
sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
showBottomSheet = false
}
}
}) {
Text("Hide bottom sheet")
}
}
}
}
Button(onClick = {
showBottomSheet = true
}, Modifier.padding(50.dp)) {
Text("show")
}
}
效果如下
3. 去除拖动条
对话框顶部的拖动条,我们可能不需要,可以将其去除。
通过定义ModalBottomSheet
的dragHandle
可以去除拖动条。
dragHandle = {
}
完整代码如下
//去除拖动条
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyModalBottomSheetTest3() {
val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
var showBottomSheet by remember {
mutableStateOf(false) }
if (showBottomSheet) {
ModalBottomSheet(
dragHandle = {
},
onDismissRequest = {
showBottomSheet = false
},
sheetState = sheetState
) {
// Sheet content
Column(Modifier.height(300.dp)) {
Button(onClick = {
scope.launch {
sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
showBottomSheet = false
}
}
}) {
Text("Hide bottom sheet")
}
}
}
}
Button(onClick = {
showBottomSheet = true
}, Modifier.padding(50.dp)) {
Text("show")
}
}
效果如下
4. 默认半屏,滑动变成全屏
这里我们把Column
的高度改为800.dp
,运行程序,对话框默认以半屏显示,向上滑动对话框,可以发现,对话框会变为全屏。
//半屏展开
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyModalBottomSheetTest4() {
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = false,
)
val scope = rememberCoroutineScope()
var showBottomSheet by remember {
mutableStateOf(false) }
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
},
sheetState = sheetState
) {
// Sheet content
Column(Modifier.height(800.dp)) {
Button(onClick = {
scope.launch {
sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
showBottomSheet = false
}
}
}) {
Text("Hide bottom sheet")
}
}
}
}
Button(onClick = {
showBottomSheet = true
}, Modifier.padding(50.dp)) {
Text("show")
}
}
效果如下所示
5. 默认全屏
将skipPartiallyExpanded
修改为true
,打开对话框的时候,会立即以全屏展示,向下滑动回收的时候,也会跳过半屏阶段。
//全屏展开
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyModalBottomSheetTest5() {
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
)
val scope = rememberCoroutineScope()
var showBottomSheet by remember {
mutableStateOf(false) }
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
},
sheetState = sheetState
) {
// Sheet content
Column(Modifier.height(800.dp)) {
Button(onClick = {
scope.launch {
sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
showBottomSheet = false
}
}
}) {
Text("Hide bottom sheet")
}
}
}
}
Button(onClick = {
showBottomSheet = true
}, Modifier.padding(50.dp)) {
Text("show")
}
}
效果如下所示
6. 返回键不隐藏弹框
将properties中的shouldDismissOnBackPress设置为false,返回键将不会关闭弹框。
properties = ModalBottomSheetProperties(shouldDismissOnBackPress = false)
7. 修改对话框的主体颜色
containerColor可以修改对话框的主体颜色,如果弹出的弹框底部横条颜色和对话框背景颜色不一样,可以通过修改containerColor来达到颜色一直的效果。
8. 更多ModalBottomSheet的使用
更多ModalBottomSheet
的使用详见 : Android Developers | 底部动作条
9. BottomSheetScaffold
BottomSheetScaffold
也是Compose
官方自带的组件。提供更灵活的交互方式,底部弹窗可通过手势或代码控制展开和折叠,用户能在主界面和底部弹窗之间自由切换操作。
适用于需要频繁在主界面和底部弹窗之间交互的场景,如音乐播放器的播放列表,用户可一边浏览歌曲列表,一边操作主界面的播放控制按钮。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomSheetScaffoldExample() {
val sheetState = rememberBottomSheetScaffoldState()
BottomSheetScaffold(
scaffoldState = sheetState,
sheetContent = {
Column(Modifier.height(300.dp)) {
Text("这是底部弹窗内容")
}
},
content = {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center){
Text("这是主内容区域")
}
}
)
}
效果如下
10. 其他三方的Compose底部对话框库
10.1 bottomsheetdialog-compose
Github地址 : workspace/bottomsheetdialog-compose
Jetpack Compose BottomSheetDialog 库,允许您像使用 Dialog 的界面一样使用BottomsheetDialog。此外,它还支持在 BottomSheetDialog 显示时设置导航栏颜色。
这个看一下它的源码,可以发现内部调用的BottomSheetDialogWrapper
,而BottomSheetDialogWrapper
继承自com.google.android.material.bottomsheet.BottomSheetDialog
,也就是说,bottomsheetdialog-compose
这个库实质上是封装了传统View
中的BottomSheetDialog
,来提供给Compose
使用。
private class BottomSheetDialogWrapper(
private var onDismissRequest: () -> Unit,
private var properties: BottomSheetDialogProperties,
private val composeView: View,
layoutDirection: LayoutDirection,
density: Density,
dialogId: UUID
) : BottomSheetDialog(
ContextThemeWrapper(
composeView.context,
if (properties.enableEdgeToEdge) {
R.style.TransparentEdgeToEdgeEnabledBottomSheetTheme
} else {
R.style.TransparentEdgeToEdgeDisabledBottomSheetTheme
}
)
)
10.2 FlexibleBottomSheet
Github地址 : skydoves/FlexibleBottomSheet
FlexibleBottomSheet 是一种高级的 Compose Multiplatform 底部工作表,支持多平台,支持分段大小调整、最大的特点是弹出后,非弹框区域依旧能够操作,类似于高德地图的底部弹框。
解决WindowCompat.setDecorFitsSystemWindows(window, false)
情况下,FlexibleBottomSheet
弹框的bug
修改源码FlexibleBottomSheetPopup
,containSystemBars
模式下,去除modifier.windowInsetsPadding(windowInsets)
,具体代码如下
@Composable
@InternalFlexibleApi
public actual fun FlexibleBottomSheetPopup(
onDismissRequest: () -> Unit,
windowInsets: WindowInsets,
sheetState: FlexibleSheetState,
content: @Composable BoxScope.() -> Unit,
) {
val view = LocalView.current
val id = rememberSaveable {
UUID.randomUUID() }
val parentComposition = rememberCompositionContext()
val currentContent by rememberUpdatedState(content)
val flexibleBottomSheetWindow = remember {
val modifier = Modifier.semantics {
this.popup() }
if (!sheetState.containSystemBars){
modifier.windowInsetsPadding(windowInsets)
}
FlexibleBottomSheetWindow(
onDismissRequest = onDismissRequest,
composeView = view,
saveId = id,
sheetState = sheetState,
).apply {
setCustomContent(
parent = parentComposition,
content = {
Box(
modifier.imePadding(),
) {
currentContent()
}
},
)
}
}
//省略其他代码...
}
11 参考
探索 Jetpack Compose 的底部抽屉:BottomSheetScaffold、ModalBottomSheetLayout 和 BackdropScaffold
手撸一个Compose三段式BottomSheetLayout