measure 过程要分情况来看,如果只是一个原始的 View,那么通过 measure 方法就完成了测量过程,如果是一个 ViewGroup,除了完成自己的测量以外,还会遍历调用所有子元素的 measure 方法,各个子元素在递归去执行这个流程,下面针对两种情况分别分析。
1. View 的 measure 过程
View 的 measure 过程有由其 measure 方法来完成,measure 方法是一个 final 方法,所以子类是不能重写此方法的,在 View 的 measure 方法中会调用 View 的 onMeasure 方法,因此只需要看 onMeasure 方法即可,View 的 onMeasure 方法如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
上述代码简洁,但并不简单,setMeasuredDimension 方法设置了 View 的宽高的测量,来看下代码:
/** * <p>This method must be called by {@link #onMeasure(int, int)} to store the * measured width and measured height. Failing to do so will trigger an * exception at measurement time.</p> * * @param measuredWidth The measured width of this view. May be a complex * bit mask as defined by {@link #MEASURED_SIZE_MASK} and * {@link #MEASURED_STATE_TOO_SMALL}. * @param measuredHeight The measured height of this view. May be a complex * bit mask as defined by {@link #MEASURED_SIZE_MASK} and * {@link #MEASURED_STATE_TOO_SMALL}. */ protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); }
可以看出,setMeasuredDimension 方法需要通过getDefaultSize 方法来得到 View 的宽高,然后在进行测量,因此我们只需要看 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 * @param measureSpec Constraints imposed by the parent * @return The size this view should be. */ 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; }
可以看出,getDefaultSize 方法的逻辑很简单,对于我们来说,我们只需要看 AT_MOST 和 EXACTLY这两种情况。简单的理解,其实 getDefaultSize 返回的大小就是 MeasureSpec 中的 specSize,而这个 specSize就是 View 测量后的宽高,这里多次提到测量后的大小,是因为 View 的最终大小是在 layout 阶段确定的,所以这里必须加以区分,但是几乎所有情况下 View 测量后的大小就是 View 的最终大小,需要注意的是不是所有情况,后续讲到 layout 过程的死后会说明,好了,我们继续往下看。
至于 UNSPECIFIEND 这种情况,一般用于系统内部的测量过程,在这种情况下,View 的大小为 getDefaultSize 的第一个参数 size,通过上边的源码我们可以看明白。而通过 onMeasure 的源码,我们可以看明白,这个 size 其实就是 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 这两个方法的返回值,我们来看下 getSuggestedMinimumWidth() 的源码,getSuggestedMinimumHeight() 和 getSuggestedMinimumWidth() 原理是一样的,所以只需要看其中一个就好:
/** * Returns the suggested minimum height that the view should use. This * returns the maximum of the view's minimum height * and the background's minimum height * ({@link android.graphics.drawable.Drawable#getMinimumHeight()}). * <p> * When being used in {@link #onMeasure(int, int)}, the caller should still * ensure the returned height is within the requirements of the parent. * * @return The suggested minimum height of the view. */ protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }从源码中,我们可以看出,如果 View 没有设置背景,那么我们得到的 View 的宽度就是 mMinWidth,而 mMinWidth 又对应于 android:minWidth 这个属性所指定的值,因此 View 的宽度即为 android:minWidth 所指定的值。如果 android:minWidth 不指定大小,那么它默认是0,也就是说,View 在 android:minWidth 不指定的情况下,宽高都是0,;如果 View 设置了背景,则 View 的宽度为 max(mMinWidth, mBackground.getMinimumWidth()),mMinWidth 的含义刚才我们已经说了,那么 mBackground.getMinimumWidth() 是什么呢?我们来看下 Drawable 的 getMinimumWidth() 方法(如果不明白为什么明明是 View 类,怎么又和 Drawable 类扯上了关系,请查看源码,这里就不解释了):
/** * Returns the minimum width suggested by this Drawable. If a View uses this * Drawable as a background, it is suggested that the View use at least this * value for its width. (There will be some scenarios where this will not be * possible.) This value should INCLUDE any padding. * * @return The minimum width suggested by this Drawable. If this Drawable * doesn't have a suggested minimum width, 0 is returned. */ public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); return intrinsicWidth > 0 ? intrinsicWidth : 0; }
可以看出 getMinimumWidth() 返回的就是 Drawable 的原始宽度,前提是 Drawable 有原始宽度,否则返回0,那么 Drawable 在什么情况下有原始宽度呢?这里先举两个例子,以后会讲解具体的内容。
① ShapeDrawable 无原始的宽高;
② BitmapDrawable 欧原始的宽高。
从 getDefaultSize 方法的实现来看,View 的宽高由 specSize 决定,所以我们可以得出结论,直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 warp_content 时的大小,否则在布局中使用 warp_content 就相当于使用 match_parent。为什么呢?这个原因结核上篇文章中的表格更好理解,如果 View 在布局中使用 warp_content,那么他的 specMode 是 AT_MOST,在这种模式下,他的宽高等于 specSize,查看表知道 View 的 specSize 是 parentSize,而 parentSize 是父容器中目前使用的大小,也就是当前父容器剩余空间的大小,很显然,View 的宽高就等于父容器当前剩余空间的大小,这种效果和在布局中使用 match_parent 完全一致,如何解决这个问题呢,方法如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, mHeight); } else if(widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, heightSpecSize); } else if(heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mHeight); } }在上面的代码中,我们只需要给 View 指定一个默认的内部宽高(mWidth 和 mHeight),并在 warp_content 的时候设置此宽高即可。对于非 warp_content 情形,我们沿用系统的测量值即可,至于这个默认的内部宽高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可,。如果查看 TextView、ImageView 等的源码就可以知道,针对 warp_content 情形,它们的 onMeasure 方法均做了特殊处,这里就不一一解读了,代码如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; BoringLayout.Metrics boring = UNKNOWN_BORING; BoringLayout.Metrics hintBoring = UNKNOWN_BORING; if (mTextDir == null) { mTextDir = getTextDirectionHeuristic(); } int des = -1; boolean fromexisting = false; if (widthMode == MeasureSpec.EXACTLY) { // Parent has told us how big to be. So be it. width = widthSize; } else { if (mLayout != null && mEllipsize == null) { des = desired(mLayout); } if (des < 0) { boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring); if (boring != null) { mBoring = boring; } } else { fromexisting = true; } if (boring == null || boring == UNKNOWN_BORING) { if (des < 0) { des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint)); } width = des; } else { width = boring.width; } final Drawables dr = mDrawables; if (dr != null) { width = Math.max(width, dr.mDrawableWidthTop); width = Math.max(width, dr.mDrawableWidthBottom); } if (mHint != null) { int hintDes = -1; int hintWidth; if (mHintLayout != null && mEllipsize == null) { hintDes = desired(mHintLayout); } if (hintDes < 0) { hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring); if (hintBoring != null) { mHintBoring = hintBoring; } } if (hintBoring == null || hintBoring == UNKNOWN_BORING) { if (hintDes < 0) { hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, mTextPaint)); } hintWidth = hintDes; } else { hintWidth = hintBoring.width; } if (hintWidth > width) { width = hintWidth; } } width += getCompoundPaddingLeft() + getCompoundPaddingRight(); if (mMaxWidthMode == EMS) { width = Math.min(width, mMaxWidth * getLineHeight()); } else { width = Math.min(width, mMaxWidth); } if (mMinWidthMode == EMS) { width = Math.max(width, mMinWidth * getLineHeight()); } else { width = Math.max(width, mMinWidth); } // Check against our minimum width width = Math.max(width, getSuggestedMinimumWidth()); if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(widthSize, width); } } int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight(); int unpaddedWidth = want; if (mHorizontallyScrolling) want = VERY_WIDE; int hintWant = want; int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth(); if (mLayout == null) { makeNewLayout(want, hintWant, boring, hintBoring, width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); } else { final boolean layoutChanged = (mLayout.getWidth() != want) || (hintWidth != hintWant) || (mLayout.getEllipsizedWidth() != width - getCompoundPaddingLeft() - getCompoundPaddingRight()); final boolean widthChanged = (mHint == null) && (mEllipsize == null) && (want > mLayout.getWidth()) && (mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want)); final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum); if (layoutChanged || maximumChanged) { if (!maximumChanged && widthChanged) { mLayout.increaseWidthTo(want); } else { makeNewLayout(want, hintWant, boring, hintBoring, width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); } } else { // Nothing has changed } } if (heightMode == MeasureSpec.EXACTLY) { // Parent has told us how big to be. So be it. height = heightSize; mDesiredHeightAtMeasure = -1; } else { int desired = getDesiredHeight(); height = desired; mDesiredHeightAtMeasure = desired; if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(desired, heightSize); } } int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom(); if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) { unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum)); } /* * We didn't let makeNewLayout() register to bring the cursor into view, * so do it here if there is any possibility that it is needed. */ if (mMovement != null || mLayout.getWidth() > unpaddedWidth || mLayout.getHeight() > unpaddedHeight) { registerForPreDraw(); } else { scrollTo(0, 0); } setMeasuredDimension(width, height); }
2. ViewGroup 的 measure 过程
对于 ViewGroup 来说,除了完成自己的 measure 过程以外,还会遍历去掉用所有子元素的 measure 方法,各个子元素在递归去执行这个过程。和 View 不同的是,ViewGroup 是一个抽象类,因此它没有重写 View 的 onMeasure 方法,但是它提供了一个 measureChildren 方法,代码如下:
/** * Ask all of the children of this view to measure themselves, taking into * account both the MeasureSpec requirements for this view and its padding. * We skip children that are in the GONE state The heavy lifting is done in * getChildMeasureSpec. * * @param widthMeasureSpec The width requirements for this view * @param heightMeasureSpec The height requirements for this view */ protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }从上面代码来看,ViewGroup 在 measure 时,会对每一个子元素进行 measure,measureChild 这个方法实现也很好理解:
/** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding. * The heavy lifting is done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * @param parentHeightMeasureSpec The height requirements for this view */ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
很显然,measureChild 的思想就是取出子元素的 LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。
我们知道,ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类具体实现,比如 LinearLayout、RelativeLayout 等,为什么 ViewGroup 不像 View 一样对其 onMeasure 方法做统一实现呢?那是因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此,ViewGroup 不能做统一实现。下面通过 LinearLayout 的 onMeasure 方法来分析 ViewGroup 的 measure 过程,其他 Layout 类型读者可以自行分析。
首先来看 LinearLayout 的 onMeasure 方法,如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }上述代码很简单,我们来看下竖直布局的 LinearLayout 的测量过程,即 measureVertical 方法,measureVertical 的源码比较长,下面只描述大概意思,首先看一段代码:
// See how tall everyone is. Also remember max width. for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); if (child == null) { mTotalLength += measureNullChild(i); continue; } if (child.getVisibility() == View.GONE) { i += getChildrenSkipCount(child, i); continue; } if (hasDividerBeforeChildAt(i)) { mTotalLength += mDividerHeight; } final 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. final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); skippedMeasure = true; } else { if (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; 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; 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); } }从上面的代码可以看出,系统会遍历子元素并对每个子元素执行 measureChildBeforeLayout 方法,这个方法内部会调用子元素的 measure 方法,我们来看下:
void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); } 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); }通过调用这个方法,这样各个子元素开始依次进入 measure 过程,并且系统会通过 mTotalLength 这个变量来存储 LinearLayout 在竖直方向的初始高度,每次量一个子元素,mTotalLength 就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的 margins、padding等,当子元素测量完毕后,LinearLayout 会测量自己的大小,代码如下:
// Add in our padding mTotalLength += mPaddingTop + mPaddingBottom; int heightSize = mTotalLength; // Check against our minimum height heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); // Reconcile our calculated size with the heightMeasureSpec int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); heightSize = heightSizeAndState & MEASURED_SIZE_MASK; ... setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);通过上述代码我们发现, 当子元素测量完毕之后,LinearLayout 会根据子元素的情况来测量自己的大小。针对竖直的 LinearLayout 而言,它在水平方向的测量过程遵循 View 的测量过程,在竖直方向的测量过程和 View 有所不同。具体来说是指,如果他的布局中高度采用的是 match_parent 或者具体的数值,那么他的测量过程和 View 一致,即高度为 specSize;如果它的布局中高度采用的是 warp_content,那么它的高度是所有子元素占用的高度的总和,但是仍然不能超过他的父容器的剩余空间,当然他的最终高度还需要考虑它在竖直方向的 padding,这个过程可以进一步看下源码:
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) { case MeasureSpec.AT_MOST: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; case MeasureSpec.UNSPECIFIED: default: result = size; } return result | (childMeasuredState & MEASURED_STATE_MASK); }
View 的 measure 过程是三大流程中最复杂的一个,measure 完成以后,通过 getMeasuredWidth/Height方法就可以正确获取到 View 的测量宽高,需要注意的是,在某些极端情况下,系统可能需要多次 measure 才能得到最终的宽高,在这种情形下,在 onMeasure 方法中拿到的测量宽高是不准确的,一个好的习惯是在 onLayout 方法中去获取 View 的测量宽高和最终宽高。
上面已经对 View 的 measure 过程进行了详细的分析,现在考虑一种情况。比如我们想在 Activity 已启动的时候就做一件任务,但是这一件任务需要获取某个 View 的宽高,读者可能会说,这很简单啊,在 onCreate 或者 onResume 里面获取这个 View 的宽高不就行了?读者可以自行试一下,实际上在 onCreate、onStart 和 onResume 中均无法正确获取到某个 View 的宽高信息,这是因为 Activity 的生命周期和 View 的 measure 过程不是同步执行的,因此无法保证 Activity 执行了 onCreate、onStart 和 onResume 时某个 View 已经测量完毕了,如果 View 还没有测量完毕,那获取到的宽高就是 0 了,那有没有办法解决这个问题呢?肯定是有的,下边来给出四种方法解决这个问题:
① Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:View 已经初始化完毕了,宽高已经准备好了。这个时候去获取狂傲是没有问题的。需要注意的是,onWindowFocusChanged 会被调用多次,当 Activity 的窗口得到焦点和失去焦点的时候各调用一次,具体来说,Activity 继续执行和暂停执行的时候,onWindowFocusChanged 均会被调用,如果频繁的进行 onResume 和 onPause,那么 onWindowFocusChanged 会被频繁的调用,具体代码如下:
@Override public void onWindowFocusChanged(boolean hasFocus) { if(hasFocus) { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }
② view.post(runnable)
通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 lopper 调用此 runnable 的时候,View 已经初始化好了,具体代码如下:
@Override protected void onStart() { super.onStart(); view.post(new Runnable() { @Override public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
③ ViewTreeObserver
使用 ViewTreeObserver 的众多回调可以完成这个功能,比如使用 OnGlobalLayoutListener 接口,当 View 树发生改变 或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法会被回调,因此这是获取 View 宽高一个焊好的时机,需要注意的是,伴随着 View 树的动态发生改变时,onGlobalLayout方法会被调用多次,具体代码如下:
ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeGlobalOnLayoutListener(this); int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } });
④ view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对 View 进行 measure 来得到 View 的宽高,这个方法比较复杂,这里要分情况处理,根据 View 的 LayoutParams来分:
a. match_parent
直接放弃,无法 measure 出具体的宽高,原因很简单,根据 View 的 measure 过程,构造此种 MeasureSpec 需要知道 parentSize,也就是父容器的剩余空间,而这个时候,我们无法知道 parentSize 的大小,所以理论上测不出 View 的大小。
b. 具体的数值(dp/px)
比如狂傲都是 100dp,如下measure:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); view.onMeasure(widthMeasureSpec, heightMeasureSpec);
c. warp_content
如下measure:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST); int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST); view.onMeasure(widthMeasureSpec, heightMeasureSpec);注意 (1 << 30) - 1,通过分析 MeasureSpec 的实现我们知道,View 的尺寸使用 30 位的二进制表示,也就是说最大 30 个 1(即 2 ^30 - 1),也就是 (1 << 30) - 1,在最大化的模式下,我们用 View 理论上支持的最大值去构造 MeasureSpec 是合理的。关于 View 的 measure,网络上有两个错误的用法,为什么说是错误的,首先是违背了系统的内部实现规范(因为无法通过错误的 MeasureSpec 去得出合法的 SpecMode,从而导致 measure 过程出错),其次不能保证一定能 measure 出结果。
第一种错误用法,不过这种用法已经被Google干掉了,makeMeasureSpec 的第一个参数必须是大于 0 的:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec( -1, MeasureSpec.UNSPECIFIED); int heightMeasureSpec = MeasureSpec.makeMeasureSpec( -1, MeasureSpec.UNSPECIFIED); view.onMeasure(widthMeasureSpec, heightMeasureSpec);
第二种错误用法:
view.onMeasure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);到此,View 的 measure 过程就完成了。篇幅太长,只能把 Layout 和 Draw 过程放在下一篇文章来讲解了。