
假设你正在使用 Jetpack Compose 构建一个屏幕界面,并且希望实现某些内容的展开或收起效果 ------ 比如常见问题解答部分、下拉面板或筛选菜单。
你可能会想:
我要如何实现平滑的过渡效果呢?
好消息是:Jetpack Compose 让这个需求变得相当容易实现。
该篇文章,让我为大家详细介绍处理展开/收起动画的所有实用方法 ------ 每种方法都适用于不同的用例。
高度的平滑增长
假设你有一个方块,当用户点击它时,下方会显示一些内容,而你希望整个过程能够自然展开,不出现跳动。
对于这种情况,Jetpack Compose 有一个非常实用的修饰符:animateContentSize()。
下面是一个简单的示例:
Kotlin
@Composable
fun ExpandableBox() {
var expanded by remember { mutableStateOf(false) }
Column(modifier = Modifier
.clickable { expanded = !expanded }
.background(Color.DarkGray)
.padding(16.dp)
.animateContentSize() // 高度变化产生动画
) {
Text("Q: What is Compose?", color = Color.White)
if (expanded) {
Text(
"A: It's a modern UI toolkit for Android that replaces XML.", color = Color.LightGray, modifier = Modifier.padding(top = 8.dp)
)
}
}
}

只需要这一个修饰符------就这样,每当高度发生变化时,就能实现平滑过渡。
当然,它的工作原理实际上是这样的:
当你将 animateContentSize 应用到一个 Composable 上时,Compose 会监听该组件内容宽高的变化。如果内容尺寸发生变化(例如,因为内部文本变长、子元素增删等),它会自动在旧尺寸和新尺寸之间执行一个平滑的过渡动画。
淡入淡出与滑动
如果你不只是想增加高度,还希望内容在可见时淡入、滑入或放大。
这就来到了 AnimatedVisibility() 的用武之地。
假设你有一个用于显示/隐藏面板的切换按钮。以下是如何巧妙地实现动画效果:
Kotlin
@Composable
fun ToggleInfoPanel() {
var show by remember { mutableStateOf(true) }
Column(modifier = Modifier.padding(16.dp)) {
Button(onClick = { show = !show }) {
Text(if (show) "Hide Info" else "Show Info")
}
AnimatedVisibility(
visible = show, enter = fadeIn() + slideInVertically(), exit = fadeOut() + slideOutVertically()
) {
Box(
Modifier
.fillMaxWidth()
.background(Color.Magenta)
.padding(16.dp)
) {
Text("This is a toggleable info section", color = Color.White)
}
}
}
}

在这里,你控制的是它如何出现或消失,而不仅仅是它是否显示。
AnimatedVisibility() 包裹另一个 Composable,并根据其 visible 的值(true/false)来决定是否显示内部内容。当 visible 状态改变时,它会自动执行指定的进入动画或退出动画。
垂直展开/收起的滑动面板
假设你正在构建一个筛选下拉菜单或手风琴式部分,并且希望它在展开时从顶部向下滑动打开,在收起时向上收缩回去。
你可以在 AnimatedVisibility() 中使用 expandVertically() 和 shrinkVertically():
Kotlin
@Composable
fun FilterDropdown() {
var expanded by remember { mutableStateOf(false) }
Column(modifier = Modifier.padding(16.dp)) {
Text("Filters", modifier = Modifier
.clickable { expanded = !expanded }
.background(Color.Gray)
.padding(12.dp), color = Color.White)
AnimatedVisibility(
visible = expanded, enter = expandVertically(), exit = shrinkVertically()
) {
Column(
Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(16.dp)
) {
Text("Sort by Price")
Text("Sort by Rating")
Text("Sort by Availability")
}
}
}
}

流畅的垂直滑动效果,非常适合可展开的面板。
多个元素的动画
如果你想让高度、背景颜色和透明度在展开时同步进行动画处理,该怎么做呢?
轮到 updateTransition() 大显身手了。它的使用稍微复杂一些,但能让你完全掌控动画效果:
kotlin
@Composable
fun FancyExpandBox() {
var expanded by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = expanded, label = "transition")
val txtSize by transition.animateInt(label = "text") {
if (it) 24 else 14
}
val height by transition.animateDp(label = "height") {
if (it) 200.dp else 60.dp
}
val bgColor by transition.animateColor(label = "color") {
if (it) Color.Blue else Color.Gray
}
val alpha by transition.animateFloat(label = "alpha") {
if (it) 1f else 0.5f
}
Box(
modifier = Modifier
.clickable { expanded = !expanded }
.fillMaxWidth()
.height(height)
.background(bgColor.copy(alpha = alpha))
.padding(16.dp)
) {
Text("Tap to expand or collapse", color = Color.White, fontSize = txtSize.sp)
}
}

该方法需要添加 androidx.compose.animation:animation-core 依赖。
你可以在这里对任何属性进行动画处理 ------ 边距、形状、文字大小 ------ 它们都会一起过渡变化。
updateTransition 是一个状态驱动的动画协调器。它通过观察一个关键状态的变化,自动管理一组相关视觉属性的动画,将它们从旧状态的目标值流畅地过渡到新状态的目标值,非常适合处理复杂的、多步骤的 UI 状态切换动画。
为布局权重做动画
如果你有两个面板并排放置或垂直堆叠,并且希望一个面板变大而另一个面板变小 ------ 那就对权重进行动画处理!
Kotlin
@Composable
fun WeightAnimationDemo() {
var expanded by remember { mutableStateOf(false) }
val weight by animateFloatAsState(targetValue = if (expanded) 0.8f else 0.3f)
Column(modifier = Modifier.height(200.dp)) {
Box(
modifier = Modifier
.weight(weight)
.fillMaxWidth()
.background(Color.Blue)
.clickable { expanded = !expanded })
Box(
modifier = Modifier
.weight(1f - weight)
.fillMaxWidth()
.background(Color.DarkGray)
)
}
}

点击顶部框会平滑地调整两个部分的大小。
总结
- 如果你的视图只是大小发生增长或缩小 ------
animateContentSize()。 - 如果你想要淡入、滑动或缩放动画 ------
AnimatedVisibility()。 - 对于更高级的布局变化或组合效果 ------
updateTransition()。 - 想要使用布局权重调整大小? ------
animateFloatAsState()。
一旦你熟悉了这些工具,你会发现在 Compose 中构建大多数展开/收起的用户界面变得超级直观 ------ 而且它们比使用 XML 时更加流畅,更加直观,更加方便。