21、Compose动画之布局动画:AnimatedVisibility的布局模式、Modifier.animateContentSize
布局动画,说白了就是让组件在「出现、消失、大小变化」这些场景下动起来。在 View 时代,我们得靠 LayoutTransition 或者自己写 ValueAnimator 监听布局变化,代码量不小。到了 Compose,事情变得简单多了——两个核心 API 就能覆盖绝大多数场景:AnimatedVisibility 和 Modifier.animateContentSize。
我个人习惯把布局动画分成两类:「进出动画」和「尺寸变化动画」。前者管的是组件从无到有、从有到无的过程,后者管的是组件内容变化导致自身大小改变时的平滑过渡。今天我们就来把这两块彻底讲透。
21.1 AnimatedVisibility:控制组件的进出场
AnimatedVisibility 是 Compose 中最常用的布局动画 API。它的用法非常直观——你只需要告诉它「当前是否可见」,剩下的动画细节由它自动处理。
@Composable
fun DemoAnimatedVisibility() {
var visible by remember { mutableStateOf(true) }
Column {
Button(onClick = { visible = !visible }) {
Text(if (visible) "隐藏" else "显示")
}
AnimatedVisibility(visible = visible) {
Text("我是会动的内容")
}
}
}
嗯,就这么简单。但如果你以为它只有这点本事,那就太小看它了。AnimatedVisibility 提供了三种布局模式,这才是真正需要理解的地方。
21.2 三种布局模式:选对模式,事半功倍
AnimatedVisibility 的布局模式通过 enter 和 exit 参数来控制。我把它归纳为三种典型模式:
| 模式 | 适用场景 | 典型 enter/exit 配置 |
|---|---|---|
| 淡入淡出 | 提示信息、Toast、状态标签 | fadeIn() / fadeOut() |
| 滑动进入 | 底部弹出面板、侧边栏、下拉菜单 | slideInVertically() / slideOutVertically() |
| 缩放进入 | 弹窗、气泡提示、头像展开 | scaleIn() / scaleOut() |
我在项目中遇到过这样一个场景:一个列表项需要展开显示详情。如果用默认的 fadeIn,效果很平淡。后来改成 expandVertically() + fadeIn() 的组合,展开时内容从顶部向下「生长」出来,视觉上舒服多了。
AnimatedVisibility(
visible = expanded,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
// 详情内容
DetailContent()
}
slideInVertically() + fadeIn() 会让组件一边滑入一边淡入,效果更丰富。
21.3 Modifier.animateContentSize:让尺寸变化「丝般顺滑」
这个 Modifier 是我个人非常喜欢的一个 API。它解决了一个很常见的痛点:当组件的内容发生变化时,组件的尺寸会「跳变」。比如一个 Text 组件,文字从短变长,如果没有动画,布局会瞬间撑开,看起来很突兀。
@Composable
fun DemoAnimateContentSize() {
var expanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.animateContentSize()
.clickable { expanded = !expanded }
) {
Text("点击我展开/收起")
if (expanded) {
Text("这里是展开后的详细内容,会撑大整个 Column")
Text("而且这个过程是平滑动画的")
}
}
}
你想想看,如果没有 animateContentSize(),这个 Column 会在 expanded 变化时瞬间跳变。加上之后,它会自动在旧尺寸和新尺寸之间做插值动画。底层原理其实是对 IntSize 做了动画,然后应用到布局约束上。
animateContentSize(animationSpec = tween(500))。
21.4 实战对比:两种 API 的典型场景
为了帮你更清晰地理解两者的区别,我画了一张对比图:
21.5 避坑指南:我曾经踩过的坑
讲几个我实际开发中遇到的坑,希望能帮你少走弯路。
- AnimatedVisibility 嵌套问题:如果你在 AnimatedVisibility 内部再嵌套一个 AnimatedVisibility,动画可能会互相干扰。我曾经在列表项的展开详情里又套了一个子面板的动画,结果动画时序全乱了。解决方案是:尽量保持单层动画,或者用
animateContentSize替代内层进出动画。 - animateContentSize 与 LazyColumn 的冲突:在 LazyColumn 中使用 animateContentSize 要小心。因为 LazyColumn 会复用 item,动画状态可能会错乱。我建议只在确定 item 不会被频繁回收的场景下使用,或者配合
key参数使用。 - 动画时长与用户感知:默认的 300ms 在大多数场景下是合适的。但如果你做的是底部弹出面板,我个人习惯把时长调到 400-500ms,配合 OvershootInterpolator,效果会更「弹」一些,用户会觉得更自然。
AnimationSpec 的 repeatable() 包装,让动画循环播放,方便观察效果。上线前再改回一次性动画。
21.6 小结
布局动画是 Compose 动画体系中最「接地气」的部分。AnimatedVisibility 管的是「有没有」,animateContentSize 管的是「变多大」。两者配合使用,基本能覆盖 90% 的布局变化场景。
我个人建议:能用 animateContentSize 解决的,就不要用 AnimatedVisibility。因为尺寸变化动画更轻量,性能开销更小。只有当你需要控制「出现/消失」的完整过程(包括透明度、位移、缩放等组合效果)时,才用 AnimatedVisibility。
嗯,这一章的内容就到这里。记住一句话:好的布局动画,用户不会注意到它;不好的布局动画,用户会一直注意到它。