View 的流程主要是指 measure、layout、draw 这三大流程,其中 measure 确定 View 的测量宽/高,layout 确定 View 的最终宽/高和四个顶点位置,而 draw 则将 View 绘制到屏幕上
MeasureSpec
我们先来简单了解一下 MeasureSpec ,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格,之所以很大程度上是因为这个过程还受父容器影响,因为父容器影响 View 的 MeasureSpec 的创建过程。再测量过程中,系统会将 View 的 LayoutParam 根据父容器所施加的规格转换成对应的 MeasureSpec ,然后根据这个 MeasureSpec 来测量出 VIew 的宽高,但是 测量宽高 不一定等于 最终宽高,至于为什么?看后面的分析。
MeasureSpec 是一个 32 位 int 值,高两位代表 SpecMode,底 30 位代表 SpecSize;SpecMode 是值测量模式(AT_MOST、EXACTLY、UNSPECIFIED),SpecSize 是指在某种测量模式下的规格大小(如:AT_MOST 模式的 wrap_content、EXACTLY 模式的 match_parent 和具体数值)。看一下 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) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
...
MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配,为了方便操作,将提供了打包和解包方法。SpecMode 和 SpecSize 也是一个 int 值,一组 SpecMode 和 SpecSize 可以打包成一个 MeasureSpec,而一个 MeasureSpec 还可以解包还原出原来的 SpecMode 和 SpecSize。
SpecMode 有三类,其含义如下:
MeasureSpec.UNSPECIFIED: 在此模式下,父容器不对子 view 的大小做限制,一般用于系统内部,或者 ListView ScrollView 等滑动控件。
MeasureSpec.AT_MOST: 在此模式下,父容器未能检测出子 view 的大小,但指定了一个最大大小 spec size,子view的大小不能超过此值。具体是什么值要看不同的 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。
MeasureSpec.EXACTLY: 在此模式下,父容器已经检测出子 view 所需要的精确大小,这个时候,view 的测量大小就是通过 getSize 得到的数值。它对应于 LayoutParams 中的 match_parent 和 具体的数值这两种模式。
measure 过程
measure 过程要分情况来看,如果只是一个原始的 View, 那么通过 measure 方法就完成其测量过程,如果是一个 ViewGroup,除了完成自己的测量外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程。
对于普通的 view 来说,这里指的是我们布局中的 View,View 的 measure 过程由 ViewGroup 传递而来,先看一下 ViewGroup 的 mrasureChildWithMargins 方法:
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);
//调用子类的measure
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
ViewGroup 的 mrasureChildWithMargins 方法会调用子元素的 measure,但在调用子元素前会通过getChildMeasureSpec 方法获取创建子元素的 MeasureSpec,并且子元素的 MeasureSpec 创建和父容器的 MeasureSpec 和 子元素本身的 LayoutParams 有关;这个可以看一下 getChildMeasureSpec 方法就知道了:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父容器的 specMode 和 specSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//获取子元素的可用size,即:父容器size - 已使用的空间size
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 = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
根据上述方法不难理解,它的主要作用是根据父容器的 MeasureSpec 同时结合子元素 View 本身的 LayoutParams 来确定子元素的 MeasureSpec。
接着上面的 measureChildWithMargins 方法带着创建好的子元素的 MeasureSpec 调用 子元素 的measure 方法,接着再调用子元素的 onMeasure 方法,我们来看一下 onMeasure 方法的实现:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension 方法会设置 View 宽高的测量值,因此我们只需要看 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;
}
可以看出这个方法很简单,对于我们来说,我们只需要看 AT_MOST 和 EXACTLY 这两种情况,简单的讲,就是 getDefaultSize 方法返回的大小就是 measureSpec 中的 specSize,而这个 specSize 就是 View 测量后的大小,这里要区分一下 View 的最终大小是在 Layout 阶段确定的,但是几乎大部分情况下 View 的测量大小和最终的大小是一致的。
从 getDefaultSize 方法的实现来看,View 的宽高是由 specSize 决定的,所以我们可以得出如下结论:直接继承 View 的自定义控件需要重写 onMeasure 方法并需要设置 wrap_content 情况下的自身大小,否则在布局中使用 wrap_content 和设置 match_content 的情况下是一样的;为什么呢?从上述 ViewGroup 的measureChildWithMargins 方法中创建子元素的 MeasureSpec 的 getChildMeasureSpec 方法中知道,如果使用 wrap_content,那它的 specMode 就是 AT_MOST 模式,在这种模式下,它的宽高等于父容器当前剩余空间的大小。如下代码所示:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
...
//获取子元素的可用size,即:父容器剩余空间
int size = Math.max(0, specSize - padding);
...
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;
}
...
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
很显然,这和 match_parent 的效果完全一致。那如何解决这个问题呢?如下所示:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(defaultWidth, defaultHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(defaultWidth, heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, defaultHeight);
}
}
只需要给 View 指定一个默认的宽高(defaultWidth、defaultHeight),并在 wrap_content 时设置此宽高即可。对于非 wrap_content 情形,我们沿用系统的测量值即可。
ViewGroup 有两个测量 View 的方法,一个是刚刚我们说的 measureChildWithMargins 方法,一个是measureChild 方法,这两个方法区别是,前者考虑了 padding 和 margin,而后者只考虑了 padding。
在 ViewGroup 里有个 measureChildren 方法通过遍历子 View 调用 measureChild ,而 measureChild 方法测量生成 MeasureSpec ,再调用子 View 的 measure;如下代码所示:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
//遍历所有的子View,如果不为 GONE 的话就调用 measureChild 创建子View 的 MeasureSpec 后调用子 View 的measure
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
//创建子View 的 MeasureSpec 后调用子 View 的 measure 方法
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
//这里的 getChildMeasureSpec 方法上面已经分析过了
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
ViewGroup 是个抽象类,没有重写 onMeasure 方法,其测量过程的 onMeasure 需要各个子类根据情况去具体实现,比如 LinearLayout、RelativeLayout等等。为什么 ViewGroup 不像 View 一样对其 onMeasure 方法做统一实现呢?那是因为不同的 ViewGroup 子类有不同的布局特性,这就导致他们的测量细节不同,因此 ViewGroup 无法做统一实现。
LinearLayout 的测量过程大致如下:(竖直情况)
系统会遍历子元素并对每个子元素执行 measureChildBeforeLayout 方法,这个方法内部会调用子元素的 measure 方法,这样各个子元素就开始依次进入 measure 过程,并且系统会通过 mTotalLength 变量存储 LinearLayout 在竖直方向的初步高度,每测量一个子元素,mTotalLength 就会增加,增加的部分主要包括子元素的高度以及子元素在竖直方向上的 margin 等,当子元素测量完毕后,LinearLayout 会测量自己的大小,测量的方式和 View 的测量方式差不多,因为总的高度都确定了。但要记得的是如果 ViewGroup 布局中采用的是 wrap_content,那么它的高度是所有子元素所占的高度总和,但是,仍然不能超过它的父容器的剩余空间。
需要注意的是,在某种情况下,系统可能需要多次 measure 才能确定最终的测量宽高,在这种情形下,在 onMeasure 方法中拿到的测量宽高很可能是不准确的,一个比较好的习惯是在onLayout 方法中去取 View 的宽高。
Layout 过程
Layout 的作用是 ViewGroup 用来确定子元素的位置的,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 Layout 方法, 在 layout 方法中 onLayout 方法又会被调用。
即 layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子 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;
//调用 setFrame 方法确定该 view 的位置
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//调用该方法确定子 View 的位置
onLayout(changed, l, t, r, b);
...
}
layout 方法大致流程如下:
- 通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop 和 mBottom这四个值,View 的四个顶点一旦确定,那么 View 在容器中的位置也就确定了。
- 接着会调用 onLayout 方法,这个方法的主要用途是父容器确定子元素的位置,和 onMeasure 方法类似,onLayout 的具体实现同样和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法
我们来看一下View的 setFrame 方法和 onLayout 方法:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
...
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
...
// 确定 View 的四个顶点
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
}
return changed;
}
// 没错,View 并没有实现该方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
我们来分析一下 LinearLayout 的 onLayout 方法:
@Override
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);
}
}
// 竖直布局
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) {
//获取子 view 的宽高,记住这里
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
...
// 是否添加分割线的高度
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
//确定当前 View 的top位置,即将前面的所有 View 的总高度 加 上当前子 View 的 topMargin
childTop += lp.topMargin;
// 设置该 View 的最终位置,该方法直接调用 View#setFrame 方法
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
// 累加总高度,为下一个 View 做准备
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
// 该方法直接调用 View#setFrame 方法
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
这样父元素在 layout 方法中完成自己的定位后,就通过 onLayout 方法去调用子元素的 layout 方法,然后子元素又会通过 layout 方法来确定自己的位置,这样一层一层地传递下去就完成了整个 View 树的 Layout 过程
前面我们提到 : onMeasure 方法中拿到的测量宽高很可能是不准确的,一个比较好的习惯是在onLayout 方法中去取 View 的宽高。 为什么这么说呢?我们先来看一下 getMeasuredWidth() 、getMeasuredHeight() 和 getWidth()、getHeight() 代码:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
这里结合上面 LinearLayout 的 layoutVertical 方法代码来看:
//获取子 view 的宽高,记住这里
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
可以知道getWidth() 和 getHeight() 方法的 mLeft、mRight、mTop、mBottom 这四个变量的由来,它们返回的宽高刚好是 View 的测量高度。经上述分析,我们可以回答这个问题了:在 View 的默认实现中, View 的 测量宽高 和 最终宽高 是相等的,只不过 测量宽高 形成于 View 的 measure 过程, 而 最终宽高 形成于 View 的Layout 过程,就是两者的时机不同,就因为两者的时机不同才会在某种情况下导致这两个宽高不同,不过在日常开发中,我们可以认为 View 的 测量宽高 就等于 最终宽高。
下面举例说明导致这两个宽高不同的一个特殊例子:
public void layout(int l, int t, int r, int b){
super.layout(l, t, r+100, b+100);
}
上述的例子会导致在任何情况下 View 的最终宽高 总是比 测量宽高大 100px。
draw 过程
draw 过程就比较简单了,它的作用是将 View 绘制到屏幕上。View 的绘制过程遵循如下几步:
- 绘制背景 background.draw (canvas)
- 绘制自己(onDraw)
- 绘制 children (dispatchDraw)
- 绘制装饰(onDrawScrollBars)
这通过 draw 源码可以看出来:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
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);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}
View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历调用所有子 View 的draw 方法,这样 draw 事件就一层一层地传递下去了。View 有一个特殊的方法 setWillNotDraw,先看一下它的源码:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
从注释中可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标志位为 true 后,系统会进行相应的优化。默认情况下,View 没有启用这个标记,但是 ViewGroup 会默认启用这个优化标记位。这个标记对实际开发的意义是:当我们的自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续优化,当然,当明确知道 ViewGroup 通过 onDraw 来绘制内容时,我们也可以显示地关闭 这个标记位。
End…
本文主要来自于《Android 开发艺术探索》和自己的理解