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

Android Jetpack Compose:LazyRow中Canvas路径动画异常及单次动画需求

问题修复方案

核心问题分析

  1. 状态管理错误:pointsCopy是普通可变列表,不属于Compose状态,且每次Canvas绘制都会添加元素,加上LazyRow的item复用机制,导致滚动时数据混乱,出现绘制异常。
  2. 动画重复触发:LaunchedEffect(Unit)在item每次进入组合时都会执行,滚动回已动画过的item时会重新触发动画。
  3. 缺少唯一标识:LazyRow的items未指定唯一key,item复用时状态会混乱。

修复步骤及代码实现

1. 为LazyRow items添加唯一key

确保每个item的状态独立,避免复用错误:

items(15, key = { it }) { index ->
    // ... 内部逻辑
}

2. 用rememberSaveable保存动画完成状态

保证item被回收重建后,动画不会重复执行:

var hasAnimated by rememberSaveable(index) { mutableStateOf(false) }

3. 改用进度值控制动画(替代索引)

用animateFloatAsState实现0到1的进度动画,更灵活且避免索引越界问题:

val animationProgress by animateFloatAsState(
    targetValue = if (hasAnimated) 1f else 0f,
    animationSpec = tween(4000, easing = LinearEasing),
    finishedListener = { hasAnimated = true }
)

4. 在Canvas中根据进度动态生成绘制点

不再维护可变列表,每次绘制时根据进度截取对应比例的点,避免数据累积:

Canvas(modifier = Modifier.fillMaxSize()) {
    val currentPoints = points.take((points.size * animationProgress).toInt())
    if (currentPoints.size >= 2) {
        drawPoints(
            points = currentPoints,
            strokeWidth = 3f,
            pointMode = PointMode.Lines,
            color = Color.White
        )
    }
}

完整修复代码

@Composable
private fun drawPointsAnimation() {
    LazyRow(
        horizontalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        // 为每个item指定唯一key,避免复用错误
        items(15, key = { it }) { index ->
            val density = LocalDensity.current
            val sizePx = density.run { 50.dp.toPx() }
            val path = getPathBox(Size(sizePx, sizePx))
            val points = remember(sizePx) { getPoints(path) }

            // 标记当前item是否已完成动画,用rememberSaveable确保状态不丢失
            var hasAnimated by rememberSaveable(index) { mutableStateOf(false) }

            // 用进度值控制动画,完成后标记状态
            val animationProgress by animateFloatAsState(
                targetValue = if (hasAnimated) 1f else 0f,
                animationSpec = tween(4000, easing = LinearEasing),
                finishedListener = { hasAnimated = true }
            )

            // 仅当未动画过时启动动画
            LaunchedEffect(index) {
                if (!hasAnimated) {
                    hasAnimated = true
                }
            }

            Box(
                modifier = Modifier
                    .size(50.dp)
                    .background(Color.Blue)
            ) {
                Canvas(modifier = Modifier.fillMaxSize()) {
                    // 根据进度截取对应数量的点,避免维护可变列表
                    val currentPointCount = (points.size * animationProgress).toInt()
                    if (currentPointCount >= 2) {
                        drawPoints(
                            points = points.take(currentPointCount),
                            strokeWidth = 3f,
                            pointMode = PointMode.Lines,
                            color = Color.White
                        )
                    }
                }
            }
        }
    }
}

private fun getPoints(path: Path): MutableList<Offset> {
    val pm = android.graphics.PathMeasure(path.asAndroidPath(), false)
    val pointsList = mutableListOf<Offset>()
    var i = 0.0f
    while (i <= 1) { // 修改为<=1,确保包含最后一个点
        val aCoordinates = floatArrayOf(0f, 0f)
        pm.getPosTan(pm.length * i, aCoordinates, null)
        pointsList.add(Offset(x = aCoordinates[0], y = aCoordinates[1]))
        i += 0.01f
    }
    return pointsList
}

private fun getPathBox(size: Size): Path {
    val path = Path()
    path.moveTo(size.width, 0f)
    path.lineTo(0f, size.height)
    return path
}

额外优化点

  • 将sizePx的计算用remember缓存,避免每次重组重复计算。
  • 修改getPoints中的循环条件为i <= 1,确保路径的最后一个点被包含。
  • 用Modifier.size(50.dp)替代单独设置宽高,代码更简洁。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 01:48:38