27、SurfaceView与动画:属性动画与SurfaceView、帧动画与SurfaceView、动画与渲染冲突解决、硬件加速
说实话,SurfaceView 和动画的搭配,是很多 Android 开发者的痛点。我自己带团队的时候,经常看到有人把动画写得卡成 PPT,或者画面疯狂闪烁。今天咱们就把这块硬骨头啃下来。
属性动画与 SurfaceView
属性动画(ValueAnimator / ObjectAnimator)本质上是不断改变某个对象的属性值,然后触发重绘。但 SurfaceView 的渲染机制和普通 View 不一样——它有自己的独立 Surface,在子线程里画图。
这就带来一个问题:你没法直接用 ObjectAnimator 去改变 SurfaceView 的绘制内容。因为 SurfaceView 的绘制是你在 Canvas 上画出来的,不是通过 setX/setY 这种属性驱动的。
核心思路:用属性动画驱动数值变化,然后在动画回调里手动触发 SurfaceView 的绘制。
我习惯的做法是这样的:
// 创建一个属性动画,驱动一个 float 值从 0 到 1
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.setDuration(2000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float progress = (float) animation.getAnimatedValue();
// 把 progress 传给 SurfaceView 的绘制线程
mySurfaceView.setProgress(progress);
// 通知 SurfaceView 重绘
mySurfaceView.requestRender();
}
});
animator.start();
这里有个细节:requestRender() 是我自己封装的方法,内部会往绘制线程的 Handler 发一个消息,触发 Canvas 绘制。千万别直接在动画回调里调用 lockCanvas(),那会阻塞 UI 线程。
小技巧:如果动画需要和 SurfaceView 的帧率同步,可以用 Choreographer 来驱动。我在做视频播放器的弹幕动画时就这么干过,效果很丝滑。
帧动画与 SurfaceView
帧动画,说白了就是快速切换图片。普通 View 用 AnimationDrawable 就能搞定,但 SurfaceView 不行——它没有 Drawable 的概念。
那怎么办?自己实现帧序列的切换逻辑。
我曾经在做一个直播礼物特效时,需要播放 60 帧的 PNG 序列。直接加载到内存?不现实,OOM 警告。我的方案是:
- 用 BitmapFactory.Options 的 inSampleSize 做采样加载
- 在绘制线程里按时间间隔切换 Bitmap
- 用 LRU 缓存管理已加载的帧
public class FrameAnimator {
private List<String> framePaths;
private int currentFrame = 0;
private long frameDuration = 33; // 30fps
private long lastFrameTime = 0;
public void onDraw(Canvas canvas, long currentTime) {
if (currentTime - lastFrameTime >= frameDuration) {
currentFrame = (currentFrame + 1) % framePaths.size();
lastFrameTime = currentTime;
// 加载当前帧的 Bitmap
Bitmap bitmap = loadFrame(currentFrame);
canvas.drawBitmap(bitmap, 0, 0, null);
}
}
}
注意:帧动画的 Bitmap 回收时机要把握好。我见过有人每帧都 new Bitmap,结果内存暴涨。建议用对象池或者复用 Bitmap。
动画与渲染冲突解决
这是最让人头疼的部分。SurfaceView 的渲染在子线程,属性动画在主线程。两个线程同时操作同一个资源,不出问题才怪。
常见的冲突场景:
- 动画改变 SurfaceView 的位置,同时绘制线程在画图
- 动画缩放 SurfaceView,导致 Canvas 裁剪区域变化
- SurfaceView 的 Surface 被销毁时,动画还在回调
我的解决思路是「加锁 + 状态检查」:
// 在 SurfaceView 的绘制线程中
synchronized (lock) {
if (!isSurfaceValid) return;
Canvas canvas = surfaceHolder.lockCanvas();
// 绘制内容
surfaceHolder.unlockCanvasAndPost(canvas);
}
// 在动画回调中
synchronized (lock) {
// 更新位置/缩放等属性
// 这些属性会在绘制线程中被读取
}
嗯,这里要注意:锁的粒度要尽量小。我早期犯过一个错,把整个绘制流程都锁住了,结果动画回调等锁等了几十毫秒,界面直接卡住。
最佳实践:用 volatile 关键字标记共享状态,配合 AtomicInteger 做计数器。这样大部分场景不需要显式加锁,性能更好。
硬件加速
SurfaceView 默认就是硬件加速的——它用的是独立的 SurfaceFlinger 图层。但动画部分呢?属性动画操作的是 View 的属性,这部分走的是软件绘制还是硬件加速?
说白了,SurfaceView 本身是硬件加速的,但它的子 View(如果有的话)不一定。我建议:
- 如果动画只改变 SurfaceView 的位置/大小,用 View.setTranslationX / setScaleX 等属性,这些是硬件加速友好的
- 如果动画需要改变绘制内容,在 SurfaceView 的 Canvas 上自己画,用 OpenGL 或者 Canvas 的硬件加速 API
举个例子,我做了一个缩放动画:
// 硬件加速的缩放
ObjectAnimator.ofFloat(surfaceView, "scaleX", 1f, 2f).start();
ObjectAnimator.ofFloat(surfaceView, "scaleY", 1f, 2f).start();
// 注意:SurfaceView 的缩放不会影响它内部的绘制内容
// 绘制内容需要自己在 Canvas 上做 Matrix 变换
你想想看,如果 SurfaceView 本身在缩放,但绘制线程还在画原始尺寸的内容,那画面就会变形。所以绘制线程里也要同步做 Matrix 变换。
经验之谈:用 HardwareLayer 可以提升动画性能。调用 surfaceView.setLayerType(View.LAYER_TYPE_HARDWARE, null),但要注意内存开销。我一般在动画开始时开启,动画结束后切回软件层。
最后,我画了一张图,帮你理清 SurfaceView 和动画的关系:
这张图把主线程的动画和子线程的绘制分开了。你仔细看中间的箭头——动画驱动绘制,但两者之间需要同步机制。这就是整个章节的核心逻辑。
总结一下:SurfaceView 做动画,关键是把动画的「数值变化」和「绘制逻辑」解耦。属性动画只管算数值,绘制线程只管画图。中间用线程安全的机制传递数据。硬件加速能帮你提升性能,但要注意 SurfaceView 本身的硬件加速和动画的硬件加速是两回事。
好了,这一章的内容就到这里。如果你在实际项目中遇到 SurfaceView 动画的问题,欢迎交流。