Compose中LaunchedEffect动画异常:新增骰子时旧元素重复触发
解决LazyColumn中仅新选中骰子触发动画的问题
问题根源
- LazyColumn元素复用逻辑:默认
items会根据列表元素的位置和引用复用Composable实例。当列表排序后,新添加的骰子可能被放到非末尾位置,但排序导致末尾元素的引用变化,会让该位置的Composable被重组并错误触发动画。 - 动画触发条件错误:
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
相关产品推荐
相关产品推荐

