[关闭]
@ltlovezh 2017-10-02T10:33:39.000000Z 字数 8471 阅读 6558

你真的了解Android ViewGroup的draw和onDraw的调用时机吗

View


前几天遇到一个ViewGroup.onDraw不会调用的问题,在网上查了一些资料,发现基本都混淆了onDrawdraw的区别,趁着十一假期有时间,简单梳理了下这里的逻辑。

View.drawView.onDraw的调用关系

首先,View.drawView.onDraw是两个不同的方法,只有View.draw被调用,View.onDraw才有可能被调用。在View.draw中有下面一段代码:

  1. final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
  2. (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);//是否是实心控件
  3. if (!dirtyOpaque) {
  4. drawBackground(canvas);//绘制背景
  5. }
  6. ...
  7. // Step 3, draw the content
  8. if (!dirtyOpaque) onDraw(canvas);//调用onDraw

通过上述代码可知:

  1. View.draw方法中会调用View.onDraw
  2. 只有dirtyOpaque为false(透明,非实心),才会调用View.onDraw方法。

因此,如果希望ViewGroup.onDraw方法被调用,那么就必须满足两个条件:

  1. 设法让ViewGroup.draw方法被调用
  2. draw方法中的dirtyOpaque为false。

既然谈到了View.drawView.onDraw,这里简单说下两者的区别。查看View源码,可知View.draw基本包含6个步骤:

  1. Draw the background,通过View.drawBackground方法来实现。
  2. If necessary, save the canvas’ layers to prepare for fading,如果需要,保存画布层(Canvas.saveLayer)为淡入或淡出做准备。
  3. draw the content,通过View.onDraw方法来实现,一般自定义View,就是通过该方法来绘制内容。获得Canvas后,可以draw任何内容,实现个性化的定制。
  4. draw the children,通过View.dispatchDraw方法来实现,ViewGroup都会实现该方法,来绘制自己的子View。
  5. If necessary, draw the fading edges and restore layers,如果需要,绘制淡入淡出的相关内容并恢复之前保存的画布层(layer)。
  6. draw decorations (scrollbars),通过View.onDrawScrollBars方法来实现,绘制滚动条的操作就是在这里实现的。

简单来说,View.draw负责绘制当前View的所有内容以及子View的内容,是一个全集。而View.onDraw则只负责绘制本身相关的内容,是一个子集。

ViewGroup.draw的调用时机

其实也是View.draw的调用时机,通过查看View源码可知:单参数的View.draw方法会在三个参数的View.draw方法中被调用,如下所示:

  1. if (!hasDisplayList) { //软件绘制
  2. // Fast path for layouts with no backgrounds
  3. if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
  4. //跳过当前View的绘制,直接绘制子view
  5. mPrivateFlags &= ~PFLAG_DIRTY_MASK;
  6. dispatchDraw(canvas);
  7. } else {
  8. //此时坐标系已经切换到View自身坐标系了,可以纯碎的绘制当前view了,又回到了draw(canvas)
  9. draw(canvas);
  10. }
  11. }

在软件绘制下,三参数的View.draw负责把View坐标系从父View那里切换到当前View,然后再交给当前View去绘制。一般情况下,交给当前View去绘制就是通过调用单参数的View.draw方法来实现。
但是,这里有一个优化逻辑:如果当前View不需要绘制(打上了PFLAG_SKIP_DRAW标志),那么会通过dispatchDraw方法直接绘制当前View的子View。

所以,我们的ViewGroup.draw方法会不会被调用,完全取决于mPrivateFlags是不是包含PFLAG_SKIP_DRAW标志:
1. 若mPrivateFlags包含PFLAG_SKIP_DRAW,那么会跳过当前View的draw方法,直接调用dispatchDraw方法绘制当前View的子View。
2. 若mPrivateFlags不包含PFLAG_SKIP_DRAW,那么会调用当前View的draw方法,完成所有内容的绘制。

那么PFLAG_SKIP_DRAW取决于哪些因素那?

setWillNotDraw

