动画体系
本教程共 100 篇 · 第 95 篇 · 更新于 2026-07-28 · 约 9 分钟阅读
95. 动画体系
本节目标:理清 Android 动画的几种方案:View 动画、属性动画、Activity 过渡、Lottie,重点掌握 Compose 动画 API(animateAsState、AnimatedVisibility、updateTransition),知道什么场景用什么。
动画让应用生动起来:按钮点击反馈、页面切换、加载效果。Android 动画方案历史上有几套,老项目混用,新项目 Compose 统一。
动画方案对比
| 方案 | 原理 | 现在用不用 |
|---|---|---|
| View 动画(Tween) | 改变绘制位置,不改变真实属性 | 老项目维护,新项目不用 |
| 属性动画 | 改变对象真实属性 | View 体系首选 |
| Activity 过渡 | Activity 切换动画 | View 体系用 |
| Lottie | AE 导出 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>(旋转)。
NoteView 动画只改视觉,不改属性,新项目别用。属性动画是正确选择。
属性动画
改变对象真实属性,按钮平移后点击区域跟着移。
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 }
)
}
TipLottie 适合复杂的设计师动画,简单动画用属性动画更轻量。
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
}
常见坑
- View 动画当属性动画用:动画结束位置不对,点击区域错乱。
- 属性动画不取消:Activity 销毁没取消,可能内存泄漏。
animate*AsState漏 label:编译警告,调试难。所有animate*AsState都要加 label。rememberInfiniteTransition滥用:常驻动画耗电,能不用就不用。- Lottie 文件太大:加载慢,优化 JSON 或者改用属性动画。
AnimatedVisibility内容高度突变:配合animateContentSize平滑。- Compose 动画卡顿:可能因为重组过多,用
derivedStateOf减少。
小结
View 体系用属性动画(ObjectAnimator/ValueAnimator),Activity 切换用过渡动画,复杂设计师动画用 Lottie。Compose 用 animate*AsState 状态驱动插值,AnimatedVisibility 显隐,updateTransition 多属性同步,rememberInfiniteTransition 无限循环,AnimatedContent 内容切换。animationSpec 控制时长、缓动、弹簧、关键帧。
下一章讲数据绑定与 ViewBinding。