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

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

View.drawView.onDraw的调用关系

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

final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);//是否是实心控件

if (!dirtyOpaque) {
    drawBackground(canvas);//绘制背景
}

...

// Step 3, draw the content
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方法中被调用,如下所示:

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

在软件绘制下,三参数的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方法,以进行优化的。我们看一下该方法:

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

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

void setFlags(int flags, int mask) {
int old = mViewFlags;
//设置flags
mViewFlags = (mViewFlags & ~mask) | (flags & mask);
int changed = mViewFlags ^ old;
//若mViewFlags前后没有变化,则直接返回
if (changed == 0) {
    return;
}
int privateFlags = mPrivateFlags;

...

if ((changed & DRAW_MASK) != 0) {
    if ((mViewFlags & WILL_NOT_DRAW) != 0) {
        //mViewFlags设置了WILL_NOT_DRAW标志
        if (mBseackground != null) {
            //如果当前View有背景,那么取消mPrivateFlags的PFLAG_SKIP_DRAW标志,但是设置另外一个PFLAG_ONLY_DRAWS_BACKGROUND标志
            mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;
        } else {
            //如果当前View没有背景,那么直接设置PrivateFlags的PFLAG_SKIP_DRAW标志
            mPrivateFlags |= PFLAG_SKIP_DRAW;
        }
    } else {
        //因为mViewFlags没有设置WILL_NOT_DRAW标志,所以取消mPrivateFlags的PFLAG_SKIP_DRAW标志
        mPrivateFlags &= ~PFLAG_SKIP_DRAW;
    }
    requestLayout();
    invalidate(true);
    }
}

通过上述代码可知,要想对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

setWillNotDraw

设置背景

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

我们看下View.setBackgroundDrawable方法的实现:
``` java
public void setBackgroundDrawable(Drawable background) {
if (background == mBackground) {
return;
}
if (background != null) {
...
mBackground = background;
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
//若当前View既设置PFLAG_SKIP_DRAW,又添加了背景,那么只能取消mPrivateFlags的PFLAG_SKIP_DRAW标志,同时替换成PFLAG_ONLY_DRAWS_BACKGROUND,这和setFlags方法里面的逻辑一致
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;

top Created with Sketch.