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 + 手势驱动 + 共享元素

参考资源

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work3 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work3 天前
ch21 签名、校验与发版
kotlin
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui