Android 动画对比指南:View 系统 vs Jetpack Compose
📚 本指南用于学习 Android 动画演进,配合
AnimationLearningApp教学项目使用
项目Gitee地址:https://gitee.com/developer_wind/AnimationLearningApp
目录
- 动画体系对比总览
- [View 动画系统详解](#View 动画系统详解)
- [Compose 动画系统详解](#Compose 动画系统详解)
- [Compose 特有动画](#Compose 特有动画)
- 实战对照表
- 最佳实践建议
动画体系对比总览
| 特性 | View 系统 | Jetpack Compose |
|---|---|---|
| 声明方式 | 命令式 / XML | 声明式 |
| 状态管理 | 手动更新 View | 状态驱动自动重绘 |
| 动画 API | 分散 (Animation/Animator) | 统一 (animate*AsState) |
| 学习曲线 | 陡峭 | 平缓 |
| 性能 | 需手动优化 | 自动优化重绘 |
| 代码量 | 多 | 少 |
View 动画系统详解
1. View Animation (补间动画)
特点:只改变视觉效果,不改变实际属性
kotlin
// XML 定义 (res/anim/scale_up.xml)
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:fromXScale="0.5"
android:toXScale="1.0"
android:fromYScale="0.5"
android:toYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:duration="300"/>
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="300"/>
</set>
// Kotlin 使用
val animation = AnimationUtils.loadAnimation(context, R.anim.scale_up)
view.startAnimation(animation)
缺点:
- ❌ 动画结束后 View 实际位置不变(点击事件仍在原位置)
- ❌ 无法监听中间状态
- ❌ XML 与代码分离
2. Property Animation (属性动画)
特点:真正改变属性值,支持任意对象
kotlin
// ObjectAnimator - 单个属性
ObjectAnimator.ofFloat(view, "rotationY", 0f, 360f).apply {
duration = 1000
interpolator = AccelerateDecelerateInterpolator()
start()
}
// AnimatorSet - 组合动画
AnimatorSet().apply {
playTogether(
ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.5f),
ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.5f),
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f)
)
duration = 500
start()
}
// ValueAnimator - 自定义值动画
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 1000
addUpdateListener { animator ->
val progress = animator.animatedValue as Float
view.translationX = progress * 100
}
start()
}
缺点:
- ❌ 代码冗长
- ❌ 需要手动管理 Animator 生命周期
- ❌ 多个属性联动复杂
3. LayoutTransition (布局变化动画)
kotlin
val layout = findViewById<LinearLayout>(R.id.container)
val transition = LayoutTransition()
transition.setDuration(300)
layout.layoutTransition = transition
// 添加/移除 View 时自动动画
layout.addView(newView)
layout.removeView(oldView)
4. Activity/Fragment 转场动画
kotlin
// 老式 API
startActivity(intent)
overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)
// Fragment 转场
fragmentTransaction.setCustomAnimations(
R.anim.fade_in,
R.anim.fade_out,
R.anim.slide_in_left,
R.anim.slide_out_right
)
Compose 动画系统详解
1. animate*AsState (状态驱动动画)
最常用! 状态变化时自动动画
kotlin
@Composable
fun AnimatedBox() {
var expanded by remember { mutableStateOf(false) }
// 各种类型的动画
val size by animateDpAsState(
targetValue = if (expanded) 200.dp else 100.dp,
animationSpec = spring(stiffness = Spring.StiffnessLow)
)
val color by animateColorAsState(
targetValue = if (expanded) Color.Red else Color.Blue,
animationSpec = tween(300)
)
val alpha by animateFloatAsState(
targetValue = if (expanded) 1f else 0.5f
)
val rotation by animateFloatAsState(
targetValue = if (expanded) 360f else 0f
)
Box(
modifier = Modifier
.size(size)
.background(color)
.alpha(alpha)
.rotate(rotation)
.clickable { expanded = !expanded }
)
}
支持类型:
animateFloatAsState- 浮点数 (alpha, rotation, scale)animateDpAsState- 尺寸 (size, padding, elevation)animateColorAsState- 颜色animateIntAsState- 整数animateOffsetAsState- 偏移量animateContentSize- 内容尺寸变化
2. AnimatedVisibility (显示/隐藏动画)
kotlin
@Composable
fun ShowHideDemo() {
var visible by remember { mutableStateOf(true) }
Column {
Button(onClick = { visible = !visible }) {
Text(if (visible) "隐藏" else "显示")
}
AnimatedVisibility(
visible = visible,
enter = fadeIn() + slideInVertically(initialOffsetY = { -50 }),
exit = fadeOut() + slideOutVertically(targetOffsetY = { 50 })
) {
Card {
Text("Hello Compose!")
}
}
}
}
enter/exit 组合:
fadeIn()/fadeOut()- 淡入淡出slideInHorizontally/Vertically()- 滑动scaleIn()/scaleOut()- 缩放expandIn()/shrinkOut()- 展开/收缩
3. updateTransition (多属性协同过渡)
适合多个属性联动的复杂动画
kotlin
@Composable
fun CardExpansionDemo() {
var expanded by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = expanded, label = "cardTransition")
val width by transition.animateDp(label = "width") { state ->
if (state) 300.dp else 100.dp
}
val height by transition.animateDp(label = "height") { state ->
if (state) 200.dp else 100.dp
}
val cornerRadius by transition.animateDp(label = "cornerRadius") { state ->
if (state) 24.dp else 8.dp
}
val elevation by transition.animateDp(label = "elevation") { state ->
if (state) 16.dp else 4.dp
}
Card(
modifier = Modifier
.width(width)
.height(height)
.clickable { expanded = !expanded },
shape = RoundedCornerShape(cornerRadius),
elevation = CardDefaults.cardElevation(elevation)
) {
Box(contentAlignment = Alignment.Center) {
Text(if (expanded) "展开" else "收起")
}
}
}
4. Crossfade (内容切换动画)
kotlin
@Composable
fun ScreenSwitcher() {
var currentScreen by remember { mutableStateOf("home") }
Crossfade(targetState = currentScreen, label = "screenCrossfade") { screen ->
when (screen) {
"home" -> HomeScreen()
"profile" -> ProfileScreen()
"settings" -> SettingsScreen()
}
}
}
5. AnimatedContent (内容替换动画)
kotlin
@Composable
fun CounterDemo() {
var count by remember { mutableStateOf(0) }
AnimatedContent(
targetState = count,
label = "counterAnimation",
transitionSpec = {
// 数字增加:从右滑入,向左滑出
if (targetState > initialState) {
slideInHorizontally { it } + fadeIn() togetherWith
slideOutHorizontally { -it } + fadeOut()
} else {
slideInHorizontally { -it } + fadeIn() togetherWith
slideOutHorizontally { it } + fadeOut()
}
}
) { targetCount ->
Text(
text = "$targetCount",
style = MaterialTheme.typography.headlineLarge
)
}
}
Compose 特有动画
1. infiniteTransition (无限循环动画)
kotlin
@Composable
fun LoadingPulse() {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(500),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
val alpha by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0.5f,
animationSpec = infiniteRepeatable(
animation = tween(500),
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
Box(
modifier = Modifier
.size(100.dp)
.scale(scale)
.alpha(alpha)
.background(Color.Blue, CircleShape)
)
}
2. animateItem (列表项动画) - Compose 1.5+
kotlin
@Composable
fun AnimatedList(items: List<String>) {
LazyColumn {
items(
items = items,
key = { it }
) { item ->
Box(
modifier = Modifier
.animateItem(
fadeInSpec = fadeIn(animationSpec = tween(300)),
fadeOutSpec = fadeOut(animationSpec = tween(300))
)
.padding(8.dp)
) {
Text(item)
}
}
}
}
3. 手势驱动动画 (draggable + spring)
kotlin
@Composable
fun DraggableCard() {
var offsetX by remember { mutableStateOf(0f) }
val draggableState = rememberDraggableState { delta ->
offsetX += delta
}
val animatedOffset by animateFloatAsState(
targetValue = offsetX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "dragOffset"
)
Box(
modifier = Modifier
.offset { IntOffset(animatedOffset.toInt(), 0) }
.draggable(
state = draggableState,
orientation = Orientation.Horizontal,
onDragStopped = { velocity ->
// 根据滑动速度决定返回或移除
if (abs(offsetX) > 100) {
offsetX = 300f // 滑出
} else {
offsetX = 0f // 弹回
}
}
)
.size(200.dp)
.background(Color.Green, RoundedCornerShape(16.dp))
)
}
4. Navigation Compose 转场动画
kotlin
@Composable
fun AppNavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = "home"
) {
composable(
route = "home",
enterTransition = { fadeIn() },
exitTransition = { fadeOut() }
) { HomeScreen(navController) }
composable(
route = "detail/{id}",
enterTransition = { slideInHorizontally { it } },
exitTransition = { slideOutHorizontally { -it } },
popEnterTransition = { slideInHorizontally { -it } },
popExitTransition = { slideOutHorizontally { it } }
) { backStackEntry ->
DetailScreen(backStackEntry.arguments?.getString("id"))
}
}
}
5. SharedElement (共享元素转场) - 实验性
kotlin
@Composable
fun SharedElementDemo(navController: NavHostController) {
NavHost(navController, startDestination = "list") {
composable("list") {
SharedElementList(onItemClick = { id ->
navController.navigate("detail/$id")
})
}
composable(
"detail/{id}",
enterTransition = {
fadeIn() + sharedElementTransition(
sharedContentState = rememberSharedContentState(key = "image-${it.targetState.arguments?.getString("id")}"),
boundsTransform = BoundsTransform { _, _ -> tween(300) }
)
}
) {
DetailScreen()
}
}
}
实战对照表
| 需求 | View 系统实现 | Compose 实现 | 代码量对比 |
|---|---|---|---|
| 按钮点击缩放 | ObjectAnimator + AnimatorSet | animate*AsState | 10:1 |
| 列表项添加动画 | LayoutTransition + notifyItemInserted | animateItem | 8:1 |
| 页面切换淡入淡出 | overridePendingTransition | AnimatedContent | 5:1 |
| 加载脉冲动画 | ValueAnimator + repeat | infiniteTransition | 6:1 |
| 拖拽卡片弹回 | ValueAnimator + 手势监听 | draggable + spring | 12:1 |
| 显示/隐藏动画 | View.VISIBLE + Animation | AnimatedVisibility | 4:1 |
最佳实践建议
✅ 推荐做法
- 优先使用
animate*AsState- 简单场景一行搞定 - 交互元素用
spring()- 物理感更强,用户体验更好 - 多属性联动用
updateTransition- 保持动画同步 - 列表用
animateItem- 自动处理增删动画 - 无限循环用
infiniteTransition- 性能优化
❌ 避免做法
- 在 Compose 中使用 View 动画 - 性能差且不兼容
- 在动画中频繁创建状态 - 会导致无限重绘
- 忽略
label参数 - 调试时无法追踪动画 - 在
remember外创建动画 - 每次重绘重置动画
学习路径
- 入门 :
animate*AsState+AnimatedVisibility - 进阶 :
updateTransition+AnimatedContent - 高级 :
infiniteTransition+ 手势驱动 + 共享元素