3、View动画的局限与帧动画:逐帧动画(Frame Animation)实现、帧动画内存优化、View动画的短板(无法改变真实位置)
说实话,View动画这套东西,在Android早期版本里确实帮了大忙。那时候手机性能一般,能做个渐变动画、旋转动画,用户就觉得挺酷了。但用久了你会发现——它其实是个「纸老虎」。
这一章,我们来聊聊View动画的硬伤,以及帧动画这个「看起来简单、用起来坑多」的东西。我会结合我自己的踩坑经历,帮你把这块彻底搞明白。
3.1 逐帧动画(Frame Animation)实现
帧动画,说白了就是「翻书动画」。你把一张张图片按顺序播放,视觉上就动起来了。Android里用 AnimationDrawable 来实现。
实现步骤其实很简单,就三步:
- 在
res/drawable/下放好每一帧的图片资源 - 写一个XML描述文件,告诉系统这些图片按什么顺序、什么速度播放
- 在代码里启动这个动画
先看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了。用户反馈说「一打开页面就闪退」,那叫一个尴尬。
那怎么优化?我总结了几个实用的方法:
- 压缩图片尺寸:不要用原图。根据显示区域的实际大小,把图片缩放到合适尺寸。比如显示区域只有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的 left、top、right、bottom 这些属性完全没有变化。
我举个例子你就明白了:
// 假设有一个按钮,原始位置在 (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.setX()和View.setY()把View的真实位置改过去,同时把translationX和translationY清零。 - 方案二:用
ObjectAnimator直接修改left、top等布局参数。但这样会触发重新布局,性能会差一些。
我个人更推荐方案一。因为 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动画的「假动」问题,更是很多线上bug的根源。记住我上面说的优化方法和解决方案,至少能帮你少踩几个坑。
公众号:蓝海资料掘金营,微信deep3321