新闻资讯
View 的工作流程梳理——Measure流程
Measure流程
View 的Measure流程从ViewGroup 这个类的measureChildWithMargins方法开始,这个方法是ViewGroup需要测量子View大小时调用的,所有ViewGroup的子类都会调用这个方法来测量子View的大小。
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed 已经用了的大小 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); }
我们看到这里传的参数是child(子view)、parentWidthMeasureSpec(父view的宽度测量规格)、widthUsed(父View已经用掉的宽度)、parentHeightMeasureSpec(父view的高度测量规格)、widthUsed(父view已经用掉的高度)。
这里的提到的测量规格是个什么?这里涉及到一个概念叫MeasureSpec。 MeasureSpec是由一个32位的int变量组成的。它包含两个部分:
-
头两位,代表SpecMode,即测量的模式。一共有三种模式
- EXACTLY 精确模式,测量的大小确定。
- AT_MOST 由父view给定一个specSize,子view最大的大小不能超过这个specSize
- UNSPECIFIED 不限定子view的大小,用于系统内部测量,我们不用管。
然后在measureChildWithMargins方法里调用getChildMeasureSpec方法分别确定子view的宽度测量规格和高度测量规格。具体方法如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//子view还能够使用的大小(MeasureSpec的大小减去已经使用的大小)
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:
// 布局文件设置的具体的值如10dp 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.
//子view的大小最大不超过父view剩余的大小
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
![](https://user-gold-cdn.xitu.io/2020/2/24/17077ac03eed5f6d?w=399&h=204&f=jpeg&s=36492)
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} break;
}
//noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
可以看到,这个方法根据父view的specMode和子view的布局设置的大小参数来确定了子view的测量规格。可以的到一个子view的MeasureSpec确定规则的表,如下:
这里看到当子view的LayoutParams 为MATCH_PARENT 和 WRAP_CONTENT 时,子view的specSize是一样的,也就是说这两者的效果是一致的。但我们实际使用布局的时候感觉不是这样的呀,例如一个TextView高宽设置成wrap_content和设置成match_parent 的显示效果是不一样的,一个是有一定的大小,另一个是充满了父布局剩余的空间。那是因为TextView 的onMeasure方法做了特殊的处理,大小变成了内容的大小,而不是充满父view的剩余空间。所以我们在自定义view的时候需要注意到这个坑,解决的方式是重写onMeasure方法设定一个默认的大小。方法如下:override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val heigthSpecSize = MeasureSpec.getSize(heightMeasureSpec) if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, mHeight)
} else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, heigthSpecSize)
} else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mHeight)
}
} 复制代码
接上面的getChildMeasureSpec方法获取到子view的MeasureSpec之后就调用子view的measure方法来测量了,measure方法是final的,无法重写,在measure方法里会调用到onMeasure方法,如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
可以看到里面调用到了getDefaultSize,来获取默认的大小,并调用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) {
//如果是UNSPECIFIED模式,则是getSuggestedMinimumWidth和getSuggestedMinimumHeight方法获取的大小 case MeasureSpec.UNSPECIFIED:
result = size; break;
//如果是AT_MOST和EXACTLY模式,则是specSize case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY:
result = specSize; break;
} return result;
} 复制代码
再来看看getSuggestedMinimumWidth:
protected int getSuggestedMinimumWidth() {
//如果背景为空,则是则是view的mMinWidth,如果不为空则是背景最小宽度和view的mMinWidth中取一个最大值,这里的mBackground.getMinimumWidth()指的就是背景的原始宽度。 return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
由此view大致的测量流程是 父view通过getChildMeasureSpec确定子view的MeasureSpec,传递给子view,子view通过父view给的MeasureSpec参数来进行测量操作并设置测量的大小。如果子view还包含有其他的view,则会先测量下级的子view,就这样一直传递下去,等到所有的子view都测量完毕,才会确定下来自己的大小。 由于viewgroup没有实现onMeasure方法,是交给子类去实现的,这里就看一下LinearLayout是怎么实现的。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
可以看到是根据mOrientation来判断是横向测量还是纵向测量。就看一个measureVertical方法:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
...
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
int consumedExcessSpace = 0;
int nonSkippedChildCount = 0;
// See how tall everyone is. Also remember max width. for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i); if (child == null) {
mTotalLength += measureNullChild(i); continue;
} if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i); continue;
}
nonSkippedChildCount++; if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0; if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else { if (useExcessSpace) {
lp.height = LayoutParams.WRAP_CONTENT;
}
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight(); if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
得到水平方向最大的宽度
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
//所有子view高度总和
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,调用measureChildBeforeLayout方法,然后这个方法里面调用到了measureChildWithMargins方法,这就是最开始提到的那个方法,到了子view的测量流程,在这个方法里分别去调用子view的measure方法,让子view分别完成自己的测量,然后累加竖直方向的高度到mTotalHeight。而水平的测量则是遍历子view的到最大的maxWidth,然后会测量并设置自己的大小。对于竖直方向排列的LiearLayout,水平方向如果specMode是AT_MOST,则宽度为maxWidth,但是这个不能超过LiearLayout的父view的剩余空间,如果是EXACTLY,则是specSize。竖直方向的测量如果specMode是AT_MOST,则高度是所有子view高度总和,还要考虑margin,padding等等,如果是EXACTLY模式则也是specSize,具体可以看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的测量流程就介绍完了。
原文链接:https://juejin.im/post/5e53e031f265da573c0c778f
回复列表