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

Compose中LaunchedEffect动画异常:新增骰子时旧元素重复触发

解决LazyColumn中仅新选中骰子触发动画的问题

问题根源

  1. LazyColumn元素复用逻辑:默认items会根据列表元素的位置和引用复用Composable实例。当列表排序后,新添加的骰子可能被放到非末尾位置,但排序导致末尾元素的引用变化,会让该位置的Composable被重组并错误触发动画。
  2. 动画触发条件错误:DieIcon中的LaunchedEffect(Unit)仅在Composable首次进入组合时触发,排序引发的元素位置变动会让末尾位置的Composable被重新创建,从而误触发动画。

解决方案

1. 为骰子添加“新添加”状态标记

在ViewModel中维护独立状态记录刚选中的骰子,避免污染原始Die数据类:

class DiceViewModel : ViewModel() {
    // 排序后的骰子列表
    val sortedDice = mutableStateListOf<Die>()
    // 记录刚添加的骰子ID(假设Die有唯一id属性)
    private val newDiceIds = mutableStateSetOf<String>()

    // 选中骰子时调用
    fun addDie(die: Die) {
        if (!sortedDice.contains(die)) {
            sortedDice.add(die)
            // 执行你的排序逻辑
            sortedDice.sortBy { /* 自定义排序规则 */ }
            // 标记为新添加的骰子
            newDiceIds.add(die.id)
        }
    }

    // 动画完成后调用,移除新标记
    fun markDieAsNotNew(dieId: String) {
        newDiceIds.remove(dieId)
    }
}

2. 修改DiceList,指定唯一Key并传递新状态

给LazyColumn的items指定唯一key,确保每个骰子的Composable实例稳定关联;同时传递isNew状态给DieIcon:

@Composable
private fun DiceList(
    dice: List<Die>,
    newDiceIds: Set<String>,
    onAnimationComplete: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    val state = rememberLazyListState()

    // 滚动到最新添加的骰子位置(可选)
    LaunchedEffect(dice) {
        val newDieIndex = dice.indexOfFirst { newDiceIds.contains(it.id) }
        if (newDieIndex != -1) {
            state.animateScrollToItem(newDieIndex)
        }
    }

    LazyColumn(
        state = state,
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(vertical = 32.dp),
        modifier = modifier
            .fillMaxWidth()
            .padding(horizontal = 8.dp)
    ) {
        // 用骰子唯一ID作为key,避免排序导致的元素复用错误
        items(dice, key = { it.id }) { die ->
            Box(contentAlignment = Alignment.Center) {
                DieIcon(
                    die = die,
                    isNew = newDiceIds.contains(die.id),
                    onAnimationComplete = { onAnimationComplete(die.id) }
                )
            }
        }
    }
}

3. 重写DieIcon的动画触发逻辑

将LaunchedEffect的依赖改为isNew,仅当骰子是新添加时触发动画,完成后通知ViewModel移除标记:

@Composable
fun DieIcon(
    die: Die,
    isNew: Boolean,
    onAnimationComplete: () -> Unit
) {
    var currentRotation by remember { mutableFloatStateOf(15f) }
    val rotation = remember { Animatable(currentRotation) }

    LaunchedEffect(isNew) {
        if (isNew) {
            val result = rotation.animateTo(
                targetValue = -15f,
                animationSpec = repeatable(
                    iterations = 3,
                    animation = tween(
                        durationMillis = 50,
                        easing = LinearEasing
                    ),
                    repeatMode = RepeatMode.Reverse
                )
            ) {
                currentRotation = rotation.value
            }

            if (result.endReason == AnimationEndReason.Finished) {
                rotation.animateTo(
                    targetValue = 0f,
                    animationSpec = tween(
                        durationMillis = 200,
                        easing = LinearOutSlowInEasing
                    )
                ) {
                    currentRotation = rotation.value
                }
            }
            // 动画完成后通知ViewModel清理状态
            onAnimationComplete()
        }
    }

    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier
            .rotate(if (isNew) currentRotation else 0f)
    ) {
        // 骰子图片展示逻辑,示例用文本代替
        Text(text = die.value.toString())
    }
}

4. 在父Composable中关联ViewModel状态

@Composable
fun DiceScreen(viewModel: DiceViewModel = viewModel()) {
    val sortedDice by viewModel.sortedDice.collectAsState()
    val newDiceIds by viewModel.newDiceIds.collectAsState()

    DiceList(
        dice = sortedDice,
        newDiceIds = newDiceIds,
        onAnimationComplete = viewModel::markDieAsNotNew
    )

    // 骰子选择按钮等其他UI组件
}

核心要点

  • 唯一Key的必要性:确保LazyColumn不会因排序错误复用Composable实例,每个骰子的UI状态独立。
  • 精准的动画触发条件:通过isNew状态控制动画,仅刚添加的骰子触发,避免无关重组导致的错误动画。
  • 状态及时清理:动画完成后移除新标记,防止下次重组重复触发动画。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 13:41:26