3、View动画的局限与帧动画:逐帧动画(Frame Animation)实现、帧动画内存优化、View动画的短板(无法改变真实位置)

说实话,View动画这套东西,在Android早期版本里确实帮了大忙。那时候手机性能一般,能做个渐变动画、旋转动画,用户就觉得挺酷了。但用久了你会发现——它其实是个「纸老虎」。

这一章,我们来聊聊View动画的硬伤,以及帧动画这个「看起来简单、用起来坑多」的东西。我会结合我自己的踩坑经历,帮你把这块彻底搞明白。

3.1 逐帧动画(Frame Animation)实现

帧动画,说白了就是「翻书动画」。你把一张张图片按顺序播放,视觉上就动起来了。Android里用 AnimationDrawable 来实现。

实现步骤其实很简单,就三步:

  1. res/drawable/ 下放好每一帧的图片资源
  2. 写一个XML描述文件,告诉系统这些图片按什么顺序、什么速度播放
  3. 在代码里启动这个动画

先看XML怎么写:

<!-- 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" />
    <item
        android:drawable="@drawable/frame_04"
        android:duration="100" />
</animation-list>

这里 oneshot 控制是否只播放一次。设为 false 就会循环播放。

然后在代码里启动:

val imageView = findViewById<ImageView>(R.id.iv_frame)
val animDrawable = imageView.drawable as AnimationDrawable
animDrawable.start()

嗯,就这么简单。但我要提醒你——千万别在 onCreate 里直接调 start()。为什么?因为这时候窗口还没完全显示,动画根本不会启动。我早期就踩过这个坑,调试了半天才发现。

我的习惯做法:onWindowFocusChanged 里启动帧动画,或者用 Handler.postDelayed 延迟100ms再启动,保证窗口已经准备好。

3.2 帧动画内存优化——这个坑我踩过

帧动画最大的问题是什么?内存爆炸

你想想看,每一帧都是一张完整的图片。如果一帧是1080×1920的PNG,一张就几MB。如果动画有30帧,那就是几十MB的内存占用。而且这些图片全部被解码后放在内存里,不会自动释放。

我曾经在一个项目中做过一个加载动画,用了20帧高清图。结果在低端机上直接OOM了。用户反馈说「一打开页面就闪退」,那叫一个尴尬。

注意:AnimationDrawable会把所有帧的Bitmap一次性加载到内存中。帧数越多、图片越大,内存压力越大。

那怎么优化?我总结了几个实用的方法:

  • 压缩图片尺寸:不要用原图。根据显示区域的实际大小,把图片缩放到合适尺寸。比如显示区域只有200dp,那就没必要用1080p的图。
  • 减少帧数:很多动画12帧/秒就够流畅了,没必要用24帧。人眼对12fps以上的动画已经能接受。
  • 使用WebP格式:同样的画质,WebP比PNG小30%-50%。Android 4.0以上都支持。
  • 考虑用Lottie替代:如果动画比较复杂,直接用Lottie加载JSON动画,内存占用小得多。

还有一个技巧——BitmapFactory.Options.inSampleSize 做降采样。比如你只需要显示一半大小,那就设置 inSampleSize = 2,内存直接降到原来的1/4。

val options = BitmapFactory.Options().apply {
    inSampleSize = 2  // 宽高各缩小一半
}
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.frame_01, options)

但注意,AnimationDrawable不支持直接设置Bitmap。如果你需要精细控制内存,我建议自己写一个自定义View,用 Handler 定时切换Bitmap。这样你可以手动管理Bitmap的加载和回收。

3.3 View动画的短板——无法改变真实位置

这是View动画最要命的问题。也是我后来转向Compose动画的直接原因。

你做一个平移动画,把按钮从左边移到右边。动画结束后,按钮看起来在右边。但如果你去点击它——点击事件还是在原来的位置触发

为什么会这样?因为View动画改变的只是View的绘制位置,而不是它的布局位置。说白了,就是「看起来动了,实际上没动」。View的 lefttoprightbottom 这些属性完全没有变化。

我举个例子你就明白了:

// 假设有一个按钮,原始位置在 (0, 0)
button.animate()
    .translationX(200f)
    .translationY(200f)
    .setDuration(1000)
    .start()

// 动画结束后,按钮显示在 (200, 200)
// 但 button.left 仍然是 0,button.top 仍然是 0
// 点击 (200, 200) 这个位置,不会触发点击事件

这个问题在交互复杂的页面里特别坑。我记得有一次做一个拖拽排序的功能,用View动画做移动效果。结果用户拖完后,点击排序后的条目,触发的是原来位置的事件。用户反馈说「点了A却跳转到B的页面」,这体验简直灾难。

核心结论:View动画只改变视觉位置,不改变触摸区域。如果你需要交互跟随动画移动,要么在动画结束后手动更新布局参数,要么直接用Compose动画。

那怎么解决?有两个思路:

  • 方案一:动画结束后,用 View.setX()View.setY() 把View的真实位置改过去,同时把 translationXtranslationY 清零。
  • 方案二:ObjectAnimator 直接修改 lefttop 等布局参数。但这样会触发重新布局,性能会差一些。

我个人更推荐方案一。因为 setX()setY() 不会触发重新布局,性能更好。而且代码写起来也简洁:

button.animate()
    .translationX(200f)
    .translationY(200f)
    .setDuration(1000)
    .withEndAction {
        // 动画结束后,把真实位置改过去
        button.setX(button.x + 200f)
        button.setY(button.y + 200f)
        button.translationX = 0f
        button.translationY = 0f
    }
    .start()

但说实话,这只是权宜之计。如果你要做复杂的交互动画,View动画这套东西真的不够用。这也是为什么Google后来推出了Compose动画——它从根本上解决了「视觉位置和真实位置不一致」的问题。

3.4 本章知识体系

下面这张图帮你梳理了本章的核心内容:

第三章:View动画的局限与帧动画 逐帧动画实现 • XML定义animation-list • oneshot控制循环 • item指定drawable+时长 • 代码中start()启动 ⚠ 不要在onCreate启动 ⚠ 建议onWindowFocusChanged 帧动画内存优化 • 压缩图片尺寸 • 减少帧数(12fps够用) • 使用WebP格式 • inSampleSize降采样 ⚠ 避免OOM ⚠ 考虑Lottie替代 View动画短板 • 只改变绘制位置 • 不改变布局位置 • 触摸区域不跟随 • 交互体验灾难 ✅ 方案:动画结束后 用setX/setY修正 核心结论:帧动画注意内存,View动画注意位置,复杂交互建议用Compose

好了,这一章的内容就到这里。帧动画虽然简单,但内存问题不容忽视。View动画的「假动」问题,更是很多线上bug的根源。记住我上面说的优化方法和解决方案,至少能帮你少踩几个坑。


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