DecorView
在了解view的绘制流程之前,首先我们要知道一个DecorView的概念,什么是DecorView?
DecorView是整个界面的最顶层View,它的尺寸通常就是屏幕尺寸,也就是说,DecorView是充满屏幕的,它实际上是一个FrameLayout
,又包含了一个子元素,LinearLayout
,这个LinearLayout
又包含两个FrameLayout
,一个用来显示标题,一个用来显示内容。显示内容的FrameLayout
,其ID为 android.R.id.content
。
我们在 Activity 中设置 Layout 时,用的方法是setContentView
,指的就是这个content。参考下图
View的绘制流程是从ViewRoot
的performTraversals
开始的,它经过measure,layout,draw三个过程最终将View绘制出来。
performTraversals
会依次调用performMeasure
,performLayout
,performDraw
三个方法,他们会依次调用measure,layout,draw方法,然后又调用了onMeasure
,onLayout
,dispatchDraw
。在onMeasure
方法中,父容器会对所有的子View进行Measure
,子元素又会作为父容器,重复对它自己的子元素进行Measure
,这样Measure
过程就从DecorView
一级一级传递下去了。Layout和Draw方法也是如此。
我们关注的重点是onMeasure方法,它决定了所有View的尺寸。
MeasureSpec
MeasureSpec
是一个32位 int 数值,它包含了两组信息。高两位代表SpecMode
,低30位代表SpecSize
。
SpecMode
指测量模式,有以下三个值:
-
EXACTLY
表示父容器已经确定好子view的大小值,这时候view的大小就是SpecSize
的值。它对应了LayoutParams
中设置match_parent
和指定具体数值的情况 -
AT_MOST
表示父容器并不确定子View的具体大小,但是确定了一个上限,就是SpecSize
,子View不能超过这个大小。具体值是多少,要看View 的具体实现。 -
UNSPECIFIED
父容器不对子View进行限制,可以按照自己的意愿随意设置,比较少见,通常用于系统内部。
说完这三种模式可能大家还是非常模糊,我们看一下MeasureSpec的源码,就一目了然了。
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec
内部封装了makeMeasureSpec
,getMode
,getSize
三个方法,方便我们对MeasureSpec
数据进行处理。
那么MeasureSpec
到底是怎样使用的呢?接下来我们就要看onMeasure
方法了。
View的onMeasure方法以及MeasureSpec的获取
首先我们回顾一下View 的绘制流程,在上文中有一句黑体显示的话,意思就是所有的View测量都是从最顶层的DecorView
开始的,我们就先看一下DecorView
的Measure过程,它的MeasureSpec
是怎样得到的。
在ViewRoot
的performTraversals
方法中可以看到:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
DecorView
的MeasureSpec
是通过getRootMeasureSpec
来得到的,它传入了两个参数,lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。
然后来看一下getRootMeasureSpec
的代码:
private int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
到这里,我们就知道了,DecorView
作为最顶级的根View,它的MeasureSpec
就是EXACTLY+WindowSize,也就是说他总是充满全屏的。
最顶层的DecorView
尺寸确定好之后,下一步就是各个子View的Measure
过程了,系统会从ViewGroup
开始一级一级向下Measure
。但是,我们又知道,一个View的大小同时还要受到父View的限制,它的大小是由本身的LayoutParams
,和父View 的MeasureSpec
共同决定的。
对于普通的View(非ViewGroup),它的Measure
过程是由ViewGroup
传递过来的,看一下ViewGrou
p的measureChildWithMargin
方法就清楚了:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看到,子View的Measure方法,实际上就是在这里调用的。在Measure之前,先获得子View的MeasureSpec
,然后调用了child.measure。
子View的MeasureSpec
又是通过getChildMeasureSpec
来获取的,代码如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这一段代码有点长,但是不难理解,该方法在调用时传入了三个参数:父View的MeasureSpec
,Padding
(父View已经被占用的空间),和子View的LayoutParams
(match_parent,wrap_content,或者精确的数值)
然后根据父view的SpecMode
进行判断,用表格的方式表示如下,横排表示父View的SpecMode
,竖排表示子View的SpecMode(LayoutParams)
,内容表示View最终的SpecMode
:
EXACTLY (match_parent 或给定数值 ) |
AT_MOST (wrap_content ) |
UNSPECIFIED |
|
---|---|---|---|
dp/px( >0) |
EXACTLY +给定值
|
EXACTLY +给定值
|
EXACTLY +给定值
|
MATCH_PARENT |
EXACTLY +父View 剩余空间(或0) |
AT_MOST +父View 剩余空间(或0) |
UNSPECIFIED + 0 |
WRAP_CONTENT |
AT_MOST +父View 剩余空间(或0) |
AT_MOST +父View 剩余空间(或0) |
UNSPECIFIED + 0 |
到这里就很清楚了,子View的大小是由父View的MeasureSpce和它本身的LayoutParams共同决定的:
- 子
View
是具体数值:最终结果一定是EXACTLY
+·给定值· - 子·View·是
MATCH_PARENT
,它的大小就是父View
的剩余空间,mode和父View相同(不考虑UNSPECIFIED ) - 子
View
是WRAP_CONTENT
,它的大小是父View的剩余空间,mode是AT_MOST
Measure的流程到这里基本已经清楚了:从顶级View开始,先调用MeasureChild
将父View的Spec传入,通过getChildMeasureSpec
方法,获取到子View的Spec,然后通过child.measure(widhSpec,heightSpec)
将得到的子View Spec传递过去。
View的Measure过程
通过上边的介绍,我们已经确定了View的MeasureSpec
,接下来就是具体的Measure方法了,因为View的Measure最终调用的是onMeasure
,我们只要看onMeasure
方法就可以了:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
代码很简单,就是一个setMeasuredDimension
,用来设置View的尺寸,传入的参数,在上文已经介绍的很清楚了,从ViewGroup
传递过来。但是他还调用了一个getDefaultSize
方法,我们来看一下:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
这个方法逻辑也很简单,不考虑UNSPECIFIED
的情况下,最终返回的结果一定是在ViewGroup
里传递来的MeasureSpec
。
到这里,整个Measure过程就已经结束了。View的最终尺寸大小,遵循的就是上面我们得出的三条结论。要注意一点,当我们直接继承View时:
子View是WRAP_CONTENT,它的大小是父View的剩余空间,mode是AT_MOST
前两个都是没有问题的,但是WRAP_CONTENT
,它的大小和MATCH_PARENT
是一样的,也就是说WRAP_CONTENT
会不起作用。解决这个问题也很简单,必须重写onMeasure
方法,然后自定义一个默认的width和height,当传递过来的SpecMode
为AT_MOST
时,设置尺寸为定义的宽高。
ViewGroup的Measure过程
相对于单一View来说,ViewGroup
的Measure
方法要复杂一些,因为它不仅仅是确定自己的尺寸,还要测量每一个子View,并得到他们的MeasureSpec
。ViewGroup
并没有重写Measure(final的)方法,也没有重写onMeasure
方法,因为ViewGrou
p是抽象类,onMeasure
方法需要在具体的实现类中去重写。
ViewGroup只是为测量子View添加了两个新的方法: measureChildren()
和measureChild()
。代码如下
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
-----------------------------------
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
首先去遍历子View,调用MeasureChild
方法,在该方法中获取各个子View的MeasureSpec
,然后调用子View的Measure方法,传递各个子View的大小。
Layout过程
Layout的作用,是ViewGroup为自己的子View指定位置,现在看一下View中的layout方法:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
关键在第11行,它最终调用了onLayout(changed, l, t, r, b)
方法,来完成最终的Layout过程。
那么我们就看一下onLayout
方法,你会发现是这样的:
View:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
ViewGroup
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
View中是一个空方法,ViewGroup中是一个抽象方法。为什么呢?其实答案很简单。Layout过程是确定子元素在自己布局中的位置,view是不存在子元素的,所以是空方法,它只需要通过setFrame
来决定自身的位置。而ViewGroup
本身就是一个抽象方法,它的不同实现会有不同的Layout方式,所以在继承ViewGroup
的布局中,必须指定自己的Layout方式,因此,ViewGroup中是一个抽象方法。
layout方法的大致流程如下:
首先通过setFrame
来设置View的四个顶点位置,并保存起来。在Layout时,会先用setFrame方法来保存四个顶点的坐标,并进行判断,值如果发生了变化,就会调用onLayout
方法来重新定位子元素。
如果是单一View,调用setFrame
方法之后,其实就已经结束了,因为他没有子元素,onLayout方法是空的,调用onlayout方法没有任何意义。而对于ViewGroup来说,他是包含子元素的,需要在onLayout方法中继续确定子元素的位置。
借用一下LinearLayout的源码来分析:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
可以看见,VERTICAL
和HORIZONTAL
的方法是不一样的。看一下layoutVertical
的代码:
void layoutVertical(int left, int top, int right, int bottom) {
......
final int count = getVirtualChildCount();
......
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
......
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
源代码比较长,我们只截取一部分,其实逻辑很简单,就是遍历所有的子元素,并调用setChildFrame方法来确定各个子元素的位置,因为是Vertical的排列方式,所以childTop 的值不断增大,子元素会依次向下排列。
setChildFrame代码如下:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
只是调用子View的Layout方法而已。View的Layout方法上边已经介绍过了,如果是单一View,调用setFrame结束,如果是ViewGroup,会继续调用他的onlayout方法,这样一层一层向下确定各个View的位置。整个流程结束以后,所有的Layout过程就结束了。
可能大家也注意到一个问题,layout方法会接收四个参数,分别代表四个顶点的位置,而该方法是通过child.layout(left, top, left + width, top + height);来调用的,实际上,只有left和top两个值是确定的
另外两个顶点是通过宽和高来确定的,宽和高是这样获取的:
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
得到四个顶点的值之后,最终会传递到setFrame方法,setFrame的部分代码:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
这样看起来,view最终的尺寸就是Measure过程中确定的尺寸
如果我们改动一下代码,改变一下ViewGroup的setChildFrame或者view的layout方法:
child.layout(left, top, left + width+100, top + height+100);
或者
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r+100, b+100);
}
这样最终得到的View就会比Measure出来的View大了100的尺寸,在setFrame的时候,得到的四个坐标值也会大100,
看一下View的getwidth和getHeight代码:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
此时获取到的View最终宽和高就会比Measure出来的宽和高(getMeasuredWidth,getMeasuredHeight)要大100px了。
所以,Measure出来的尺寸,通常情况下是View的最终尺寸,实际上View的最终尺寸是在Layout阶段来决定的,它并不一定等于MeasureSpec的大小。
获取View的尺寸
上边我们提到了两个方法:getMeasuredWidth
,getWidth
,用来获取View的尺寸,我们知道,View的尺寸是通过Measure
和Layout
共同决定的,那么获取View的尺寸就很简单了,调用这两个方法就可以了。实际上并不是这么简单的。因为Activity的生命周期和View的绘制不是同步的。在onCreate
,onStart
,onResume
里简单的调用这两个方法,得到的结果是不确定的,因为View可能还没有绘制完成。那么怎样才能得到正确的View尺寸?
1.重写View的onFocusChanged
方法。
在View得到或者失去焦点的时候,该方法都会被调用。可以这么理解,既然View已经得到焦点了,那么它的宽和高必定已经准备好了。所以获取尺寸是完全可以的。所以我们可以重写该方法,在此处调用getMeasuredWith
方法。
2.利用view.post(runnable)
View在处理post的消息时,肯定已经初始化好了,所以利用此方法也能正确的获取到View尺寸。
3.ViewTreeObserver
它是view事件的一个观察者,用来监听ViewTree的各种事件,针对不同的事件它定义了许多不同的接口,可以利用onGlobalLayout
方法来获取View尺寸。
4.Activity的onPostCreate()
Draw过程
measure和layout的过程都结束后,接下来就进入到draw的过程了,源码如下:
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
}
可以看到,第一步是从第9行代码开始的,这一步的作用是对视图的背景进行绘制。这里会先得到一个mBGDrawable
对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable
的draw()
方法来完成背景的绘制工作。那么这个mBGDrawable
对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()
、setBackgroundResource()
等方法进行赋值。
接下来的第三步是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()
方法,那么onDraw()
方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。
第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()
方法又是一个空方法,而ViewGroup
的dispatchDraw()
方法中就会有具体的绘制代码。
以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView
或者ScrollView
,为什么要绘制滚动条呢?其实不管是Button也好,TextView
也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。
通过以上流程分析,相信大家已经知道,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察TextView
、ImageView
等类的源码,你会发现它们都有重写onDraw()
这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()
方法中,供给每个视图使用。
另外,view还有一个特殊方法:setWillNotDraw。默认情况下,view是不启用这个参数的,而viewGroup
会启用。因为单一View是肯定需要绘制自身的,而ViewGroup
只是作为单一View的载体,draw工作是交给子View的,它不需要绘制自身,也就是默认ViewGroup
是透明的,如果想让ViewGroup绘制自身,需要调用setWillNotDraw(false)
,来启用draw功能,然后重写的onDraw
方法才会起作用。