view的测量模式有如下三种:
- EXACTLY:精确的,确切的知道大小值,如在控件中设置xxdp,或者设置match_parent
- AT_MOST:尽可能大,如在控件中设置wrap_content
- UNSPECIFIED:系统自带的一种,一般只是系统使用,自定义控件使用比较少
下面通过LinearLayout来分析这些模式,通过绘制流程可以知道测量是在 onMeasure 方法中实现:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//判断 LinearLayout 的 Orientation 是水平还是垂直
//我们分析垂直的情况,水平的也差不多
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
//总高度
mTotalLength = 0;
//最大宽度
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;
//获取子view的个数
final int count = getVirtualChildCount();
//获取测量模式
//注意,这个widthMeasureSpec 是通过父布局传过来的
//说明自身view的测量模式是在父布局中生成的
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//循环遍历所有的子view
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//子view为null跳过
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
//判断子view是否设置成GONE
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
//.......
//获取子view的LayoutParams
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//获取权重
totalWeight += lp.weight;
//判断子view的高度是否设置成0并且设置了weight
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
//如果当前LinearLayout设置的是EXACTLY,并且 useExcessSpace 为true
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
//设置 skippedMeasure 为true,后面在测量子 view
skippedMeasure = true;
} else {
if (useExcessSpace) {
//如果子 view 的height为0 并且设置了weight,将子 view的height设置为WRAP_CONTENT
lp.height = LayoutParams.WRAP_CONTENT;
}
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
//测量子 view,就是调用子view的 onMeasure 方法
//这个后面分析
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
//获取子 view的测量后高度
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
//将子 view height 设置为0
lp.height = 0;
consumedExcessSpace += childHeight;
}
//定义一个中间变量,保存上一个子 view的高度
final int totalLength = mTotalLength;
//将 mTotalLength 设置为子 view的高度+topMargin+bottomMargin
//totalLength 为前一个子 view的高度,所以去最大值,应为上一个子 view的高度+当前子 view高度+topMargin+bottomMargin,是一个累加过程
//这样,就将所有的子 view高度累加到一块
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
//........
//获取左右的Margin,去所有子 view的最大宽度为maxWidth
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//......
// 最大高度加上自身的Padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// 检测计算出来的高度和默认高度,取最大值
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// 根据模式,得到最后的高度
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
//.....
//设置当前LinearLayou的宽高,也就是设置 mMeasuredWidth 和 mMeasuredHeight 的值,到此LinearLayou的测量就完成
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
}
先来看 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;
//判断当前的测量模式,根据测量模式来分别判断最后取值是取 specSize 还是传过来的size
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);
}
根据判断当前的测量模式,根据测量模式来分别判断最后取值是取 specSize 还是传过来的size其中 specSize 是父布局传过来的,size 是自身测量得到的
那么父布局传过来的 specSize 是怎么计算呢?回到 measureChildBeforeLayout 方法中,假如一个LinearLayou中包裹一个LinearLayou,来看
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
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);
//调用子 view的measure方法,最后也会调用到 onMeasure 方法中
//这个在绘制流程中分析过
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
通过上面的方法可以知道子 view的MeasureSpec 是根据当前的MeasureSpec和子 view的LayoutParams决定的
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) {
// 如果当前的为EXACTLY
case MeasureSpec.EXACTLY:
//如果子 view有实际大小
if (childDimension >= 0) {
//将resultSize置为子 view实际大小
resultSize = childDimension;
//将子 view的模式置为EXACTLY
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//如果子 view为MATCH_PARENT
//将resultSize 设置为当前view 的大小
resultSize = size;
//将子 view的模式设置为EXACTLY
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//如果子 view为WRAP_CONTENT
//将resultSize 设置为当前view 的大小
resultSize = size;
//将子 view的模式设置为AT_MOST
resultMode = MeasureSpec.AT_MOST;
}
break;
//.....
//根据resultSize 和 resultMode 生成MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面子分析了一个,其他的模式都差不多,自己分析即可。所以MeasureSpec 是由父布局一层一层的往下传递,最后当最底层view测量出宽高之后,再一层一层往上传递。那么最顶层的view的MeasureSpec是怎么来的呢?
通过view的绘制流程可以知道,view开始绘制是在 ViewRootImpl 的 performTraversals 方法中:
public final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
private void performTraversals() {
WindowManager.LayoutParams lp = mWindowAttributes;
//.....
//其中lp.width为LayoutParams.MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//测量流程开始,最后会调用到 onMeasure 方法中
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
//得到对应的MeasureSpec
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
到此,从ViewRootImpl得到最顶层的 MeasureSpec ,然后在根据子 view的 LayoutParams 来得到子 view的MeasureSpec,最后这样一层一层传递下去,最终得到所有的控件的宽高。
网友评论