所有东西都是难者不会,会者不难,Android开发中有很多小伙伴觉得自定义View和事件分发或者Binder机制等是难点,其实不然,如果静下心来花点时间把这几个技术点都研究一遍,你会发现其实这些东西都很简单。OK,废话不多说,今天我们就来看看View的测量。View的测量纷繁复杂,不过如果能够做到提纲挈领,其实也不难。那么今天,我们就来扒一扒View的测量。本文主要涉及如下知识点:

1.View的测量

2.在父容器中对View进行测量

3.LinearLayout测量举例

4.最根上容器测量

如果小伙伴们还没看过我之前关于View绘制的文章的话,请先移步这里,这两篇文章有助于你理解本篇文章:

1.View绘制详解,从LayoutInflater谈起

2.View绘制详解(二),从setContentView谈起

OK,那我们开始今天的话题吧。

1.View的测量

关于View的测量我其实在之前的一篇文章中已经提到过了(Android自定义View之ProgressBar出场记 ),在我们自定义View的时候,除了一个onDraw方法可以重写之外,还有一个onMeasure方法也可以重写,这个onMeasure方法就是用来确定一个View的宽和高的,onMeasure方法的方法头如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 

大家看到,onMeasure方法有两个参数,第一个参数叫做widthMeasureSpec、第二个参数叫做heightMeasureSpec,在开发中我们把这两个参数称作测量规格。测量规格是一个32位的int型数据,其中高2位表示测量模式,低30位表示测量值,测量模式一共分为三种:

1.EXACTLY:精确模式,对应我们在布局文件中设置宽高时给一个具体值或者match_parent

2.AT_MOST:最大值模式:对应设置宽高时给一个wrap_content

3.UNSPECIFIED:这种测量模式多用在ScrollView中,或者系统内部调用

在实际开发过程中,我们一般通过MeasureSpec.getMode()方法来从测量规格中获取测量模式,然后通过MeasureSpec.getSize()方法来从测量规格中获取测量值。但是小伙伴们注意,这个时候获取到的测量值实际上是系统建议的测量值,并不是控件最终显示的大小,在onMeasure方法中我们可以根据自己的需求再对这些值做进一步的修正,修正完之后再调用setMeasuredDimension()方法,调用完该方法之后View才算是有了MeasureWidth和MeasureHeight了。OK,基于以上的表述,我们在自定义View中onMeasure方法的典型写法可以是如下样子:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  super.onMeasure(widthMeasureSpec, heightMeasureSpec);  //获取宽的测量模式  int widthMode = MeasureSpec.getMode(widthMeasureSpec);  //获取宽的测量值  int widthSize = MeasureSpec.getSize(widthMeasureSpec);  //获取高的测量模式  int heightMode = MeasureSpec.getMode(heightMeasureSpec);  //获取高的测量值  int heightSize = MeasureSpec.getSize(heightMeasureSpec);  switch (widthMode) {  case MeasureSpec.EXACTLY:  break;  case MeasureSpec.AT_MOST:  case MeasureSpec.UNSPECIFIED:  //如果宽为wrap_content,则给定一个默认值  widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULTWIDTH, getResources().getDisplayMetrics());  break;  }  switch (heightMode) {  case MeasureSpec.EXACTLY:  break;  case MeasureSpec.AT_MOST:  case MeasureSpec.UNSPECIFIED:  heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULTHEIGHT, getResources().getDisplayMetrics());  break;  }  widthSize = heightSize = Math.min(widthSize, heightSize);  //设置测量结果  setMeasuredDimension(widthSize, heightSize);
}

2.在父容器中对View进行测量

看完了View的测量之后,很多小伙伴可能都会有疑问了,那么View的onMeasure方法到底是在哪里调用的呢(小伙伴们注意,我这里的View既包括普通控件,也包括容器)?其实就是在它的父容器中调用。那么这里就要我们来到ViewGroup中探究一番了,首先,在ViewGroup中,系统给我们提供了三个方法用来测量ViewGroup中子控件的大小,如下:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) 

