Viewviewandroid开发

自定义View基础之View的绘制流程

2017-04-23  本文已影响100人  Cris_Ma

DecorView

在了解view的绘制流程之前,首先我们要知道一个DecorView的概念,什么是DecorView?

DecorView是整个界面的最顶层View,它的尺寸通常就是屏幕尺寸,也就是说,DecorView是充满屏幕的,它实际上是一个FrameLayout,又包含了一个子元素,LinearLayout,这个LinearLayout又包含两个FrameLayout,一个用来显示标题,一个用来显示内容。显示内容的FrameLayout,其ID为 android.R.id.content

我们在 Activity 中设置 Layout 时,用的方法是setContentView,指的就是这个content。参考下图

DecorView

View的绘制流程是从ViewRootperformTraversals开始的,它经过measure,layout,draw三个过程最终将View绘制出来。

performTraversals会依次调用performMeasureperformLayoutperformDraw三个方法,他们会依次调用measure,layout,draw方法,然后又调用了onMeasureonLayoutdispatchDrawonMeasure方法中,父容器会对所有的子View进行Measure,子元素又会作为父容器,重复对它自己的子元素进行Measure,这样Measure过程就从DecorView一级一级传递下去了。Layout和Draw方法也是如此。

我们关注的重点是onMeasure方法,它决定了所有View的尺寸。

MeasureSpec

MeasureSpec是一个32位 int 数值,它包含了两组信息。高两位代表SpecMode,低30位代表SpecSize

SpecMode指测量模式,有以下三个值:

说完这三种模式可能大家还是非常模糊,我们看一下MeasureSpec的源码,就一目了然了。
<pre>
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);
}
</pre>

MeasureSpec 内部封装了makeMeasureSpecgetModegetSize三个方法,方便我们对MeasureSpec数据进行处理。
那么MeasureSpec到底是怎样使用的呢?接下来我们就要看onMeasure方法了。

View的onMeasure方法以及MeasureSpec的获取

首先我们回顾一下View 的绘制流程,在上文中有一句黑体显示的话,意思就是所有的View测量都是从最顶层的DecorView开始的,我们就先看一下DecorView的Measure过程,它的MeasureSpec是怎样得到的。

ViewRootperformTraversals方法中可以看到:

<pre>
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
</pre>

DecorViewMeasureSpec是通过getRootMeasureSpec来得到的,它传入了两个参数,lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。
然后来看一下getRootMeasureSpec的代码:

<pre>
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;
}
</pre>

到这里,我们就知道了,DecorView作为最顶级的根View,它的MeasureSpec就是EXACTLY+WindowSize,也就是说他总是充满全屏的。

最顶层的DecorView尺寸确定好之后,下一步就是各个子View的Measure过程了,系统会从ViewGroup开始一级一级向下Measure。但是,我们又知道,一个View的大小同时还要受到父View的限制,它的大小是由本身的LayoutParams,和父View 的MeasureSpec共同决定的。

对于普通的View(非ViewGroup),它的Measure过程是由ViewGroup传递过来的,看一下ViewGroup的measureChildWithMargin方法就清楚了:

<pre>
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);
}
</pre>

可以看到,子View的Measure方法,实际上就是在这里调用的。在Measure之前,先获得子View的MeasureSpec,然后调用了child.measure。

子View的MeasureSpec又是通过getChildMeasureSpec来获取的,代码如下:

<pre>
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);
}
</pre>

这一段代码有点长,但是不难理解,该方法在调用时传入了三个参数:父View的MeasureSpecPadding(父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共同决定的:

Measure的流程到这里基本已经清楚了:从顶级View开始,先调用MeasureChild将父View的Spec传入,通过getChildMeasureSpec方法,获取到子View的Spec,然后通过child.measure(widhSpec,heightSpec)将得到的子View Spec传递过去。

View的Measure过程

通过上边的介绍,我们已经确定了View的MeasureSpec,接下来就是具体的Measure方法了,因为View的Measure最终调用的是onMeasure,我们只要看onMeasure方法就可以了:

<pre>
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
</pre>

代码很简单,就是一个setMeasuredDimension,用来设置View的尺寸,传入的参数,在上文已经介绍的很清楚了,从ViewGroup传递过来。但是他还调用了一个getDefaultSize方法,我们来看一下:

<pre>
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;
}
</pre>

这个方法逻辑也很简单,不考虑UNSPECIFIED的情况下,最终返回的结果一定是在ViewGroup里传递来的MeasureSpec

到这里,整个Measure过程就已经结束了。View的最终尺寸大小,遵循的就是上面我们得出的三条结论。要注意一点,当我们直接继承View时:

子View是WRAP_CONTENT,它的大小是父View的剩余空间,mode是AT_MOST

前两个都是没有问题的,但是WRAP_CONTENT,它的大小和MATCH_PARENT是一样的,也就是说WRAP_CONTENT会不起作用。解决这个问题也很简单,必须重写onMeasure方法,然后自定义一个默认的width和height,当传递过来的SpecModeAT_MOST时,设置尺寸为定义的宽高。

ViewGroup的Measure过程

相对于单一View来说,ViewGroupMeasure方法要复杂一些,因为它不仅仅是确定自己的尺寸,还要测量每一个子View,并得到他们的MeasureSpecViewGroup并没有重写Measure(final的)方法,也没有重写onMeasure方法,因为ViewGroup是抽象类,onMeasure方法需要在具体的实现类中去重写。

ViewGroup只是为测量子View添加了两个新的方法: measureChildren()measureChild()。代码如下

<pre>
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);
}

</pre>

