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.

更多相关文章

  1. Android中可以做的两件坏事---破解锁屏密码和获取Wifi密码
  2. Android中使用语音引擎入门七步曲
  3. Android自定义View的实现方法,带你一步步深入了解View(三) 。
  4. Android(安卓)如何自定义一个简单的组件和自定义的点击事件(中级)
  5. Android五大基本组件
  6. 进度圈的显示
  7. Android(安卓)Graphics专题(1)--- Canvas基础
  8. 浅谈Java中Collections.sort对List排序的两种方法
  9. Python list sort方法的具体使用

随机推荐

  1. React Native接入现有Android原生工程并
  2. Android res .9.png android九宫图
  3. Android Training学习笔记之适配不同的设
  4. android布局基础及范例:人人android九宫格
  5. [Android(安卓)学习笔记] 判断 Android(
  6. Android底部弹出iOS7风格对话选项框
  7. 【NFC】Android(安卓)NFC API Reference
  8. Android系统架构——揭开Android系统框架
  9. Android 5.1.1 源码目录结构
  10. Swing中引入Android的NinePatch技术,让Swi