Android 动画详解:从原理到实战
Android 动画体系经历了从传统 View 动画 → 属性动画 → 过渡动画 → MotionLayout → Jetpack Compose 的演进。以下从分类、原理、API、实战四个维度系统梳理。
一、动画分类总览
Android 动画按实现方式可分为四大类:
| 类别 | 包名 | 适用场景 | 特点 |
|---|---|---|---|
| View 动画(补间动画) | android.view.animation |
简单 UI 动效 | 只改变绘制位置,不改变实际属性 |
| 帧动画(Drawable 动画) | android.graphics.drawable |
加载动画、序列帧 | 逐帧切换图片,内存占用高 |
| 属性动画 | android.animation |
任意对象属性动画 | 真正改变对象属性,功能最强大 |
| 过渡动画 | android.transition |
Activity/Fragment 切换 | 场景切换间的连贯体验 |
| MotionLayout | androidx.constraintlayout.motion |
复杂交互动效 | 基于 ConstraintLayout 的声明式动画 |
| Compose 动画 | androidx.compose.animation |
现代 UI 开发 | 声明式、状态驱动、代码简洁 |
二、View 动画(补间动画)
⚠️ 已不推荐 :View 动画只能作用于 View,且只改变绘制位置,不改变实际属性(如按钮移动后点击区域仍在原处)。新项目请优先使用属性动画。
2.1 四种基本变换
| 动画类型 | 类名 | 属性 |
|---|---|---|
| 平移 | TranslateAnimation |
fromXDelta, toXDelta, fromYDelta, toYDelta |
| 缩放 | ScaleAnimation |
fromXScale, toXScale, fromYScale, toYScale |
| 旋转 | RotateAnimation |
fromDegrees, toDegrees, pivotX, pivotY |
| 透明度 | AlphaAnimation |
fromAlpha, toAlpha |
2.2 XML 定义
xml
<!-- res/anim/scale_rotate.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fillAfter="true"
android:interpolator="@android:anim/accelerate_decelerate_interpolator">
<scale
android:fromXScale="1.0"
android:toXScale="2.0"
android:fromYScale="1.0"
android:toYScale="2.0"
android:pivotX="50%"
android:pivotY="50%" />
<rotate
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%" />
</set>
2.3 代码调用
kotlin
val animation = AnimationUtils.loadAnimation(context, R.anim.scale_rotate)
view.startAnimation(animation)
三、帧动画(Drawable 动画)
将一系列 Drawable 按时间间隔逐帧显示,适合简单的加载动画。
xml
<!-- res/drawable/loading_animation.xml -->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/frame1" android:duration="100" />
<item android:drawable="@drawable/frame2" android:duration="100" />
<item android:drawable="@drawable/frame3" android:duration="100" />
</animation-list>
kotlin
val imageView: ImageView = findViewById(R.id.imageView)
imageView.setImageResource(R.drawable.loading_animation)
val animationDrawable = imageView.drawable as AnimationDrawable
animationDrawable.start()
⚠️ 注意:帧动画会预加载所有帧到内存,图片过多或过大时容易造成 OOM。建议用 Lottie 替代复杂帧动画。
四、属性动画(Property Animation)
属性动画是 Android 3.0(API 11)引入的动画框架,真正修改对象的属性值,功能强大且灵活。
4.1 核心类
表格
| 类 | 作用 |
|---|---|
ValueAnimator |
动画引擎,计算动画值,不直接操作对象 |
ObjectAnimator |
ValueAnimator 子类,直接操作目标对象的属性 |
AnimatorSet |
组合多个动画,控制执行顺序 |
Interpolator |
插值器,定义动画变化速率 |
TypeEvaluator |
估值器,定义属性值的计算方式 |
4.2 ValueAnimator
kotlin
val animator = ValueAnimator.ofFloat(0f, 100f).apply {
duration = 1000
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { animation ->
val value = animation.animatedValue as Float
// 手动将计算值应用到目标对象
view.translationX = value
}
}
animator.start()
4.3 ObjectAnimator(最常用)
kotlin
// 平移
ObjectAnimator.ofFloat(view, "translationX", 0f, 200f).apply {
duration = 1000
start()
}
// 旋转
ObjectAnimator.ofFloat(view, "rotation", 0f, 360f).apply {
duration = 1000
start()
}
// 透明度
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f, 1f).apply {
duration = 1500
start()
}
// 背景色变化(需要 ArgbEvaluator)
ObjectAnimator.ofInt(view, "backgroundColor", Color.RED, Color.BLUE).apply {
duration = 2000
setEvaluator(ArgbEvaluator())
start()
}
4.4 可动画的 View 属性
| 属性 | 说明 |
|---|---|
translationX / translationY |
相对于 left/top 的偏移 |
x / y |
最终位置(left + translationX) |
rotation / rotationX / rotationY |
2D/3D 旋转 |
scaleX / scaleY |
缩放 |
pivotX / pivotY |
旋转/缩放的中心点 |
alpha |
透明度 |
width / height |
宽高(需配合 wrapper) |
4.5 AnimatorSet 组合动画
kotlin
val fadeOut = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f)
val mover = ObjectAnimator.ofFloat(view, "translationX", -500f, 0f)
val fadeIn = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f)
AnimatorSet().apply {
play(mover).with(fadeIn).after(fadeOut) // fadeOut 先执行,然后 mover 和 fadeIn 同时执行
duration = 2000
start()
}
4.6 ViewPropertyAnimator(链式调用)
kotlin
view.animate()
.translationX(200f)
.translationY(100f)
.rotation(360f)
.alpha(0.5f)
.setDuration(1000)
.setInterpolator(BounceInterpolator())
.withLayer() // 开启硬件层加速
.withStartAction { /* 动画开始前 */ }
.withEndAction { /* 动画结束后 */ }
.start()
ViewPropertyAnimator内部优化了多个属性同时动画的性能,比多个ObjectAnimator更高效。
4.7 插值器(Interpolator)
| 插值器 | 效果 |
|---|---|
LinearInterpolator |
匀速 |
AccelerateInterpolator |
加速 |
DecelerateInterpolator |
减速 |
AccelerateDecelerateInterpolator |
先加速后减速 |
BounceInterpolator |
弹跳效果 |
OvershootInterpolator |
冲过头再回弹 |
AnticipateInterpolator |
先回退再前进 |
AnticipateOvershootInterpolator |
先回退,冲过头再回弹 |
4.8 XML 声明属性动画
xml
<!-- res/animator/property_animator.xml -->
<set android:ordering="sequentially">
<objectAnimator
android:propertyName="x"
android:duration="500"
android:valueTo="400"
android:valueType="intType" />
<objectAnimator
android:propertyName="alpha"
android:duration="500"
android:valueTo="1f" />
</set>
kotlin
val set = AnimatorInflater.loadAnimator(context, R.animator.property_animator) as AnimatorSet
set.setTarget(view)
set.start()
五、过渡动画(Transition)
5.1 Activity 过渡动画
kotlin
// 传统方式(API 21 以下兼容)
val intent = Intent(this, TargetActivity::class.java)
startActivity(intent)
overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)
5.2 共享元素过渡(Shared Element)
kotlin
// Activity A
val options = ActivityOptions.makeSceneTransitionAnimation(
this,
sharedImageView,
"shared_image" // transitionName 必须匹配
)
startActivity(intent, options.toBundle())
// Activity B 的 layout 中
<ImageView
android:id="@+id/sharedImageView"
android:transitionName="shared_image"
... />
5.3 Fragment 过渡
kotlin
val fragmentB = FragmentB()
fragmentB.sharedElementEnterTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragmentB)
.addSharedElement(sharedView, "shared_name")
.commit()
六、MotionLayout
MotionLayout 是 ConstraintLayout 的子类,通过声明式 XML 定义复杂动画,是传统 View 系统中处理复杂交互动画的最佳选择。
6.1 核心概念
| 概念 | 说明 |
|---|---|
MotionScene |
动画场景描述文件 |
ConstraintSet |
定义布局的起始/结束状态 |
Transition |
定义两个状态之间的过渡 |
KeyFrameSet |
关键帧,控制中间状态 |
OnClick / OnSwipe |
触发方式 |
6.2 基本示例
xml
<!-- res/layout/activity_main.xml -->
<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/scene">
<View
android:id="@+id/box"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@color/purple" />
</androidx.constraintlayout.motion.widget.MotionLayout>
xml
<!-- res/xml/scene.xml -->
<MotionScene xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- 起始状态 -->
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/box"
android:layout_width="64dp"
android:layout_height="64dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
<!-- 结束状态 -->
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/box"
android:layout_width="64dp"
android:layout_height="64dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>
<!-- 过渡定义 -->
<Transition
app:constraintSetStart="@id/start"
app:constraintSetEnd="@id/end"
app:duration="1000">
<OnClick app:targetId="@id/box" app:clickAction="toggle" />
</Transition>
</MotionScene>
6.3 关键帧(KeyFrame)
xml
<Transition ...>
<KeyFrameSet>
<!-- 中间位置关键帧:动画 50% 时位于屏幕中心 -->
<KeyPosition
app:framePosition="50"
app:motionTarget="@id/box"
app:keyPositionType="parentRelative"
app:percentX="0.5"
app:percentY="0.5" />
<!-- 中间属性关键帧:50% 时旋转 180 度 -->
<KeyAttribute
app:framePosition="50"
app:motionTarget="@id/box"
android:rotation="180" />
</KeyFrameSet>
</Transition>
6.4 代码控制
kotlin
motionLayout.transitionToState(R.id.end) // 过渡到结束状态
motionLayout.transitionToStart() // 回到起始状态
motionLayout.progress = 0.5f // 设置进度(0~1)
motionLayout.setTransition(R.id.start, R.id.end) // 设置过渡
七、Jetpack Compose 动画(推荐)
2026 年,Jetpack Compose 已成为 Android UI 开发的默认方式,其动画 API 声明式、简洁且功能强大。
7.1 状态驱动动画
kotlin
@Composable
fun AnimatedBox() {
var expanded by remember { mutableStateOf(false) }
// 尺寸动画
val size by animateDpAsState(
targetValue = if (expanded) 120.dp else 60.dp,
animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing),
label = "size"
)
// 颜色动画
val color by animateColorAsState(
targetValue = if (expanded) Color.Green else Color.Blue,
label = "color"
)
Box(
modifier = Modifier
.size(size)
.background(color, CircleShape)
.clickable { expanded = !expanded }
)
}
7.2 可见性动画
kotlin
var visible by remember { mutableStateOf(true) }
AnimatedVisibility(
visible = visible,
enter = fadeIn() + slideInHorizontally(),
exit = fadeOut() + slideOutHorizontally()
) {
Text("Hello Compose!")
}
7.3 内容切换动画
kotlin
var selected by remember { mutableStateOf(false) }
Crossfade(targetState = selected, label = "crossfade") { state ->
when (state) {
true -> Icon(Icons.Default.Check, null)
false -> Icon(Icons.Default.Close, null)
}
}
7.4 内容变更动画
kotlin
var count by remember { mutableIntStateOf(0) }
AnimatedContent(
targetState = count,
transitionSpec = {
if (targetState > initialState) {
slideInVertically { it } + fadeIn() togetherWith
slideOutVertically { -it } + fadeOut()
} else {
slideInVertically { -it } + fadeIn() togetherWith
slideOutVertically { it } + fadeOut()
}
},
label = "count"
) { targetCount ->
Text(text = "$targetCount", fontSize = 48.sp)
}
7.5 无限循环动画
kotlin
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 1.5f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
Box(
modifier = Modifier
.size(100.dp)
.scale(scale)
.background(Color.Red, CircleShape)
)
7.6 手势驱动动画
kotlin
val offset = remember { Animatable(Offset.Zero, Offset.VectorConverter) }
Box(
modifier = Modifier
.offset { offset.value.toIntOffset() }
.size(100.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, dragAmount ->
change.consume()
scope.launch {
offset.snapTo(offset.value + dragAmount)
}
},
onDragEnd = {
scope.launch {
// 弹簧回弹到原点
offset.animateTo(
targetValue = Offset.Zero,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
}
}
)
}
)
7.7 共享元素过渡(Compose)
kotlin
SharedTransitionLayout {
AnimatedContent(
targetState = selectedItem,
transitionSpec = {
(fadeIn() togetherWith fadeOut()).using(SizeTransform(clip = false))
}
) { item ->
if (item == null) {
// 列表页
LazyColumn {
items(items) { photo ->
Image(
painter = rememberAsyncImagePainter(photo.url),
contentDescription = null,
modifier = Modifier
.sharedElement(
state = rememberSharedContentState(key = photo.id),
animatedVisibilityScope = this@AnimatedContent
)
.clickable { selectedItem = photo }
)
}
}
} else {
// 详情页
Image(
painter = rememberAsyncImagePainter(item.url),
contentDescription = null,
modifier = Modifier
.sharedElement(
state = rememberSharedContentState(key = item.id),
animatedVisibilityScope = this@AnimatedContent
)
.fillMaxSize()
)
}
}
}
八、动画核心机制
8.1 Vsync + Choreographer 机制
Android 动画的流畅性依赖于 Vsync(垂直同步) 和 Choreographer 的协同工作:
-
Vsync 信号:显示器硬件每帧刷新完成后发出同步信号(60Hz = 16.6ms/帧,120Hz = 8.3ms/帧)
-
Choreographer :通过
postCallback()注册动画回调,在 Vsync 信号到达时触发doFrame() -
动画更新 :
ValueAnimator计算当前帧的属性值 → 调用Interpolator计算插值 → 调用TypeEvaluator计算最终值 → 更新 View 属性 →invalidate()触发重绘 -
SurfaceFlinger:合成所有图层,输出到屏幕
8.2 属性动画计算流程
plain
start()
→ Choreographer 注册回调
→ 等待 Vsync 信号
→ doFrame(frameTimeNanos)
→ 计算 elapsedFraction (0~1)
→ Interpolator 计算 interpolatedFraction
→ TypeEvaluator 计算属性值
→ 更新目标对象属性
→ invalidate() 请求重绘
→ 下一帧循环直到动画结束
九、性能优化最佳实践
9.1 避免掉帧
表格
| 优化手段 | 说明 |
|---|---|
| 使用硬件层 | view.animate().withLayer() 或 view.setLayerType(View.LAYER_TYPE_HARDWARE, null) |
| 避免主线程阻塞 | 动画回调中不做 IO/复杂计算 |
| 减少过度绘制 | 使用 GPU 调试模式分析 |
| 控制同时动画数 | MotionLayout 同时动画 50+ 个 View 可能掉帧 |
| 使用 Profile GPU Rendering | 开发者选项中开启,查看各阶段耗时 |
9.2 各场景选型建议
| 场景 | 推荐方案 |
|---|---|
| 简单属性变化(位移、透明度) | ObjectAnimator / ViewPropertyAnimator |
| 复杂交互动效(折叠头、拖拽) | MotionLayout |
| 新项目的所有动画 | Jetpack Compose Animation API |
| 列表项增删动画 | RecyclerView.ItemAnimator / Compose LazyColumn + AnimatedVisibility |
| 跨页面共享元素 | SharedElementTransition / Compose SharedTransitionLayout |
| 复杂帧动画 | Lottie(替代 AnimationDrawable) |
9.3 Compose 动画性能
-
Compose 动画基于重组(Recomposition) 和智能跳过,只有状态变化的 Composable 会重新执行
-
使用
remember缓存动画状态,避免不必要的计算 -
复杂动画使用
LaunchedEffect+Animatable精确控制
十、总结对比
| 维度 | View 动画 | 属性动画 | MotionLayout | Compose 动画 |
|---|---|---|---|---|
| 实际改变属性 | ❌ 否 | ✅ 是 | ✅ 是 | ✅ 是 |
| 非 View 对象 | ❌ 不支持 | ✅ 支持 | ❌ 仅 View | ✅ 任意状态 |
| 代码量 | 中 | 中 | 少(XML 声明) | 少 |
| 复杂交互 | ❌ 困难 | ⚠️ 较复杂 | ✅ 擅长 | ✅ 擅长 |
| 维护性 | 一般 | 一般 | 好 | 优秀 |
| 推荐度 | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
2026 年建议 :新项目直接使用 Jetpack Compose + Compose Animation API ;维护中的传统项目,复杂动画迁移到 MotionLayout ,简单动画使用 属性动画 替代 View 动画。