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

Jetpack Compose实现每秒发射小球且不干扰内部协程的问题

解决方案

你的问题核心在于用单个布尔值isLoading控制小球显示,Compose只会渲染一个Ball实例,切换布尔值只会让这个实例隐藏/显示,无法创建多个独立小球。要实现持续发射,需要用列表管理所有活跃小球,每个小球用唯一标识区分,让Compose渲染多个独立组件。

修改步骤及代码

1. 定义小球数据类

封装每个小球的参数和唯一ID,确保Compose能识别不同实例:

import java.util.UUID

data class BallData(
    val id: String,
    val noteSize: Dp,
    val verticalOffset: Dp,
    val target: Float
)

2. 修改发射控制代码

用列表保存所有小球,每秒添加新实例;用Box容纳所有小球(因为每个小球通过offset控制位置):

val coroutineScope = rememberCoroutineScope()
var balls by remember { mutableStateOf(emptyList<BallData>()) }
var isShooting by remember { mutableStateOf(false) }

Row(
    horizontalArrangement = Arrangement.End,
    verticalAlignment = Alignment.Top,
    modifier = Modifier
        .width(noteSize * scoreLength)
        .fillMaxHeight()
) {
    Button(
        colors = ButtonDefaults.buttonColors(containerColor = Color.Red),
        onClick = {
            isShooting = !isShooting
            if (isShooting) {
                coroutineScope.launch {
                    while (isShooting) {
                        // 每秒创建一个新小球
                        val newBall = BallData(
                            id = UUID.randomUUID().toString(),
                            noteSize = noteSize,
                            verticalOffset = noteSize / 2 + (noteSize / 2) * 8,
                            target = w // 替换为你原本的target参数值
                        )
                        balls = balls + newBall
                        delay(1000)
                    }
                }
            }
        }
    ) {
        Text(text = if (isShooting) "停止发射" else "发射")
    }

    // 用Box承载所有小球,每个小球通过offset控制位置
    Box(modifier = Modifier.fillMaxSize()) {
        balls.forEach { ballData ->
            // 用唯一ID作为key,确保Compose渲染独立的Ball组件
            key(ballData.id) {
                Ball(
                    noteSize = ballData.noteSize,
                    verticalOffSet = ballData.verticalOffset,
                    target = ballData.target,
                    onAnimationComplete = {
                        // 动画结束后移除小球,释放内存
                        balls = balls.filter { it.id != ballData.id }
                    }
                )
            }
        }
    }
}

3. 修改小球组件

用LaunchedEffect管理动画协程(符合Compose生命周期),添加动画完成回调:

@Composable
fun Ball(
    noteSize: Dp,
    verticalOffSet: Dp,
    target: Float,
    onAnimationComplete: () -> Unit
) {
    val offsetX = remember { Animatable(350f) }
    val v = verticalOffSet.dpToPx().toInt()

    OutlinedButton(
        colors = ButtonDefaults.buttonColors(containerColor = Color.Red),
        onClick = {},
        modifier = Modifier
            .offset {
                IntOffset(
                    offsetX.value.toInt(),
                    v
                )
            }
            .size(noteSize),
        shape = CircleShape
    ) {}

    // LaunchedEffect在组件进入组合时启动动画,离开时自动取消
    LaunchedEffect(Unit) {
        runCatching {
            offsetX.animateTo(
                targetValue = -target,
                animationSpec = tween(durationMillis = 3000, easing = LinearEasing)
            )
        }.finally {
            // 动画完成后通知父组件移除当前小球
            onAnimationComplete()
        }
    }
}

关键说明

  • 列表管理小球:每个新小球都作为独立元素加入列表,Compose会为每个元素渲染独立的Ball组件,互不干扰。
  • 唯一ID作为key:确保Compose不会复用组件实例,每个小球的动画都是独立的。
  • LaunchedEffect替代直接launch:遵循Compose生命周期,避免重组时重复启动协程,动画更稳定。
  • 动画完成后移除:防止列表无限增长,优化内存占用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 06:16:02