第一个方法是measureChildren,Children是child的复数形式,很明显,这个是测量所有子控件的,这个方法中是一个for循环,遍历了容器中所有的子控件进行测量,第二个方法measureChild则是测量单个子控件,最后一个measureChildWidthMargins也是测量单个子控件,不过在测量的时候加入margin而已。OK,那我们这里就以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,然后在第6行通过getChildMeasureSpec方法获取一个childWidthMeasureSpec,高度的测量规格获取方式和宽度测量规格的获取方式一致,所以这里我就以宽度的测量规格获取方式为例,我们来看看系统是如何测来子控件的:

    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 uscase 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 uscase MeasureSpec.AT_MOST:if (childDimension >= 0) {// Child wants a specific size... so be itresultSize = 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 becase MeasureSpec.UNSPECIFIED:if (childDimension >= 0) {// Child wants a specific size... let him have itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size... find out how big it should// beresultSize = 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 beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;}break;}//noinspection ResourceTypereturn MeasureSpec.makeMeasureSpec(resultSize, resultMode);}

getChildMeasureSpec方法共接收三个参数,第一个参数表示父容器的测量规格,第二个参数表示子控件的padding,margin等,第三个参数表示子控件期望显示的宽高。在getChildMeasureSpec方法中,首先获取父容器宽度的测量模式和测量值,然后定义变量size,size是一个大于等于0的数,specSIze-padding为父容器宽度减去子控件的padding、margin之后所剩的大小。然后从第10行开始,通过一个switch来分别处理不同的情况。这里的逻辑都很简单,我以第一个case为例,如果父容器的测量模式是精确测量的话,childDimension分三种情况,如果childDimension>=0的话,即子控件的宽度为具体值(因为MATCH_PARENT对应的常量为-1,WRAP_CONTENT对应的常量为-2),则resultSize=childDimension,resultMode=EXACTLY。如果子控件宽度设置为MATCH_PARENT,则resultSize的大小为父容器的宽度的测量值减去子控件padding,margin等,此时resultMode依然是EXACTLY,如果子控件的宽度设置为了wrap_content,那么resultSize的大小依然是父容器中可容纳控件的最大大小,表示子控件最大宽度不能超过父容器可显示的宽度,只不过在这里把resultMode设置为了AT_MOST,OK,做完这些事情之后,最后一个通过一个makeMeasureSpec方法将resultSize和resultMode再整合成一个MeasureSpec。这就是为什么很多人嘴里经常念叨控件的具体大小并不是由控件本身决定,还包括它的父容器的原因。OK,针对以上三种case,我整理了如下一张表格:

OK ,现在再回到我们ViewGroup的measureChildWithMargins方法中,在该方法中获取到子控件的测量规格之后,接下来调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec);这一行代码,进入到View的measure方法中进行测量,我们点到这个方法里边来看看:

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {boolean optical = isLayoutModeOptical(this);if (optical != isLayoutModeOptical(mParent)) {Insets insets = getOpticalInsets();int oWidth  = insets.left + insets.right;int oHeight = insets.top  + insets.bottom;widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);}// Suppress sign extension for the low byteslong key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;// Optimize layout by avoiding an extra EXACTLY pass when the view is// already measured as the correct size. In API 23 and below, this// extra pass is required to make LinearLayout re-distribute weight.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);if (forceLayout || needsLayout) {// first clears the measured dimension flagmPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;resolveRtlPropertiesIfNeeded();int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);if (cacheIndex < 0 || sIgnoreMeasureCache) {// measure ourselves, this should set the measured dimension flag backonMeasure(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 neededsetMeasuredDimensionRaw((int) (value >> 32), (int) value);mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}// flag not set, setMeasuredDimension() was not invoked, we raise// an exception to warn the developerif ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {throw new IllegalStateException("View with id " + getId() + ": "+ getClass().getName() + "#onMeasure() did not set the"+ " measured dimension by calling"+ " setMeasuredDimension()");}mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;}mOldWidthMeasureSpec = widthMeasureSpec;mOldHeightMeasureSpec = heightMeasureSpec;mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension}

首先小伙伴们注意到,这个方法是一个final类型的,也就是不可以被复写,然后我们在measure方法的第38行见到了一个久违的名字,onMeasure方法。这下终于回到了本文的第一小节了,这下小伙伴们也知道了View的onMeasure方法是在哪里调用了吧!就是在它的父容器中调用。

3.LinearLayout测量举例

