View 工作流程
View 的工作流程包括 Measure、Layout、Draw 三个过程。其中 Measure 测量 View 的宽高,Layout 确定 View 最终宽高和四个顶点的位置,Draw 将 View 绘制到屏幕上。
Measure
Measure 过程分为两种情况:原始 View 和 ViewGroup。如果是原始 View,则在 measure 方法中完成测量过程。如果是 ViewGroup,除了完成了自己的测量过程,还会遍历并调用所有子元素的 measure 方法,然后子元素中再递归执行该流程。
View 的 Measure 过程
View 的 Measure 由 measure 方法完成,measure 方法中会调用 onMeasure 方法,来看 onMeasure 方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasureDimension 方法会设置 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 测量后的大小。这里说的测量后的大小,指的是 Measure 过程中确定的大小,而最终的大小在 Layout 中确定,但大部分情况中,View 的测量大小与最终大小相等。
UNSPECIFIED 的情况适用于系统内部测量过程。这种情况 getDefaultSize 返回的是传入的第一个参数,即为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 两个方法的返回值
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth,
mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight,
mBackground.getMinimumHeight());
}
从 getSuggestedMinimumWidth 中可以看到,当 View 没有设置背景,那么宽度为 mMinWidth,mMinWidth 对应于 android:minWidth 这个属性值,如果这个属性不指定,则默认为 0。当 View 指定了背景,则宽度为 max(mMinWidth, mBackground.getMinimumWidth()),于是转到 Drawable 中的 getMinimumWidth 方法
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
getMinimumWidth 返回的是 Drawable 的原始宽度(待补充),如果没有就返回 0。getSuggestedMinimumHeight 原理是一样的,这里不重复。
从以上可知,当 View 没有设置背景,那么 getSuggestedMinimumWidth 返回 android:Width 的属性值,如果值没有指定,返回 0。当 View 设置了背景,那么返回 android:minWidth 和背景最小宽度这两者中的最大值。getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 的返回值就是 UNSPECIFIED 情况下的测量宽高。
在 getDefaultSize 方法中可以看到,View 的宽高由 SpecSize 指定,所以直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则使用 wrap_content 就等于 match_parent。原因是,当布局使用 wrap_content,SpecMode 为 AT_MOST 模式,根据 View 工作原理(一)中普通 View 的 MeasureSpec 创建规则可知,这种情况下 View 的 SpecSize 为 parentSize,而 parentSize 就是当前父容器的剩余大小,即 View 的宽高等于父容器的剩余大小,这就导致了使用 wrap_content 相当于 match_parent。
解决方法是,当使用 wrap_content 时设置一个默认内部宽高,非 wrap_content 则使用系统的测量宽高。
ViewGroup 的 Measure 过程
在 ViewGruop 中,除了完成自己的 Measure 过程,还会遍历调用子元素的 measure 方法,所有子元素再递归执行这个过程。由于 ViewGroup 是一个抽象类,它没有重写 View 的 onMeasure 方法,而是提供了 measureChildren 方法
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);
}
}
}
可以看到,它会对每一个子元素进行 measure,再看 measureChild 方法
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);
}
measureChild 取出子元素的 LayoutParams,再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,最后将 MeasureSpec 传给 View 的 measure 方法测量。getChildMeasureSpec 方法在 View 工作原理(一)中有分析。
由于 ViewGroup 是抽象类,所以测量过程的 onMeasure 方法需要子类去实现,下面以 LinearLayout 的 onMeasure 方法为例,来继续分析 ViewGroup 的 Measure 过程。
先看 LinearLayout 的 onMeasure 方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
接着看 measureVertical 方法,由于太长,省略了部分代码
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
...
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
代码中遍历了子元素并对每个都执行了 measureChildBeforeLayout 方法,该方法内部会调用子元素的 measure 方法,并且还会通过 mTotalLength 变量来存储 LinearLayout 在竖直方向的初步高度,其中主要是子元素的高度和子元素在竖直方向上的 margin 值。当子元素测量完毕,LinearLayout 会测量自己的大小。
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
对竖直方向的 LinearLayout 来说,水平方向的测量与 View 测量过程相同,竖直方向上,当布局中使用的是 match_parent 时,它与 View 测量过程一致,即高度为 specSize。如果是 wrap_content,那么高度是不超过父容器剩余空间下,所有子元素所占用的高度加上其他竖直方向的 padding。resolveSizeAndState 方法如下
public static int resolveSizeAndState(int size, int measureSpec,
int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
经过以上的过程,View 的 Measure 过程就完成了。这时可以通过 getMeasuredWidth 和 getMeasuredHeight 方法得到 View 的测量宽高。