动画与过渡:让界面“活”起来

说实话,动画是 Android 开发里最容易被低估的一块。很多人觉得它就是个花架子,把按钮弄个旋转、加个淡入淡出,没什么技术含量。但我在项目里踩过不少坑之后,才真正意识到——动画做得好,用户体验能上一个台阶;做得不好,反而会让 App 显得廉价、卡顿。

今天这一章,我会把 Android 动画体系从头到尾捋一遍。从最基础的 View 动画,到更灵活的属性动画,再到过渡动画和 Lottie 库。每个部分我都会结合自己的实战经验来讲,希望能帮你少走弯路。

Android 动画体系 动画引擎 View 动画 属性动画 过渡动画 Lottie 动画库 Tween 补间 Frame 逐帧 ObjectAnimator ValueAnimator 场景切换动画

View 动画:最基础的两种玩法

View 动画是 Android 最早支持的动画方式。它分两种:Tween 补间动画Frame 逐帧动画。说白了,补间动画就是告诉系统「从 A 到 B,中间你帮我算」,逐帧动画就是「我画好每一帧,你按顺序播」。

Tween 补间动画

补间动画支持四种基本变换:平移、缩放、旋转、透明度。你可以单独用,也可以组合起来用。我记得刚入行那会儿,做一个按钮点击后的缩放效果,就是用 ScaleAnimation 配合 AlphaAnimation 实现的。

// XML 方式:res/anim/scale_alpha.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <scale
        android:fromXScale="1.0"
        android:toXScale="1.2"
        android:fromYScale="1.0"
        android:toYScale="1.2"
        android:pivotX="50%"
        android:pivotY="50%"
        android:duration="200"/>
    <alpha
        android:fromAlpha="1.0"
        android:toAlpha="0.6"
        android:duration="200"/>
</set>

// 代码中启动
val animation = AnimationUtils.loadAnimation(context, R.anim.scale_alpha)
view.startAnimation(animation)
注意:补间动画只改变 View 的绘制位置,不改变实际坐标。也就是说,按钮看起来移到了右边,但点击事件还在原地。我曾经因为这个被测试妹子追着改 bug……后来果断换成了属性动画。

Frame 逐帧动画

逐帧动画就简单了——你把每一帧的图片准备好,放到一个 AnimationDrawable 里,然后 start() 就行。适合做加载中的小菊花、心跳图标这类效果。

// res/drawable/frame_animation.xml
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/frame_01" android:duration="100"/>
    <item android:drawable="@drawable/frame_02" android:duration="100"/>
    <item android:drawable="@drawable/frame_03" android:duration="100"/>
</animation-list>

// 代码中
val frameAnim = imageView.drawable as AnimationDrawable
frameAnim.start()
小技巧:逐帧动画的图片不要太大,否则内存容易爆。我一般控制在 10 帧以内,每帧不超过 50KB。如果帧数多,建议用 Lottie 或者视频纹理。

属性动画:真正的“动”

属性动画是 Android 3.0 引入的,它解决了补间动画的痛点——真正改变对象的属性值。你想想看,如果你想让一个按钮从左边移到右边,并且点击事件也跟着走,那就得用属性动画。

属性动画的核心是两个类:ObjectAnimatorValueAnimator。前者是后者的简化版,直接操作对象的某个属性;后者更底层,你需要自己监听数值变化并更新 UI。

ObjectAnimator:简单粗暴

我个人习惯用 ObjectAnimator,因为它写起来最省事。比如让一个 View 在 1 秒内水平移动 300 像素:

ObjectAnimator.ofFloat(view, "translationX", 0f, 300f)
    .setDuration(1000)
    .start()

就这么一行。你还可以组合多个动画:

val animatorSet = AnimatorSet().apply {
    playTogether(
        ObjectAnimator.ofFloat(view, "translationX", 0f, 300f),
        ObjectAnimator.ofFloat(view, "translationY", 0f, 200f),
        ObjectAnimator.ofFloat(view, "alpha", 1f, 0.5f)
    )
    duration = 1000
    start()
}
核心要点:ObjectAnimator 要求目标对象必须有对应属性的 getter/setter 方法。比如 "translationX" 对应的是 View 的 getTranslationX() 和 setTranslationX()。如果没有,你就得自己包装一层。

ValueAnimator:更灵活,更底层

ValueAnimator 不直接操作任何对象,它只负责产生数值。你需要自己监听数值变化,然后手动更新 UI。这在做自定义动画时特别有用。

我在做一个自定义进度条的时候就用到了它:

ValueAnimator.ofFloat(0f, 100f).apply {
    duration = 2000
    addUpdateListener { animator ->
        val progress = animator.animatedValue as Float
        progressBar.progress = progress.toInt()
        progressText.text = "${progress.toInt()}%"
    }
    start()
}

你看,这里我同时更新了进度条和文字,非常灵活。ValueAnimator 的插值器(Interpolator)和估值器(TypeEvaluator)也支持自定义,你可以做出各种非线性效果。

经验之谈:属性动画默认在主线程执行。如果动画过程中有耗时操作,记得用 Handler 或者协程切到子线程。我曾经在动画回调里做数据库查询,结果界面直接卡成 PPT……

过渡动画:场景切换的优雅方案

过渡动画(Transition)是 Android 4.4 引入的,专门用来处理界面切换时的动画效果。比如从一个 Activity 跳转到另一个,或者 Fragment 切换,甚至同一个布局内的 View 增删改。

它的核心思想是:记录开始场景和结束场景,然后自动计算中间帧。你只需要告诉系统「我要从 A 变到 B」,剩下的交给 Transition 框架。

// 在 Activity 跳转时使用
val transition = TransitionInflater.from(this)
    .inflateTransition(R.transition.slide_and_fade)

// 设置进入和退出动画
window.enterTransition = transition
window.exitTransition = transition

// 启动目标 Activity
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle())

嗯,这里要注意:过渡动画对 API 版本有要求。如果你要兼容低版本,建议用 androidx.transition 包,它提供了向下兼容的实现。

我在项目中用过 ChangeBoundsChangeImageTransform 的组合,实现了一个「点击小图放大到大图」的效果,用户体验非常好。代码大概长这样:

val transition = TransitionSet().apply {
    addTransition(ChangeBounds())
    addTransition(ChangeImageTransform())
    duration = 300
}
TransitionManager.beginDelayedTransition(container, transition)

// 改变布局参数
imageView.layoutParams.width = expandedWidth
imageView.layoutParams.height = expandedHeight
imageView.requestLayout()
避坑指南:我曾经在 RecyclerView 的 item 动画里用 TransitionManager,结果因为频繁触发导致性能问题。后来发现,对于列表动画,用 DefaultItemAnimator 或者自定义 ItemAnimator 更合适。过渡动画更适合页面级别的切换。

Lottie 动画库:设计师的“亲儿子”

Lottie 是 Airbnb 开源的一个动画库。它最大的特点是:设计师用 After Effects 做动画,导出 JSON 文件,你直接拿来用。不需要写一行动画代码,就能实现非常复杂的矢量动画。

我强烈建议团队里引入 Lottie。以前设计师给一个加载动画,我得逐帧切图,还要考虑不同屏幕适配。现在好了,设计师导出 JSON,我直接丢到 LottieAnimationView 里,完事。

// 添加依赖
implementation 'com.airbnb.android:lottie:6.0.0'

// XML 布局
<com.airbnb.lottie.LottieAnimationView
    android:id="@+id/animation_view"
    android:layout_width="200dp"
    android:layout_height="200dp"
    app:lottie_rawRes="@raw/loading_animation"
    app:lottie_autoPlay="true"
    app:lottie_loop="true"/>

// 或者代码控制
animation_view.apply {
    setAnimation(R.raw.loading_animation)
    repeatCount = LottieDrawable.INFINITE
    playAnimation()
}

Lottie 还支持动态修改颜色、控制播放进度、监听动画事件等。比如你可以让一个点赞动画在用户点击时播放一次,然后停在最后一帧:

animation_view.apply {
    setAnimation(R.raw.like_animation)
    repeatCount = 0  // 只播一次
    addAnimatorListener(object : Animator.AnimatorListener {
        override fun onAnimationEnd(animation: Animator?) {
            // 停在最后一帧
            this@apply.progress = 1f
        }
        // 其他回调省略...
    })
    playAnimation()
}
性能建议:Lottie 动画虽然方便,但 JSON 文件太大也会影响性能。我一般要求设计师把动画控制在 3 秒以内,文件大小不超过 200KB。如果动画复杂,可以拆分成多个小动画,按需加载。

总结一下

动画这块,我的建议是:

  • 简单效果(平移、缩放、淡入淡出)用属性动画,别用补间动画了,除非你确定不需要交互反馈。
  • 逐帧动画只适合帧数少、图片小的场景,否则内存扛不住。
  • 页面切换用过渡动画,用户体验好,代码也简洁。
  • 复杂矢量动画无脑上 Lottie,省时省力,效果还漂亮。

最后说一句:动画不是越多越好。恰到好处的动画能提升体验,过度使用反而让人眼花。我在项目里定过一个原则——每个页面不超过 3 个动画,每个动画不超过 500ms。你可以参考一下。


公众号:蓝海资料掘金营,微信deep3321