OK,上文我们已经说过,View的测量其实是很简单的,难点在于ViewGroup的测量。其实这里不该用难点这个词,用麻烦可能更合适,因为并不难,只是麻烦而已。一般来说,我们在自定义ViewGroup的时候,控件宽高的测量都是比较费事的。一般来说,思路是这样的:如果用户已经指定了ViewGroup的宽高为固定值或者为MATCH_PARENT,那我们不用做过多处理,如果用户指定了ViewGroup的宽或者高为WRAP_CONTENT,WRAP_CONTENT表示ViewGroup的宽高是包裹内容,即容器中控件的宽高为多少,容器的宽高就为多少,这就需要我们先来遍历一遍容器中控件的宽高,算出来子控件整体的宽高在设置给容器就可以了,整体上思路就是这样,接下来我们就以LinearLayout的源码为例,来看看ViewGroup的宽高是如何测量的。一般来说,我们的所有容器都会复写onMeasure'方法进行控件宽高的重新测量,我们先来看看LinearLayout的onMeasure方法:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {if (mOrientation == VERTICAL) {measureVertical(widthMeasureSpec, heightMeasureSpec);} else {measureHorizontal(widthMeasureSpec, heightMeasureSpec);}}

这里是根据控件的排列方向来测量的,OK,那这里我们就以竖直方向上的测量为例:

    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;//获取子控件总数final int count = getVirtualChildCount();//获取宽度的测量模式final int widthMode = MeasureSpec.getMode(widthMeasureSpec);//获取高度的测量模式final int heightMode = MeasureSpec.getMode(heightMeasureSpec);boolean matchWidth = false;boolean skippedMeasure = false;final int baselineChildIndex = mBaselineAlignedChildIndex;       final boolean useLargestChild = mUseLargestChild;//记录每一行最高子控件的高度int largestChildHeight = Integer.MIN_VALUE;int consumedExcessSpace = 0;//这个for循环用来记录总高度,同时记录最大宽度// See how tall everyone is. Also remember max width.for (int i = 0; i < count; ++i) {//获取第一个子Viewfinal View child = getVirtualChildAt(i);if (child == null) {//measureNullChild方法的返回值为0mTotalLength += measureNullChild(i);continue;}//如果控件被隐藏,则不计入计算if (child.getVisibility() == View.GONE) {i += getChildrenSkipCount(child, i);continue;}if (hasDividerBeforeChildAt(i)) {mTotalLength += mDividerHeight;}//获取子控件的lpfinal LayoutParams lp = (LayoutParams) child.getLayoutParams();//记录总权重totalWeight += lp.weight;//是否分配剩余空间final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {// Optimization: don't bother measuring children who are only// laid out using excess space. These views will get measured// later if we have space to distribute.//将mTotalLength的值暂时赋值给totalLengthfinal int totalLength = mTotalLength;//重新给mTotalLength赋值,这次加上上下margin,并获取最大值mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);//跳过测量(后面用到)skippedMeasure = true;} else {//如果子控件的高度为不为具体数值或者MATCH_PARENTif (useExcessSpace) {// The heightMode is either UNSPECIFIED or AT_MOST, and// this child is only laid out using excess space. Measure// using WRAP_CONTENT so that we can find out the view's// optimal height. We'll restore the original height of 0// after measurement.lp.height = LayoutParams.WRAP_CONTENT;}// Determine how big this child would like to be. If this or// previous children have given a weight, then we allow it to// use all available space (and we will shrink things later// if needed).//如果目前还没有任何设置了权重的子控件,则在子控件测量时去除已经分配的父容器的空间final int usedHeight = totalWeight == 0 ? mTotalLength : 0;//子控件的测量,实际上调用了measureChildWithMargins方法measureChildBeforeLayout(child, i, widthMeasureSpec, 0,heightMeasureSpec, usedHeight);//获取子控件的高度final int childHeight = child.getMeasuredHeight();if (useExcessSpace) {// Restore the original height and record how much space// we've allocated to excess-only children so that we can// match the behavior of EXACTLY measurement.lp.height = 0;//记录非0dp或者非MATCH_PARENTA的控件 的总高度consumedExcessSpace += childHeight;}final int totalLength = mTotalLength;//重新计算当前总高度mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));if (useLargestChild) {largestChildHeight = Math.max(childHeight, largestChildHeight);}}/*** If applicable, compute the additional offset to the child's baseline* we'll need later when asked {@link #getBaseline}.*/if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {mBaselineChildTop = mTotalLength;}// if we are trying to use a child index for our baseline, the above// book keeping only works if there are no children above it with// weight.  fail fast to aid the developer.if (i < baselineChildIndex && lp.weight > 0) {throw new RuntimeException("A child of LinearLayout with index "+ "less than mBaselineAlignedChildIndex has weight > 0, which "+ "won't work.  Either remove the weight, or don't set "+ "mBaselineAlignedChildIndex.");}boolean matchWidthLocally = false;if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {// The width of the linear layout will scale, and at least one// child said it wanted to match our width. Set a flag// indicating that we need to remeasure at least that view when// we know our width.matchWidth = true;matchWidthLocally = true;}//获取左右边距final int margin = lp.leftMargin + lp.rightMargin;//计算控件总宽度final int measuredWidth = child.getMeasuredWidth() + margin;//记录最大宽度maxWidth = Math.max(maxWidth, measuredWidth);childState = combineMeasuredStates(childState, child.getMeasuredState());allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;if (lp.weight > 0) {/** Widths of weighted Views are bogus if we end up* remeasuring, so keep them separate.*/weightedMaxWidth = Math.max(weightedMaxWidth,matchWidthLocally ? margin : measuredWidth);} else {alternativeMaxWidth = Math.max(alternativeMaxWidth,matchWidthLocally ? margin : measuredWidth);}i += getChildrenSkipCount(child, i);}if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {mTotalLength += mDividerHeight;}//useLargestChild对应着xml文件中的measureWithLargestChild属性,默认为falseif (useLargestChild &&(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {mTotalLength = 0;for (int i = 0; i < count; ++i) {final View child = getVirtualChildAt(i);if (child == null) {mTotalLength += measureNullChild(i);continue;}if (child.getVisibility() == GONE) {i += getChildrenSkipCount(child, i);continue;}final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)child.getLayoutParams();// Account for negative marginsfinal int totalLength = mTotalLength;mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));}}// Add in our paddingmTotalLength += mPaddingTop + mPaddingBottom;int heightSize = mTotalLength;// Check against our minimum heightheightSize = Math.max(heightSize, getSuggestedMinimumHeight());// Reconcile our calculated size with the heightMeasureSpecint heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);heightSize = heightSizeAndState & MEASURED_SIZE_MASK;// Either expand children with weight to take up available space or// shrink them if they extend beyond our current bounds. If we skipped// measurement on any children, we need to measure them now.//remainingExcess表示当前LinearLayout剩余空间int remainingExcess = heightSize - mTotalLength+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);//如果还有剩余空间需要再分配if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {//remainingWeightSum表示总权重float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;mTotalLength = 0;//再次遍历子控件for (int i = 0; i < count; ++i) {final View child = getVirtualChildAt(i);if (child == null || child.getVisibility() == View.GONE) {continue;}final LayoutParams lp = (LayoutParams) child.getLayoutParams();final float childWeight = lp.weight;//计算每一个控件除了本身设置的宽高之外还可以分享的剩余空间的大小if (childWeight > 0) {final int share = (int) (childWeight * remainingExcess / remainingWeightSum);remainingExcess -= share;remainingWeightSum -= childWeight;final int childHeight;//重新计算子控件的高度if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {childHeight = largestChildHeight;} else if (lp.height == 0 && (!mAllowInconsistentMeasurement|| heightMode == MeasureSpec.EXACTLY)) {// This child needs to be laid out from scratch using// only its share of excess space.childHeight = share;} else {// This child had some intrinsic height to which we// need to add its share of excess space.childHeight = child.getMeasuredHeight() + share;}//构造子控件的测量规格final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(0, childHeight), MeasureSpec.EXACTLY);final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,lp.width);//测量子控件child.measure(childWidthMeasureSpec, childHeightMeasureSpec);// Child may now not fit in vertical dimension.childState = combineMeasuredStates(childState, child.getMeasuredState()& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));}//获取控件的左右marginfinal int margin =  lp.leftMargin + lp.rightMargin;//获取控件总宽度final int measuredWidth = child.getMeasuredWidth() + margin;maxWidth = Math.max(maxWidth, measuredWidth);boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&lp.width == LayoutParams.MATCH_PARENT;alternativeMaxWidth = Math.max(alternativeMaxWidth,matchWidthLocally ? margin : measuredWidth);allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;final int totalLength = mTotalLength;mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));}// Add in our padding//最终再给总高度加上上下内边距mTotalLength += mPaddingTop + mPaddingBottom;// TODO: Should we recompute the heightSpec based on the new total length?} else {alternativeMaxWidth = Math.max(alternativeMaxWidth,weightedMaxWidth);// We have no limit, so make all weighted views as tall as the largest child.// Children will have already been measured once.if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {for (int i = 0; i < count; i++) {final View child = getVirtualChildAt(i);if (child == null || child.getVisibility() == View.GONE) {continue;}final LinearLayout.LayoutParams lp =(LinearLayout.LayoutParams) child.getLayoutParams();float childExtra = lp.weight;if (childExtra > 0) {child.measure(MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(largestChildHeight,MeasureSpec.EXACTLY));}}}}if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {maxWidth = alternativeMaxWidth;}maxWidth += mPaddingLeft + mPaddingRight;// Check against our minimum widthmaxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());//设置测量结果setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),heightSizeAndState);if (matchWidth) {forceUniformWidth(count, heightMeasureSpec);}}

