视图测量的入口在ViewRootImpl类,进行一次performTraversals的过程会实现测量、布局和绘制流程,从它的measureHierarchy方法开始,分析视图测量过程。
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth,
final int desiredWindowHeight){
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
boolean goodMeasure = false;
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
...
}
if (!goodMeasure) {//第二步
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()){
windowSizeMayChange = true;
}
}
return windowSizeMayChange
}
树形结构层次测量,入参desiredWindowXxx代表希望获取的窗体宽高。若第一次触发performTraversals方法,将其设置为屏幕像素,我使用的测试手机分辨率是1080x1920。
ViewRootImpl#performTraversals方法代码段。
final Rect mWinFrame; //ViewRootImpl类中定义
Rect frame = mWinFrame;
if (mFirst) {
...
DisplayMetrics packageMetrics = mView.getContext().
getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}else {
...
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
第一次时,继续relayoutWindow方法,frame初始化(0,0,1080,1920)。向mWidth与mHeight赋值。
ViewRootImpl#performTraversals方法代码段。
//设置mWidth、mHeight与mWinFrame区域相同。
if (mWidth != frame.width() || mHeight != frame.height()) {
mWidth = frame.width();
mHeight = frame.height();
}
所以,在下一次时,上面代码中mWidth与desiredWindowWidth相同,返回windowSizeMayChange是false,不会再执行relayoutWindow,除非窗体frame又发生了变化。
继续走代码,此时发现mHeight是1920,但是测量的DecorView的measureHeight是1794,再测一次。
继续回到measureHierarchy方法分析。LayoutParams布局参数,创建时,默认MATCH_PARENT,直接到第二步,getRootMeasureSpec方法根据desiredWindowXxx与模式(MATCH_PARENT)获取顶层视图的MeasureSpec。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
MeasureSpec两部分信息模式与数值,MeasureSpec传给子视图,根视图DecorView第一个子视图。模式根据MATCH_PARENT、WRAP_CONTENT判断。
EXACTLY模式:代表子视图对宽高的要求是确定的,windowSize。这里就是desiredWindowXxx,DecorView就是这种模式,大小与desiredWindowXxx一致。
AT_MOST模式:代表子视图对宽高的要求不确定,最大不能超过窗体提供的windowSize,传给子视图时,最大值限定为desiredWindowXxx。
ViewRootImpl构造方法mWidth和mHeight初始化为-1。
第一次performTraversals测量,getMeasuredWidth的值与mWidth不同,返回值windowSizeMayChange设置true。
第二次performTraversals测量,mWidth已初始化窗体frame的值,desiredWindowWidth也设置frame的值,最终测量值是相同的,返回值windowSizeMayChange设置false。
在ViewRootImpl类中的测量过程比较复杂,主要涉及根据窗体大小计算顶层视图的MeasureSpec,然后,通过顶层视图的measure方法进入树形结构测量。窗体区域由WindowSession#relayout方法远程访问Wms服务获取。
树形结构视图测量流程
从顶层视图开始,ViewRootImpl的performMeasure方法。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
}
}
测量时序图。

顶层视图DecorView的measure方法,它继承FrameLayout,但是FrameLayout和ViewGroup均未重写measure。因此,最终调用的是View的measure方法。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
分析一下View的measure方法,final修饰,说明系统不希望子类重写measure方法,它View类提供测量的一套确定步骤,某个视图一旦有测量需求,即会调用View#measure方法,步骤逻辑不可被改变,模板设计模式。调用onMeasure方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
子类可以通过重写onMeasure,实现满足需要的测量方法。容器视图和非容器视图的测量方法是不同的。容器视图包括子视图测量,因此特定视图的实际测量方法特定分析。若不重写,getDefaultSize获取默认测量值。建议值由系统背景Drawable和设置的最小宽高决定,一般开发都会重写此方法。
下面我们看一下顶层视图的#onMeasure方法,它直接调用父类的方法。看一下FrameLayout的onMeasure方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
//遍历子视图
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);
}
}
}
}
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
1.遍历子视图,执行非GONE子视图#measure方法。
2.若子视图是容器视图,再递归遍历它的子视图。
因此,下面整个树形结构的测量顺序图。

由上图可知,测量顺序是
顶层视图—>>节点1—>>节点4—>>节点5—>>节点8—>>节点9—>>节点2—>>节点6—>>节点3—>>节点7
- 遍历子视图后,执行了几次measureChildWithMargins方法。记录子视图的最大测量宽高,FrameLayout布局的子视图是层叠覆盖的,最大测量值可以反映出子视图占用区域。最后,根据这个最大值maxXxx和传入的MeasureSpec计算父容器Size(由resolveSizeAndState方法计算)。这里计算的就是DecorView的Size。
EXACTLY模式:DecorView是MATCH_PARENT,属于这种模式,根据传入的XxxMeasureSpec计算出Size,确定DecorView的宽高值,其他FrameLayout布局若设置WRAP_CONTENT,即AT_MOST模式,需要比较Size和maxXxx。
AT_MOST模式:由子视图占据区域最大值计算。 - setMeasuredDimension方法,将测量值交给视图内部的mMeasuredXxx,代表测量宽高。
子视图MeasureSpec计算
根据子视图LayoutParams记录的宽高和父容器对他的区域限制规则一,在ViewGroup的measureChildWithMargins方法,计算MeasureSpec。
MeasureSpec一共32位,模式+数值。高两位xx代表模式,低30位代表数值。
模式:
EXACTLY:视图精确大小,也是最终测量值,固定值和match_parent是这种模式。
AT_MOST:视图大小不能大于父视图指定的值。
UNSPECIFIED:无限制。
上面FrameLayout的onMeasure方法中,测量子视图,需要计算子视图的MeasureSpec,在调用子视图measure方法时,传递给它。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//计算子视图MeasureSpec。
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);
}
首先,读取子视图LayoutParams的宽高,子视图margin+父容器padding,然后,根据父视图模式,一起计算childXxxMeasureSpec。
以下几种情况。
父视图模式是EXACTLY
1:子视图的lp.width是大于0的精确值,子视图模式EXACTLY,SpecSize是lp.width。
2:子视图的lp.width是MATCH_PARENT,与父视图一致,子视图模式EXACTLY,SpecSize设置是父MeasureSpec获取的Size。
3:子视图的lp.width是WRAP_CONTENT,子视图模式AT_MOST,SpecSize设置为父MeasureSpec获取的Size,表示子视图不确定,但最大不可超过Size。
父视图模式是AT_MOST
1:子视图的lp.width是大于0的精确值,子视图模式EXACTLY,SpecSize是lp.width。
2:子视图的lp.width是MATCH_PARENT,与父视图一致,但是父视图的Size不确定,只有一个最大Size,所以子视图模式AT_MOST,SpecSize设置父MeasureSpec获取的最大Size,即与父保持一样的最大Size。
3:子视图的lp.width是WRAP_CONTENT,子视图模式AT_MOST,Size设置为父MeasureSpec获取的Size,表示子视图不确定,最大不可超过Size。
总结
1.从顶层视图开始,每个视图的测量入口是View的measure方法。
2.容器视图和叶子视图的测量方式不同,需要考虑子视图。
3.由父视图根据自己的MeasureSpec和子视图LayoutParams计算子视图MeasureSpec。
4.重写onMeasure方法测量具体视图。
5.在测量中,MeasureSpec层层传递,贯穿始终。
任重而道远
网友评论