You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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, "")
                                }
                            }
                        }
                    }
                )
            }
        }
    }
}

关键修改点

  1. 添加NestedScrollConnection:在中间滚动列表上添加nestedScroll修饰符,当列表滚动到顶部且用户向下拖拽时,将手势事件交给BottomSheet处理,避免冲突。
  2. 优化动画逻辑:给animateDpAsState添加明确动画时长,避免状态切换过于敏感。
  3. 调整内边距:给TopTitleItem的Row添加底部内边距,优化视觉效果。

修改后,无论内容高度如何,拖拽中间滚动列表时都能正常滚动,滚动到顶部后继续拖拽,BottomSheet会正常响应拖拽动作,不会出现弹回顶部的问题。

内容的提问来源于stack exchange,提问作者John Liu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 15:45:23