Android View 测量流程(Measure)源码解析
前言
任何View要显示在屏幕上,都需要经过测量(measure)、布局(layout)、绘制(draw)三大流程,measure负责确定View的大小,layout负责确定View的位置,draw负责绘制View的内容。这篇我们就先来通过源码分析一下View的测量(measure)流程。源码基于Android API 21。
测量由ViewRootImpl#performTraversals开始
在[由setContentView探究Activity加载流程]中,我们提到View三大工作流程是从ViewRootImpl#performTraversals方法开始的,其中performMeasure、performLayout、performDraw方法分别对应了View的测量、布局、绘制。如下:
private void performTraversals() {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
performDraw();
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
可以看到,在performMeasure
方法中调用了 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
,这里的mView其实是DecorView,它并没有重写measure
方法,因为View#measure方法被final修饰,不可被重写。因此我们看下View#measure方法。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
······
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
······
}
在View#measure中又调用了onMeasure(widthMeasureSpec, heightMeasureSpec)
方法。并且DecorView重写了onMeasure
方法,在DecorView#onMeasure方法中主要是
进一步确定自己的widthMeasureSpec
、heightMeasureSpec
,并调用super.onMeasure(widthMeasureSpec, heightMeasureSpec)
即FrameLayout#onMeasure方法。
ViewGroup 的Measure过程
ViewGroup是一个抽象类,它并没有重写onMeasure方法,具体的实现交由子类去处理,如LinearLayout、RelativeLayout、FrameLayout,这是因为不同ViewGroup的布局特性和实现细节各异,无法统一处理。在这里我们以FrameLayout为例分析ViewGroup的测量过程。
FrameLayout#onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();//获取子View的数量
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
getPaddingLeftWithForeground() - getPaddingRightWithForeground() -
lp.leftMargin - lp.rightMargin,
MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
if (lp.height == LayoutParams.MATCH_PARENT) {
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
getPaddingTopWithForeground() - getPaddingBottomWithForeground() -
lp.topMargin - lp.bottomMargin,
MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
View的Measure过程
先来看下View中的measure方法
/**
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
······
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
······
}
可以看到此方法是final的,不可以被重写,并且注释中也表明实际的测量工作是在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;
}
这个方法是用来确定View最终测量大小的。由View自身的measureSpec(测量规则)获取specMode(测量模式)和specSize(测量大小),当specMode为AT_MOST、EXACTLY这两种模式时,返回的大小就是此View的测量大小。当specMode为UNSPECIFIED时,返回大小是getSuggestedMinimumWidth和getSuggestedMinimumHeight这两个方法的返回值。
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
这两个方法原理是一样的,这里选择getSuggestedMinimumWidth来分析。
首先判断View有没有设置背景,如果没有,则返回mMinWidh,mMinWidth对应于android:minWidth这个属性对应的值,如果没有指定这个属性值,则默认为0。
如果设置了背景,则返回mMinWidth和 getMinimumWidth返回值之间的最大值。
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
在getMinimumWidth方法中,getIntrinsicWidth得到是背景的原始高度,如果背景原始高度大于0,则返回背景的原始高度,否则返回0。
至此getDefaultSize方法具体返回值就确定了,View最终的测量大小也就确定了。
注意:
当自定义View时,如果直接继承了View,在必要的时候需要重写onMeasure方法并在其中对wrap_content的情况进行处理。这是为什么呢?
从getDefaultSize方法中可以看到,当specMode为AT_MOST时,返回的大小为specSize,这个specSize其实是父View的大小,如果这里不明白,可以参考Android中View测量之MeasureSpec中普通View的MeasureSpec创建过程。
当确定了View的最终测量大小后,会把测量的宽高作为参数传入setMeasuredDimension方法。
/**
* <p>This method must be called by {@link #onMeasure(int, int)} to store the
* measured width and measured height. Failing to do so will trigger an
* exception at measurement time.</p>
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
......
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
......
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
看注释就可以明白,其实这个方法就是用来存储测量的宽和高,并且如果没有设置这个方法,测量时将会产生异常。
其内部通过setMeasuredDimensionRaw方法会将View的最终测量宽高赋值给变量mMeasuredWidth,mMeasuredHeight进行存储。
另外我们可以通过getMeasuredWidth,getMeasuredHeight方法得到mMeasuredWidth、mMeasuredHeight对应的值,即View的最终的测量宽高。如下:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
至此View的measure过程就分析完了,不过这里的View指的是像TextView、ImageView、Button这中不能含有子View的View,如果是一个ViewGroup类型的View,像LinearLayout、RelativeLayout、FrameLayout可以包含多个子View,那么它的measure过程又是怎样的,我们接着分析ViewGroup的measure过程。
ViewGroup的Measure过程
ViewGroup是一个抽象类,它并没有重写onMeasure方法,具体的实现交由子类去处理,如LinearLayout、RelativeLayout、FrameLayout,这是因为不同ViewGroup的布局特性和实现细节各异,无法统一处理。对于ViewGroup,它不但要测量自身大小,还要去测量每个子View的大小,我们先来分析一下ViewGroup中measureChildren方法,看它是如何测量子View的,如下:
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);
}
}
}
这个方法比较清晰,首先获取子View的数量,然后遍历子View,最后调用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方法中先获取了子View的布局参数,然后通过父View的MeasureSpec、和子View的布局参数经过getChildMeasureSpec方法创建子View的MeasureSpec,这个方法已经在Android中View测量之MeasureSpec中做了详细介绍,当确定了子View的MeasureSpec后,就可以测量子View的大小了,接着调用子View的measure方法。至此,测量过程就从父View传递到了子View了。如果子View也是个ViewGroup将会重复ViewGroup的Measure过程,如果子View是单个View,将执行View的Measure过程,如此反复,就完成了整个View树的测量。
那ViewGroup具体是怎么确定自身最终测量大小的呢,下面我们以LiearLayout为例,来分析一下其onMeasure方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
我们以竖直方向为例,来看下measureVertical方法的主要内容。
...
for (int i = 0; i < count; ++i) {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
...
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
...
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));
...
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
...
}
...
// 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;
...
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
上述代码做了如下工作:
- 遍历LinearLayout中的所有子View,并调用measureChildBeforeLayout方法,这个方法内部会调用measureChildWithMargins(这个方法和上文提到的measureChild方法原理一样,只不过在创建子View的MeasureSpec的时候考虑了子View的margin)方法,接着在measureChildWithMargins内部会执行子View的measure方法来测量子View的大小。
- 当子View测量完毕后,就可以通过getMeasuredWidth方法获取子View的测量宽度,并且用变量maxWidth来存储所有子View中(状态为View.Gone的除外)child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + mPaddingLeft + mPaddingRight 中的最大值;
通过getMeasuredHeight方法获取子View的测量高度,并用变量mTotalLength来存储所有子View(状态为View.Gone的除外)的childHeight + lp.topMargin +lp.bottomMargin + getNextLocationOffset(child) + mPaddingTop + mPaddingBottom之和; - 最后执行resolveSizeAndState方法最终确定自身测量大小。
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
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:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result | (childMeasuredState&MEASURED_STATE_MASK);
}
- 水平方向上,如果LinearLayout的宽度为match_parent 或具体的数值(MeasureSpec.EXACTLY),则LinearLayout的宽度就为自身测量规则确定的大小,如果LinearLayout的宽度为wrap_parent(MeasureSpec.AT_MOST),则LinearLayout的高度就为所有子View中占用水平空间最大的那个View所占空间大小加上LinearLayout自身的水平方向上的padding,但是大小仍然不会超过父View的宽度。
- 在竖直方向上,如果LinearLayout的高度为match_parent 或具体的数值(MeasureSpec.EXACTLY),则LinearLayout的高度就为自身测量规则确定的大小,如果LinearLayout的高度为wrap_parent(MeasureSpec.AT_MOST),则LinearLayout的高度就为所有子View占用的空间大小加上LinearLayout自身的竖直方向上的padding,但是大小仍然不会超过父View的高度。
以上就是ViewGroup的measure过程。
至此,measure篇就讲完了,真心希望对各位猿宝宝在measure过程的理解上有所帮助!!!
若文中有错误或表述不当的地方还望指出,多多交流,共同进步。