OK,关键地方我已经在代码中注释了,整体思路就是先遍历一遍所有的子控件,测量出子控件的高度,这是第一步,第二步则将容器中剩余的空间根据权重再分配,整体思路就是这样,当然这里还有很多条条框框,我都已在注释中写明。

4.最根上的容器测量

OK,说到这里,相信小伙伴们对View的测量已经有了一个大体的认识了,一个View能够显示出来,它的大小要依靠它的父容器和它自己共同来决定,比如下面一段代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><RelativeLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content" /></RelativeLayout>
</LinearLayout>

RelativeLayout的LayoutParams一方面它和LinearLayout的LayoutParams共同决定了RelativeLayout的大小,另一方面,它也和TextView的LayoutParams共同决定了TextView的大小。那么有小伙伴们可能会有疑问了,那么LinearLayout的大小又由谁决定呢?它的父容器又是谁呢?读过View绘制详解,从LayoutInflater谈起,View绘制详解(二),从setContentView谈起 这两篇博客的小伙们应该因该知道,对于一个页面而言,最顶层的View是DecorView,所以针对本案例中LinearLayout的测量方式其实和普通容器的测量方式是一致的。不必赘述,问题是总会有一个View没有父容器的,那么这个View的宽高又是如何测量的呢?要弄清楚这个问题,我们首先需要明白View系统启动measure是从ViewRootImpl的performMeasure方法开始的,而performMeasure方法则是在performTraversals中调用的,关于performTraversals方法我贴出一部分源码如下(ViewRootImpl.java):

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed!  mWidth="+ mWidth + " measuredWidth=" + host.getMeasuredWidth()+ " mHeight=" + mHeight+ " measuredHeight=" + host.getMeasuredHeight()+ " framesChanged=" + framesChanged);// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

