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

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

View.drawView.onDraw的调用关系

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

1
2
3
4
5
6
7
8
9
10
11
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方法中被调用,如下所示:

1
2
3
4
5
6
7
8
9
10
11
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方法,以进行优化的。我们看一下该方法:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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

设置背景

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
}
}else{
//这里取消了背景图
mBackground = null;
if ((mPrivateFlags & PFLAG_ONLY_DRAWS_BACKGROUND) != 0){
/*
* This view ONLY drew the background before and we're removing
* the background, so now it won't draw anything
* (hence we SKIP_DRAW)
*/

//如果mPrivateFlags包含PFLAG_ONLY_DRAWS_BACKGROUND标志,说明之前mViewFlags设置了WILL_NOT_DRAW标志,但是因为之前当前View有背景图,那么只能先设置PFLAG_ONLY_DRAWS_BACKGROUND标志。现在当前View的背景图取消了,所以可以重新对mPrivateFlags设置PFLAG_SKIP_DRAW了
mPrivateFlags &= ~PFLAG_ONLY_DRAWS_BACKGROUND;
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
}
}

上述代码里的注释已经说的很清楚了。如果取消了当前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
2
3
4
5
6
7
8
 private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}

...
}

总结一下,决定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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//这里的child表示直接调用invalidate的子View。
public final void invalidateChild(View child, final Rect dirty) {
//计算子View是否是实心的
final boolean isOpaque = child.isOpaque() && !drawAnimation && child.getAnimation() == null && childMatrix.isIdentity();
//PFLAG_DIRTY和PFLAG_DIRTY_OPAQUE是互斥的
int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;

do { //循环遍历到ViewRootImpl为止
View view = null;//父View
if (parent instanceof View) {
view = (View) parent;
}
if (view != null) { //给当前父View打上相应的flag
//父View若包含FADING_EDGE_MASK标识,那么只能打上FLAG_DIRTY标识,表示会调用ViewGroup.onDraw方法
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
//PFLAG_DIRTY和PFLAG_DIRTY_OPAQUE是互斥的
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
...
}

通过上述代码可知: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 protected void computeOpaqueFlags() {
// Opaque if:
// - Has a background
// - Background is opaque
// - Doesn't have scrollbars or scrollbars overlay
//若View包含背景,且背景是不透明的,则打上PFLAG_OPAQUE_BACKGROUND标识
if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
} else {
mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
}

final int flags = mViewFlags;
//若没有横竖滚动条,或者滚动条是OVERLAY类型的,则打上PFLAG_OPAQUE_SCROLLBARS标识
if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
(flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
(flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;
} else {
mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
}
}

只有同时打上了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方法的调用逻辑图