Android View和ViewGroup的measure过程


本篇文章主要分析View和ViewGroup的measure过程, 由于ViewGroup还可以包含子元素, 所以相对于View来说会有几个对子View measure的方法, 下面讲分别分析。

概述

View和ViewGroup的工作流程主要指measure、layout、draw这三大流程, 即测量、布局和绘制。measure流程用来测量View和ViewGroup所占矩形区域的大小, 即测量宽高(最终宽高可能会因为layout过程而改变)。layout流程确定View和ViewGroup的最终宽高和矩形区域在父布局中的位置坐标(父布局再一层层往上, 最终会是确定了在屏幕中的位置坐标)。draw流程则是讲View和ViewGroup绘制在屏幕上(底层调用SurfaceFlinger进行绘制, 这里不进行分析)。

View 的measure过程

首先看一下android 7.0 中View.java中的measure()方法如下,被定义成了final的,View的子类将不能重写此方法。方法文档写到:调用该方法用来查明一个view应该是多大,该view所在的父容器提供宽高参数的约束信息。实际的measure工作在onMeasure方法中进行,因此只有onMeasure方法可以被子类重写。

    /**
     * <p>
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     * </p>
     * <p>
     * The actual measurement work of a view is performed in
     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
     * </p>
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent
     * @see #onMeasure(int, int)
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        // 判断当前view的LayoutMode是否为opticalbounds
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) { // 判断当前view的父容器的LayoutMode是否为opticalbounds
            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 bytes
        // 根据我们传入的widthMeasureSpec和heightMeasureSpec计算key值,我们在mMeasureCache中存储我们view的信息
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        // 如果mMeasureCache为null,则进行new一个对象
        if (mMeasureCache == null)  mMeasureCache = new LongSparseLongArray(2);
        // mOldWidthMeasureSpec和mOldHeightMeasureSpec分别表示上次对View进行量算时的widthMeasureSpec和heightMeasureSpec
        // 执行View的measure方法时,View总是先检查一下是不是真的有必要费很大力气去做真正的量算工作
        // mPrivateFlags是一个Int类型的值,其记录了View的各种状态位
        // 如果(mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT,
        // 那么表示当前View需要强制进行layout(比如执行了View的forceLayout方法),所以这种情况下要尝试进行量算
        // 如果新传入的widthMeasureSpec/heightMeasureSpec与上次量算时的mOldWidthMeasureSpec/mOldHeightMeasureSpec不等,
        // 那么也就是说该View的父ViewGroup对该View的尺寸的限制情况有变化,这种情况下要尝试进行量算
        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 flag
            // 通过运算,重置mPrivateFlags值,即View的测量状态
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            // 解决布局中的Rtl问题
            resolveRtlPropertiesIfNeeded();
            // 判断当前View是否是强制进行测量,如果是则将cacheIndex=-1,反之从mMeasureCache中获取
            // 对应的index,即从缓存中读取存储的大小。
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
           // 根据cacheIndex的大小判断是否需要重新测量,或者根据布尔变量sIgnoreMeasureCache进行判断。
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                // 重新测量,则调用我们重写的onMeasure()方法进行测量,然后重置View的状态
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                // 通过我们计算的cacheIndex值,从缓存中读取我们的测量值。
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                // 通过setMeasuredDimension()方法设置我们的测量值,然后重置View的状态
                setMeasuredDimensionRaw((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 developer
            // 如果View的状态没有改变,则会抛出异常"我们没有调用”setMeasuredDimension()"方法,一般出现在我们重写onMeasure方法,
            // 但是没有调用setMeasuredDimension方法导致的。
            if ((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;
        // 将最新的widthMeasureSpec和heightMeasureSpec进行存储到mMeasureCache
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }   

上面的measure方法大致的描述了测量的流程。

  • 1 首先判断当前View的layoutMode模式,通过调用isLayoutModeOptical方法进行判断。这个方法就是判断view是否为ViewGroup类型,然后判断layoutMode设定是否为opticalBounds。如果是,则对传入的widthMeasureSpec、heightMeasureSpec进行重新计算封装。
  • 2 判断当前view是否强制重新计算,或者传入进来的MeasureSpec是否和上次不同。这两种情况满足一种则进行测量运算。
  • 3 如果为强制测量或者忽略缓存,则调用onMeasure()方法进行测量,反之,从mMeasureCache缓存中读取上次的测量数据。
  • 4 将最新的widthMeasureSpec和heightMeasureSpec进行存储到mMeasureCache。

上面这段代码主要涉及到3个主要的地方:

  • 1 android:layoutMode属性的判断,如果是opticalBounds则需要重新计算MeasureSpec。
  • 2 MeasureSpec的概念。
  • 3 onMeasure方法。

ViewGroup的android:layoutMode属性

android:layoutMode属性我们很少用的到,设置layoutMode属性有两个常量值,默认是clipBounds,clipBounds 表示了getLeft()、getRight()等返回的原始值组成的边界;opticalBounds描述了视觉上的边界,视觉边界位于Clip bounds中,Clip bounds会更大,因为用来显示一些其他效果比如阴影和闪光等。比如Button,它的clipBounds比opticalBounds大一圈。


button.png
    /**
     * This constant is a {@link #setLayoutMode(int) layoutMode}.
     * Clip bounds are the raw values of {@link #getLeft() left}, {@link #getTop() top},
     * {@link #getRight() right} and {@link #getBottom() bottom}.
     */
    public static final int LAYOUT_MODE_CLIP_BOUNDS = 0;

    /**
     * This constant is a {@link #setLayoutMode(int) layoutMode}.
     * Optical bounds describe where a widget appears to be. They sit inside the clip
     * bounds which need to cover a larger area to allow other effects,
     * such as shadows and glows, to be drawn.
     */
    public static final int LAYOUT_MODE_OPTICAL_BOUNDS = 1;