performTraversals方法原本太长了,我这里贴出其中一部分。小伙伴们注意,在这里系统调用了performMeasure方法进行控件的测量工作,测量的时候传递了两个参数,一个是childWidthMeasureSpec,另一个是childHeightMeasureSpec。这两个参数都是通过一个叫做getRootMeasureSpec方法获得到的,该方法接收两个参数,第一个参数mWidth/mHeight表示窗口期望显示的大小,在这里实际上就是手机屏幕的大小,第二个参数lp.width/lp.height实际都是MATCH_PARENT,这个从它们初始化的地方就能看出端倪。OK,那我们就来看看这个方法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;}

这个方法很简单,根据传进来的windowSize和rootDimension,然后通过MeasureSpec.makeMeasureSpec方法将这两个数据组合成一个int类型数据。通过这个方式获取到最根上的widthMeasureSpec和heightMeasureSpec之后接下来就可以调用performMeasure方法来测量了,来看看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);}}

小伙伴们看到,从这里又进入到了measure方法中,后面的流程就和我们前面所述的一样了,这里我就不再赘述。

OK,以上就是View测量过程的一个简单分析,有问题欢迎留言讨论。

以上。

转载于:https://www.cnblogs.com/qitian1/p/6461663.html

View绘制详解(三),扒一扒View的测量过程相关推荐

  1. View绘制体系(三)——AttributeSet与TypedArray详解

    View绘制体系(三)--AttributeSet与TypedArray详解 前言 上篇博客中讲了LayoutInflater.inflate机制,其中提到了AttributeSet和XmlPullP ...

  2. python绘制单线图_CAD制图管道单线图绘制详解.ppt

    您所在位置:网站首页 > 海量文档 &nbsp>&nbsp计算机&nbsp>&nbsp计算机辅助设计 CAD制图管道单线图绘制详解.ppt89页 本文 ...

  3. Android init.rc文件解析过程详解(三)

    Android init.rc文件解析过程详解(三) 三.相关结构体 1.listnode listnode结构体用于建立双向链表,这种结构广泛用于kernel代码中, android源代码中定义了l ...

  4. android标尺自定义view,android尺子的自定义view——RulerView详解

    项目中用到自定义尺子的样式: 原效果为 因为跟自己要使用的view稍有不同 所以做了一些修改,修改的注释都放在代码中了,特此记录一下. 首先是一个自定义View: public class RuleV ...

  5. linux 进程间通信 dbus-glib【实例】详解三 数据类型和dteeth(类型签名type域)(层级结构:服务Service --> Node(对象、object) 等 )(附代码)

    linux 进程间通信 dbus-glib[实例]详解一(附代码)(d-feet工具使用) linux 进程间通信 dbus-glib[实例]详解二(上) 消息和消息总线(附代码) linux 进程间 ...

  6. Windows 7防火墙设置详解(三)

    Windows 7防火墙设置详解(三) 一.如何禁用或启用规则 方法:只需要在需要禁用或启动的规则上,鼠标右键选择启用或禁止规则即可,或点击右侧的操作栏进行规则启用或禁止. 二.入站规则和出站规则 由 ...

  7. Android Studio 插件开发详解三:翻译插件实战

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/78113868 本文出自[赵彦军的博客] 系列目录 Android Gradle使用 ...

  8. 数据结构--图(Graph)详解(三)

    数据结构–图(Graph)详解(三) 文章目录 数据结构--图(Graph)详解(三) 一.深度优先生成树和广度优先生成树 1.铺垫 2.非连通图的生成森林 3.深度优先生成森林 4.广度优先生成森林 ...

  9. SharePoint2007安装图文详解三:安装SqlServer2005

    SharePoint2007 中的很多功能会用到数据库,如分析服务,报表服务等.本文介绍SqlServer2005的安装,数据库的安装很简单,基本上安装默认选项点击下一步即可,需要注意的地方在下面会提 ...

