android 动画详解
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/ ComposeLazyColumn+AnimatedVisibility |
| 跨页面共享元素 | SharedElementTransition/ ComposeSharedTransitionLayout |
| 复杂帧动画 | 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 动画。