View中有一个setWillNotDraw方法,从注释上来看,就是控制是否要跳过View.draw方法,以进行优化的。我们看一下该方法:

  1. public void setWillNotDraw(boolean willNotDraw) {
  2. setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
  3. }

该方法很简单,我们继续看下setFlags方法:

  1. void setFlags(int flags, int mask) {
  2. int old = mViewFlags;
  3. //设置flags
  4. mViewFlags = (mViewFlags & ~mask) | (flags & mask);
  5. int changed = mViewFlags ^ old;
  6. //若mViewFlags前后没有变化,则直接返回
  7. if (changed == 0) {
  8. return;
  9. }
  10. int privateFlags = mPrivateFlags;
  11. ...
  12. if ((changed & DRAW_MASK) != 0) {
  13. if ((mViewFlags & WILL_NOT_DRAW) != 0) {
  14. //mViewFlags设置了WILL_NOT_DRAW标志
  15. if (mBseackground != null) {
  16. //如果当前View有背景,那么取消mPrivateFlags的PFLAG_SKIP_DRAW标志,但是设置另外一个PFLAG_ONLY_DRAWS_BACKGROUND标志
  17. mPrivateFlags &= ~PFLAG_SKIP_DRAW;
  18. mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;
  19. } else {
  20. //如果当前View没有背景,那么直接设置PrivateFlags的PFLAG_SKIP_DRAW标志
  21. mPrivateFlags |= PFLAG_SKIP_DRAW;
  22. }
  23. } else {
  24. //因为mViewFlags没有设置WILL_NOT_DRAW标志,所以取消mPrivateFlags的PFLAG_SKIP_DRAW标志
  25. mPrivateFlags &= ~PFLAG_SKIP_DRAW;
  26. }
  27. requestLayout();
  28. invalidate(true);
  29. }
  30. }

通过上述代码可知,要想对mPrivateFlags设置PFLAG_SKIP_DRAW标识,必须满足两个条件:
1. 针对mViewFlags,设置WILL_NOT_DRAW标志
2. 当前View没有背景图

通过setWillNotDraw(true)一定会对mViewFlags设置WILL_NOT_DRAW标识。如果此时当前View没有背景图,那么就会对mPrivateFlags设置PFLAG_SKIP_DRAW标识。
但是若此时当前View有背景图,那么就会取消mPrivateFlags的PFLAG_SKIP_DRAW标识,同时设置另外一个PFLAG_ONLY_DRAWS_BACKGROUND标识。setWillNotDraw方法的相关逻辑如下图所示:
setWillNotDraw

设置背景

那这里就有一个疑问,如果我们在运行过程中,取消了当前View的背景图,那么当前View还会重新为mPrivateFlags设置PFLAG_SKIP_DRAW标志吗?
答案:会,这也正是PFLAG_ONLY_DRAWS_BACKGROUND标志的作用。

我们看下View.setBackgroundDrawable方法的实现:

  1. public void setBackgroundDrawable(Drawable background) {
  2. if (background == mBackground) {
  3. return;
  4. }
  5. if (background != null) {
  6. ...
  7. mBackground = background;
  8. if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
  9. //若当前View既设置PFLAG_SKIP_DRAW,又添加了背景,那么只能取消mPrivateFlags的PFLAG_SKIP_DRAW标志,同时替换成PFLAG_ONLY_DRAWS_BACKGROUND,这和setFlags方法里面的逻辑一致
  10. mPrivateFlags &= ~PFLAG_SKIP_DRAW;
  11. mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;
  12. }
  13. }else{
  14. //这里取消了背景图
  15. mBackground = null;
  16. if ((mPrivateFlags & PFLAG_ONLY_DRAWS_BACKGROUND) != 0){
  17. /*
  18. * This view ONLY drew the background before and we're removing
  19. * the background, so now it won't draw anything
  20. * (hence we SKIP_DRAW)
  21. */
  22. //如果mPrivateFlags包含PFLAG_ONLY_DRAWS_BACKGROUND标志,说明之前mViewFlags设置了WILL_NOT_DRAW标志,但是因为之前当前View有背景图,那么只能先设置PFLAG_ONLY_DRAWS_BACKGROUND标志。现在当前View的背景图取消了,所以可以重新对mPrivateFlags设置PFLAG_SKIP_DRAW了
  23. mPrivateFlags &= ~PFLAG_ONLY_DRAWS_BACKGROUND;
  24. mPrivateFlags |= PFLAG_SKIP_DRAW;
  25. }
  26. }
  27. }

