19、Compose动画:animate*AsState、AnimatedVisibility、AnimatedContent、Transition、实战:流畅动画
动画,在移动端开发里,从来都不是锦上添花的东西。它直接影响用户对App「流畅感」的认知。我见过太多应用,功能没问题,但就是觉得「硬邦邦的」,点个按钮、切个页面,生硬得像在翻纸质书。嗯,Compose 的动画系统,就是为了解决这个问题而生的。
今天这一章,我们集中火力,把 Compose 里最核心的动画 API 过一遍。我会结合我自己的踩坑经历,告诉你哪些地方容易翻车,哪些技巧能让动画真正「丝滑」。
核心观点:Compose 动画的核心思想是「状态驱动」。你不需要手动控制动画的开始和结束,只需要告诉 Compose:「这个值要变了」,剩下的插值、刷新、重绘,框架全包了。
19.1 animate*AsState:最简单的单值动画
如果你只需要让一个值「动起来」,比如透明度从0到1,或者一个按钮的尺寸从小变大,那 animate*AsState 就是你的首选。说白了,它就是一个带缓动效果的 State。
举个例子,我想让一个浮点数在 0f 和 1f 之间平滑过渡:
@Composable
fun FadeInDemo() {
var visible by remember { mutableStateOf(false) }
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = tween(durationMillis = 500)
)
Box(
modifier = Modifier
.size(100.dp)
.graphicsLayer(alpha = alpha)
.background(Color.Blue)
.clickable { visible = !visible }
)
}
你看,代码量非常少。我只需要定义目标值,剩下的交给 animateFloatAsState。它内部会帮你做差值计算,并且只在值变化时才触发重组。这一点很重要——不会因为动画的每一帧都触发整个 Composable 重组,性能有保障。
我的习惯:对于简单的颜色、尺寸、偏移量动画,我几乎只用 animate*AsState。它足够轻量,而且不容易出错。但要注意,如果你需要同时控制多个属性(比如位置+大小+颜色),那它就不太够用了,得用后面的 Transition。
19.2 AnimatedVisibility:显示与隐藏的优雅方案
这个 API 我太喜欢了。以前在 View 系统里,要做个淡入淡出或者滑动出现,得写一堆 Animation 和 AnimationListener。在 Compose 里,一行代码搞定。
@Composable
fun AnimatedVisibilityDemo() {
var visible by remember { mutableStateOf(true) }
Column {
Button(onClick = { visible = !visible }) {
Text("切换显示")
}
AnimatedVisibility(
visible = visible,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
Text("我是会动的内容", fontSize = 24.sp)
}
}
}
这里有个细节:enter 和 exit 参数支持「组合动画」。你可以用 + 运算符把多个动画效果叠加在一起。比如上面这个例子,就是「从上方滑入 + 淡入」同时发生。
我曾经踩过的坑:如果你在 AnimatedVisibility 内部使用了 LazyColumn 或者 LazyRow,记得给它们设置 animateItemPlacement() 修饰符。否则,当列表项增删时,其他项会「瞬移」而不是平滑移动。这个坑我花了一下午才定位到。
19.3 AnimatedContent:内容切换的「变形金刚」
当你的 UI 需要在两种完全不同的布局之间切换时,AnimatedContent 就派上用场了。比如一个详情页,从「加载中」状态切换到「展示数据」状态,或者一个卡片从「折叠」变成「展开」。
@Composable
fun AnimatedContentDemo() {
var state by remember { mutableStateOf("loading") }
AnimatedContent(targetState = state) { targetState ->
when (targetState) {
"loading" -> LoadingView()
"content" -> ContentView()
"error" -> ErrorView()
}
}
}
它的核心思想是:根据当前状态,决定显示哪个 Composable。切换时,旧的 Composable 执行退出动画,新的执行进入动画。而且,它默认会尝试「对齐」两个布局中相同 key 的元素,实现更自然的过渡。
我个人觉得,AnimatedContent 最适合的场景是「状态机驱动的 UI」。比如网络请求的三个状态:加载中、成功、失败。用这个 API 做切换,用户体验会好很多。
19.4 Transition:多值动画的「指挥官」
当你需要同时控制多个属性,并且这些属性之间有联动关系时,Transition 就是最佳选择。它允许你定义一个「动画状态机」,然后为每个状态指定一组目标值。
@Composable
fun TransitionDemo() {
var selected by remember { mutableStateOf(false) }
val transition = updateTransition(
targetState = selected,
label = "cardTransition"
)
val color by transition.animateColor(
transitionSpec = { tween(300) }
) { isSelected ->
if (isSelected) Color.Blue else Color.Gray
}
val size by transition.animateDp(
transitionSpec = { spring(dampingRatio = 0.6f) }
) { isSelected ->
if (isSelected) 120.dp else 80.dp
}
Box(
modifier = Modifier
.size(size)
.background(color)
.clickable { selected = !selected }
)
}
你看,Transition 把多个动画「绑定」到了同一个状态上。当 selected 变化时,颜色和尺寸会同时开始动画。而且,你可以为每个属性指定不同的 animationSpec。比如颜色用线性插值,尺寸用弹簧效果。
关键区别:animate*AsState 是「单值单状态」,Transition 是「多值单状态」。如果你需要管理多个状态之间的切换(比如 A→B→C),那就要考虑用 AnimatedContent 或者自定义 Transition 状态机了。
19.5 实战:打造流畅动画的四个原则
理论说完了,我们来点实际的。我在项目中总结了几条让动画「流畅」的经验,分享给你。
| 原则 | 说明 | 我的做法 |
|---|---|---|
| 1. 避免过度重组 | 动画每一帧都触发重组,如果 Composable 太大,会卡顿 | 把动画部分抽成独立 Composable,用 derivedStateOf 减少计算 |
| 2. 选择合适的 Spec | tween 适合线性运动,spring 适合弹性效果 |
按钮点击用 spring,页面切换用 tween |
3. 使用 graphicsLayer |
修改位置、大小、透明度时,优先用 graphicsLayer 而不是 offset 或 size |
因为 graphicsLayer 只触发绘制阶段,不触发布局 |
| 4. 测试低端设备 | 高端机动画流畅,不代表低端机也流畅 | 我习惯在模拟器上把 CPU 降频到 50% 测试 |
嗯,这里重点说一下第二条。很多人觉得 spring 效果很酷,就到处用。但你要知道,spring 动画的时长是不固定的,它取决于阻尼比和刚度。如果你在列表滚动时用 spring,可能会导致动画「追不上」用户的操作。我一般只在「一次性交互」中使用 spring,比如点赞按钮的弹跳效果。
一个小技巧:如果你想让动画看起来更「跟手」,可以试试 animateDecay 配合 fling 效果。比如列表滑动到边缘时的回弹动画。这个 API 比较冷门,但效果非常好。
19.6 避坑指南:动画性能的隐形杀手
最后,说几个我实际项目中踩过的坑。希望能帮你少走弯路。
- 不要在动画中做大量计算:比如在
animateFloatAsState的onFinish回调里做网络请求或者数据库操作。这会导致动画帧率骤降。 - 注意
remember的作用域:如果你把动画状态定义在父 Composable 中,而父 Composable 频繁重组,动画会被打断。我习惯把动画状态定义在「最靠近使用它的地方」。 - 避免嵌套
AnimatedVisibility:两个AnimatedVisibility嵌套时,内部的动画可能会被外部的「吞掉」。如果必须嵌套,考虑用Transition替代。 - 动画时长不要太长:我个人认为,移动端动画超过 500ms 就会让用户觉得「慢」。除非是引导页或者特殊效果,否则尽量控制在 300ms 以内。
好了,这一章的内容就到这里。Compose 动画其实不难,难的是「什么时候用哪个 API」。记住:简单场景用 animate*AsState,显示隐藏用 AnimatedVisibility,内容切换用 AnimatedContent,多值联动用 Transition。按照这个思路选,基本不会错。
公众号:蓝海资料掘金营,微信deep3321