Kotlin Compose两张卡片交换位置动画失效问题求助
卡片交换动画修复方案
问题根源
- 不可观察状态导致重组失效:用
mutableListOf存储选中卡片和位置,Compose无法监测到变化,自然不会触发重组和动画。 - Card类位置计算逻辑错误:
xPosition和yPosition是val,依赖position但position是var,修改position后这两个值不会自动更新,动画没有正确的目标值。 - Transition状态设计不合理:用布尔值
doAnimation作为过渡状态,逻辑判断绕且未直接关联卡片的位置变化,动画触发条件不清晰。 - 动画后状态未重置:选中两张卡片触发动画后,未清空选中状态,后续操作会混乱。
分步修复
1. 替换为Compose可观察状态
把普通的mutableListOf换成mutableStateListOf,确保修改能触发重组:
// 封装选中卡片的信息 data class SelectedCard( val card: Card, val originalX: Int, val originalY: Int ) // 可观察的选中列表和动画状态 var selectedCards by remember { mutableStateListOf<SelectedCard>() } var isAnimating by remember { mutableStateOf(false) }
2. 重构Card数据类的位置逻辑
不在数据类里硬编码位置,改为在Compose中动态计算:
data class Card( val content: String = "", val id: Int, // 用唯一ID标识,避免引用比较坑 var currentPosition: Int = -1 // 记录当前位置,用于动画和布局 ) // 在Compose组件中实时计算位置 val xPos = card.currentPosition.div(4) val yPos = card.currentPosition.mod(4)
3. 基于位置变化设计Transition
直接用卡片的currentPosition作为Transition的目标状态,位置变化时自动触发动画:
// 为每张卡片创建独立过渡 val transition = updateTransition(targetState = card.currentPosition, label = "card_${card.id}") val animatedX = transition.animateInt( transitionSpec = { tween(1000) }, label = "x_anim_${card.id}" ) { targetPos -> targetPos.div(4) } val animatedY = transition.animateInt( transitionSpec = { tween(1000) }, label = "y_anim_${card.id}" ) { targetPos -> targetPos.mod(4) } // 应用动画后的padding modifier = modifier.padding( start = (animatedX.value * (cardWidth + padding) + padding).dp, top = (animatedY.value * (cardHeight + padding) + padding).dp )
4. 修正点击逻辑与动画触发
仅在非动画状态下允许选中卡片,选中两张后触发位置交换,动画结束后重置状态:
// 点击事件 clickable { if (!isAnimating && selectedCards.size < 2) { val x = card.currentPosition.div(4) val y = card.currentPosition.mod(4) selectedCards.add(SelectedCard(card, x, y)) } } // 监听选中列表变化,触发动画 LaunchedEffect(selectedCards.size) { if (selectedCards.size == 2 && !isAnimating) { isAnimating = true // 交换两张卡片的位置 val card1 = selectedCards[0].card val card2 = selectedCards[1].card val tempPos = card1.currentPosition card1.currentPosition = card2.currentPosition card2.currentPosition = tempPos // 等待动画完成后重置状态 delay(1000) // 和tween时长保持一致 selectedCards.clear() isAnimating = false } }
5. 初始化16张卡片
确保每张卡片有唯一的初始位置:
val cardList by remember { mutableStateListOf<Card>().apply { repeat(16) { index -> add(Card(content = index.toString(), id = index, currentPosition = index)) } } }
完整可运行示例
@Composable fun CardSwapGame() { val cardSize = 80.dp val padding = 16.dp val cardSizeInt = 80 val paddingInt = 16 data class SelectedCard( val card: Card, val originalX: Int, val originalY: Int ) data class Card( val content: String = "", val id: Int, var currentPosition: Int = -1 ) val cardList by remember { mutableStateListOf<Card>().apply { repeat(16) { index -> add(Card(content = index.toString(), id = index, currentPosition = index)) } } } var selectedCards by remember { mutableStateListOf<SelectedCard>() } var isAnimating by remember { mutableStateOf(false) } LaunchedEffect(selectedCards.size) { if (selectedCards.size == 2 && !isAnimating) { isAnimating = true val card1 = selectedCards[0].card val card2 = selectedCards[1].card // 交换位置触发动画 val tempPos = card1.currentPosition card1.currentPosition = card2.currentPosition card2.currentPosition = tempPos delay(1000) selectedCards.clear() isAnimating = false } } Column(modifier = Modifier.padding(padding)) { repeat(4) { row -> Row { repeat(4) { col -> val targetPos = row * 4 + col val card = cardList.first { it.currentPosition == targetPos } val transition = updateTransition(targetState = card.currentPosition, label = "card_${card.id}") val animatedX = transition.animateInt( transitionSpec = { tween(1000) }, label = "x_${card.id}" ) { pos -> pos.div(4) } val animatedY = transition.animateInt( transitionSpec = { tween(1000) }, label = "y_${card.id}" ) { pos -> pos.mod(4) } androidx.compose.material3.Card( modifier = Modifier .size(cardSize) .padding( start = (animatedX.value * (cardSizeInt + paddingInt) + paddingInt).dp, top = (animatedY.value * (cardSizeInt + paddingInt) + paddingInt).dp ) .clickable(enabled = !isAnimating && selectedCards.size < 2) { val x = card.currentPosition.div(4) val y = card.currentPosition.mod(4) selectedCards.add(SelectedCard(card, x, y)) }, elevation = androidx.compose.material3.CardDefaults.cardElevation(defaultElevation = 4.dp) ) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(text = card.content, fontSize = 20.sp) } } } } } } }
内容的提问来源于stack exchange,提问作者blazed
相关产品推荐
相关产品推荐