首先去遍历子View,调用MeasureChild方法,在该方法中获取各个子View的MeasureSpec,然后调用子View的Measure方法,传递各个子View的大小。

Layout过程

Layout的作用,是ViewGroup为自己的子View指定位置,现在看一下View中的layout方法:

<pre>
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;
}
</pre>

关键在第11行,它最终调用了onLayout(changed, l, t, r, b)方法,来完成最终的Layout过程。
那么我们就看一下onLayout方法,你会发现是这样的:
View:

<pre>
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
</pre>

ViewGroup

<pre>
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
</pre>

View中是一个空方法,ViewGroup中是一个抽象方法。为什么呢?其实答案很简单。Layout过程是确定子元素在自己布局中的位置,view是不存在子元素的,所以是空方法,它只需要通过setFrame来决定自身的位置。而ViewGroup本身就是一个抽象方法,它的不同实现会有不同的Layout方式,所以在继承ViewGroup的布局中,必须指定自己的Layout方式,因此,ViewGroup中是一个抽象方法。

layout方法的大致流程如下:

首先通过setFrame来设置View的四个顶点位置,并保存起来。在Layout时,会先用setFrame方法来保存四个顶点的坐标,并进行判断,值如果发生了变化,就会调用onLayout方法来重新定位子元素。
如果是单一View,调用setFrame方法之后,其实就已经结束了,因为他没有子元素,onLayout方法是空的,调用onlayout方法没有任何意义。而对于ViewGroup来说,他是包含子元素的,需要在onLayout方法中继续确定子元素的位置。

借用一下LinearLayout的源码来分析:

<pre>
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);
}
}
</pre>

可以看见,VERTICALHORIZONTAL的方法是不一样的。看一下layoutVertical的代码:

<pre>
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);
}
}
}
</pre>

源代码比较长,我们只截取一部分,其实逻辑很简单,就是遍历所有的子元素,并调用setChildFrame方法来确定各个子元素的位置,因为是Vertical的排列方式,所以childTop 的值不断增大,子元素会依次向下排列。
setChildFrame代码如下:

<pre>
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
</pre>

只是调用子View的Layout方法而已。View的Layout方法上边已经介绍过了,如果是单一View,调用setFrame结束,如果是ViewGroup,会继续调用他的onlayout方法,这样一层一层向下确定各个View的位置。整个流程结束以后,所有的Layout过程就结束了。

可能大家也注意到一个问题,layout方法会接收四个参数,分别代表四个顶点的位置,而该方法是通过child.layout(left, top, left + width, top + height);来调用的,实际上,只有left和top两个值是确定的
另外两个顶点是通过宽和高来确定的,宽和高是这样获取的:

<pre>
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
</pre>

得到四个顶点的值之后,最终会传递到setFrame方法,setFrame的部分代码:

<pre>
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
</pre>

这样看起来,view最终的尺寸就是Measure过程中确定的尺寸
如果我们改动一下代码,改变一下ViewGroup的setChildFrame或者view的layout方法:

<pre>
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);
}
</pre>

这样最终得到的View就会比Measure出来的View大了100的尺寸,在setFrame的时候,得到的四个坐标值也会大100,
看一下View的getwidth和getHeight代码:

<pre>
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
</pre>

此时获取到的View最终宽和高就会比Measure出来的宽和高(getMeasuredWidth,getMeasuredHeight)要大100px了。
所以,Measure出来的尺寸,通常情况下是View的最终尺寸,实际上View的最终尺寸是在Layout阶段来决定的,它并不一定等于MeasureSpec的大小。

获取View的尺寸

上边我们提到了两个方法:getMeasuredWidthgetWidth,用来获取View的尺寸,我们知道,View的尺寸是通过MeasureLayout共同决定的,那么获取View的尺寸就很简单了,调用这两个方法就可以了。实际上并不是这么简单的。因为Activity的生命周期和View的绘制不是同步的。在onCreateonStartonResume里简单的调用这两个方法,得到的结果是不确定的,因为View可能还没有绘制完成。那么怎样才能得到正确的View尺寸?

1.重写View的onFocusChanged方法。
在View得到或者失去焦点的时候,该方法都会被调用。可以这么理解,既然View已经得到焦点了,那么它的宽和高必定已经准备好了。所以获取尺寸是完全可以的。所以我们可以重写该方法,在此处调用getMeasuredWith方法。

2.利用view.post(runnable)
View在处理post的消息时,肯定已经初始化好了,所以利用此方法也能正确的获取到View尺寸。

3.ViewTreeObserver
它是view事件的一个观察者,用来监听ViewTree的各种事件,针对不同的事件它定义了许多不同的接口,可以利用onGlobalLayout方法来获取View尺寸。

Draw过程

measure和layout的过程都结束后,接下来就进入到draw的过程了,源码如下:
<pre>
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;
}
}
</pre>
可以看到,第一步是从第9行代码开始的,这一步的作用是对视图的背景进行绘制。这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawabledraw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()setBackgroundResource()等方法进行赋值。

接下来的第三步是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。

第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroupdispatchDraw()方法中就会有具体的绘制代码。

以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。

通过以上流程分析,相信大家已经知道,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察TextViewImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。

另外,view还有一个特殊方法:setWillNotDraw。默认情况下,view是不启用这个参数的,而viewGroup会启用。因为单一View是肯定需要绘制自身的,而ViewGroup只是作为单一View的载体,draw工作是交给子View的,它不需要绘制自身,也就是默认ViewGroup是透明的,如果想让ViewGroup绘制自身,需要调用setWillNotDraw(false),来启用draw功能,然后重写的onDraw方法才会起作用。

上一篇下一篇

猜你喜欢

热点阅读