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

Jetpack Compose:如何获取LazyRow滚动百分比并实现平滑指示器?

解决方案:平滑获取滚动进度 + 适配非全屏Item

要实现平滑过渡的滚动指示器,你可以通过LazyListState的layoutInfo和firstVisibleItemScrollOffset计算连续的滚动进度,而非仅依赖firstVisibleItemIndex。同时针对Item占屏幕90%的场景,需调整滚动范围计算逻辑,甚至自定义Snap行为让Item居中对齐。

步骤1:计算平滑滚动进度

通过LazyListState的visibleItemsInfo获取当前可见Item的位置和偏移,结合每个Item的实际宽度(含间距),算出0~1之间的连续滚动进度:

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SnappingRowWithIndicator(items: List<Int>) {
    val state = rememberLazyListState()
    val density = LocalDensity.current
    val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp
    // 计算单个Item的总宽度(Item宽度 + 间距)
    val itemTotalWidth = with(density) {
        (screenWidthDp * 0.9f + 16.dp).toPx()
    }
    val totalScrollRange = (items.size - 1) * itemTotalWidth // 总滚动范围

    // 滚动进度:0f表示第一个Item完全显示,1f表示最后一个Item完全显示
    var scrollProgress by remember { mutableStateOf(0f) }

    // 监听滚动状态变化,实时计算进度
    LaunchedEffect(state) {
        snapshotFlow {
            val visibleItems = state.layoutInfo.visibleItemsInfo
            val firstItem = visibleItems.firstOrNull() ?: return@snapshotFlow 0f

            // 当前滚动的总偏移量:已滚动的Item数*单个Item宽度 + 当前Item的偏移
            val currentOffset = firstItem.index * itemTotalWidth + firstItem.offset
            // 计算进度(避免除以0)
            if (totalScrollRange == 0f) 0f else currentOffset / totalScrollRange
        }.collect { progress ->
            scrollProgress = progress
            println("scroll progress: $progress")
        }
    }

    // 自定义Snap行为:让Item居中显示(适配90%宽度的Item)
    val snapFlingBehavior = rememberSnapFlingBehavior(
        lazyListState = state,
        snapOffset = { _, _ ->
            // 计算居中偏移:(屏幕宽度 - Item宽度)/2
            with(density) {
                (screenWidthDp - screenWidthDp * 0.9f).toPx() / 2
            }
        }
    )

    // LazyRow部分
    LazyRow(
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        state = state,
        flingBehavior = snapFlingBehavior,
        contentPadding = PaddingValues(horizontal = (screenWidthDp - screenWidthDp * 0.9f)/2) // 左右添补让第一个Item默认居中
    ) {
        items(items) { dto ->
            Text(
                modifier = Modifier
                    .height(32.dp)
                    .fillParentMaxWidth(0.9f)
                    .background(Color.White)
                    .padding(vertical = 8.dp),
                color = Color.Black,
                text = dto.toString(),
                textAlign = TextAlign.Center
            )
        }
    }

    // 平滑过渡的圆点指示器
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 8.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
    ) {
        items.forEachIndexed { index, _ ->
            // 计算当前Item对应的进度区间
            val itemStartProgress = index / (items.size - 1f).coerceAtLeast(1f)
            val itemEndProgress = (index + 1) / (items.size - 1f).coerceAtLeast(1f)

            // 根据滚动进度计算圆点的透明度和大小,实现平滑过渡
            val alpha = when {
                scrollProgress <= itemStartProgress -> 0.3f // 未激活状态
                scrollProgress >= itemEndProgress -> 0.3f
                else -> {
                    val transitionFraction = (scrollProgress - itemStartProgress) / (itemEndProgress - itemStartProgress)
                    0.3f + 0.7f * transitionFraction // 从0.3过渡到1
                }
            }
            val size = if (scrollProgress in itemStartProgress..itemEndProgress) {
                8.dp + 2.dp * ((scrollProgress - itemStartProgress) / (itemEndProgress - itemStartProgress))
            } else 8.dp

            Box(
                modifier = Modifier
                    .size(size)
                    .background(Color.White.copy(alpha = alpha), shape = CircleShape)
            )
        }
    }
}

关键细节说明:

  1. 滚动进度计算:

    • 通过snapshotFlow监听LazyListState变化,实时获取当前可见Item的偏移量
    • 以单个Item的总宽度(Item宽度+间距)为单位,计算总滚动范围和当前偏移的比例,得到0~1的连续进度值
  2. 适配90%宽度的Item:

    • 添加contentPadding让第一个Item默认居中显示
    • 自定义snapOffset参数,让Snap行为将Item居中对齐,而非默认的左对齐
  3. 平滑指示器实现:

    • 针对每个圆点,根据滚动进度计算其透明度和大小,实现相邻圆点间的平滑过渡
    • 避免了仅依赖firstVisibleItemIndex导致的跳变问题

如果偏好更简洁的连续滑动式指示器,可替换圆点部分为以下代码:

// 连续滑动式指示器
Row(
    modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 32.dp, vertical = 8.dp),
    verticalAlignment = Alignment.CenterVertically
) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(4.dp)
            .background(Color.Gray.copy(alpha = 0.3f), shape = RoundedCornerShape(2.dp))
    ) {
        val indicatorWidth = 100.percent / items.size
        Box(
            modifier = Modifier
                .width(indicatorWidth)
                .height(4.dp)
                .background(Color.White, shape = RoundedCornerShape(2.dp))
                .offset(x = scrollProgress * (100.percent - indicatorWidth))
        )
    }
}

内容的提问来源于stack exchange,提问作者Felipe Ribeiro R. Magalhaes

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 14:57:04