最新文章

  1. 探索频道和谷歌联合制作七大洲人文VR视频,11月3日可收看
  2. 牙齿间隙变大怎么办_牙齿矫正会让牙缝变大吗?
  3. nginx的master和worker进程间的通信
  4. Python Django 自定义Manager实现批量删除(逻辑删除)
  5. 重载练习1_四种不同参数类型的方法
  6. Leetcode Maximal Rectangle
  7. java转net容易吗_每日一醒(1):学习Java容易忽视的小错误,你注意到了吗?
  8. 转】Eclipse编辑Spring配置文件xml时自动提示类class包名
  9. HashMap的底层原理
  10. 浏览器一直不停的异步请求(环境:vs.net mvc)
  11. Python——OpenCV形态学处理(膨胀与腐蚀)
  12. 【w3cschool】C语言复习
  13. 工厂模型——简单工厂和工厂方法
  14. 地下城php补丁怎么用,dnf补丁怎么用,教你如何学会使用补丁
  15. Java国际手机号正则校验
  16. gabor滤波 matlab,图像处理 – 使用matlab应用Gabor方程创建Gabor滤波器
  17. 【历史上的今天】8 月 20 日:两位传奇程序员的诞生日!
  18. 商界男士西服着装技巧
  19. Windows如何使用自带的桌面整理工具?
  20. 迷你播放器--第一阶段(1)--检索媒体音乐并添加到List播放列表

热门文章

  1. 计算机基本知识实训报告,计算机实训报告小结
  2. 数据结构期末复习之插入排序
  3. Tensorflow动态seq2seq使用总结
  4. 乘法更新规则对于并发的非负矩阵分解和最大间隔分类
  5. 自动泊车算法中混合A*粗路径的MATLAB实现
  6. Java jar 版本 查看
  7. 湖首大学计算机科学硕士申请,湖首大学王牌专业之一丨计算机科学专业
  8. angular 注入器配置_Angular2 多级注入器详解及实例
  9. .interface文件怎么看啊_【干货】Java关键字合集,看这篇就够了!
  10. 2021年上半年系统集成项目管理工程师综合知识真题及答案解析