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

Kotlin Compose两张卡片交换位置动画失效问题求助

卡片交换动画修复方案

问题根源

  1. 不可观察状态导致重组失效:用mutableListOf存储选中卡片和位置,Compose无法监测到变化,自然不会触发重组和动画。
  2. Card类位置计算逻辑错误:xPosition和yPosition是val,依赖position但position是var,修改position后这两个值不会自动更新,动画没有正确的目标值。
  3. Transition状态设计不合理:用布尔值doAnimation作为过渡状态,逻辑判断绕且未直接关联卡片的位置变化,动画触发条件不清晰。
  4. 动画后状态未重置:选中两张卡片触发动画后,未清空选中状态,后续操作会混乱。

分步修复

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 04:00:59