26、Compose动画与状态:animate*AsState与remember结合、MutableTransitionState、动画与Compose状态管理
Compose的动画体系,说白了就是「状态驱动」的。你想想看,在传统View动画里,我们要写AnimationSet、写监听器、手动控制开始结束。但在Compose里,动画变成了状态的「影子」——状态变了,动画自动跟着走。
我个人习惯把Compose动画分成三类:值动画、可见性动画、自定义动画。今天咱们重点聊前两个,因为它们跟状态管理的关系最紧密。
animate*AsState:最常用的值动画
先看一个最简单的场景:点击按钮让一个方块从红色渐变到蓝色。在Compose里,你只需要一行代码:
val color by animateColorAsState(
targetValue = if (isSelected) Color.Blue else Color.Red,
animationSpec = tween(durationMillis = 500)
)
嗯,这里要注意:animate*AsState 系列函数(包括 animateFloatAsState、animateDpAsState、animateColorAsState 等)本质上是一个「带动画的状态包装器」。它会自动监听 targetValue 的变化,然后从旧值平滑过渡到新值。
我在项目中遇到过一个问题:动画执行到一半,用户又触发了新的状态变化。这时候Compose会怎么做?它会「截断」当前动画,从当前位置开始向新目标过渡。这个行为在大多数情况下是合理的,但如果你需要「排队执行」,就得用 Animatable 了。
remember + animate*AsState:状态与动画的绑定
为什么要把 remember 和 animate*AsState 放在一起讲?因为在实际项目中,动画的目标值往往来自某个 remember 的状态。
var progress by remember { mutableStateOf(0f) }
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = spring(dampingRatio = 0.3f)
)
你看,progress 是「原始状态」,animatedProgress 是「动画状态」。UI 组件应该只依赖 animatedProgress,这样每次 progress 变化时,UI 会平滑过渡而不是突变。
曾经有个同事把动画状态也放进了 ViewModel,结果每次重组都要重新创建动画对象,性能直接崩了。记住:动画状态应该属于 Composable 层,用 remember 持有。
MutableTransitionState:控制进入/退出动画
如果你做过列表项的「出现动画」或者「消失动画」,一定遇到过这样的需求:某个元素要移除时,先播放一个淡出动画,再从列表里真正删除。
在 Compose 里,MutableTransitionState 就是干这个的。它管理一个「可见性状态机」:Entering → Visible → Exiting → Gone。
val transitionState = remember { MutableTransitionState(false) }
transitionState.targetState = isVisible
// 用 AnimatedVisibility 配合 transitionState
AnimatedVisibility(
visibleState = transitionState,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically()
) {
// 你的内容
}
这里有个坑:MutableTransitionState 的初始值决定了「第一次渲染时是否播放动画」。如果初始值是 true,组件会直接显示,没有进入动画。我刚开始用的时候没注意,结果列表第一次加载时所有 item 都「闪」了一下——因为它们在执行进入动画,但用户期望的是直接显示。
动画与Compose状态管理:一个完整的例子
咱们把上面这些知识点串起来,写一个「可拖拽的评分卡片」。用户拖拽滑块时,评分数字平滑变化;点击提交时,卡片淡出消失。
@Composable
fun RatingCard(
onDismiss: () -> Unit
) {
var rating by remember { mutableStateOf(0f) }
val animatedRating by animateFloatAsState(
targetValue = rating,
animationSpec = spring(dampingRatio = 0.5f)
)
val transitionState = remember { MutableTransitionState(true) }
transitionState.targetState = true
AnimatedVisibility(
visibleState = transitionState,
exit = fadeOut(animationSpec = tween(300)) + shrinkVertically()
) {
Card(modifier = Modifier.padding(16.dp)) {
Column {
Text("评分:${(animatedRating * 10).toInt() / 10f}")
Slider(
value = rating,
onValueChange = { rating = it }
)
Button(onClick = {
transitionState.targetState = false
// 延迟删除,等动画播完
coroutineScope.launch {
delay(300)
onDismiss()
}
}) {
Text("提交")
}
}
}
}
}
这个例子包含了三个关键点:
- 评分数字的平滑变化:用
animateFloatAsState包装rating,让数字过渡更自然 - 卡片的消失动画:用
MutableTransitionState控制退出动画 - 动画完成后的回调:用
delay等待动画结束再执行真正的删除逻辑
我曾经在这个「延迟删除」上栽过跟头。如果动画时长和 delay 不匹配,要么动画没播完就删了(闪一下),要么删得太慢(卡顿)。后来我改用 LaunchedEffect 监听 transitionState.currentState,当状态变成 Gone 时再触发回调,这样更精确。
知识体系总览
下面这张图总结了本章的核心脉络:
总结一下
Compose 动画的核心思路就是「状态驱动」。你不需要像 View 动画那样写 start()、stop()、setListener(),只需要告诉 Compose「目标状态是什么」,剩下的动画过渡由框架自动完成。
我个人建议你记住三条原则:
- 值变化用 animate*AsState,简单直接,覆盖 80% 的场景
- 出现/消失用 MutableTransitionState,配合 AnimatedVisibility 效果最佳
- 动画状态永远留在 Composable 层,别往 ViewModel 里塞
嗯,其实 Compose 动画还有更高级的玩法,比如用 Animatable 做精细控制、用 updateTransition 管理多属性动画。但今天讲的这些,已经足够你应对日常开发中 90% 的动画需求了。
公众号:蓝海资料掘金营,微信deep3321