官方介绍

layoutMode.png

View.java中的isLayoutModeOptical()方法

    /**
     * Return true if o is a ViewGroup that is laying out using optical bounds.
     * @hide
     */
    public static boolean isLayoutModeOptical(Object o) {
        return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
    }

ViewGroup.java中的isLayoutModeOptical() 方法

    /** Return true if this ViewGroup is laying out using optical bounds. */
    boolean isLayoutModeOptical() {
        return mLayoutMode == LAYOUT_MODE_OPTICAL_BOUNDS;
    }

MeasureSpec的概念

measure()方法中传的是MeasureSpec变量,在View测量的时候,系统会将该View的LayoutParams在父容器的约束(父容器的MeasureSpec)下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View的测量宽高。它是View的一个内部类,代表了一个32位int值,高2位代表SpecMode,即测量模式。低30位代表SpecSize,即在该SpecMode下面的规格大小。

SpecMode有三种模式:

  • 1 UNSPECIFIED(未指定),父容器不对子元素施加任何束缚,子元素可以得到任意想要的大小,一般用于系统内部,标识一种测量的状态。
  • 2 EXACTLY(精准),父容器已经检测出子元素所需要的确切大小,这个时候子元素的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
  • 3 AT_MOST(至多),父容器指定了一个可用大小,即SpecSize,子元素的大小不能大于这个值,具体是什么要看不同子元素的具体实现,它对应于LayoutParams中的wrap_content。
    MeasureSpec的部分源码:
/**
  * A MeasureSpec encapsulates the layout requirements passed from parent to child.
  * Each MeasureSpec represents a requirement for either the width or the height.
  * A MeasureSpec is comprised of a size and a mode.
  */
  public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    /**
     * 根据specSize和specMode打包成MeasureSpec
     */
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    /**
     * 根据MeasureSpec拆分出specMode
     */
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    /**
     * 根据MeasureSpec拆分出specSize
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    ...
 }

onMeasure方法

onMeasure方法测量View和它的内容来决定测量宽高,该方法被measure方法调用,它应该被子类重写用来提供精准高效的测量。

/**
 * <p>
 * Measure the view and its content to determine the measured width and the
 * measured height. This method is invoked by {@link #measure(int, int)} and
 * should be overridden by subclasses to provide accurate and efficient
 * measurement of their contents.
 * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
 *                         The requirements are encoded with
 *                         {@link android.view.View.MeasureSpec}.
 * @param heightMeasureSpec vertical space requirements as imposed by the parent.
 *                         The requirements are encoded with
 *                         {@link android.view.View.MeasureSpec}.
 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension()方法用来存储测量后的宽高,调用此方法完成后,测量过程就已经结束了,这之后可以拿到测量后的宽高值。

    /**
     * <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);
    }

在onMeasure方法调用setMeasuredDimension()时, setMeasuredDimension()中传入的是getDefaultSize()的返回值,看一下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 ,而这传入的measureSpec是在父容器中由父容器的measureSpec和当前子元素的LayoutParams已经共同确定的,所以由measureSpec拆分出来的specSize 就是子元素测量后的大小。针对UNSPECIFIED的情况,一般用于系统内部的测量过程,下面以分析getSuggestedMinimumWidth()为例分析该函数的返回值意义:

    /**
     * Returns the suggested minimum width that the view should use. This
     * returns the maximum of the view's minimum width)
     * and the background's minimum width
     *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
     * <p>
     * When being used in {@link #onMeasure(int, int)}, the caller should still
     * ensure the returned width is within the requirements of the parent.
     *
     * @return The suggested minimum width of the view.
     */
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

