Android View measure流程详解

Posted 低调小一

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android View measure流程详解相关的知识,希望对你有一定的参考价值。

android View measure流程详解

Android中View绘制的流程包括:measure(测量)->layout(布局)->draw(绘制).

因为Android中每个View都占据了一块矩形的空间,当我们要在屏幕上显示这个矩形的View的时候

  1. 首先我们需要知道这个矩形的大小(宽和高)这就对应了View的measure流程.
  2. 有了View的宽和高,我们还需要知道View左上角的起点在哪里,右下角的终点在哪里,这就对应了View的layout流程.
  3. 当矩形的区域在屏幕上确定之后,相当于屏幕上有了一块属于View的画布,接下来通过draw方法就可以在这块画布上画画了.

本文重点介绍View的measure流程.一般情况下,我们都是在xml文件里去定义一个布局文件,针对每个View来说,是一定要声明layout_width和layout_height的,不然编译期就会报错.

那layout_width和layout_height数值包括:

  • 具体的长度单位,例如20dp或者20px.
  • match_parent,代表充满父控件.
  • wrap_content,代表能包含View中内容即可.

从layout_width和layout_height的取值来看,如果Android规定只能使用具体的长度,那可能就不需要measure流程了,因此宽和高已经知道了.因此,从取值上我们知道了measure方法的作用是:将match_parent和wrap_content转成具体的度量值.


measure方法

接下来,我们从源码的角度去分析measure方法.

/**
 * <p>
 * 这个方法是用来测试View的宽和高的. View的父附件提供了宽和高的限制参数,即View的最值.
 * </p>
 *
 * <p>
 * View的真正测量工作是在 {@link #onMeasure(int, int)} 函数里进行的. 因此, 只有
 * {@link #onMeasure(int, int)} 方法才能被子类重写.
 * </p>
 *
 */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    // 判断View的layoutMode是否为LAYOUT_MODE_OPTICAL_BOUNDS
    boolean optical = isLayoutModeOptical(this);
    // 子View是LAYOUT_MODE_OPTICAL_BOUNDS,父附件不是LAYOUT_MODE_OPTICAL_BOUNDS的情况很少见,不需要去care
    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);
    }

    // 生成View宽高的缓存key,并且如果缓存Map为null,则构建缓存Map.
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

    // 判断是否为强制布局或者宽、高发生了变化
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {

        // 清除PFLAG_MEASURED_DIMENSION_SET标记,表示该View还没有被测量.
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        // 解析从右向左的布局
        resolveRtlPropertiesIfNeeded();

        // 判断是否能用cache缓存中view宽和高
        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // View宽和高真正测量的地方
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            // 获取缓存中的View宽和高
            long value = mMeasureCache.valueAt(cacheIndex);
            // long占8个字节,前4个字节为宽度,后4个字节为高度.
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        // 无论是调用onMeasure还是使用缓存,都应该设置了PFLAG_MEASURED_DIMENSION_SET标志位.
        // 没有设置,则说明测量过程出了问题,因此抛出异常.
        // 并且,一般出现这种情况一般是子类重写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;
    }

    // 记录View的宽和高,并将其存储到缓存Map里.
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL);
}

从measure方法中,我们可以看出,measure方法只要是加了一个缓存机制,真正的测量还是在onMeasure去执行.为了防止缓存的滥用,Android系统直接将measure设置为final类型,即子类不能重写,意味着子类只需要提供测量的具体实现,不需要care缓存等提速功能的实现.

onMeasure方法

onMeasure的默认实现非常简单,注释源码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 将getDefaultSize函数处理后的值设置给宽和高的成员变量
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    // 特殊处理LAYOUT_MODE_OPTICAL_BOUNDS的情况
    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;
    }

    // 真正设置View宽和高的地方
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    // 保存宽度到View的mMeasuredWidth成员变量中
    mMeasuredWidth = measuredWidth;
    // 保存高度到View的mMeasuredHeight成员变量中
    mMeasuredHeight = measuredHeight;
    // 设置View的PFLAG_MEASURED_DIMENSION_SET,即代表当前View已经被测量过.
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

通过源码,我们发现onMeasure只是给mMeasuredWidth和mMeasuredHeight两个成员变量赋值.那这个值是如何获取到的呢?和Parent提供的限制又有神马关系呢?
想回答这个问题,需要先跟踪一下getDefaultSize的源码.

public static int getDefaultSize(int size, int measureSpec) {
    // size表示View想要的尺寸信息,比如最小宽度或者最小高度
    int result = size;
    // 解析SpecMode信息
    int specMode = MeasureSpec.getMode(measureSpec);
    // 解析SpecSize信息
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        // 如果View的度量值设置为WARP_CONTENT的时候
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        // 如果View的度量值设置为具体数值或者MATCH_PARENT的时候
        result = specSize;
        break;
    }
    return result;
}

同时,需要看一下getSuggested一系列方法,以getSuggestedMinimumWidth为例:

