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

Jetpack Compose实现带固定激活项的BottomSheet列表

解决方案:动态适配高度的BottomSheetScaffold实现

核心思路是通过状态变量动态绑定sheetPeekHeight,结合全局位置监听获取固定内容的实际高度,同时计算底部弹窗的展开高度以适配列表内容,具体实现如下:

关键步骤

  • 用mutableStateOf保存动态的peek高度,状态变化会自动触发BottomSheetScaffold重组
  • 通过onGloballyPositioned监听固定内容的布局位置,获取其实际高度并更新状态
  • 计算弹窗展开高度:若列表总高度小于屏幕剩余高度,展开高度为列表总高;否则设为屏幕可用高度,列表内部滚动
  • 基于内容自适应处理列表项高度,适配系统字体缩放

完整代码示例

@Composable
fun AdaptiveBottomSheet() {
    // 保存动态的peek高度,初始值设为0,后续会更新
    val sheetPeekHeight = remember { mutableStateOf(0.dp) }
    // 模拟列表数据
    val listItems = remember { List(20) { "Item $it" } }
    // 激活项状态(示例:默认激活第0项)
    val activeItemIndex = remember { mutableStateOf(0) }

    // 获取屏幕可用高度(减去状态栏、导航栏等系统UI高度)
    val windowSize = rememberWindowSize()
    val availableScreenHeight = windowSize.height - LocalWindowInsets.current.systemBars.bottom.dp

    BottomSheetScaffold(
        sheetPeekHeight = sheetPeekHeight.value,
        // 计算展开高度:取列表总高度和屏幕可用高度的较小值
        sheetMaxHeight = with(LocalDensity.current) {
            val itemHeight = 48.sp.toDp() // 列表项高度随字体缩放
            val totalListHeight = itemHeight * listItems.size + sheetPeekHeight.value
            min(totalListHeight, availableScreenHeight)
        },
        sheetContent = {
            Column {
                // 固定显示的内容(alwaysVisibleSheetContent)
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.LightGray)
                        .onGloballyPositioned { coordinates ->
                            // 获取固定内容的实际高度,转换为dp后更新状态
                            val height = with(LocalDensity.current) {
                                coordinates.size.height.toDp()
                            }
                            sheetPeekHeight.value = height
                        }
                ) {
                    if (activeItemIndex.value != -1) {
                        Row(
                            modifier = Modifier.fillMaxWidth(),
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            Text(
                                text = "激活项: ${listItems[activeItemIndex.value]}",
                                modifier = Modifier.weight(1f).padding(16.dp)
                            )
                            CircularProgressIndicator(
                                modifier = Modifier.size(24.dp).padding(end = 16.dp),
                                strokeWidth = 2.dp
                            )
                        }
                    } else {
                        Text(
                            text = "无激活项",
                            modifier = Modifier.fillMaxWidth().padding(16.dp),
                            textAlign = TextAlign.Center
                        )
                    }
                }

                // 列表内容
                LazyColumn(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    items(listItems) { item ->
                        val isActive = listItems.indexOf(item) == activeItemIndex.value
                        Text(
                            text = item,
                            modifier = Modifier
                                .fillMaxWidth()
                                .height(48.sp.toDp()) // 高度随字体缩放
                                .padding(horizontal = 16.dp)
                                .align(Alignment.CenterVertically)
                                .clickable { activeItemIndex.value = listItems.indexOf(item) },
                            verticalAlignment = Alignment.CenterVertically
                        )
                    }
                }
            }
        },
        content = {
            // 主内容区域
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Button(onClick = { activeItemIndex.value = -1 }) {
                    Text("清除激活项")
                }
            }
        }
    )
}

// 辅助函数:获取窗口尺寸
@Composable
fun rememberWindowSize(): WindowSize {
    val configuration = LocalConfiguration.current
    return remember(configuration) {
        WindowSize(
            width = configuration.screenWidthDp.dp,
            height = configuration.screenHeightDp.dp
        )
    }
}

data class WindowSize(val width: Dp, val height: Dp)

关键点解释

  1. 动态peekHeight更新:通过onGloballyPositioned监听固定内容的布局完成事件,获取其真实高度后更新sheetPeekHeight的状态变量,触发BottomSheetScaffold重组,从而自适应固定内容的高度(包括系统字体缩放后的变化)。
  2. 自适应展开高度:计算列表总高度(列表项高度×数量 + 固定内容高度),与屏幕可用高度取最小值,确保弹窗不会超出屏幕,超出时列表内部可滚动。
  3. 列表项高度适配:使用48.sp.toDp()设置列表项高度,sp单位会随系统字体缩放自动调整,保证高度适配字体设置。
  4. 激活项状态处理:通过activeItemIndex状态控制固定内容的显示(文本/带进度指示器的激活项),点击列表项可切换激活状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 11:02:44