首页 / Android 入门教程 / 动画体系

Android 入门教程

动画体系

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

AndroidAndroid 入门教程动画属性动画LottieCompose

95. 动画体系

本节目标:理清 Android 动画的几种方案:View 动画、属性动画、Activity 过渡、Lottie,重点掌握 Compose 动画 API(animateAsState、AnimatedVisibility、updateTransition),知道什么场景用什么。

动画让应用生动起来:按钮点击反馈、页面切换、加载效果。Android 动画方案历史上有几套,老项目混用,新项目 Compose 统一。

动画方案对比

方案原理现在用不用
View 动画(Tween)改变绘制位置,不改变真实属性老项目维护,新项目不用
属性动画改变对象真实属性View 体系首选
Activity 过渡Activity 切换动画View 体系用
LottieAE 导出 JSON 渲染复杂设计师动画
Compose 动画状态驱动自动插值Compose 项目首选

View 动画(补间)

老式动画,只改绘制位置不改变属性。比如按钮平移后,点击区域还在原位。

<!-- res/anim/rotate.xml -->
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="0"
    android:toDegrees="360"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="1000" />
val anim = AnimationUtils.loadAnimation(context, R.anim.rotate)
view.startAnimation(anim)

四种类型:<alpha>(透明度)、<scale>(缩放)、<translate>(平移)、<rotate>(旋转)。

Note

View 动画只改视觉,不改属性,新项目别用。属性动画是正确选择。

属性动画

改变对象真实属性,按钮平移后点击区域跟着移。

ObjectAnimator

val animator = ObjectAnimator.ofFloat(view, "translationX", 0f, 200f)
animator.duration = 1000
animator.start()

改变 view.translationX 从 0 到 200,1 秒。

属性必须有 setter:

// 自定义属性
class MyView : View {
    var progress: Float = 0f
        set(value) {
            field = value
            invalidate()
        }
}

val animator = ObjectAnimator.ofFloat(myView, "progress", 0f, 1f)
animator.duration = 1000
animator.start()

ValueAnimator

自己处理每帧的值:

val animator = ValueAnimator.ofFloat(0f, 1f)
animator.duration = 1000
animator.addUpdateListener { animator ->
    val value = animator.animatedValue as Float
    view.alpha = value
}
animator.start()

AnimatorSet

组合多个动画:

AnimatorSet().apply {
    playTogether(
        ObjectAnimator.ofFloat(view, "alpha", 0f, 1f),
        ObjectAnimator.ofFloat(view, "translationY", 100f, 0f)
    )
    duration = 500
    start()
}

XML 定义

<!-- res/animator/fade_in.xml -->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="alpha"
    android:valueFrom="0"
    android:valueTo="1"
    android:duration="500" />
val animator = AnimatorInflater.loadAnimator(context, R.animator.fade_in)
animator.setTarget(view)
animator.start()

插值器

控制动画速率:

animator.interpolator = AccelerateDecelerateInterpolator()  // 默认,先快后慢
animator.interpolator = LinearInterpolator()  // 匀速
animator.interpolator = AccelerateInterpolator()  // 越来越快
animator.interpolator = DecelerateInterpolator()  // 越来越慢
animator.interpolator = OvershootInterpolator()  // 超出再回弹

Activity 过渡

Activity 切换的动画。overridePendingTransition(老式)或者共享元素过渡(Android 5+)。

// 老式
startActivity(intent)
overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)

// 共享元素(更现代)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
    this,
    imageView,
    "image_transition"  // 共享元素名
)
startActivity(intent, options.toBundle())

XML 里给共享元素命名:

<ImageView
    android:transitionName="image_transition"
    ... />

Lottie 动画

设计师用 After Effects 做的动画导出成 JSON,应用里直接播放。

dependencies {
    implementation("com.airbnb.android:lottie-compose:6.5.2")
}
@Composable
fun LottieAnimation() {
    val composition by rememberLottieComposition(
        LottieCompositionSpec.Asset("animation.json")
    )
    val progress by animateLottieCompositionAsState(composition)

    com.airbnb.lottie.compose.LottieAnimation(
        composition = composition,
        progress = { progress }
    )
}
Tip

Lottie 适合复杂的设计师动画,简单动画用属性动画更轻量。

Compose 动画

Compose 用状态驱动动画,API 简洁强大。下面是核心 API。

animateAsState

状态变化时自动插值:

