简介
在第一部分中,我们掌握了 animate*AsState 的声明式简洁性。但当 UI 的结构发生变化时------比如一个组件突然消失,或者容器的大小需要调整------简单的属性动画就不够用了。
本章我们将深入探讨布局动画。这些工具能让你的 UI 在组件出现、消失或调整大小时,表现得像水流一样顺滑且富有逻辑。
1. 进入与退出:AnimatedVisibility
直接使用 if (visible) 切换组件会导致 UI "瞬间弹出或消失",这在视觉上非常生硬。AnimatedVisibility 是 Compose 专门为这种"存在性过渡"设计的容器。
核心机制:
- 状态驱动 :基于
visible: Boolean自动触发。 - 布局感知 :当组件退出动画结束后,它会自动从组合树(Composition Tree)中移除,不再占用布局空间。
示例:平滑通知栏
Kotlin
@Composable
fun AnimatedNotification(isVisible: Boolean, message: String) {
AnimatedVisibility(
visible = isVisible,
// 组合动画:滑入 + 淡入
enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(initialAlpha = 0.3f),
// 组合动画:滑出 + 淡出
exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut()
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.error,
elevation = 4.dp
) {
Text(text = message, modifier = Modifier.padding(16.dp), color = Color.White)
}
}
}
2. 容器大小的魔法:Modifier.animateContentSize()
当卡片内的文本从"摘要"变为"全文"时,高度的剧烈跳变会干扰用户。animateContentSize() 告诉布局系统:"当我的子组件改变我的大小时,请通过动画平滑过渡,不要跳变。"
场景:可展开的详情卡片
Kotlin
@Composable
fun ExpandableCard(text: String) {
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
// 核心:当内部内容变化时,自动动画化 Card 的大小
.animateContentSize(animationSpec = spring(Spring.DampingRatioLowBouncy))
.clickable { expanded = !expanded }
) {
Column(Modifier.padding(16.dp)) {
Text(
text = text,
maxLines = if (expanded) Int.MAX_VALUE else 2,
overflow = TextOverflow.Ellipsis
)
Text(
text = if (expanded) "收起" else "阅读更多",
style = MaterialTheme.typography.button,
color = MaterialTheme.colors.primary
)
}
}
}
注意 :
animateContentSize()的顺序很重要。通常将其放在 Modifier 链的较前位置,确保容器整体受到影响。
3. 列表项的灵动:Modifier.animateItem()
在 LazyColumn 中删除或重新排序项时,剩余项如果不动画化,会显得杂乱无章。
关键规则:
- 必须提供 Key :在
items(items, key = { it.id })中明确指定唯一 ID。 - API 更新 :在 Compose 1.7+ 中,建议使用
Modifier.animateItem()替代旧的animateItemPlacement()。它现在支持进入、退出、以及位置改变的全套动画。
Kotlin
@Composable
fun TaskList(tasks: List<Task>) {
LazyColumn {
items(tasks, key = { it.id }) { task ->
TaskRow(
modifier = Modifier
.fillMaxWidth()
.animateItem( // 自动处理添加、删除和重新排序动画
fadeInSpec = tween(300),
placementSpec = spring(stiffness = Spring.StiffnessLow),
fadeOutSpec = fadeOut()
),
task = task
)
}
}
}
💡 核心知识点对比 (FAQ)
| 场景 | 推荐工具 | 为什么不直接用属性动画? |
|---|---|---|
| 组件显示/隐藏 | AnimatedVisibility |
它能管理生命周期,在动画结束后真正移除组件,释放布局空间。 |
| 容器尺寸变化 | animateContentSize() |
手动测量并用 animateDpAsState 改变高度非常复杂且性能较差。 |
| 列表重排 | animateItem() |
在 Lazy 布局中,它能自动处理 Item 的相对偏移和淡入淡出。 |
总结
通过掌握这三个工具,你的 UI 结构变化将不再生硬:
- 使用
AnimatedVisibility优雅地引入/移除组件。 - 使用
animateContentSize()让容器尺寸平滑呼吸。 - 使用
animateItem()赋予动态列表生命力。