可以看出,如果View没有设置背景,那么View的宽度即为mMinWidth ,而mMinWidth 对应于android:mMinWidth 这个属性的值,如果没有设置该属性则mMinWidth 默认为0;如果View指定了背景。则View的宽度为max(mMinWidth, mBackground.getMinimumWidth()),那么mBackground.getMinimumWidth()是什么呢?看一下Drawable的getMinimumWidth()方法:

    /**
     * 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的原始宽度,如果没有原始宽度就返回0。总结一下getSuggestedMinimumWidth()的主要工作:如果View没有设置背景就返回android:mMinWidth 这个属性的值,可以人为设置该值,默认为0;如果View设置了背景,则返回mMinWidth和背景最小宽度两者中的最大值。getSuggestedMinimumWidth()的返回值就是View在UNSPECIFIED情况下的测量宽。

直接继承自View的自定义控件如果在xml布局中设置了wrap_content属性,则会实际表现起来相当于match_parent。从getDefaultSize()方法可以看出,当View设置wrap_content属性时,会是MeasureSpec.AT_MOST模式,和match_parent一样,所以需要给改自定义View设置默认的宽高值以表现出wrap_content的效果。TextView针对wrap_content进行了特殊处理,在TextView的onMeasure方法中传入的MeasureSpec中的specSize重新计算了一遍,并没有使用从父容器传来的specSize(该specSize指父容器所能提供的最大空间,和match_parent一样的效果)。

说到最后,可能会有疑问,顶层View--DecorView的MeasureSpec是如何获得的呢?因为子元素的测量需要父容器的MeasureSpec和自身的LayoutParams共同决定(这点在ViewGroup的测量过程中能看出,接下来分析),所以顶层父容器DecorView的MeasureSpec必须最先获取,然后才能开始遍历子元素的测量工作。

分析Activity的setContentView方法可知,DecorView在PhoneWindow中被创建,在Activity的onResume方法回调的时候继而调用Activity的makeVisible()方法,Activity对应的PhoneWindow对象才被WindowManager添加。此后开始View的三大流程。

// ActivityThread.java
final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    ...
    r = performResumeActivity(token, clearHide, reason);
    if (r != null) {
        final Activity a = r.activity;
        ...
        
        if (r.activity.mVisibleFromClient) {
            r.activity.makeVisible();
        }
    }
    
}

// Activity.java的makeVisible()方法:
void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

之后:
WindowManager.addView()->WindowManagerImpl.addView()->WindowManagerGlobal.addView()->创建ViewRootImpl对象->ViewRootImpl.setView(DecorView对象)->ViewRootImpl.requestLayout()->ViewRootImpl.performTraversals()->然后触发performMeasure()、performLayout()、performDraw()。

WindowManagerGlobal.addView()核心代码:
...
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    ...
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    ...
    root.setView(view, wparams, panelParentView);
    ...
}

先说结论:DecorView的MeasureSpec是由窗口的尺寸(PhoneWindow)和其自身的LayoutParams共同决定的,(对于普通View,MeasureSpec是父容器的MeasureSpec和其自身的LayoutParams共同决定),MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽高。

对于DecorView,在ViewRootImpl的measureHierarchy方法计算出DecorView的MeasureSpec值,如下:

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    ...
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
}

其中desiredWindowWidth、desiredWindowHeight分别对应着Window的宽高(屏幕的尺寸)。下面看一下getRootMeasureSpec()方法:

    /**
     * Figures out the measure spec for the root view in a window based on it's
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    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;
    }
  • 1 WindowManager.LayoutParams.MATCH_PARENT:精准模式,SpecSize就是窗口的大小。
  • 2 WindowManager.LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
  • 3 固定大小:精准模式,大小为LayoutParams中指定的值。

顶层DecorView的MeasureSpec确定后,开始遍历递归子元素进行测量流程,然后如果遇到了View就是上面分析的测量流程,如果遇到的是ViewGroup则接下来进行分析。


ViewGroup 的measure过程

ViewGroup继承自View,View中的measure方法由于final不能被重新,onMeasure也没有重写。ViewGroup 的measure情况分为两种,一个是measure自己,另一个是measure子元素。但是ViewGroup并没有定义具体的measure自己的过程,其测量过程的onMeasure需要由各个具体的子类去实现,比如LinearLayout、RelativeLayout,因为不同的ViewGroup子类有不同的布局特性。

ViewGroup measure子元素过程有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);
            }
        }
    }

其内部通过循环遍历所有的子元素,对可见的子元素再调用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);
    }

measureChildWithMargins()方法也类似,在调用getChildMeasureSpec()方法获取子元素的MeasureSpec中增加了子元素的Margin值。measureChild()函数内部清晰的表达了子元素 MeasureSpec的产生过程:先得到子元素的LayoutParams,然后调用getChildMeasureSpec()方法将父容器的parentMeasureSpec与padding值、子元素的LayoutParams一起合成了子元素的MeasureSpec。getChildMeasureSpec()方法如下:

    /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @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:
            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
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面代码的主要逻辑是:如果子元素的LayoutParams中宽高是具体精准数值时,不管父容器的 MeasureSpec是什么,子元素的MeasureSpec都是精准模式切大小遵循LayoutParams中的具体数值宽高;如果子元素的LayoutParams中宽高是match_parent,当父容器是精准模式时,子元素也是精准模式并且大小不会超过父容器剩余空间大小,当父容器是最大模式时,子元素也是最大模式并且其大小不会超过父容器剩余空间;如果子元素的LayoutParams中宽高是wrap_content时,不管父容器是精准模式还是最大化,子元素的模式总是最大化且不能超过父容器的剩余空间。


以LinearLayout分析ViewGroup的measure自己过程

LinearLayout的onMeasure()方法:

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

看得到有垂直测量和水平测量两种,下面分析 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;

    ...
    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,heightMeasureSpec, usedHeight);
    ...
    final int totalLength = mTotalLength;
    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
    ...

从上面这段代码可以看出,会遍历子元素并对每个子元素执行measureChildBeforeLayout()方法,这个方法内部会调用子元素的measure()方法,然后各个子元素开始一次进入measure过程,系统通过mTotalLength来存储LinearLayout在竖直方向的初步高度。每测量一个元素,mTotalLength就会增加,增加的部分包括子元素的高度以及子元素在竖直方向的margin等,当子元素测量完毕后,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在布局中指定的高度是wrap_content,那么LinearLayout的高度是等有所子元素测量完毕后子元素所占用的高度总和,但是仍然不能超过LinearLayout的父容器的剩余空间,还需要考虑竖直方向的pading。resolveSizeAndState()函数主要是完成最后的测量。


获取View宽高的时机

  • 1 Activity/View的onWindowFocusChanged.View已经初始化完毕,宽高准备好了,onWindowFocusChanged会被调用多次,当Activity的窗口得到焦点和失去焦点的时候均会被调用,对应Activity的onResume和onPause方法。
    @Override
    public void onWindowFocusChanged(boolean hasFocus)
    {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus)
        {
            System.out.println("onWindowFocusChanged width=" + mTextView.getWidth() + " height=" + mTextView.getHeight());
        }
    }
  • 2 View.post(runnable).通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View已经初始化好了。
view.post(new Runnable(){
     @Override
     public void run() {
          int width = view.getMeasuredWidth();          
          int height = view.getMeasuredHeight();
      }
});
  • 3 ViewTreeObserver. 使用ViewTreeObserver的很多回调方法可以获取View的宽高,比如OnGlobalLayoutListener和OnPreDrawListener。当View树的状态发生改变或者View树内部的View可见性发生改变时,OnGlobalLayoutListener会被回调,伴随着View树的状态改变等,addOnGlobalLayoutListener方法会被回调多次。
  // MineFragment中,显示"圈子,新人必读"引导页的PopupWindow中使用
  private void showPopWindow() {
    layoutAbovePopWindow.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
      @Override
      public void onGlobalLayout() {
        changeWindowAlpha(0.7f);
        popupWindow.showAsDropDown(layoutAbovePopWindow, 0, Tools.getPixelByDip(getActivity(), 10));
        popupWindow.update();
        layoutAbovePopWindow.getViewTreeObserver().removeGlobalOnLayoutListener(this);
      }
    });
  }
  // 开户上传身份证照片UploadPhotoActivity.java中使用
  private void initUI() {
    getPresenter().initUploadLayout(mCurrentPartnerId);
    mUploadPhotoView.setClearListener(this);
    mUploadPhotoView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
      @Override
      public boolean onPreDraw() {
        int photoLayoutHeight = mUploadPhotoView.getMeasuredHeight();
        if (photoLayoutHeight > 0) {
          mUploadPhotoView.setPhotoLayout(photoLayoutHeight);
        }
        mUploadPhotoView.getViewTreeObserver().removeOnPreDrawListener(this);
        return false;
      }
    });
  }
  • 4 手动对View进行measure来得到View的宽高,该方法不常用。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容