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

如何修复Jetpack Compose中ModalBottomSheet内LazyColumn滚动选中项问题

问题描述

用ModalBottomSheetLayout实现了可选择的地点列表,列表由内部的LazyColumn承载。点击「Open place selector」打开底部弹窗时,希望自动滚动到之前选中的地点,但遇到以下问题:

  • 当弹窗处于半展开状态时,LazyColumn会以全屏高度计算,而非弹窗的半展开高度
  • 场景1:18项列表,选中项为第6项,滚动功能正常
  • 场景2:18项列表,选中项为第15项,滚动到选中项的功能失效
问题分析

问题根源是弹窗半展开阶段,LazyColumn还没完成布局、获取到自己实际的可视区域高度,就提前执行了animateScrollToItem。此时它会错误地以全屏高度计算滚动位置:靠前的项刚好落在错误计算的可视范围内,所以滚动看起来正常;但靠后的项会被滚到超出弹窗实际可视区域的位置,导致用户看不到,看起来像是滚动失效。另外原代码中LaunchedEffect用固定字符串作为触发key,没有和弹窗的展开状态绑定,触发时机不准确。

修复方案

核心思路是等弹窗和列表布局完全稳定后再执行滚动,同时确保LazyColumn使用弹窗的实际可用高度。具体修改如下:

修改点说明

  1. 将弹窗状态传递给BottomSheet组件,以便监听弹窗的展开状态
  2. 调整LaunchedEffect的触发条件,绑定弹窗状态和选中地点,确保在弹窗可见且布局稳定后执行滚动
  3. 移除不必要的Column容器,避免额外的布局层级影响高度计算
  4. 可选:设置skipHalfExpanded = true直接跳过半展开状态,彻底规避半展开的高度计算问题

修复后的完整代码

@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
@Composable
internal fun PlaceScreen(places: List<String>, selectedPlace: String) {
    // 可选:设为true直接跳过半展开状态,简化逻辑
    val skipHalfExpanded by remember { mutableStateOf(true) }
    val modalBottomSheetState = rememberModalBottomSheetState(
        initialValue = ModalBottomSheetValue.Hidden,
        skipHalfExpanded = skipHalfExpanded
    )
    val scope = rememberCoroutineScope()
    
    Surface(modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())) {
        ModalBottomSheetLayout(
            sheetContent = { 
                BottomSheet(
                    places = places, 
                    selectedPlace = selectedPlace,
                    sheetState = modalBottomSheetState
                )
            }, 
            sheetState = modalBottomSheetState
        ) {
            Button(onClick = { scope.launch { modalBottomSheetState.show() }}) {
                Text(text = "Open place selector")
            }
        }
    }
}

@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
@Composable
fun BottomSheet(
    places: List<String>, 
    selectedPlace: String,
    sheetState: ModalBottomSheetState
) {
    val listState = rememberLazyListState()
    val targetIndex = remember(selectedPlace) { places.indexOf(selectedPlace) }

    LaunchedEffect(sheetState.currentValue, targetIndex) {
        // 等待弹窗变为可见状态
        if (!sheetState.isVisible) return@LaunchedEffect
        // 等待弹窗和列表布局完全稳定
        sheetState.awaitIdle()
        // 等待LazyColumn加载完所有列表项
        listState.waitUntil { it.layoutInfo.totalItemsCount == places.size }
        
        if (targetIndex != -1) {
            listState.animateScrollToItem(
                index = targetIndex,
                scrollOffset = 0 // 让选中项对齐列表顶部,可选
            )
        }
    }

    Surface(modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())) {
        LazyColumn(
            state = listState,
            modifier = Modifier.fillMaxHeight()
        ) {
            items(places.size) { pos ->
                Item(place = places[pos], isSelected = places[pos] == selectedPlace)
            }
        }
    }
}

@Composable
fun Item(place: String, isSelected: Boolean) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(60.dp)
            .padding(horizontal = 16.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(text = place)
        Spacer(modifier = Modifier.weight(1f))
        RadioButton(selected = isSelected, onClick = null)
    }
}

补充说明

  • 如果需要保留半展开状态,只需把skipHalfExpanded设为false,代码中的等待逻辑会自动适配半展开的布局高度
  • listState.waitUntil是LazyListState的扩展方法,用于等待列表布局完成,确保滚动时能获取到正确的项位置

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 17:50:44