首页 / Android 入门教程 / Compose 动画

Android 入门教程

Compose 动画

本教程共 100 篇 · 第 51 篇 · 更新于 2026-07-28 · 约 8 分钟阅读

AndroidAndroid 入门教程Jetpack Compose动画animateAsStateAnimatedVisibility过渡动画

51. Compose 动画

本节目标:掌握 Compose 的几种动画 API,学会用 animateAsState 做属性动画、AnimatedVisibility 控制显隐、Crossfade 淡入淡出、InfiniteTransition 做循环动画。

动画为什么简单了

传统 View 做动画要写 ObjectAnimatorValueAnimator、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 动画简单太多。下一节学习导航。