15、View的动画与绘制:属性动画如何触发invalidate、View动画与属性动画在绘制上的区别、动画过程中的measure/layout/draw
各位同学,今天我们来聊聊动画和绘制的关系。说实话,这个知识点我当年也踩过不少坑。你可能会觉得,动画不就是让View动起来吗?跟绘制流程有什么关系?嗯,关系大了去了。
我们先从最基础的问题开始:属性动画到底是怎么触发invalidate的?
属性动画触发invalidate的机制
属性动画的核心,说白了就是不断改变某个属性的值,然后通知View重绘。但具体怎么通知的?我带你看看源码里的关键路径。
当你调用ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).start()时,内部会做这几件事:
- 创建一个
ValueAnimator,注册AnimationHandler到Choreographer - 每一帧回调时,计算当前进度对应的属性值
- 通过反射或setter方法设置新值
- 触发
invalidate()
关键在第4步。属性动画本身并不知道View需要重绘,它只是调用了setter。那invalidate是谁调的呢?
答案是:View自己。
当View的某个属性(比如translationX)发生变化时,如果这个属性被标记为“需要重绘”,View会在setter内部调用invalidate()。
举个例子,View.setTranslationX()的源码大致是这样的:
public void setTranslationX(float translationX) {
mTranslationX = translationX;
// 关键:通知父布局和自身需要重绘
invalidateViewProperty(true, false);
}
这里的invalidateViewProperty最终会调用invalidate()。所以你看,属性动画只是负责改值,重绘是View自己的事。
我个人习惯:在自定义View时,如果我要支持属性动画,一定会在setter里显式调用invalidate()。否则动画跑完了画面都不带变的,那可就尴尬了。
View动画与属性动画在绘制上的区别
这个问题,我面试时经常问候选人。很多人能说出“View动画只改变视觉效果,不改变实际位置”,但问到绘制层面的区别,就答不上来了。
咱们直接上对比表:
| 对比维度 | View动画(补间动画) | 属性动画 |
|---|---|---|
| 触发重绘方式 | 通过ViewGroup的drawChild(),在绘制时应用Matrix变换 |
通过invalidate()触发完整绘制流程 |
| 是否走measure/layout | 不走。只改变绘制时的Matrix | 如果改变的是布局相关属性(如width),会走measure/layout |
| 绘制层级 | 在ViewGroup的dispatchDraw()中应用变换 |
在View自身的onDraw()中体现变化 |
| 性能特点 | 轻量,不触发measure/layout | 可能触发完整三流程,开销更大 |
| 实际位置 | 不变。点击事件还在原地 | 改变。点击事件跟随新位置 |
这里有个关键点:View动画是在绘制阶段“作弊”。它不改变View的left/top/right/bottom,而是在draw()时通过Canvas的Matrix做平移、缩放、旋转。你想想看,这就像你拍照时用了滤镜——照片看起来变了,但人还是那个人。
而属性动画是“动真格”的。它直接修改View的属性值,然后触发invalidate(),让View重新走绘制流程。如果改的是layoutParams.width这种布局属性,连measure和layout都得重走一遍。
我曾经在一个列表页里同时用了View动画和属性动画做平移动画。结果发现View动画流畅得多,但点击事件错位了。后来我改用属性动画,点击正常了,但列表滑动时有点卡顿。嗯,这就是典型的性能与正确性的取舍。
动画过程中的measure/layout/draw
这部分是今天的重头戏。咱们分两种情况讨论:
情况一:属性动画改变的是“绘制属性”
比如alpha、translationX、rotation等。这些属性只影响绘制,不影响布局。
流程是这样的:
- 动画每帧回调,调用setter
- setter内部调用
invalidate() - View只走draw流程,不走measure和layout
为什么?因为View的尺寸和位置没变,只是绘制时的Matrix变了。所以ViewRootImpl会跳过measure和layout,直接执行draw。
情况二:属性动画改变的是“布局属性”
比如width、height、margin等。这些属性会影响View的尺寸或位置。
流程就重了:
- 动画每帧回调,调用setter
- setter内部调用
requestLayout()+invalidate() - ViewRootImpl收到请求,重新执行measure → layout → draw
你想想看,如果每帧都走一遍完整的measure/layout/draw,那性能开销是很大的。特别是measure阶段,如果View层级深,可能一次measure要遍历整个子树。
避坑指南:尽量不要用属性动画去改变layoutParams.width或height。如果非要改变尺寸,建议用scaleX/scaleY代替,它们只触发draw,不触发measure/layout。
View动画的流程
View动画就简单多了。它根本不走measure/layout,只在draw阶段做手脚。
具体来说:
- ViewGroup的
dispatchDraw()被调用 - 遍历子View时,检查是否有动画
- 如果有,获取当前帧的变换Matrix
- 将Matrix应用到Canvas上,然后调用子View的
draw()
整个过程完全绕过了measure和layout。所以View动画非常轻量,但代价就是“假动”——实际位置没变。
下面我用一张图来总结整个知识体系:
这张图把整个流程串起来了。你看,从动画类型出发,根据修改的属性不同,走的绘制路径完全不同。我个人建议,在实际开发中优先考虑用属性动画改绘制属性,这样既保证了点击事件的正确性,又不会触发昂贵的measure/layout。
小技巧:如果你不确定某个属性会不会触发measure/layout,可以看看它的setter里有没有调用requestLayout()。如果有,那就意味着每帧都要走完整三流程,慎用。
好了,关于动画与绘制的关系,今天就聊到这里。记住一句话:动画的本质是不断改变 + 不断重绘。理解了这个,你就能在性能和正确性之间做出更好的权衡。
公众号:蓝海资料掘金营,微信deep3321