企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
### 绪 很多朋友都沉迷于自定义View, 而自定义View离不开measure、layout、draw三个步骤,在测量方面,很多朋友仅仅是知道怎么去测量一个控件,而对于为什么要这么做等等问题都搞的不是很清楚,今天这篇文章我们就从View树的最顶层`DecorView`开始分析测量到底是怎么一回事。 这篇文章要解决的问题有: > 1. onMeasure的两个参数从哪来。 > 1. 最开始的参数是怎么计算出来的。 > 1. 测量规格是根据什么得到的。 ### 一切从DecorView说起 大家都知道在我们的应用窗口中最顶层的View是`DecorView`, 那么自然而然,一个测量的开始肯定就是从DecorView开始的,而且,我们还知道,一个测量的开始是从`ViewRootImpl`的`performTraversals`方法开始,所以我们理所当然的要从`performTraversals`方法开始看起。`performTraversals`很长,看起来甚至有点可怕,不过没关系,我们仅仅关心我们需要的代码就ok, ~~~ private void performTraversals() { ... if (!mStopped) { boolean focusChangedDueToTouchMode = ensureTouchModeLocally( (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0); if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged) { // mark // 获取测量规格 mWidth和mHeight当前视图frame的大小 // lp是WindowManager.LayoutParams int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed! mWidth=" + mWidth + " measuredWidth=" + host.getMeasuredWidth() + " mHeight=" + mHeight + " measuredHeight=" + host.getMeasuredHeight() + " coveredInsetsChanged=" + contentInsetsChanged); // Ask host how big it wants to be performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... } ... } } ~~~ 在这里我们看到了performMeasure,那这里肯定就是测量的开始了,但是,重点是我们关心的`childWidthMeasureSpec`和`childWidthMeasureSpec`是怎么计算出来的, 这里调用了`getRootMeasureSpec`方法,我们来到这个方法一探究竟。 ~~~ 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; } ~~~ 这个方法很简单,就是根据`rootDimension`的值来定义不同的`measureSpec` > 当是MATCH_PARENT的时候,我们make一个大小是windowSize,规格是精确值的MeasureSpec 当是WRAP_CONTENT的时候,我们make一个大小是windowSize,规格是最大为windowSize的MeasureSpec 其他的情况,也就是rootDimension是具体值,那我们得到的是一个大小为rootDimension,规格为精确的MeasureSpec ### DecorView的测量 通过看上面的代码,我们最终有了一个测量规格,而且,我们可以猜测到宽高值都是固定的,就是我们视图的大小,所以,最后的measureSpec是精确的。现在我们就来到DecorView,看看到底是怎么测量的。 ~~~ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 获取测量规格, 这里是EXACTLY final int widthMode = getMode(widthMeasureSpec); final int heightMode = getMode(heightMeasureSpec); boolean fixedWidth = false; // 这里有点意思,如果不是EXACTLY的 // 这里还是要获取下视图的大小 // 让测量规格是EXACTLY if (widthMode == AT_MOST) { final TypedValue tvw = isPortrait ? mFixedWidthMinor : mFixedWidthMajor; if (tvw != null && tvw.type != TypedValue.TYPE_NULL) { final int w; if (tvw.type == TypedValue.TYPE_DIMENSION) { w = (int) tvw.getDimension(metrics); } else if (tvw.type == TypedValue.TYPE_FRACTION) { w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels); } else { w = 0; } if (w > 0) { // 这里重新设置了测量规格 // 大小是performMeasure中给出的大小和自己获取的大小的最小值 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); widthMeasureSpec = MeasureSpec.makeMeasureSpec( Math.min(w, widthSize), EXACTLY); fixedWidth = true; } } } ... // 下面同样有一个高度的判断 ... super.onMeasure(widthMeasureSpec, heightMeasureSpec); ... } ~~~ DecorView的测量有点意思,在发现`performMeasure`中给的测量规格不是精确值的时候,自己又去获取一下并且取两者的最小值,当然这里肯定是吧测量规格设置为精确值了。判断好后,接着调用了super.onMeasure,通过源码我们可以知道其实DecorView继承自FrameLayout,所以,现在我们要去FrameLayout的onMeasure方法看看了。 ### FrameLayout的测量 ~~~ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); // 如果测量规格有一个不是精确值,这里就为true 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) { // 测量子view measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); // 宽度为前一个计算出来的宽度和当前view的宽度取最大值 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) { // 如果当前view有match_parent的地方, // 记录一下当前view 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)); count = mMatchParentChildren.size(); // 这里有值表明了两点: // 1 当前FrameLayout的宽和高的建议规格有不是精确值的 // 2 子view有含有match_parent的地方 if (count > 1) { for (int i = 0; i < count; i++) { final View child = mMatchParentChildren.get(i); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidthMeasureSpec; int childHeightMeasureSpec; if (lp.width == LayoutParams.MATCH_PARENT) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width); } if (lp.height == LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTopWithForeground() - getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin, lp.height); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } ~~~ FrameLayout的测量很简单,先去计算所有子view中最大的宽和高,然后调用resolveSizeAndState去最终确认大小, 那我们来看看resolveSizeAndState方法到底干了嘛,这个方法位于View类中, ~~~ public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { 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: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; } return result | (childMeasuredState&MEASURED_STATE_MASK); } ~~~ 这里的逻辑也很简单,但是绝对是有代表性的,我们自己写的测量跟这里有很大的相似之处,首先这里去判断测量规格,如果是EXACTLY,则结果直接是MeasureSpec里获取的大小,如果是AT_MOST,这里取两个大小的最小值。到这里FrameLayout的测量也就完成了,而且我们也看懂了测量是如何从DecorView开始一步步的到child的测量过程,不过这个过程我们还没有细看。 ### 测量子view 下面我们就从measureChildWithMargins方法开始分析一下如何进行的子view的测量,这个方法在ViewGroup中定义的。 ~~~ 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); } ~~~ 代码我们也都似曾相识,关键点还是在`getChildMeasureSpec`中,这里获取了父Group对子View的建议,最后调用child.measure将建议传递进去,从而开始了整个View树的测量流程。 ~~~ 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 u // 如果父view的规格是精确值 case MeasureSpec.EXACTLY: // 如果子view的layout_XXX是一个确定的值 if (childDimension >= 0) { // 测建议的值是子view指定的值 // 规格是EXACTLY resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 如果子view的layout_XXX是MATCH_PARENT // 则建议的值是子view自己想要的的大小,也就是父view剩下的大小 // 规格是EXACTLY // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 如果子view的layout_XXX是WRAP_CONTENT // 则建议的值是子view自己想要的的大小,也就是父view剩下的大小 // 规格是AT_MOST // 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 // 如果父view的规格是AT_MOST case MeasureSpec.AT_MOST: // 如果子view的layout_XXX是一个确定的值 if (childDimension >= 0) { // 测建议的值是子view指定的值 // 规格是EXACTLY // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 如果子view的layout_XXX是MATCH_PARENT // 则建议的值是子view自己想要的的大小,也就是父view剩下的大小 // 规格是AT_MOST // 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) { // 如果子view的layout_XXX是WRAP_CONTENT // 则建议的值是子view自己想要的的大小,也就是父view剩下的大小 // 规格是AT_MOST // 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 // 如果父view的规格是不确定的 case MeasureSpec.UNSPECIFIED: // 如果子view的layout_XXX是一个确定的值 if (childDimension >= 0) { // 测建议的值是子view指定的值 // 规格是EXACTLY // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 如果子view的layout_XXX是MATCH_PARENT // 则建议的值0 // 规格是UNSPECIFIED // Child wants to be our size... find out how big it should // be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 如果子view的layout_XXX是WRAP_CONTENT // 则建议的值0 // 规格是UNSPECIFIED // Child wants to determine its own size.... find out how // big it should be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } ~~~ 这里的代码正是measureSpec依据什么而来最有力的回答,在代码中已经注释的很详细了,不过下面还是要总结一下: > 1. 父View是EXACTLY,子view的大小指定为确定值,则给子view的建议大小是子view自己设置的大小,规格为EXACTLY。 > 1. 父View是EXACTLY,子view的大小指定为MATCH_PARENT,则给子view的建议大小是父view剩下的大小,规格为EXACTLY。 > 1. 父View是EXACTLY,子view的大小指定为WRAP_CONTENT,则给子view的建议大小是父view剩下的大小,规格为AT_MOST。 > 1. 父View是AT_MOST,子view的大小指定为确定值,则给子view的建议大小是子view自己设置的大小,规格为EXACTLY。 > 1. 父View是AT_MOST,子view的大小指定为MATCH_PARENT,则给子view的建议大小是父view剩下的大小,规格为AT_MOST。 > 1. 父View是AT_MOST,子view的大小指定为WRAP_CONTENT,则给子view的建议大小是父view剩下的大小,规格为AT_MOST。 > 1. 父View是UNSPECIFIED,子view的大小指定为确定值,则给子view的建议大小是子view自己设置的大小,规格为EXACTLY。 > 1. 父View是UNSPECIFIED,子view的大小指定为MATCH_PARENT,则给子view的建议大小0,规格为UNSPECIFIED。 > 1. 父View是UNSPECIFIED,子view的大小指定为WRAP_CONTENT,则给子view的建议大小0,规格为UNSPECIFIED。 ### 最后的最后 ok, 到这里,我们虽然只是分析了DecorView和他的父类FrameLayout的测量流程,不过这也算是将整个流程分析完了,为什么这么说呢? 只要我们看到了measure child的部分,就是走完了一个闭环,接下来的子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) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; } ~~~ 是不是又一种似曾相识的感觉,这里的逻辑也很简单,就是根据测量规格是定义不用的大小,如果是UNSPECIFIED,则结果就是我们传递进来的size,如果是AT_MOST或者EXACTLY,则结果就是我们父布局建议的大小。很多同学可能喜欢这么用, ~~~ int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); ~~~ 这样做有一个坑,在一些viewgroup中获得的结果是0,为什么呢? 例如HorizontalScrollView,从源码上看, 它最后给子view建议的规格会是UNSPECIFIED,从`getDefaultSize`源码上看,此时结果就是我们传递的size,也就是`getSuggestedMinimumWidth`返回的值,我们来看看这个方法的定义, ~~~ protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } ~~~ 这里取值是我们设置的minWidth和背景的minWidth的最大值,如果minWidth和背景我们都没有设置的话,这里返回的也是0了,这样,我们就能理解结果为什么是0了。 从这个小问题是还能看出一点,我们在自己写测量的时候不能只依靠父布局,而是要参考父布局的建议和自己的测量结果。任何有**霸权**倾向的测量都是不可取的。 好了,这篇文章就到这里吧,相信大家在仔细阅读后会对View的测量机制有一个全新的认识。