前言
从上一篇中同 Activity 的布局加载了解了整个 View 树加载的流程最后是通过 View 的三大流程来实现布局的显示的那么我们这篇来讲下布局的三大流程之一 -->measure
1.MeasureSpec
在讲解测量之前我们要先清楚什么是 MeasureSpec?MeasureSpaec 可以理解为测量规格在 View.measure()中多次被用到它是有一个 32 位的 int 值, 高 2 位代表 SpecMode(指测量模式), 低 30 位代表 SpecSize(在指定模式下的规格大小)
他们对应的二进制值分别是:
- UNSPECIFIED=00000000000000000000000000000000
- EXACTLY =01000000000000000000000000000000
- AT_MOST =10000000000000000000000000000000
由于最前面两位代表模式, 所以他们分别对应十进制的 0,1,2;
在测量中, 会根据子 View 的 LayoutParames 与父容器的 MeasureSpec 的规格来生成子 View 的 MeasureSpec 然后根据它来测量出 View 的宽 / 高所以这个概念该是非常重要的下面我来看下它的具体模式的含义
SpecMode | SpecSize |
---|---|
MeasureSpec.UNSPECIFIED | 不确定模式:子视图 View 请求多大就是多大,父容器不限制其大小范围,一般用于系统内部 |
MeasureSpec.EXACTLY | 精确模式,父容器已经检测 View 所需要的精确大小,View 的最终大小就 SpecSize 所指定的值。对应 Layout 中的 match_parent 和具体的数值两种模式 |
MeasureSpec.AT_MOST | 最大模式,父容器制定一个可用大小 SpecSize,子 View 不能大于这个值。对应 LayoutParames 中的 warp_content |
2.View#measure()
measure 测量分成两种一种是原始 View, 那么通过 measure 方法就完成了其自己的测量, 如果是 ViewGroup, 除了完成自己的测量完还要遍历子元素的 measure 方法, 各个子元素如果是 View 就测量自己, 如果是 ViewGroup 就接着遍历, 最后都是调用 View 的 measure
因为 View 是所有 View 与 ViewGroup 的老祖宗, 那么我们先抛开 ViewGroup 先直接来了解下 View.measure()方法:
- int mOldWidthMeasureSpec = Integer.MIN_VALUE;
- int mOldHeightMeasureSpec = Integer.MIN_VALUE;
- public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
- ..................
- final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
- || heightMeasureSpec != mOldHeightMeasureSpec;
- final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
- && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
- final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
- && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
- final boolean needsLayout = specChanged
- && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
- // 如果上一次的测量规格和这次不一样, 则条件满足, 重新测量视图 View 的大小
- if (forceLayout || needsLayout) {
- int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
- mMeasureCache.indexOfKey(key);
- if (cacheIndex <0 || sIgnoreMeasureCache) {
- // measure ourselves, this should set the measured dimension flag back
- onMeasure(widthMeasureSpec, heightMeasureSpec);
- mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- } else {
- long value = mMeasureCache.valueAt(cacheIndex);
- // Casting a long to int drops the high 32 bits, no mask needed
- setMeasuredDimensionRaw((int) (value>> 32), (int) value);
- mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- }
- mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
- }
- mOldWidthMeasureSpec = widthMeasureSpec;
- mOldHeightMeasureSpec = heightMeasureSpec;
- }
这段代码比较简单, 首先判断与上次测量的 MeasureSpec 是否相等, 不等就重新测量可以看到此方法调用了 onMeasure()方法, 并将传来的值直接传递下去, 那么就说明测量的主要的逻辑都在此方法中, 我们跟往下走 View#onMeasure():
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
- getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
- }
通过注释我们知道, 参数中的 MeasureSpec 是父布局给我们传递过来的这点我们要清楚这段代码看起来比较简单, 但是实际理解起来却不容易可以看到在 onMeasure()只调用了 setMeasuredDimension(); 就结束了了那么我们就可以知道当调用此方法的时候就证明测量流程结束那么我们来看下他里面参数分别是 measuredWidth,measuredHeight 并通过 getDefaultSize()方法来计算的, 进入此方法:
- /**
- * Utility to return a default size. Uses the supplied size if the
- * MeasureSpec imposed no constraints. Will get larger if allowed
- * by the MeasureSpec.
- *
- * @param size Default size for this view 这个 view 的默认尺寸大小
- * @param measureSpec Constraints imposed by the parent 这个参数是父容器提供的子 View 的 MeasureSpec
- * @return The size this view should be. 这个 view 在经过此方法后返回的 view 的尺寸大小
- */
- 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 传递的测量规格重新计算 View 的测量宽高下面我们进入 getSuggestedMinimumWidth()方法看看是如何获得 View 的尺寸大小的:
- protected int getSuggestedMinimumWidth() {
- return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
- }
如果 View 没有设置背景那么返回 mMinWidth(它对应 XML 中的 android:minWidth, 如果不指定默认为 0), 如果设置了背景就为 Drawable 的原始高度
总结:
在通常情况下我们没有设置 android:minWidth 属性, 那么 getDefaultSize()的返回值就为 specSize(父容器提供的)那我们通过 getDefaultSize()方法知道了, 在自定义 View 的时候如果直接继承 View 要重写 onMeasure()方法, 否者 warp_content 和 match_parent 效果相同
在 View 的 onMeasure 方法只提供了一种默认的实现, 通常继承 View 的控件都会重写这个方法来满足自己的要求
sizeSpec 大小是有父容器决定的, 我们由上篇文章知道知道父容器 DecorView 的测量模式是 MeasureSpec.EXACTLY, 测量大小 sizeSpec 是整个屏幕的大小
到这里我们就把 View 的绘制流程梳理完成了下面我们就接着上篇讲解从 performTraversals()方法触发查看 view 的三大流程
3.ViewGroup 测量
从上一篇文章我们知道顶级 View(DecorView)继承 FrameLayout(ViewGroup)那我们继续上一篇的中 performTraversals()方法中的 performMeasure()测量这个方法看下:
- 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);
- }
- }
那么我们进入到 FrameLayout(ViewGroup), 为了完全理清流程我们先来看下它父类 ViewGroup#onMearsure()方法发现 ViewGroup 是一个抽象类, 它里面没有实现 onMearsure(), 这也能理解, 因为 ViewGroup 是所有空间容器的父类, 具体的测量方式应该是子类容器控件实现的比如 LinearLayout 与 RelativeLayout 他们的方法都是不一样的但是它有一下两个方法:
- protected void measureChildWithMargins(View child,
- int parentWidthMeasureSpec, int widthUsed,
- int parentHeightMeasureSpec, int heightUsed)
- protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)
在 measureChildren 方法中会循环遍历子 View, 然后调用 measureChild()方法, 通过对 measureChild()与 measureChildWithMargins()方法的比较发现两者基本相同, 只不过后者加入了边距的运算不管那种方法最后都会调用 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 方法也就是 View.measure(), 之后就会走 View#measure 流程那么我们现在回过头来进入 FrameLayout#onMearsure()方法看他是如何实现的:
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // 获取容器下的所有子空间
- int count = getChildCount();
- 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());
- }
/$/ 设置当前 FrameLayout 测量结果, 此方法的调用表示当前 View 测量的结束
- setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
- resolveSizeAndState(maxHeight, heightMeasureSpec,
- childState << MEASURED_HEIGHT_STATE_SHIFT));
- }
- ......
上面讲到在调用 setMeasuredDimension()方法后就表示测量完成了, 所以我们主要看它上面的代码首先取出容器下的所有子控件, 然后调用 measureChildWithMargins(); 方法测量每个子控件如下:
- 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);
- }
上面的方法先取出子 View 的 LayoutParams, 然后通过 getChildMeasureSpec()方法来得到子 View 的 MeasureSpec, 最后调用 View.measure()完成测量子 View 还是 ViewGroup 继续走 ViewGroup 的测量, 如果是子控件是 View 就会测量自己完成整个测量过程那么我在跟踪代码看下 getChildMeasureSpec()方法做了什么:
- /
- * @param spec The requirements for this view
- * @param padding The padding of this view for the current dimension and
- * margins, if applicable
- * @param childDimension How big the child wants to be in the current
- * dimension
- * @return a MeasureSpec integer for the child
- 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) {
- // Parent has imposed an exact size on us
- case MeasureSpec.EXACTLY:
- 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:
- // 子 View 的宽 / 高是具体的值
- 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.
- 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
- resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
- resultMode = MeasureSpec.UNSPECIFIED;
- }
- break;
- }
- //noinspection ResourceType
- //MeasureSpec.makeMeasureSpec--> 根据大小个模式生成一个 MeasureSpec
- return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
- }
这个方法首先获取了当前 DecorView 容器的测量模式, 然后减去传进来的 padding 参数, 得到一个子元素可用的大小 size, 代码如下:
- int specMode = MeasureSpec.getMode(spec);
- int specSize = MeasureSpec.getSize(spec);
- int size = Math.max(0, specSize - padding);//(padding 表示不可用的范围, 由上面的代码可知 padding = 当前容器 (FrameLayout) 的 padding + 子元素的 margins)
有上一篇我们知道我们 DecorView 是 match_parent, 所以直接看 MeasureSpec.EXACTLY: 分支, 其他分支是一样的, 通过观察我们可以将上面 MeasureSpec.EXACTLY: 分支下的三个 if 语句总结如下:
LayoutParams.MATCH_PARENT(精确模式): 当子 View 宽 / 高为 LayoutParams.MATCH_PARENT 大小就是父容器大小
LayoutParams.WRAP_CONTENT(最大模式):: 大小不定, 但是不能超过父容器的大小
固定模式(比如 100dp)(精确模式):: 大小为 LayoutParams 指定大小(这里注意如果你的大小超过窗口大小比如 1200dp, 那么你的大小就是 1200dp, 虽然窗口显示不下, 但是窗口会能显示多少显示多少)
总结:
在 1.MeasurseSpec 中我们讲到这么一句话在测量中, 会根据子 View 的 LayoutParames 与父容器的 MeasureSpec 的规格来生成子 View 的 MeasureSpec 然后根据它来测量出 View 的宽 / 高 通过上面的代码和下面的总结, 现在我们在来理解这句话就很好理解了
measureChildWithMargins(): 子元素的 MeasureSpec 的创建与父容器的 MeasureSpec 和子元素本省的 LayoutParames 以及父容器的 padding 与子元素的 margins 决定
getChildMeasureSpec(): 通过传入子元素 LayoutParames(也就是 XML 中宽 / 高所具体定义的), 来决定自己的 MearsureSpec
对于顶级 View(即 DecorView)和普通 View 来说 MearsureSpec 转换过程略有不同, 对于 DecorView, 其 MearsureSpec 是窗口尺寸和其自己的 LayoutParames 共同决定, 对于普通 View,MearsureSpec 是由父容器的 MearsureSpec 和自身的 LayoutParames 共同决定同时对于普通 View 针对不同的父容器和 View 本身不同的 LayoutParames,View 就可以有多重 MeasureSpec 具体不同参照下表:
图片. png
普通 View 的 MeasureSpec 的创建规则 (此图来自 Android 开发艺术探索)
4. 顶级 DecorView 测量
对于好奇的小伙伴可能会问: 上面提到 DecorView 的 MearsureSpec 是窗口尺寸和其自己的 LayoutParames 共同决定那么是如何决定的呢?
其实在 ViewRootImpl 中的 measureHierarchy 中展示了 MeasureSpec 创建过程 (此方法在 performTraversals() 被调用同时在三大流程之前):
- childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);//1139
- childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
- performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
getRootMeasureSpec()方法中的一个参数就是窗口的尺寸大小, 第二个就是当前 View(顶级 DecorView)的 LayoutParames, 他的方法如下:
- 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.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;
- }
与 getChildMeasureSpec 的原理是一样的具体看 getChildMeasureSpec 关于固定大小精确模式最大模式总结我认为在测量时先经过 ViewRootImpl#measureHierarchy 方法测量出 DecorView 的
5. 整个 View 三大流程之测量概括总结
上面我们把整个 View 的测量相关流程基本上都滤清了关于这些纯概念源码的东西看着乏味, 也不容易理解, 偏底层也没有什么程序运行效果那么我用流程图来梳理下整个流程:
来源: http://mp.weixin.qq.com/s/d-btYUORyJJCC6k-fDKyWg