protected int getSuggestedMinimumWidth() {
    // 如果View没有设置背景,则返回View本身的最小宽度mMinWidth.
    // 如果View设置了背景,那么就取mMinWidth和背景宽度的最大值.
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

这里有同学会奇怪View的mMinWidth(最小宽度)是什么时候设置的?我们可以看一下View的构造函数(截取部分代码):

case R.styleable.View_minWidth:
    mMinWidth = a.getDimensionPixelSize(attr, 0);
    break;

可以看到,和我们自定义一个View是一样的,mMinWidth成员变量对应着的自定义属性是minWidth,如果xml中未定义则默认值是0.
示例设置代码:

<TextView
    android:minWidth=100dp
    android:id="@+id/id_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:text="Hello World!"
    android:textColor="#ffffff"
    android:textSize="30sp" />

或许还有同学会对上面的MeasureSpec感到陌生,不要着急,这就带来MeasureSpec的详解.

MeasureSpec

MeasureSpec的值由specSize和spceMode共同组成,其中specSize记录的是大小,specMode记录的是规格.
我们来看一下MeasureSpec的源码定义,他是View的内部类,源码位于: /frameworks/base/core/java/android/view/View.java

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

    // specMode一共有三种类型
    public static final UNSPECIFIED = 0 << MODE_SHIFT;  // 任意大小
    public static final int EXACTLY = 1 << MODE_SHIFT;  // 表示父容器希望子视图的大小由specSize来决定
    public static final int AT_MOST = 2 << MODE_SHIFT;  // 表示子视图最多只能是specSize中指定的大小

    public static int makeMeasureSpec(int size, int mode) {
        return size + mode;
    }

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

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

只看源码可能大家还比较陌生,这里结合源码讲解一下.首先,Android只所以提供MeasureSpec类,我认为Android开发人员觉得View尺寸最大不超过2^30,所以将剩余的2位来表示模式,这样节约了空间.
前面说过,View的宽和高只有三种情况,分别是:具体数值,WARP_CONTENT,MATCH_PARENT.而measure就是将WARP_CONTENT,MATCH_PARENT转换为具体数值的方法,所以需要有MeasureSpec.getSize()方法获取具体数值.
那MeasureSpec.getMode的几种类型和View的赋值也是有对应关系的:

  • EXACTLY: 对应着具体数值或者MATCH_PARENT
  • AT_MOST: 对应着WRAP_CONTENT
  • UNSPECIFIED: 父容器对View无限制,这中情况一般出现在Android系统内部.

ViewGroup的onMeasure流程

对于非ViewGroup的View,上面介绍的measure->onMeasure方法足以完成View的测量.
但是对于自定义ViewGroup,我们不仅要测量自己的宽和高,同时还需要负责测量child view的宽和高.

测量child view通常会用到measureChild方法.源码实现如下:

/**
 * 测量每个子View的宽高.测量时需要排除padding的影响,如果需要排除margins,则调用measureChildWithMargins方法.
 */
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方法只要是获取子View的widthMeasureSpec和heightMeasureSpec,然后调用measure方法设置子View的实际宽高值.同时,从getChildMeasureSpec的方法传递中,我们可以看出child View的值是由父View和child view共同决定的.

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 获取父容器的MeasureSpecMode
    int specMode = MeasureSpec.getMode(spec);
    // 获取父容器的具体大小
    int specSize = MeasureSpec.getSize(spec);
    // 子view的可能大小=父View的大小-padding
    int size = Math.max(0, specSize - padding);

    // 设置子View的初始MeasureSpecSize和MeasureSpecMode均为0
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // 当父容器有固定大小(具体数值或MATCH_PARENT)的情况下
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            // view的宽或高赋值为具体数值,例如20dp
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // view的宽或高赋值为MATCH_PARENT
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // view的宽或高赋值为WRAP_CONTENT,这时View的最大值就是父容器大小-padding.所以MeasureSpecSize=父容器大小-padding.
            // 同时,View的最大值为MeasureSpecSize,所以MeasureSpecMode设置为AT_MOST
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 当父容器是AT_MOST情况下(即父容器大小赋值为WRAP_CONTENT)
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // 由于父容器的mode为AT_MOST,虽然子View设置为MATCH_PARENT,但是父容器大小不是精确的,所以子View也只能是AT_MOST了.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 这个case不需要care,自定义控件不会遇到这种case的.
    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;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

getChildMeasureSpec的总体思路:
通过其父容器提供的MeasureSpec参数得到specMode和specSize,并根据计算出来的specMode以及子视图的childDimension(layout_width和layout_height中定义的值)来计算子View自身的measureSpec和measureMode.

以上是关于Android View measure流程详解的主要内容,如果未能解决你的问题,请参考以下文章

Android View 测量流程(Measure)完全解析

Android View体系从源码解析View的measure流程

Android应用层View绘制流程之measure,layout,draw三步曲

Android - View的绘制流程一(measure)

Android View的绘制流程三部曲 —— Measure

Android自定义view之view显示流程