Jetpack Compose ModalBottomSheet可滚动列拖拽异常问题求助
问题描述
需求目标
- 顶部固定项:带有状态栏内边距和自适应圆角
- 底部固定项:带有导航栏内边距
- 中间自适应项:空间充足时不可滚动,空间不足时可滚动
当前问题
当Sheet内容高度超过窗口高度 - 状态栏高度时,拖拽中间可滚动列表,一旦sheetState.offset接近状态栏高度,Sheet就会自动弹回顶部;但拖拽顶部固定栏或底部固定项时,滚动表现正常。
复现方式
使用Pixel 2模拟器,将测试代码中的itemCount设为18或19即可看到差异。
解决方案
问题根源是滚动组件与BottomSheet的拖拽手势冲突,当中间列表滚动到顶部时,手势没有正确传递给BottomSheet,或者BottomSheet的状态判断逻辑在滚动过程中误触发了复位。以下是修复方案:
1. 协调滚动与BottomSheet的手势交互
给中间滚动区域添加nestedScroll修饰符,通过NestedScrollConnection控制手势传递,确保只有当列表滚动到顶部时,才允许BottomSheet响应拖拽手势。
2. 优化BottomSheet的状态判断逻辑
调整动态内边距和圆角的计算逻辑,避免滚动过程中因offset微小变化触发状态切换。
修改后的完整代码
@Composable fun CEModalBottomSheet( sheetState: ModalBottomSheetState, onCloseDialogClicked: () -> Unit, title: String = "BottomSheet Title", toolbarElevation: Dp = 0.dp, sheetContent: @Composable ColumnScope.() -> Unit, ) { val density = LocalDensity.current val sheetOffset = sheetState.offset val statusBar = WindowInsets.statusBars.asPaddingValues() val statusBarHeightPx = with(density) { statusBar.calculateTopPadding().toPx() } val shapeRadius by animateDpAsState( targetValue = if (sheetOffset.value >= statusBarHeightPx) 12.dp else 0.dp, animationSpec = tween(durationMillis = 150) ) val dynamicStatusBarPadding by remember { derivedStateOf { val offsetValuePx = sheetOffset.value if (offsetValuePx >= statusBarHeightPx) { 0.dp } else { with(density) { (statusBarHeightPx - offsetValuePx).toDp() } } } } ModalBottomSheetLayout( sheetState = sheetState, sheetShape = RoundedCornerShape(topStart = shapeRadius, topEnd = shapeRadius), content = {}, sheetContent = { Column(modifier = Modifier.fillMaxWidth()) { TopTitleItemForDialog( title = title, elevation = toolbarElevation, topPadding = dynamicStatusBarPadding, onClick = onCloseDialogClicked ) sheetContent() } }) } @Composable fun TopTitleItemForDialog( title: String, elevation: Dp = 0.dp, topPadding: Dp = 0.dp, onClick: () -> Unit ) { Surface( modifier = Modifier.fillMaxWidth(), color = Color.LightGray, elevation = elevation ) { Row( modifier = Modifier.padding(top = topPadding, start = 16.dp, end = 16.dp, bottom = 12.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = title, maxLines = 1, modifier = Modifier.weight(1f) ) IconButton(onClick = onClick) { Icon( imageVector = Icons.Default.Close, contentDescription = stringResource(R.string.cancel), tint = Color.Gray, modifier = Modifier.size(24.dp) ) } } } } class SheetPaddingTestActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.statusBarColor = android.graphics.Color.TRANSPARENT window.navigationBarColor = android.graphics.Color.TRANSPARENT WindowCompat.setDecorFitsSystemWindows(window, false) setContent { Box( Modifier .fillMaxSize() .background(Color.Green), contentAlignment = Alignment.Center ) { var itemCount by remember { mutableStateOf(20) } val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) { false } val scope = rememberCoroutineScope() val scrollState = rememberScrollState() // 定义NestedScrollConnection协调滚动与BottomSheet拖拽 val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // 列表滚动到顶部且向下拖拽时,将事件交给BottomSheet处理 if (scrollState.value == 0 && available.y > 0 && source == NestedScrollSource.Drag) { return Offset.Zero } return Offset.Zero } } } Text("显示弹窗", modifier = Modifier.clickable { scope.launch { state.animateTo(ModalBottomSheetValue.Expanded) } }) CEModalBottomSheet(sheetState = state, onCloseDialogClicked = { scope.launch { state.hide() } }, sheetContent = { Column( Modifier .verticalScroll(scrollState) .nestedScroll(nestedScrollConnection) // 添加嵌套滚动协调 .weight(1f, fill = false) .padding(horizontal = 16.dp), ) { repeat(itemCount) { Box( modifier = Modifier .fillMaxWidth() .height(30.dp) .background(Color.Blue.copy(alpha = 1f - it * 0.04f)) .padding(vertical = 4.dp) ) } } CompositionLocalProvider( LocalContentColor.provides(Color.White) ) { Row( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colors.primary) .padding(vertical = 12.dp) .navigationBarsPadding(), verticalAlignment = Alignment.CenterVertically ) { IconButton(modifier = Modifier.weight(1f), onClick = { itemCount = max(itemCount - 1, 0) }) { Icon(Icons.Default.KeyboardArrowLeft, "") } Text( modifier = Modifier.weight(1f), text = "$itemCount", textAlign = TextAlign.Center ) IconButton(modifier = Modifier.weight(1f), onClick = { itemCount++ }) { Icon(Icons.Default.KeyboardArrowRight, "") } } } } ) } } } }
关键修改点
- 添加NestedScrollConnection:在中间滚动列表上添加
nestedScroll修饰符,当列表滚动到顶部且用户向下拖拽时,将手势事件交给BottomSheet处理,避免冲突。 - 优化动画逻辑:给
animateDpAsState添加明确动画时长,避免状态切换过于敏感。 - 调整内边距:给TopTitleItem的Row添加底部内边距,优化视觉效果。
修改后,无论内容高度如何,拖拽中间滚动列表时都能正常滚动,滚动到顶部后继续拖拽,BottomSheet会正常响应拖拽动作,不会出现弹回顶部的问题。
内容的提问来源于stack exchange,提问作者John Liu
相关产品推荐
相关产品推荐