上述代码里的注释已经说的很清楚了。如果取消了当前View的背景图,系统会把mPrivateFlags的PFLAG_ONLY_DRAWS_BACKGROUND标志重新替换为PFLAG_SKIP_DRAW标志。setBackgroundDrawable方法的相关逻辑如下图所示:
setBackgroundDrawable

到这里关于PFLAG_SKIP_DRAW标志的分析已经结束了。回到我们开头的问题:为什么默认情况下,ViewGroup.draw(ViewGroup.onDraw)方法不会被调用。对照上面的分析,可知:肯定是ViewGroup的mPrivateFlags打上了PFLAG_SKIP_DRAW标志,那么究竟是在哪里设置的该标志那?
原来默认情况下,ViewGroup在初始化的时候,会通过下面的代码为为mViewFlags设置WILL_NOT_DRAW标志。并且默认情况下,ViewGroup也没有背景图,所以就为ViewGroup的mPrivateFlags打上了PFLAG_SKIP_DRAW标志。导致ViewGroup.draw方法不会被调用,那么ViewGroup.onDraw方法就更不会被调用了。

  1. private void initViewGroup() {
  2. // ViewGroup doesn't draw by default
  3. if (!debugDraw()) {
  4. setFlags(WILL_NOT_DRAW, DRAW_MASK);
  5. }
  6. ...
  7. }

总结一下,决定View.draw方法是否被调用的直接因素是:View.mPrivateFlags是否包含PFLAG_SKIP_DRAW标识;而要包含此标识,需要同时满足两个条件:
1. View.mViewFlags包含WILL_NOT_DRAW标识,可通过View.setWillNotDraw(true)设置该标识。
2. 当前View没有背景图。
因此,如果我们想让ViewGroup.draw被调用,只要破坏上述任何一个条件就可以了。
1. 调用View.setWillNotDraw(false),取消View.mViewFlags中的WILL_NOT_DRAW标识
2. 为ViewGroup设置背景图

ViewGroup.onDraw的调用时机

由上文可知,即使ViewGroup.draw被调用了,ViewGroup.onDraw也不一定会被调用。必须满足不是实心控件(View.mPrivateFlags没有打上PFLAG_DIRTY_OPAQUE标识),ViewGroup.onDraw才会被调用。

实心控件:控件的onDraw方法能够保证此控件的所有区域都会被其所绘制的内容完全覆盖。换句话说,通过此控件所属的区域无法看到此控件之下的内容,也就是既没有半透明也没有空缺的部分。

那么View.mPrivateFlags在什么情况下会被打上PFLAG_DIRTY_OPAQUE标识那。通过查看源码,发现相关逻辑在ViewGroup.invalidateChild方法中:

  1. //这里的child表示直接调用invalidate的子View。
  2. public final void invalidateChild(View child, final Rect dirty) {
  3. //计算子View是否是实心的
  4. final boolean isOpaque = child.isOpaque() && !drawAnimation && child.getAnimation() == null && childMatrix.isIdentity();
  5. //PFLAG_DIRTY和PFLAG_DIRTY_OPAQUE是互斥的
  6. int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;
  7. do { //循环遍历到ViewRootImpl为止
  8. View view = null;//父View
  9. if (parent instanceof View) {
  10. view = (View) parent;
  11. }
  12. if (view != null) { //给当前父View打上相应的flag
  13. //父View若包含FADING_EDGE_MASK标识,那么只能打上FLAG_DIRTY标识,表示会调用ViewGroup.onDraw方法
  14. if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
  15. view.getSolidColor() == 0) {
  16. opaqueFlag = PFLAG_DIRTY;
  17. }
  18. if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
  19. //PFLAG_DIRTY和PFLAG_DIRTY_OPAQUE是互斥的
  20. view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
  21. }
  22. }
  23. ...
  24. }

