Android——View的工作流程——单一View的measure过程

目录结构

一、measure 过程

measure 过程

1. measure()

测量单一 view 大小的入口方法是 View 类的measure()方法,在该方法中会调用 View 类的onMeasure()方法,我们直接看onMeasure()方法的实现即可。

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 bytes
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffff L;
    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 flag
        mPrivateFlags &= ~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 back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            long value = mMeasureCache.valueAt(cacheIndex);
            // Casting a long to int drops the high 32 bits, no mask needed
            setMeasuredDimensionRaw((int)(value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        // flag not set, setMeasuredDimension() was not invoked, we raise
        // an exception to warn the developer
        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;

    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
        (long) mMeasuredHeight & 0xffffffff L); // suppress sign extension
}

2. onMeasure()

onMeasure()方法中主要有两个方法setMeasuredDimension()getDefultSize()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

3. getDefaultSize()

通过 View 类中的getDefultSize()方法测量 View 的尺寸大小。该方法逻辑如下所示。

view 宽/高测量逻辑
// size:传入 view 的默认宽/高
// measureSpec:传入 view 的测量规格
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result;
}

4. setMeasuredDimension()

View 类中的setMeasuredDimension()方法存储测量后的大小(宽/高)

二、重点说明

1.getDefaultSize()中如何得到 view 的默认宽/高

其中,view 的默认宽度通过getSuggestedMinimumWidth()获得。

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

view 的默认高度通过getSuggestedMinimumHeight()获得。

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

上述两个方法原理相同,下面说明getSuggestedMinimumWidth()的逻辑:

  • 若 view 无背景图片:view的默认宽度 = mMinWidth。
    指定了android:minWidth属性,则mMinWidth为指定值;
    未指定该属性的值,则默认为0。
  • 若 view 有背景图片:view的默认宽度=max(mMinWidth , 背景图片的原始宽度)

那么,如何获得 Drawable 的原始宽度?见获得 Drawable 的原始宽度

2.getDefaultSize()中子 View 测量规格 MeasureSpec 的创建过程

MeasureSpec类是View类中的一个静态内部类。

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0x3 << MODE_SHIFT;

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    public static final int EXACTLY = 1 << MODE_SHIFT;

    public static final int AT_MOST = 2 << MODE_SHIFT;

    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
        @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

}

测量规格 MeasureSpec 是一个 32 位的 int 值,前 2 位保存测量模式(specMode),后 30 位保存测量大小(specSize)。将这两个值打包成一个 int 是为了避免过多的对象内存分配。

View 的 MeasureSpec 的创建过程对于顶层 View(DecorView)和普通 View 有所不同。

(1) DecorView 的 MeasureSpec 创建
取决于窗口尺寸大小,和 View 自身的 LayoutParams

(2)普通 View 的 MeasureSpec 创建
取决于父容器的 MeasureSpec,和 View 自身的 LayoutParams

  • 创建时机

对于普通 View 来说,View 的 measure 过程由 ViewGroup 传递而来,ViewGroup 的measureChildWithMargins()方法如下。该方法中在对子元素进行 measure 之前,先调用方法getChildMeasureSpec()得到子元素的测量规格

// 测量ViewGroup的子View:child
protected void measureChildWithMargins(View child,
    int parentWidthMeasureSpec, int widthUsed,
    int parentHeightMeasureSpec, int heightUsed) {
    // 得到子View的LayoutParams参数
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    // 调用方法getChildMeasureSpec()得到子View的测量规格 childWidthMeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
        mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin +
        widthUsed, lp.width);
    // 调用方法getChildMeasureSpec()得到子View的测量规格 childHeightMeasureSpec
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
        heightUsed, lp.height);
    // 进行单一View的测量过程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
  • 计算方法

因此,子元素 MeasureSpec 的创建过程是在 ViewGroup 的getChildMeasureSpec()方法中完成的。

/**
 * ViewGroup 类中的方法,用于根据父容器的MeasureSpec和子View自身的LayoutParams得到子View的MeasureSpec
 *
 * @param spec :父View的测量规格(MeasureSpec) 
 * @param padding :父view中已占用的空间大小
 * @param childDimension :当前view的布局参数(宽/高),是我们所说的LayoutParams相关参数
 * @return a MeasureSpec integer for the child:返回子View的测量规格(MeasureSpec)
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 父view的测量模式
    int specMode = MeasureSpec.getMode(spec);
    // 父view的测量大小
    int specSize = MeasureSpec.getSize(spec);
    //通过父view计算出的子view大小 = 父大小-边距(父要求的大小,但子view不一定用这个值)   
    int size = Math.max(0, specSize - padding);

    // 子view想要的实际大小和模式(需要计算)
    int resultSize = 0;
    int resultMode = 0;

    // 通过父view的MeasureSpec和子view自身的LayoutParams计算子view的测量规格
    switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:

            if (childDimension >= 0) {
                // 当子view的LayoutParams>0,即有确切的值时
                // 子view的测量大小为自身所指定的值,测量模式为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 当子view的LayoutParams==-1为MATCH_PARENT时
                // 子view的测量大小为通过父view计算出的子view大小 ,测量模式为EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 当子view的LayoutParams==-2为MATCH_PARENT时
                // 子view的测量大小自己决定,但不能超过通过父view计算出的子view大小 ,测量模式为AT_MOST
                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
            // 当父view的模式为UNSPECIFIED时,父view不对view有任何限制,要多大给多大
        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);
}

通过下表对getChildMeasureSpec() 方法的逻辑进行梳理。表中的parentSize为父容器中可使用的大小,childSize为子 View 指定的具体数值。

摘自《Android开发艺术探索》

由上表可知:
普通 View 的 MeasureSpec 由父容器的 MeasureSpec 和 View 自身的 LayoutParams 决定。针对不同的父容器和 View 自身不同的 LayoutParams,View 就可以有多种 MeasureSpec。

规律总结

参考文献

自定义View Measure过程 - 最易懂的自定义View原理系列(2)
任玉刚_Android开发艺术探索

你可能感兴趣的:(Android——View的工作流程——单一View的measure过程)