如何在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) ) } } } } ) }
问题根源
- 无平滑过渡:通过索引直接计算x坐标,新形状加入时所有形状位置瞬间改变,属于跳帧式移动,没有插值动画过渡
- 坐标计算不一致:圆形的x坐标额外加了
spacing * 2,导致布局错位,进一步放大跳跃感 - 重绘效率低:
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
相关产品推荐
相关产品推荐

