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

如何在Compose中实现Canvas平滑滚动的形状跑马灯动画?

修复Jetpack Compose形状跑马灯动画卡顿问题

我向绘制管线传入一串shapes数据,任意时刻shapes中的元素数量最多可填满屏幕宽度。目标是实现类似Android Views跑马灯文本的效果:形状从屏幕右侧开始绘制,新形状加入时整体向左滚动。当前代码能实现基本逻辑,但动画卡顿,形状移动有明显跳跃感。

当前代码如下:

@Composable
fun ShapeWidget(shapes: List<Shape>, size: Float, spacing: Float, screenWidth: Float) {
    val color = MaterialTheme.colorScheme.tertiary
    Canvas(
        modifier = Modifier.fillMaxWidth(),
        onDraw = {
            shapes.forEachIndexed { i, s ->
                val x = screenWidth - ((size + spacing) * i)
                when (s.type) {
                    Type.RECT -> {
                        drawRect(
                            color = color,
                            size = Size(size, size),
                            topLeft = Offset(x, this.center.y - size / 2),
                        )
                    }
                    Type.CIRCLE -> {
                        drawCircle(
                            color = color,
                            radius = size / 2,
                            center = Offset(x + spacing * 2, center.y)
                        )
                    }
                }
            }
        }
    )
}

问题根源

  1. 无平滑过渡:通过索引直接计算x坐标,新形状加入时所有形状位置瞬间改变,属于跳帧式移动,没有插值动画过渡
  2. 坐标计算不一致:圆形的x坐标额外加了spacing * 2,导致布局错位,进一步放大跳跃感
  3. 重绘效率低:shapes更新时直接触发Canvas全量重绘,未利用Compose动画API的优化机制

修复方案

使用Compose的动画API管理滚动偏移量,实现平滑过渡,同时统一坐标计算逻辑:

@Composable
fun ShapeWidget(shapes: List<Shape>, size: Float, spacing: Float, screenWidth: Float) {
    val color = MaterialTheme.colorScheme.tertiary
    // 计算所有形状加间距的总宽度
    val totalContentWidth = shapes.size * (size + spacing) - spacing
    // 用动画平滑过渡滚动偏移量:总宽度超过屏幕时,偏移量等于超出部分
    val scrollOffset by animateFloatAsState(
        targetValue = maxOf(0f, totalContentWidth - screenWidth),
        animationSpec = tween(durationMillis = 300, easing = LinearEasing)
    )

    Canvas(
        modifier = Modifier.fillMaxWidth(),
        onDraw = {
            shapes.forEachIndexed { i, s ->
                // 基准x坐标:从右侧起始位置,减去滚动偏移量
                val baseX = screenWidth - ((size + spacing) * i) - scrollOffset
                when (s.type) {
                    Type.RECT -> {
                        drawRect(
                            color = color,
                            size = Size(size, size),
                            topLeft = Offset(baseX, center.y - size / 2),
                        )
                    }
                    Type.CIRCLE -> {
                        drawCircle(
                            color = color,
                            radius = size / 2,
                            center = Offset(baseX + size / 2, center.y)
                        )
                    }
                }
            }
        }
    )
}

// 配套的Shape和Type定义
data class Shape(val type: Type)
enum class Type { RECT, CIRCLE }

关键修改说明

  • 滚动偏移动画:animateFloatAsState自动处理偏移量的平滑插值,新形状加入时,偏移量从旧值过渡到新值,带动所有形状匀速左移,彻底消除跳跃感
  • 统一坐标逻辑:修复圆形的位置计算错误,矩形和圆形的布局基准一致,视觉效果更整齐
  • 线性动画曲线:LinearEasing保证滚动速度均匀,符合跑马灯的自然体验,也可根据需求替换为其他缓动曲线

额外优化建议

  • 若shapes更新频繁,用remember缓存totalContentWidth的计算结果,减少重复计算:
    val totalContentWidth by remember(shapes, size, spacing) {
        derivedStateOf { shapes.size * (size + spacing) - spacing }
    }
    
  • 对于超大量形状的场景,建议改用LazyRow配合AnimatedVisibility,利用Compose的懒加载机制优化性能,避免Canvas绘制压力过大
  • 如需无限滚动效果,可使用InfiniteTransition实现循环动画

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 23:50:34