@Composable
fun ColorBox() {
    var enabled by remember { mutableStateOf(false) }
    val color by animateColorAsState(
        targetValue = if (enabled) Color.Red else Color.Gray,
        animationSpec = tween(durationMillis = 500),
        label = "color"
    )

    Box(
        Modifier
            .size(100.dp)
            .background(color)
            .clickable { enabled = !enabled }
    )
}

变体:

  • animateColorAsState:颜色。
  • animateFloatAsState:浮点。
  • animateDpAsState:尺寸。
  • animateIntAsState:整数。
  • animateSizeAsState:尺寸。
  • animateOffsetAsState:位置。

AnimatedVisibility

显示/隐藏动画:

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(visible = visible) {
    Text("我会淡入淡出")
}

Button(onClick = { visible = !visible }) {
    Text("切换")
}

自定义进出动画:

AnimatedVisibility(
    visible = visible,
    enter = slideInHorizontally() + fadeIn(),
    exit = slideOutHorizontally() + fadeOut()
) {
    Text("滑动 + 淡入")
}

animateContentSize

内容尺寸变化时平滑过渡:

var expanded by remember { mutableStateOf(false) }

Text(
    text = if (expanded) longText else shortText,
    modifier = Modifier.animateContentSize()
)

Button(onClick = { expanded = !expanded }) {
    Text("展开/收起")
}

updateTransition

多个属性同时变:

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox() {
    var state by remember { mutableStateOf(BoxState.Collapsed) }
    val transition = updateTransition(targetState = state, label = "box")

    val width by transition.animateDp { state ->
        when (state) { BoxState.Collapsed -> 64.dp; BoxState.Expanded -> 128.dp }
    }
    val height by transition.animateDp { state ->
        when (state) { BoxState.Collapsed -> 64.dp; BoxState.Expanded -> 256.dp }
    }
    val color by transition.animateColor { state ->
        when (state) { BoxState.Collapsed -> Color.Gray; BoxState.Expanded -> Color.Red }
    }

    Box(
        Modifier
            .size(width, height)
            .background(color)
            .clickable {
                state = if (state == BoxState.Collapsed) BoxState.Expanded else BoxState.Collapsed
            }
    )
}

rememberInfiniteTransition

无限循环动画:

val infiniteTransition = rememberInfiniteTransition(label = "loading")
val rotation by infiniteTransition.animateFloat(
    initialValue = 0f,
    targetValue = 360f,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Restart
    ),
    label = "rotation"
)

Box(
    Modifier
        .size(48.dp)
        .rotate(rotation)
        .background(Color.Blue)
)

AnimatedContent

内容切换动画:

var count by remember { mutableStateOf(0) }

AnimatedContent(
    targetState = count,
    transitionSpec = {
        slideInHorizontally { it } togetherWith slideOutHorizontally { -it }
    }
) { target ->
    Text("$target", fontSize = 32.sp)
}

Button(onClick = { count++ }) {
    Text("增加")
}

数字切换时有滑入滑出效果。

animationSpec

控制动画细节:

// 时长 + 缓动
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)

// 弹簧
animationSpec = spring(
    dampingRatio = Spring.DampingRatioMediumBouncy,
    stiffness = Spring.StiffnessLow
)

// 关键帧
animationSpec = keyframes {
    durationMillis = 1000
    0f at 0
    0.5f at 500 with FastOutLinearInEasing
    1f at 1000
}

常见坑

  1. View 动画当属性动画用:动画结束位置不对,点击区域错乱。
  2. 属性动画不取消:Activity 销毁没取消,可能内存泄漏。
  3. animate*AsState 漏 label:编译警告,调试难。所有 animate*AsState 都要加 label。
  4. rememberInfiniteTransition 滥用:常驻动画耗电,能不用就不用。
  5. Lottie 文件太大:加载慢,优化 JSON 或者改用属性动画。
  6. AnimatedVisibility 内容高度突变:配合 animateContentSize 平滑。
  7. Compose 动画卡顿:可能因为重组过多,用 derivedStateOf 减少。

小结

View 体系用属性动画(ObjectAnimator/ValueAnimator),Activity 切换用过渡动画,复杂设计师动画用 Lottie。Compose 用 animate*AsState 状态驱动插值,AnimatedVisibility 显隐,updateTransition 多属性同步,rememberInfiniteTransition 无限循环,AnimatedContent 内容切换。animationSpec 控制时长、缓动、弹簧、关键帧。

下一章讲数据绑定与 ViewBinding。