如何在Android Jetpack Compose中实现顶部弹出的Upper Sheet?
实现从顶部展开的Upper Sheet(替代ModalBottomSheet)
直接旋转ModalBottomSheet的方式不可行,因为它的入场动画、布局约束都是针对底部展开设计的,旋转只会导致内容反转但交互逻辑依然是底部向上的逻辑,触摸区域也会混乱。下面是两种可行的实现方案:
方案一:自定义UpperSheet组件(基于Dialog和动画)
利用Dialog作为容器,配合动画实现从顶部滑入的效果,同时保持类似ModalBottomSheet的交互体验:
@OptIn(ExperimentalMaterial3Api::class) @Composable fun UpperSheet( onDismissRequest: () -> Unit, sheetState: SheetState = rememberSheetState(skipPartiallyExpanded = true), content: @Composable () -> Unit ) { Dialog( onDismissRequest = onDismissRequest, properties = DialogProperties( usePlatformDefaultWidth = false, decorFitsSystemWindows = false ) ) { Box( modifier = Modifier .fillMaxSize() .background(Color.Transparent) .clickable { onDismissRequest() } ) { Column( modifier = Modifier .fillMaxWidth() .align(Alignment.TopCenter) .animateOffset( targetOffsetY = if (sheetState.isVisible) 0.dp else -sheetState.height.toDp(), animationSpec = tween(durationMillis = 300) ) .background(MaterialTheme.colorScheme.surface) .clickable(enabled = false) {} // 阻止点击透传到底层 ) { // 顶部拖拽手柄(可选) Box( modifier = Modifier .padding(top = 12.dp) .align(Alignment.CenterHorizontally) .size(40.dp, 4.dp) .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), shape = RoundedCornerShape(2.dp)) ) content() } } } } // 自定义偏移动画Modifier fun Modifier.animateOffset(targetOffsetY: Dp, animationSpec: AnimationSpec<Dp> = spring()): Modifier { val offsetPx = animateDpAsState(targetValue = targetOffsetY, animationSpec = animationSpec).value return this.offset(y = offsetPx) }
在主界面中使用:
@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen() { Column( modifier = Modifier.fillMaxSize().background(Color(0xFFE0E0E0)), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top ) { val sheetState = rememberSheetState(skipPartiallyExpanded = true) var sheetIsOpened by remember { mutableStateOf(false) } Button( onClick = { sheetIsOpened = true }, modifier = Modifier.padding(top = 20.dp) ) { Text("打开顶部Sheet") } if (sheetIsOpened) { UpperSheet( onDismissRequest = { sheetIsOpened = false }, sheetState = sheetState ) { Column( modifier = Modifier .fillMaxWidth() .height(500.dp) .padding(16.dp) ) { Text("这是顶部展开的Sheet内容", style = MaterialTheme.typography.headlineSmall) // 自定义更多内容 } } } } }
方案二:使用BottomSheetScaffold反向布局(进阶)
如果想复用Material3的SheetState和拖拽逻辑,可以通过反转布局实现顶部展开效果:
@OptIn(ExperimentalMaterial3Api::class) @Composable fun UpperSheetWithScaffold() { val sheetState = rememberBottomSheetState(skipPartiallyExpanded = true) BottomSheetScaffold( sheetContent = { Column( modifier = Modifier .fillMaxWidth() .height(500.dp) .background(MaterialTheme.colorScheme.surface) .padding(16.dp) ) { Box( modifier = Modifier .padding(bottom = 12.dp) .align(Alignment.CenterHorizontally) .size(40.dp, 4.dp) .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), shape = RoundedCornerShape(2.dp)) ) Text("顶部Sheet内容", style = MaterialTheme.typography.headlineSmall) } }, sheetState = sheetState, sheetPeekHeight = 0.dp, modifier = Modifier.graphicsLayer(rotationX = 180f) // 反转整个scaffold ) { innerPadding -> Column( modifier = Modifier .fillMaxSize() .padding(innerPadding) .background(Color(0xFFE0E0E0)) .graphicsLayer(rotationX = 180f) // 把内容再反转回来,恢复正常显示 .padding(top = 20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Button( onClick = { if (sheetState.isCollapsed) sheetState.expand() else sheetState.collapse() } ) { Text(if (sheetState.isCollapsed) "打开顶部Sheet" else "关闭Sheet") } } } }
注意事项
- 方案一自定义程度高,动画和交互完全可控,适合大多数场景;
- 方案二复用官方组件逻辑,但需要两次反转布局,适合依赖官方拖拽交互的场景;
- 禁止使用旋转
ModalBottomSheet的方式,会导致触摸区域、动画方向与内容显示冲突。
内容的提问来源于stack exchange,提问作者Marcel
相关产品推荐
相关产品推荐

