Android 动画对比指南:View 系统 vs Jetpack Compose

Android 动画对比指南:View 系统 vs Jetpack Compose

📚 本指南用于学习 Android 动画演进,配合 AnimationLearningApp 教学项目使用

项目Gitee地址:https://gitee.com/developer_wind/AnimationLearningApp

目录

  1. 动画体系对比总览
  2. [View 动画系统详解](#View 动画系统详解)
  3. [Compose 动画系统详解](#Compose 动画系统详解)
  4. [Compose 特有动画](#Compose 特有动画)
  5. 实战对照表
  6. 最佳实践建议

动画体系对比总览

特性 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))
    )
}

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

最佳实践建议

✅ 推荐做法

  1. 优先使用 animate*AsState - 简单场景一行搞定
  2. 交互元素用 spring() - 物理感更强,用户体验更好
  3. 多属性联动用 updateTransition - 保持动画同步
  4. 列表用 animateItem - 自动处理增删动画
  5. 无限循环用 infiniteTransition - 性能优化

❌ 避免做法

  1. 在 Compose 中使用 View 动画 - 性能差且不兼容
  2. 在动画中频繁创建状态 - 会导致无限重绘
  3. 忽略 label 参数 - 调试时无法追踪动画
  4. remember 外创建动画 - 每次重绘重置动画

学习路径

  1. 入门animate*AsState + AnimatedVisibility
  2. 进阶updateTransition + AnimatedContent
  3. 高级infiniteTransition + 手势驱动 + 共享元素

参考资源

相关推荐
我命由我123456 小时前
C++ - 面向对象 - 析构函数
android·c语言·开发语言·c++·visualstudio·visual studio·android runtime
失眠的咕噜6 小时前
PDA 安卓设备上传多张图片
android·前端·javascript
zb200641206 小时前
Laravel6.x新特性全解析
android
plainGeekDev7 小时前
Kotlin核心:空安全都搞不明白,还敢说熟练Kotlin?
android·面试·kotlin
huaCodeA8 小时前
Android面试-Flow相关
android·面试·职场和发展
繁星星繁8 小时前
Python基础语法(二)
android·服务器·python
Lang-12108 小时前
Frida + Android Hook 完整指南
android·逆向·hook·frida
jzlhll1238 小时前
Kotlin 协程高级用法之 NonCancellable
android·开发语言·kotlin
lxysbly9 小时前
2026 年 Android PSV模拟器下载推荐(汉化版)
android