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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 23:54:18