15、View的动画与绘制:属性动画如何触发invalidate、View动画与属性动画在绘制上的区别、动画过程中的measure/layout/draw

各位同学,今天我们来聊聊动画和绘制的关系。说实话,这个知识点我当年也踩过不少坑。你可能会觉得,动画不就是让View动起来吗?跟绘制流程有什么关系?嗯,关系大了去了。

我们先从最基础的问题开始:属性动画到底是怎么触发invalidate的?

属性动画触发invalidate的机制

属性动画的核心,说白了就是不断改变某个属性的值,然后通知View重绘。但具体怎么通知的?我带你看看源码里的关键路径。

当你调用ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).start()时,内部会做这几件事:

  1. 创建一个ValueAnimator,注册AnimationHandler到Choreographer
  2. 每一帧回调时,计算当前进度对应的属性值
  3. 通过反射或setter方法设置新值
  4. 触发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

这部分是今天的重头戏。咱们分两种情况讨论:

情况一:属性动画改变的是“绘制属性”

比如alphatranslationXrotation等。这些属性只影响绘制,不影响布局。

流程是这样的:

  1. 动画每帧回调,调用setter
  2. setter内部调用invalidate()
  3. View只走draw流程,不走measure和layout

为什么?因为View的尺寸和位置没变,只是绘制时的Matrix变了。所以ViewRootImpl会跳过measure和layout,直接执行draw。

情况二:属性动画改变的是“布局属性”

比如widthheightmargin等。这些属性会影响View的尺寸或位置。

流程就重了:

  1. 动画每帧回调,调用setter
  2. setter内部调用requestLayout() + invalidate()
  3. ViewRootImpl收到请求,重新执行measure → layout → draw

你想想看,如果每帧都走一遍完整的measure/layout/draw,那性能开销是很大的。特别是measure阶段,如果View层级深,可能一次measure要遍历整个子树。

避坑指南:尽量不要用属性动画去改变layoutParams.widthheight。如果非要改变尺寸,建议用scaleX/scaleY代替,它们只触发draw,不触发measure/layout。

View动画的流程

View动画就简单多了。它根本不走measure/layout,只在draw阶段做手脚。

具体来说:

  1. ViewGroup的dispatchDraw()被调用
  2. 遍历子View时,检查是否有动画
  3. 如果有,获取当前帧的变换Matrix
  4. 将Matrix应用到Canvas上,然后调用子View的draw()

整个过程完全绕过了measure和layout。所以View动画非常轻量,但代价就是“假动”——实际位置没变。

下面我用一张图来总结整个知识体系:

动画与绘制流程关系图 属性动画 View动画(补间) 改变绘制属性(alpha/translation) 改变布局属性(width/height) invalidate() requestLayout() 仅 draw measure → layout → draw 在dispatchDraw中应用Matrix 不触发measure/layout 核心结论 • 属性动画改绘制属性 → 仅触发 draw,性能较好 • 属性动画改布局属性 → 触发 measure + layout + draw,性能开销大 • View动画 → 不触发 measure/layout,性能最好,但位置不变

这张图把整个流程串起来了。你看,从动画类型出发,根据修改的属性不同,走的绘制路径完全不同。我个人建议,在实际开发中优先考虑用属性动画改绘制属性,这样既保证了点击事件的正确性,又不会触发昂贵的measure/layout。

小技巧:如果你不确定某个属性会不会触发measure/layout,可以看看它的setter里有没有调用requestLayout()。如果有,那就意味着每帧都要走完整三流程,慎用。

好了,关于动画与绘制的关系,今天就聊到这里。记住一句话:动画的本质是不断改变 + 不断重绘。理解了这个,你就能在性能和正确性之间做出更好的权衡。


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