14、高级动画:AnimatedVisibility、AnimatedContent、Crossfade
动画这东西,说实话,是 UI 开发的灵魂。没有动画的界面,就像一杯白开水——解渴,但没滋味。Compose 的声明式动画,跟传统 View 系统比起来,简直是降维打击。你不需要再跟 Animator、AnimationSet 这些老古董打交道了。
今天咱们聊三个高级动画 API:AnimatedVisibility、AnimatedContent、Crossfade。这三个家伙,基本覆盖了日常开发中 80% 的动画场景。我个人习惯把它们叫做「动画三剑客」。
核心要点:这三个 API 都是基于 Compose 的声明式思想设计的。你只需要描述「状态变化后的 UI 应该是什么样」,动画的中间过程,框架帮你搞定。
14.1 AnimatedVisibility:让组件优雅地出现和消失
先聊聊 AnimatedVisibility。这玩意儿,说白了就是给组件的显示/隐藏加个过渡动画。你想想看,一个列表项突然冒出来或者突然消失,用户会不会觉得突兀?
我记得有一次做电商项目,购物车列表的删除操作,一开始直接调用了 removeAt,结果测试妹子反馈说「东西突然就没了,我怀疑是不是点错了」。后来加上 AnimatedVisibility,用户体验瞬间提升了一个档次。
@Composable
fun AnimatedVisibilityDemo() {
var visible by remember { mutableStateOf(true) }
Column(modifier = Modifier.padding(16.dp)) {
Button(onClick = { visible = !visible }) {
Text(if (visible) "隐藏" else "显示")
}
Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility(
visible = visible,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
Card(
modifier = Modifier.fillMaxWidth().height(100.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFE3F2FD))
) {
Text(
text = "我是一个会滑入滑出的卡片",
modifier = Modifier.padding(16.dp)
)
}
}
}
}
小技巧:enter 和 exit 参数支持组合多个动画效果。用 + 运算符连接即可。我个人习惯用 slideInVertically() + fadeIn() 的组合,既有滑动又有淡入,视觉上很自然。
这里有个坑,我曾经踩过——AnimatedVisibility 默认会占用布局空间,即使组件不可见。如果你希望组件完全消失(不占位),需要配合 Modifier.animateContentSize() 或者使用 AnimatedVisibility(visible, enter, exit, initiallyVisible = false) 来控制初始状态。
14.2 AnimatedContent:内容切换的优雅方案
接下来是 AnimatedContent。这个 API 解决的是「同一个位置,不同内容之间的切换动画」。比如 Tab 切换、轮播图、状态机切换等场景。
为什么说它强大?因为它不仅仅是淡入淡出,而是可以根据目标状态,自动计算过渡动画。你想想看,如果两个内容的尺寸不一样,它还能自动处理尺寸变化。
@Composable
fun AnimatedContentDemo() {
var page by remember { mutableIntStateOf(0) }
val pages = listOf("首页", "发现", "我的")
Column(modifier = Modifier.padding(16.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
pages.forEachIndexed { index, title ->
FilterChip(
selected = page == index,
onClick = { page = index },
label = { Text(title) }
)
}
}
Spacer(modifier = Modifier.height(16.dp))
AnimatedContent(
targetState = page,
transitionSpec = {
if (targetState > initialState) {
slideInHorizontally { width -> width } + fadeIn() togetherWith
slideOutHorizontally { width -> -width } + fadeOut()
} else {
slideInHorizontally { width -> -width } + fadeIn() togetherWith
slideOutHorizontally { width -> width } + fadeOut()
}
},
label = "page_transition"
) { currentPage ->
// 根据 currentPage 渲染不同内容
when (currentPage) {
0 -> PageContent("首页内容", Color(0xFFE3F2FD))
1 -> PageContent("发现内容", Color(0xFFF3E5F5))
2 -> PageContent("我的内容", Color(0xFFE8F5E9))
}
}
}
}
@Composable
fun PageContent(text: String, bgColor: Color) {
Card(
modifier = Modifier.fillMaxWidth().height(200.dp),
colors = CardDefaults.cardColors(containerColor = bgColor)
) {
Box(contentAlignment = Alignment.Center) {
Text(text = text, fontSize = 20.sp)
}
}
}
注意:AnimatedContent 的 transitionSpec 中,togetherWith 是关键。它把入场动画和退场动画「绑定」在一起,让新旧内容同时运动。如果你用 using,则是串行执行,效果完全不同。
嗯,这里要特别说一下 SizeTransform。如果你切换的内容尺寸不同,AnimatedContent 默认会裁剪内容。想要平滑过渡尺寸?加上 sizeTransform = SizeTransform(clip = false) 即可。我在做个人中心页面时,头像和列表切换就用了这个,效果很丝滑。
14.3 Crossfade:最简单的淡入淡出
最后聊聊 Crossfade。这玩意儿,说白了就是 AnimatedContent 的简化版。它只做一件事:在两个内容之间做淡入淡出。
什么时候用?当你不需要滑动、缩放等复杂效果,只需要一个简单的交叉淡入淡出时。比如图片切换、主题切换、加载状态切换等。
@Composable
fun CrossfadeDemo() {
var isDark by remember { mutableStateOf(false) }
Column(modifier = Modifier.padding(16.dp)) {
Button(onClick = { isDark = !isDark }) {
Text("切换主题")
}
Spacer(modifier = Modifier.height(16.dp))
Crossfade(
targetState = isDark,
animationSpec = tween(durationMillis = 500),
label = "theme_crossfade"
) { darkMode ->
Card(
modifier = Modifier.fillMaxWidth().height(150.dp),
colors = CardDefaults.cardColors(
containerColor = if (darkMode) Color.DarkGray else Color.White
)
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = if (darkMode) "深色模式" else "浅色模式",
color = if (darkMode) Color.White else Color.Black,
fontSize = 18.sp
)
}
}
}
}
}
对比总结:
| API | 适用场景 | 动画类型 | 复杂度 |
|---|---|---|---|
| AnimatedVisibility | 组件出现/消失 | 入场/退场 | 低 |
| AnimatedContent | 内容切换(Tab、轮播) | 自定义过渡 | 中 |
| Crossfade | 简单淡入淡出 | 交叉淡入淡出 | 极低 |
我曾经在图片加载库中用过 Crossfade。当图片从占位图切换到实际图片时,用 Crossfade 包裹,效果比直接替换好太多。用户几乎感觉不到「闪一下」的问题。
性能建议:这三个 API 都是 Compose 内置的,性能经过优化。但要注意,不要在 AnimatedVisibility 或 AnimatedContent 内部放太多重型组件。动画过程中,Compose 会频繁重组。如果内容太重,掉帧是必然的。
好了,关于这三个高级动画 API,咱们就聊到这儿。记住一个原则:能用声明式动画解决的问题,就别手写 Animator。Compose 的动画系统,说白了就是让你用更少的代码,做更好的效果。
公众号:蓝海资料掘金营,微信deep3321