Compose 动画进阶:AnimatedVisibility、AnimatedContent、Crossfade
说实话,Compose 的动画 API 是我从 View 体系迁移过来后,最让我「真香」的部分。以前在 XML 里写个淡入淡出,要定义两个动画文件,还要在代码里手动控制 startAnimation。现在?几行代码搞定。
今天咱们聊聊三个最常用的进阶动画组件:AnimatedVisibility、AnimatedContent 和 Crossfade。它们覆盖了日常开发中 80% 的 UI 切换场景。
AnimatedVisibility:让组件优雅地出现和消失
这个 API 是我项目里用得最频繁的。说白了,它就是给 if/else 显示逻辑加了一层动画包装。
核心用法:传入一个 visible 布尔值,Compose 会自动处理 enter/exit 动画。
@Composable
fun AnimatedVisibilityDemo() {
var visible by remember { mutableStateOf(true) }
Column {
Button(onClick = { visible = !visible }) {
Text(if (visible) "隐藏" else "显示")
}
AnimatedVisibility(
visible = visible,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
Card(
modifier = Modifier.padding(8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Text(
text = "这段内容会带着动画出现/消失",
modifier = Modifier.padding(16.dp)
)
}
}
}
}
我个人习惯把 enter 和 exit 参数拆开写,这样可读性更好。你想想看,如果所有动画都挤在一行,后期维护时自己都看不懂。
小技巧:slideInVertically 默认是从底部滑入。如果想从顶部滑入,可以设置 initialOffsetY = { -it }。
AnimatedContent:内容切换的「变形金刚」
这个组件比 AnimatedVisibility 更灵活。它不只是控制显示/隐藏,而是在两个不同的内容之间做过渡动画。
我在项目中遇到过这样一个需求:一个详情页需要根据用户选择的 Tab 切换不同的信息卡片。如果用传统的 if/else,切换会非常生硬。用 AnimatedContent 就丝滑多了。
@Composable
fun AnimatedContentDemo() {
var state by remember { mutableIntStateOf(0) }
Column {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { state = 0 }) { Text("卡片A") }
Button(onClick = { state = 1 }) { Text("卡片B") }
Button(onClick = { state = 2 }) { Text("卡片C") }
}
AnimatedContent(
targetState = state,
transitionSpec = {
// 使用 infix 写法,更简洁
fadeIn() togetherWith fadeOut()
},
label = "contentAnimation"
) { currentState ->
// 根据 currentState 渲染不同内容
when (currentState) {
0 -> CardA()
1 -> CardB()
2 -> CardC()
}
}
}
}
注意:transitionSpec 里的 lambda 返回的是 ContentTransform。我见过有人把 fadeIn 和 fadeOut 写反了,结果新内容淡出、旧内容淡入——完全反过来了。
嗯,这里要注意一点:AnimatedContent 的 targetState 可以是任何类型,不一定是 Int。我习惯用枚举或者 sealed class,这样类型更安全。
Crossfade:最简单的淡入淡出方案
Crossfade 是三个里面最「专一」的——它只做一件事:在两个内容之间交叉淡入淡出。
说白了,就是旧内容慢慢消失的同时,新内容慢慢出现。中间会有一段时间两者半透明叠加。
@Composable
fun CrossfadeDemo() {
var isLogin by remember { mutableStateOf(true) }
Crossfade(
targetState = isLogin,
animationSpec = tween(durationMillis = 500)
) { loggedIn ->
if (loggedIn) {
UserProfile()
} else {
LoginButton()
}
}
}
我曾经在一个登录/登出的切换场景里用过 Crossfade。效果比直接跳转好太多——用户不会感觉页面「闪」了一下,而是平滑过渡。
什么时候用 Crossfade?当你只有两个状态,而且希望过渡尽可能平滑时。比如:登录态/非登录态、白天模式/夜间模式、展开/收起。
三个 API 的对比总结
| API | 核心能力 | 适用场景 | 动画控制粒度 |
|---|---|---|---|
| AnimatedVisibility | 控制单个组件的出现/消失 | 列表项展开、提示条显示、加载状态 | 细(可分别控制 enter/exit) |
| AnimatedContent | 在不同内容之间切换 | Tab 切换、页面转场、状态机 | 中(可自定义过渡组合) |
| Crossfade | 两个内容交叉淡入淡出 | 登录/登出、主题切换、简单状态切换 | 粗(只有淡入淡出) |
避坑指南
这几个 API 用起来虽然爽,但坑也不少。我踩过的几个,分享给你:
- 动画不执行?检查一下
key是否正确。Compose 通过 key 来识别内容是否变化。如果 key 没变,动画不会触发。 - 动画卡顿?看看是不是在动画内容里做了耗时操作。比如在
AnimatedContent的 content lambda 里加载网络图片——这会导致动画掉帧。 - 动画方向反了?检查
slideIn和slideOut的偏移量设置。我曾经把slideOutVertically的targetOffsetY设成了正数,结果组件往下滑出去了——其实应该用负数。
我的建议:刚开始用的时候,先只加 fadeIn/fadeOut。等效果跑通了,再叠加 slide 或 scale。一口吃不成胖子,动画也是。
好了,这三个 API 的核心用法就这些。你想想看,以前在 View 体系里要实现类似效果,得写多少代码?现在 Compose 把这些复杂度都封装好了,我们只需要关注业务逻辑和动画效果本身。
下次遇到 UI 切换的场景,别再用 if/else 硬切了——试试这几个动画组件,用户体验会提升一个档次。