通过上述代码可知:View.invalidate方法会向上回溯到ViewRootImpl,在此过程中,若子控件是实心的,则会将当前父控件标记为PFLAG_DIRTY_OPAQUE,否则为PFLAG_DIRTY
对于包含PFLAG_DIRTY_OPAQUE标识的控件,在绘制过程中,会跳过drawBackground方法(绘制背景)和onDraw方法(绘制自身内容)。

决定一个View是否实心完全取决于isOpaque方法,该方法的默认实现是检查View.mPrivateFlags是否包含PFLAG_OPAQUE_MASK标识。PFLAG_OPAQUE_MASK标识(实心)又由PFLAG_OPAQUE_BACKGROUND(背景实心)和PFLAG_OPAQUE_SCROLLBARS(滚动条实心)组成。即:只有View同时满足背景实心和滚动条实心,那么它才是opaque的。
真正计算View是否实心的方法是computeOpaqueFlags,如下所示:

  1. protected void computeOpaqueFlags() {
  2. // Opaque if:
  3. // - Has a background
  4. // - Background is opaque
  5. // - Doesn't have scrollbars or scrollbars overlay
  6. //若View包含背景,且背景是不透明的,则打上PFLAG_OPAQUE_BACKGROUND标识
  7. if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
  8. mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
  9. } else {
  10. mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
  11. }
  12. final int flags = mViewFlags;
  13. //若没有横竖滚动条,或者滚动条是OVERLAY类型的,则打上PFLAG_OPAQUE_SCROLLBARS标识
  14. if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
  15. (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
  16. (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
  17. mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;
  18. } else {
  19. mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
  20. }
  21. }

只有同时打上了PFLAG_OPAQUE_BACKGROUNDPFLAG_OPAQUE_SCROLLBARS标识,当前View才是实心的。
该方法会在View中的很多地方被调用,以实时确定View是否是实心的。
当然,如果isOpaque方法的默认实现不符合我们的需求,我们可以自己实现,这也是官方推荐的做法。

Demo验证

下面我们通过一个Demo验证上述逻辑:
1. 设定一个自定义父ViewGroupA和子ViewB。
2. 对父ViewGroupA调用setWillNotDraw(false),保证父ViewGroupA的draw方法会被调用。
3. 对子ViewB设置一个Click事件,具体实现就是调用子ViewB.invalidate方法。
4. 通过点击子ViewB,观察父ViewGroupA和子ViewB的draw和onDraw方法是否会被调用。

上述Demo必须采用软件绘制才有效。在硬件绘制下,子ViewB调用invalidate方法,只会触发子ViewB自己的draw方法,它的父View是不需要重绘的。

假如我们对子ViewB设置了一个纯色的背景(子ViewB变成实心了),那么可以得到如下结论:
1. 在View树第一次渲染的时候,父ViewGroupA和子ViewB的draw和onDraw方法都会被调用。
2. 在后续点击子ViewB的时候,子ViewB的draw和onDraw方法都会被调用,父ViewGroupA的draw方法也会被调用,但是父ViewGroupA的onDraw方法不会被调用

假如我们没有对子ViewB设置背景(子ViewB变成非实心了),那么可以得到如下结论:
1. 在View树第一次渲染的时候,父ViewGroupA和子ViewB的draw和onDraw方法都会被调用。
2. 在后续点击子ViewB的时候,父ViewGroupA和子ViewB的draw和onDraw方法都会被调用。

当然控制一个View是否实心,我们也可以直接重写isOpaque方法,没必要像上面这么麻烦。

总结一下,首次渲染View树的时候,只要ViewGroup.draw方法被调用了,那么ViewGroup.onDraw就会被调用
但是后续子View.invalidate的时候,在ViewGroup.draw方法被调用的前提下,还要子View是非实心的,那么ViewGroup.onDraw和ViewGroup.drawBackground才会被调用

总结

最后用一张图来总结下ViewGroup的draw和onDraw方法的调用逻辑图。
ViewGroup的draw和onDraw方法的调用逻辑图

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注