深挖View测量流程之FrameLayout源码详解
背景:在android应用开发中,接触的最多的应该是android中各种各样的控件,或者自定义的各种控件了。应用层开发人员基本上每天都在和控件打交道,但是虽然我们经常在用,但是我们很少去关注源码实现,只有出现问题或者需要实现一些原生控件不能满足的需求的时候,我们可能才去了解View的原理和实现。既然View使用如此频繁,那我们就更需要去了解,这样才能在开发工作中得心应手!
我们都知道View有三大流程,分别是
- Measure 测量。
- Layout 布局。
- Draw 绘制。
我们今天就先从第一步Measure测量开始查看源码,看看里面原理是什么?都做了些什么操作?
好了直接上手分析,首先我们在ViewGroup源码分析那篇文中,我们知道Activity的顶层View是DecorView,而DecorView的本质其实是一个继承自FrameLayout的View。所以既然是继承自FrameLayout,那么我们就以FrameLayout为切入点。
前置过程为:ViewRootImpl.java 中的performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);方法。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
performMeasure()方法里面 直接调用了View的measure()方法,measure()方法里面又调用了onMeasure(widthMeasureSpec, heightMeasureSpec); 这里我们找到了onMeasure()方法。因为在FrameLayout里面重写了这个方法,所以我们去FrameLayout里面看看实现逻辑。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/** 因为Window里面第一层是FrameLayout,所以先看FrameLayout的onMeasure()方法 */
/** 获取孩子数量 */
int count = getChildCount();
/**
* 如果当前view的父view施加的宽度或者高度模式有一个不是 精确模式 EXACTLY
* 那么就需要把MatchParent的子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);
/** 如果孩子不是Gone状态,执行以下逻辑 */
if (mMeasureAllChildren || child.getVisibility() != GONE) {
/** 子view测量 */
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);
/**
* 保存的是所有子view测量之后的宽高信息,有点类似 childState += XXX 的意思,
* 只不过这里具体是通过 按位与 和 按位或 运算的,不过可以理解为 += 的意思。
* 就是说全部运算后保存在 childState 这个局部变量里面。
*
* */
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));
/** 孩子宽度或者高度中有Match-Parent属性的View集合 */
count = mMatchParentChildren.size();
if (count > 1) {
/**
* 如果当前FrameLayout 不是精确模式,那么只要子view的宽度或者高度中有MATCH_PARENT
* 那么需要单独进行二次测量,以确保给子view MATCH_PARENT 施加精确模式,有利于子view的
* 测量。
* */
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
这里的代码并不多,我把这里逻辑大概分成2大块
- 测量每个子view的宽度和高度,来确定FrameLayout的宽度和高度,并保存。
- 就是把FrameLayout中拥有Match-Parent属性的子view进行重新测量(这是一步优化)。
第一步:确定FrameLayout的宽度和高度
在具体分析源码之前,我们可以简单的思考一下,FrameLayout要如何确定自己的宽高,根据我们在开发中的经验,大概应该是这个样子:FrameLayout的宽度应该是宽度最大的子view的宽度,FrameLayout的高度应该是高度最大子view的高度。目前我们大概就知道这么多,然后我们看看源码是怎么做的。
/** 最大高度,最大宽度 */
int maxHeight = 0;
int maxWidth = 0;
先定义了一个最大宽度和一个最大高度的局部变量,我们可以猜到这应该是最后给FrameLayout赋值宽高的变量。继续向下看:
/** 遍历自己的孩子 */
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
/** 如果孩子不是Gone状态,执行以下逻辑 */
if (mMeasureAllChildren || child.getVisibility() != GONE) {
/** 子view测量 */
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);
/**
* 保存的是所有子view测量之后的宽高信息,有点类似 childState += XXX 的意思,
* 只不过这里具体是通过 按位与 和 按位或 运算的,不过可以理解为 += 的意思。
* 就是说全部运算后保存在 childState 这个局部变量里面。
*
* */
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
是的,没错就是遍历所有的孩子,这里面调用了一个重要的方法来测量子view的宽高。
/** 子view测量 */
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
进入这个方法:
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* 翻译:让view的一个孩子测量自己,考虑到view的测量规格和他的内边距和外边距。孩子必须有
* MarginLayoutParams,在getChildMeasureSpec()方法中去做。
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这个方法的介绍,意思是说,要考虑到FrameLayout的内边距 和 子view的外边距,子view的测量规格是在getChildMeasureSpec()这个方法中获取的。其实也很容易想明白,FrameLayout剩下的空间 = FrameLayout最大空间 - (自己的内边距 + 子view的外边距)。计算出来的空间才是子view真正能获得的最大空间。
继续进入getChildMeasureSpec()方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
/** 当前ViewGroup的测量规格,用来计算得到子view的测量规格 */
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) {
// Parent has imposed an exact size on us
/** 1、父view施加了一个确切的尺寸 */
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
/** 如果子view是一个确切的数值 */
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
/** 子view设置了,充满父view,子view的size = 父view的size - (父view的padding + 子view的marging) */
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.
/** 如果子view是自适应,那么它的最大size不能比父view的size大, 即:子view的size <= 父view的size */
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
/** 2、父view施加了一个最大限度的尺寸 */
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
/** 子view设置了具体数值,那么size = 具体数值 */
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的size最大不能超过父view的size */
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.
/** 子view的size最大不能超过父view的size */
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
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
/** 不检查资源类型 */
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
看到这个这里,相信不陌生了,这就是根据父view的测量规格和子view的宽高size,计算得到子view的测量规格。测量规格大家应该都知道吧,我简单描述下:
测量规格就是MeasureSpec 这个类,主要用来帮助View的测量,这个类主要包含两部分,一个是测量模式 Mode, 一个是测量尺寸Size。用32位二进制表示:
Mode:占高2位。11000000 00000000 00000000 00000000
Size:占低30位。00111111 11111111 11111111 11111111
因为模式只有三种,所以用两位表示。
在这个方法中,计算出resultMode 和 resultSize 最后通过MeasureSpec.makeMeasureSpec(resultSize, resultMode);这个方法生成一个测量规格,这就是子view的测量规格。
代码中间那部分switch判断可以用一个表格来说明:
计算子view测量规格.png
子view的尺寸其实就是我们在xml布局文件中,写的layout-width= "100dp"这个属性,
这个尺寸可以有三种类型:
- 固定数值,类似100dp、130px这种是具体的一个数值,但是必须是大于0的!
- Match-Parent, 表示和父控件一样大。
- Wrap-Content,表示内容自适应,但是最大不会超过父view。
根据父view的测量规格,和子view自身的尺寸值,会得到一个施加给子view的测量规格。通过这个测量规格,子view可以测量自身的宽高。
到这里我们就把measureChildWithMargins() 方法看完了,这里面其实就是:
- 先计算子view宽高的测量规格。
- 测量子view。
然后记录maxWidth 和 maxHeight:
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
这一步是将子view中包含有MATCH_PARENT的view添加到mMatchParentChildren这个列表中,第二步中的优化要用到这个列表。
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
最后调用setMeasuredDimension()这个方法,保存FrameLayout的宽高。这个方法调用了resolveSizeAndState()这个方法,继续进入看看这个方法实现:
/**
* Utility to reconcile a desired size and state, with constraints imposed
* by a MeasureSpec. Will take the desired size, unless a different size
* is imposed by the constraints. The returned value is a compound integer,
* with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
* optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
* resulting size is smaller than the size the view wants to be.
*
* 大概意思是说:这个方法通过view的父view施加的测量规格MeasureSpec,来得到当前view的尺寸size
* 和状态state。除非这个约束给了一个不一样的size尺寸,否则当前view可以得到自己想要的尺寸size。
* 返回值是把得到的尺寸size和MEASURED_SIZE_MASK 或者 MEASURED_STATE_TOO_SMALL
* 这个标志结合起来得到的一个复合值;
* 如果计算出来的result size 比当前view想要的size要小,
* 那么就把计算出来的size和MEASURED_STATE_TOO_SMALL复合。
* 复合其实就是做一个 按位或运算。
*
*
* 当前view想要多大尺寸
* @param size How big the view wants to be.
*
* 当前view的父view施加的约束
* @param measureSpec Constraints imposed by the parent.
*
* 当前view的子view尺寸信息,这个尺寸信息里面包含了标记消息。
* @param childMeasuredState Size information bit mask for the view's
* children.
*
* 返回当前view带有标记的size尺寸信息
* @return Size information bit mask as defined by
* {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
*
* 这个方法其实就是根据计算所有的子view后,当前view得到一个大概的尺寸
* 比如:FrameLayout 通过测量计算每个子view的宽高,然后记录子view中
* 宽度最大和高度最大的值。最终把得到的最大宽度和高度,丢到这个方法里面
* 来重新计算,得到最合适的宽高。
*
* 也就是说这个方法就是用来最后计算当前view具体应该显示多少宽度和高度的。
*/
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) {
/** 当前view的模式是,最大尺寸不超过XXX模式 */
case MeasureSpec.AT_MOST:
if (specSize < size) {
/**
* 如果传进来的尺寸size 大于 当前view的父view施加的测量值,
* 那么result 就等于 父view施加的最大值,并且标记上 MEASURED_STATE_TOO_SMALL。
* */
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
/**
* 当前view模式是,精准模式。
* 精准模式下,当前view的宽高并不会受子view影响,
* 是多大就是多大不受任何影响。
*
* 比如:FrameLayout 设置了宽度为200px。
* FrameLayout 有个子view设置了宽度为500px,
* 那么我们可以知道FrameLayout的宽度并不会因为子view的500px而变大,
* FrameLayout依然还是设置好的精准的200px。
* specSize 就是我们设置的精准值。
*
* 通过源码我们理解的更清楚。
* */
case MeasureSpec.EXACTLY:
result = specSize;
break;
/**
* 当前view的模式是,未指定大小模式
* 处于这种模式下,当前view的宽高均为传入的尺寸size,
* 即当前view计算出来自己的大概的size,多大都不做限制。
*
* 比如:有一个经典的案例就是ListView嵌套ListView的冲突问题,
* 在以前ListView用的很多的时候,网上经常看到有一种解决方案就是
* 将里面的ListView的高度设置为所有子view的高度之和,即不让里层
* 的ListView滑动,那么这里就用到了这个姿势,我们将里层ListView
* 的测量模式改成MeasureSpec.UNSPECIFIED,那么把他所有的子view
* 的高度加起来,计算出来的size,传到这个方法来的时候,我们发现
* result = size; 这就表明当前ListView的高度取决于,你传进来的size,
* 现在传进来的是所有子view高度之和,那么里层ListView的高度也就是所有
* 子view的高度之和,那么嵌套滑动冲突的问题也就解决了。
*
* 这里就是解决上述问题的关键!
* */
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
详细的注释说明,我都在代码里面加了,概括一下说就是:我们上面遍历每个子view,得到了maxWidth 和 maxHeight ,但是这个maxWidth 和 maxHeight 是有可能超出父view的最大尺寸的,如果是在FrameLayout是AT_MOST模式下,那么当前的FrameLayout的尺寸只能是规格中的specSize,不能超过。当然如果FrameLayout是精准模式,那么就不受影响,我指的不受影响是不受子view的宽高影响,比如FrameLayout设置了宽度为300px那么随便你子view是多宽,FrameLayout都是300px不会有任何影响。最后那种模式UNSPECIFIED,遇到过ListView嵌套问题的开发者,应该很有感觉。
总结下这个方法resolveSizeAndState() 其实就是计算FrameLayout我自己的宽高别搞混了,其实我们之前做的所有工作都是为了最后这一步计算自身的宽高的,当然也顺便把测量事件一层层传递下去。
第二步:优化带有Match-Parent属性的子view
/** 孩子宽度或者高度中有Match-Parent属性的View集合 */
count = mMatchParentChildren.size();
if (count > 1) {
/**
* 如果当前FrameLayout 不是精确模式,那么只要子view的宽度或者高度中有MATCH_PARENT
* 那么需要单独进行二次测量,以确保给子view MATCH_PARENT 施加精确模式,有利于子view的
* 测量。
* */
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
先检查列表中是否有值,如果有,进行遍历,遍历里面的主要逻辑是:如果子view的width 或者 height 带有Match-parent 那么会将子view的测量规格设置为EXACTLY,然后对子view进行重新测量。看到这里,就有很大的疑问了:明明上面第一步的时候已经遍历了所有的子view,并且已经测量过了,为什么还要再测量一次了,不浪费资源和性能吗?
首先我们看,在第一步我们确实已经把FrameLayout的宽高已经计算出来了,就是说当前这个ViewGroup已经是明确知道宽度和高度的了,如果子view中有Match-Parent,这种情况下,把子view设置为EXACTLY,确实也是没什么问题的。而且我们在看为什么一定要重新再设置为EXACTLY,在FrameLayout onMeasure() 方法最顶部有一个判断
/**
* 如果当前view的父view施加的宽度或者高度模式有一个不是 精确模式 EXACTLY
* 那么就需要把MatchParent的子view找出来,再次测量
* */
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
这两段代码连起来看,是不是发现了什么,如果当前这个FrameLayout不是精确模式的话,那么在遍历子view的逻辑中就会把包含Match-Parent的子view添加到列表中,这个列表就是我们第二步中关键的遍历列表,也就是说第二步中把子view测量模式改成EXACTLY也正是为了不让FrameLayout进入两次测量;而且相对来说,EXACTLY测量模式相对来说会优于AT-Most这种测量模式。
最后这一点可能有点拗口的感觉,不好理解,我们用demo测试代码来说明一下吧:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.demo.viewoffset.view.CustomMeasureView
android:id="@+id/view"
android:background="#409"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</LinearLayout>
// 自定义类
public class CustomMeasureView extends View {
public static final String TAG = "CustomMeasureView";
public CustomMeasureView(Context context) {
super(context);
}
public CustomMeasureView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomMeasureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.e(TAG, "onMeasure: 执行");
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
Log.e(TAG, "onMeasure: widthMode = " + widthMode);
Log.e(TAG, "onMeasure: widthMode = " + (widthMode >> 30));
}
}
我们可以写一个这样的布局,和一个自定义View,我们通过打印日志来看,测量方法执行了几次。
上面的xml文件中,我们看到FrameLayout 宽高都是wrap-content,由此我们可以知道FrameLayout的测量模式是At-most,即满足:
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
这个条件,而此时FrameLayout的子view宽高都是Match-Parent,那么满足我们刚刚的分析,也就是说我们的自定义View的onMeasure()方法会被执行两遍,打印日志我们确实看到了两遍打印:
日志.png
结论:也就是说触发FrameLayout对子view绘制两次的条件有三个:
- 当前FrameLayout的宽高的任一一个测量模式是At-most。
- 子view的宽高中有一个包含Match-Parent。
- 满足条件的子view至少有两个。
如果满足以上3个条件,那么FrameLayout就会对包含Match-Parent的子view进行第二次绘制。
结尾
FrameLayout的测量流程基本就这些,主要就是做三件事:
- 测量子view。
- 确定自己的宽高。
- 是否对子view进行二次绘制。
看完上面的分析,我们至少要知道,
- FrameLayout的测量机制。
- ViewGroup是如何计算子view的测量规格的。
- 所有的测量都是在onMeasure()方法中做的,每个view都是重写了onMeasure()方法,并在自己的onMeasure()方法中实现自己的逻辑。