Android 入门教程
Compose 动画
本教程共 100 篇 · 第 51 篇 · 更新于 2026-07-28 · 约 8 分钟阅读
AndroidAndroid 入门教程Jetpack Compose动画animateAsStateAnimatedVisibility过渡动画
51. Compose 动画
本节目标:掌握 Compose 的几种动画 API,学会用 animateAsState 做属性动画、AnimatedVisibility 控制显隐、Crossfade 淡入淡出、InfiniteTransition 做循环动画。
动画为什么简单了
传统 View 做动画要写 ObjectAnimator、ValueAnimator、XML 动画文件,还要手动管理起止值。Compose 把动画做成了状态—数据变了,框架自动算出中间帧,你只管描述「从什么到什么」。
animateAsState 属性动画
最基础的动画 API。给它一个目标值,它自动从当前值过渡到目标值:
@Composable
fun ColorAnimation() {
var expanded by remember { mutableStateOf(false) }
val backgroundColor by animateColorAsState(
targetValue = if (expanded) Color.Blue else Color.Gray,
animationSpec = tween(durationMillis = 500),
label = "bgColor"
)
Box(
modifier = Modifier
.size(100.dp)
.background(backgroundColor)
.clickable { expanded = !expanded }
)
}
数据类型不同用不同的函数:
| 函数 | 动画类型 |
|---|---|
animateColorAsState | 颜色 |
animateFloatAsState | 浮点数 |
animateDpAsState | 尺寸(dp) |
animateIntAsState | 整数 |
animateSizeAsState | 尺寸 |
AnimationSpec 动画规格
// tween:匀速过渡,可加缓动
tween(durationMillis = 300, easing = FastOutSlowInEasing)
// spring:弹簧效果(默认)
spring(dampingRatio = Spring.DampingRatioMediumBouncy)
// keyframes:关键帧
keyframes {
durationMillis = 1000
0.0f at 0
0.5f at 500 with FastOutLinearInEasing
1.0f at 1000
}
Tip
spring()是 Compose 的默认动画规格,有自然的物理感。弹簧动画不会突然开始和结束,看起来比匀速更舒服。
AnimatedVisibility 显隐动画
@Composable
fun VisibilityAnimation() {
var visible by remember { mutableStateOf(true) }
Column {
Button(onClick = { visible = !visible }) {
Text(if (visible) "隐藏" else "显示")
}
AnimatedVisibility(visible = visible) {
Card(modifier = Modifier.padding(top = 8.dp)) {
Text("我会淡入淡出", modifier = Modifier.padding(16.dp))
}
}
}
}
自定义进出场动画
AnimatedVisibility(
visible = visible,
enter = fadeIn() + slideInVertically { it },
exit = fadeOut() + slideOutVertically { -it }
) {
Text("滑入滑出 + 淡入淡出")
}
用 + 组合多种动画效果。slideInVertically { it } 中 it 是组件高度,从下方滑入。
animateContentSize 尺寸动画
内容变化时高度自动平滑过渡:
@Composable
fun ExpandableCard() {
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.animateContentSize() // 尺寸变化自动动画
.clickable { expanded = !expanded }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("标题", fontWeight = FontWeight.Bold)
if (expanded) {
Spacer(Modifier.height(8.dp))
Text("这是展开后才显示的详细内容...")
}
}
}
}
Note
animateContentSize()加在 Modifier 链上,组件内容变化导致尺寸变化时会自动平滑过渡。非常适合折叠/展开卡片。
Crossfade 交叉淡入淡出
切换不同内容时做淡入淡出:
@Composable
fun TabSwitch() {
var tab by remember { mutableStateOf(0) }
val screens = listOf("首页", "搜索", "我的")
Column {
Row {
screens.forEachIndexed { index, screen ->
TextButton(onClick = { tab = index }) {
Text(screen)
}
}
}
Crossfade(targetState = tab, label = "crossfade") { current ->
Text(
"当前页:${screens[current]}",
modifier = Modifier.padding(16.dp)
)
}
}
}
updateTransition 过渡动画
一个状态变化同时驱动多个动画:
@Composable
fun MultiAnimation() {
var selected by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = selected, label = "select")
val borderColor by transition.animateColor(label = "border") { isSelected ->
if (isSelected) Color.Blue else Color.Gray
}
val borderWidth by transition.animateDp(label = "width") { isSelected ->
if (isSelected) 3.dp else 1.dp
}
val padding by transition.animateDp(label = "padding") { isSelected ->
if (isSelected) 16.dp else 8.dp
}
Box(
modifier = Modifier
.size(120.dp)
.border(borderWidth, borderColor, RoundedCornerShape(8.dp))
.padding(padding)
.clickable { selected = !selected }
)
}
一个 selected 状态变化,边框颜色、边框宽度、内边距三个属性同时动画。它们共享同一个过渡时间线,协调一致。
rememberInfiniteTransition 无限循环
做加载动画、呼吸效果这类不停重复的动画:
@Composable
fun PulsingHeart() {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(800, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse // 来回反复
),
label = "scale"
)
Icon(
Icons.Default.Favorite,
contentDescription = "心跳",
modifier = Modifier
.size(48.dp)
.scale(scale),
tint = Color.Red
)
}
| RepeatMode | 效果 |
|---|---|
Restart | 每次从头开始 |
Reverse | 来回反复 |
AnimatedContent 内容切换动画
@Composable
fun NumberCounter() {
var count by remember { mutableStateOf(0) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
AnimatedContent(
targetState = count,
transitionSpec = {
slideInVertically { it } togetherWith slideOutVertically { -it }
},
label = "counter"
) { target ->
Text("$target", fontSize = 48.sp)
}
Button(onClick = { count++ }) { Text("+1") }
}
}
togetherWith 把进出场动画组合起来。每次 count 变化,旧数字滑出、新数字滑入。
Warning
AnimatedContent默认会同时显示新旧两个内容,注意布局空间。如果用尺寸动画,可能需要加Modifier.animateContentSize()配合。
动画选择指南
| 需求 | 推荐方案 |
|---|---|
| 单个属性变化 | animateXxxAsState |
| 显示/隐藏组件 | AnimatedVisibility |
| 内容尺寸变化 | animateContentSize() |
| 切换不同内容 | AnimatedContent / Crossfade |
| 一个状态驱动多动画 | updateTransition |
| 无限循环动画 | rememberInfiniteTransition |
小结
Compose 动画的核心思路是「描述状态,框架自动算中间帧」。animateAsState 做单属性动画,AnimatedVisibility 控显隐,animateContentSize 处理尺寸变化,AnimatedContent 切内容,InfiniteTransition 做循环。比传统 View 动画简单太多。下一节学习导航。