Jetpack Compose中无需多协程实现Canvas多对象同时动画
实现方案
不需要为每个圆形启动独立协程,最低成本的改造方式是把圆形属性抽象为统一数据结构,用单协程驱动所有圆形的动画插值计算,和你现有代码逻辑完全兼容:
- 先抽离单个圆形的状态模型,收拢坐标、颜色属性
- 替换原来单组x/y的Animatable,统一维护所有圆形的状态列表
- 点击时一次性生成所有圆形的随机目标位置,在单个协程内完成整段动画的插值计算,自动同步更新所有圆形的当前位置
改造后可直接运行的代码
import androidx.compose.foundation.Canvas import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.IntSize import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import kotlinx.coroutines.launch import kotlin.math.roundToInt import androidx.compose.ui.util.lerp @Composable fun CanvasScreen(){ val animationScope = rememberCoroutineScope() // 统一圆形半径常量,方便全局调整 val circleRadius = 90f // 存储画布实际尺寸,避免硬编码坐标范围导致圆形出界 val canvasSize: MutableState<IntSize> = remember { mutableStateOf(IntSize.Zero) } // 初始化3个圆形的初始状态 val circleStates = remember { mutableStateListOf<CircleState>().apply { repeat(3) { add( CircleState( x = circleRadius, y = circleRadius, color = Color((Math.random() * 16777215).toInt() or (0xFF shl 24)) ) ) } } } // 单个Animatable持有0-1的动画进度,所有圆形共用这一个动画驱动 val animationProgress = remember { Animatable(1f) } Canvas( modifier = Modifier .fillMaxSize() .clickable { // 捕获动画起始时所有圆形的位置 val oldStates = circleStates.toList() val currentSize = canvasSize.value val maxX = currentSize.width - circleRadius val maxY = currentSize.height - circleRadius // 为每个圆形生成独立的随机目标位置 val targetStates = oldStates.map { old -> old.copy( x = (circleRadius.roundToInt()..maxX.roundToInt()).random().toFloat(), y = (circleRadius.roundToInt()..maxY.roundToInt()).random().toFloat() ) } animationScope.launch { // 仅启动1个协程跑完整段动画 animationProgress.snapTo(0f) animationProgress.animateTo( targetValue = 1f, animationSpec = tween(durationMillis = 600) // 可自行替换为spring等其他动画规格 ) { val currentProgress = value // 每帧根据进度计算所有圆形的实时位置 circleStates.forEachIndexed { index, _ -> val oldPos = oldStates[index] val targetPos = targetStates[index] circleStates[index] = oldPos.copy( x = lerp(oldPos.x, targetPos.x, currentProgress), y = lerp(oldPos.y, targetPos.y, currentProgress) ) } } } } ){ // 记录画布真实尺寸 canvasSize.value = size // 遍历绘制所有圆形 circleStates.forEach { circle -> drawCircle( color = circle.color, radius = circleRadius, center = Offset(circle.x, circle.y), ) } } } // 单个圆形的状态数据结构 private data class CircleState( val x: Float, val y: Float, val color: Color )
方案说明
- 全程只启动1个协程,所有圆形共用同一个进度值驱动,没有多余协程开销
- 后续增减圆形数量只需要修改初始化时
repeat(3)的数值即可,不需要重复声明Animatable、编写协程启动逻辑 - 坐标范围自动适配画布实际宽高,不会出现圆形绘制到画布外的问题,适配性比硬编码数值范围更好
- 插值逻辑直接使用Compose内置的
lerp函数,动画效果和原有单圆实现完全一致,支持自由切换动画规格
内容的提问来源于stack exchange,提问作者Kuba Szczukin
相关产品推荐
相关产品推荐

