自定义控件(三) 源码分析measure流程

来源:互联网 发布:好用的护手霜 知乎 编辑:程序博客网 时间:2024/05/20 02:53

系列文章传送门 (持续更新中..) :
自定义控件(一) Activity的构成(PhoneWindow、DecorView)
自定义控件(二) 从源码分析事件分发机制
自定义控件(四) 源码分析 layout 和 draw 流程


在之前的文章中,我们比较清晰的了解了Activity的构成和事件分发机制的原理, 从这篇文章我们开始分析 view 的三个流程:测量,布局,绘制。


  • 在Android的知识体系中, 自定义控件扮演着很重要的角色, 可以说, view的重要性不低于Activity, 在和用户的各种交互中离不开各式各样的view。Android提供了一套GUI库,里面有很多控件,但是我们日常开发中有时并不能满足于此,对于很多五花八门的效果,我们常常需要通过自定义控件去实现,创造出和别人不一样的炫酷效果。

自定义view是有一定难度的,尤其是复杂的自定义view,仅仅了解普通控件的基本使用是无法完成复杂的自定义空间的。为了更好的完成自定义view,我们必须去掌握它的底层工作原理,即三个步骤:测量流程,布局流程,绘制流程,分别对应 measure、layout 和 draw。

  • 测量:决定 View 的尺寸大小;
  • 布局:决定 View 在父容器中的位置;
  • 绘制:决定怎么绘制这个 View。

  • 其中测量流程是最复杂的,很多初学者看了一点就觉得脑阔子疼,希望通过这篇文章能够帮助你理清楚头绪。

    (一)理解 MeasureSpec

    MeasureSpec 的作用:

    在view的measure过程中, MeasureSpec 参与了很重要的角色, 所以首先要理解 MeasureSpec 是个什么. 从字面上看, 是 Measure 、Specification 两个单词的缩写,直译貌似大约像是“测量规格”。在源码中,它用于处理两个信息:尺寸大小和测量模式。
    • MeasureSpec 代表一个 32 位的int值,高2位代表 specMode 即测量模式,低30位代表 specSize 即尺寸大小,我们看一下 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( int size, int mode) {    if (sUseBrokenMakeMeasureSpec) {        return size + mode;    } else {        return (size & ~MODE_MASK) | (mode & MODE_MASK);    }}public static int getMode(int measureSpec) {    return (measureSpec & MODE_MASK);}public static int getSize(int measureSpec) {    return (measureSpec & ~MODE_MASK);}

    可以看到 MeasureSpec 通过把 specMode 和 specSize 打包成一个int值来避免过多的内存分配,内部也提供了打包和解包的方法,即可以把 specMode、specSize 打包为一个 MeasureSpec 的32位int值,也可以通过解包 MeasureSpec 得到 specMode、specSize 的int值。

    SpecMode 的三种类型:

    • UNSPECIFIED: 父容器不对 view 有任何限制,view 要多大给多大。一般用于系统内部,可以不用特别关注

    • EXACTLY: 父容器检测到 view 所需要的精确大小,这时view的最终测量结果就是 specSize 指定的值。它对应于 LayoutParams 中的 match_parent 和 具体数值这两种情况

    • AT_MOST: 父容器指定了一个可用大小即 specSize,子view 大小不能大于这个值。对应 LayoutParams 中的 wrap_content

    MeasureSpec 的生成 :

    MeasureSpec 的生成是由父容器的 MeasureSpec 和当前 view 的LayoutParams 共同决定的,但是对于顶级VIew (DecorView)和普通 View 来说它的转换过程则有所不同。对于 DecorView,它的 MeasureSpec 由窗口的尺寸和自身的 LayoutParams 来决定。而普通 View,则是由父容器的 MeasureSpec 和自身的 LayoutParams 来决定。

    • 如果这段话你看的糊里糊涂脑阔子疼,请先往下看,了解了 DecorView 和 普通 View 的测量过程后,这段话就很明朗了

    (二)了解 ViewRoot

    在介绍View的三大流程前,首先需要了解 ViewRoot,它对应 ViewRootImpl 这个类,它是连接WindowManager 和 DecorView 的纽带,View的三大流程是由 ViewRootImpl 来完成的。在 ActivityThread 中, 当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 和 DecorView 相关联

    root = new ViewRootImpl(view.getContext(), display);root.setView(view, wparams, panelParentView);
    • View 的绘制流程是从 ViewRootImpl 的 performTraversals() 开始的,这个方法巨长,我就挑几个大家看一下就明白了
    private void performTraversals() {    ...    measureHierarchy(host, lp, mView.getContext().getResources(),desiredWindowWidth, desiredWindowHeight);      ...    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);    ...    performLayout(lp, mWidth, mHeight);    ...     performDraw();    ...}

    如上可以清晰的看到, 方法内部会依次调用 performMeasure、performLayout、performDraw,这三个方法分别完成顶级 View 的 measure、layout、draw,大体流程如下图

    借用一下刚哥《Android开发艺术探索》里的图

    performMeasure 方法中会调用 measure 方法, measure 方法又调用 onMeasure 方法, 在 onMeasure 中遍历所有子元素并对子元素进行 measure 过程, 这时 measure 流程就从父容器传递到子元素中了, 这样就完成了一次 measure 流程。接着子元素重复进行父容器的 measure 过程, 如此反复直到完成整个 view 树的遍历。performLayout 和 performDraw 的传递流程是类似的,唯一不同的是 performDraw 的传递是在 draw 方法中通过 dispatchDraw 来实现的,不过这没有本质区别。

    而在performTraversals 的 measureHierarchy() 方法中, 可以看到 DecorView 的 MeasureSpec 创建过程, 其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸

    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

    看一下 getRootMeasureSpec 方法的实现:

    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;}

    通过上面的代码,可以明确看到 DecorView 的 MeasureSpec 产生过程是由它的 LayoutParams 中的宽/高参数来划分

    • ViewGroup.LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小
    • ViewGroup.LayoutParams.WRAP_CONTENT:最大模式,大小不定但不能超过窗口的大小
    • 固定大小(dp、px):大小为 LayoutParams 中指定的大小

    (三)measure 流程

    • 什么时候需要调用 onMeasure( )? : 当父容器要放置该View时调用View的onMeasure()。ViewGroup会问子控件View一个问题:“你想要用多大地方啊?”,然后传入两个参数 —— widthMeasureSpec 和 heightMeasureSpec;这两个参数指明控件可获得的空间大小 (SpecSize) 以及关于这个空间描述 (SpecMode) 的元数据。然后子控件把自己的尺寸保存到 setMeasuredDimension() 里,告诉父容器需要多大的控件放置自己。在 onMeasure() 的最后都会调用 setMeasuredDimension();如果不调用,将会由 measure() 抛出一个 IllegalStateException()。

    • setMeasuredDimension():可以简单理解为给 mMeasuredWidth 和 mMeasuredHeight 设值,如果这两个值一旦设置了,则意味着对于这个View的测量结束了,View的宽高已经有了测量的结果。如果我们想设定某个View的高宽,完全可以直接通过setMeasuredDimension(100,200)来设置死它的高宽(不建议),但是 setMeasuredDimension 方法必须在 onMeasure 方法中调用,不然会抛异常。

    1. View 的 测量过程 :

    View 的测量过程比较简单,因为没有子元素,通过 measure 方法就完成了其的测量过程,而 measure 方法是被 final 修饰的, 意味着子类不能重写这个方法。在 measure() 方法中则会去调用 onMeasure() 方法, 我们主要看一下 onMeasure() 方法内部的实现:

    /** * 参数 widthMeasureSpec 和 heightMeasureSpec 是父容器当前剩余控件的大小,即子元素的可用尺寸 */protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    setMeasuredDimension(        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}

    内部很简洁,调用 setMeasuredDimension 会设置 View 的测量值,继续看 getDefaultSize 方法实现:

    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;}

    我们只需要关注 AT_MOST 和 EXACTLY 的情况,则 getDefaultSize 的返回值就是 specSize,而 specSize 就是 View 测量后的尺寸大小 (注意区分测量后的大小和最终的大小, 最终的大小是在 layout 流程结束后确定的,虽然几乎所有的情况下两个值是相等的)。

    至于 UNSPECIFIED 一般用于系统内部的测量过程,这时 getDefaultSize 的返回值是传入的第一个参数 size,此时这个 size 的值则由 getSuggestedMinimumWidth() 方法决定,看一下内部实现:

    protected int getSuggestedMinimumWidth() {    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());}#Drawable.javapublic int getMinimumWidth() {    final int intrinsicWidth = getIntrinsicWidth();    return intrinsicWidth > 0 ? intrinsicWidth : 0;}

    getSuggestedMinimumWidth 的返回值和View设置的背景有关, 如果没有设置背景, 则返回 mMinWidth 的值, 即对应 xml 中 android:minWidth 属性的值, 没设置默认是0。设置了背景则调用它(Drawable)的 getMinimumWidth 方法,该方法获取的是 Drawable 的原始尺寸值,没有的原始尺寸值则为0。

    • 从上述代码中我们可以得出:直接继承 View 的自定义控件,需要重写 onMeasure 方法并设置在 wrap_content 时自身的尺寸大小,否则在 xml 布局中使用 wrap_content 相当于使用 match_parent 。

    • 为啥?:从 getDefaultSize 方法中清晰的看到,当 AT_MOST 情况即布局是 wrap_content 时,getDefaultSize 返回的结果是 specSize 也就是父容器当前剩余的控件大小,这和在布局中使用 match_parent 的效果完全一致。

    • 怎么处理?: 解决也很简单,在 onMeasure 中对于布局中使用 wrap_content 的情况,即 mode = MeasureSpec.AT_MOST 时, 调用 setMeasuredDimension() 给 View 的宽和高设置一个默认的尺寸, 对于其它情况则沿用系统的测量值即可。具体的默认尺寸看实际需求就可以。

    2. ViewGroup 的 测量过程 :

    测量子元素的过程: measureChildren

    在 ViewGroup 的测量过程中,需要先遍历并测量子View (通过调用它们的 measure 方法, 然后各个子元素再去递归执行这个过程),等子View测量结果出来后,再对自己进行测量。而 ViewGroup 是一个抽象类,它并没有重写 onMeasure 方法,但是它提供了一个 measureChildren 方法, 是用来遍历子元素并进行测量的方法, 方法内部调用 measureChild 测量子元素, 看一下 measureChildren 的内部实现 :

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

    在 measureChildren 方法中, 先遍历所有的子元素, 然后执行 measureChild 方法对子元素进行测量。在实际情况中,ViewGroup 的实现子类 (例如FrameLayout、LinearLayout) 则是直接使用它封装的另外一个方法 measureChildWithMargins 来测量某个子元素,该方法实现和 measureChild 方法基本类似,所以这里直接分析 measureChildWithMargins 方法:

    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,             int widthUsed, nt parentHeightMeasureSpec, int heightUsed) {    // 先提取子元素的 LayoutParams, 即在xml中设置的 你在xml的layout_width和    // layout_height, layout_xxx的值最后都会封装到这个个LayoutParams    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();    // 调用 getChildMeasureSpec 方法, 传入父容器的 MeasureSpec ,父容器自己的padding    // 和子元素的margin以及已经用掉的大小(widthUsed), 来计算出子元素的 MeasureSpec     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);    // 接着把 MeasureSpec 传给子元素的 measure 方法进行测量    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}

    在 measureChildWithMargins方法中,先提取子元素的 LayoutParams,再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,然后把 MeasureSpec 直接传递给子元素的 measure 方法进行测量。继续看 getChildMeasureSpec 方法内部实现:

    /** * spec: 父容器的 MeasureSpec * padding: 父容器的Padding + 子View的Margin + 已经用掉的大小(widthUsed) * childDimension: 表示该子元素的 LayoutParams 属性的值(lp.width、lp.height) */public static int getChildMeasureSpec(int spec, int padding, int childDimension) {    int specMode = MeasureSpec.getMode(spec);    // specSize 是父容器的尺寸    int specSize = MeasureSpec.getSize(spec);    // size 是子元素可用的尺寸, 即父容器减去padding剩下的尺寸大小    int size = Math.max(0, specSize - padding);    // resultSize 和 resultMode 是最终要返回的结果    int resultSize = 0;    int resultMode = 0;    // 根据父容器的 specMode 测量模式进行分别处理    switch (specMode) {    // Parent has imposed an exact size on us    // 父容器的测量模式是EXACTLY    case MeasureSpec.EXACTLY:        // 根据子元素的 LayoutParams 属性分别处理        if (childDimension >= 0) {            // 子元素的 LayoutParams 是精确值(dp/px)            resultSize = childDimension;      // 等于设置的尺寸            resultMode = MeasureSpec.EXACTLY; // Mode是EXACTLY        } else if (childDimension == LayoutParams.MATCH_PARENT) {            // Child wants to be our size. So be it.            // 子元素的 LayoutParams 是MATCH_PARENT            resultSize = size;                // 等于父容器尺寸            resultMode = MeasureSpec.EXACTLY; // Mode是EXACTLY        } else if (childDimension == LayoutParams.WRAP_CONTENT) {            // Child wants to determine its own size. It can't be            // bigger than us.            // 子元素的 LayoutParams 是WRAP_CONTENT            resultSize = size;                // 暂时等于父容器尺寸            resultMode = MeasureSpec.AT_MOST; // Mode是AT_MOST        }        break;    // Parent has imposed a maximum size on us    // 父容器的测量模式是AT_MOST    case MeasureSpec.AT_MOST:        if (childDimension >= 0) {            // Child wants a specific size... so be it            // 子元素的 LayoutParams 是精确值(dp/px)            resultSize = childDimension;      // 等于设置的尺寸            resultMode = MeasureSpec.EXACTLY; // Mode是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; // Mode是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; // Mode是AT_MOST        }        break;    // Parent asked to see how big we want to be    // 父容器的测量模式是UNSPECIFIED    case MeasureSpec.UNSPECIFIED:        if (childDimension >= 0) {            // Child wants a specific size... let him have it            resultSize = childDimension;      // 等于设置的尺寸            resultMode = MeasureSpec.EXACTLY; // Mode是EXACTLY         } else if (childDimension == LayoutParams.MATCH_PARENT) {            // Child wants to be our size... find out how big it should            // be            resultSize = ? 0;                     // 暂等于0, 值未定            resultMode = MeasureSpec.UNSPECIFIED; // Mode是UNSPECIFIED        } else if (childDimension == LayoutParams.WRAP_CONTENT) {            // Child wants to determine its own size.... find out how            // big it should be            resultSize =  0;                      // 暂等于0, 值未定            resultMode = MeasureSpec.UNSPECIFIED; // Mode是UNSPECIFIED        }        break;    }    //noinspection ResourceType    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}

    上面清楚展示了普通 View 的 MeasureSpec 创建规则,通过下面的表,可以对该内容进行清晰的梳理:
    再次借用刚哥的图
    通过之前 View 对自身的测量过程,和 ViewGroup 对子元素的测量过程,可以清楚的看到 View 的 MeasureSpec 的生成,是由父容器的 MeasureSpec 和当前 view 的LayoutParams 共同决定的, 验证了我之前说的那一段话。

    • 另外需要注意的是, 当父容器是 AT_MOST 而子元素的 LayoutParams 是 WRAP_CONTENT 时, 父View的大小是不确定(只知道最大只能多大),子View又是WRAP_CONTENT,那么在子View的Content没算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST,而size 暂定父View的 size。这是 View 中的默认实现。

    • 而对于其他的一些View的派生类,如TextView、Button、ImageView等,它们的onMeasure方法系统了都做了重写,不会这么简单直接拿 MeasureSpec 的size来当大小,而去会先去测量字符或者图片的高度等,然后拿到View本身content这个高度(字符高度等),如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度(字符高度等),而不是像 View.java 中直接用MeasureSpec的size做为View的大小。

    测量自己的过程 : onMeasure (通过 LinearLayout 分析)

    onMeasure ( )

    在 ViewGroup 中没有定义其测量的具体过程, 它本身是一个抽象类, 它的测量过程需要子类去具体实现。因为不同的子类有不同的布局特性,从而导致它们的测量过程各不相同,VIewGroup 无法对此做统一实现。下面通过 LinearLayout 的 onMeasure 方法来分析 ViewGroup 的测量过程。

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

    measureVertical( )

    方法比较简洁,明显是根据设置的 orientation 来对应不同的测量方法,measureVertical 和 measureHorizontal 内部实现类似,我们选择看一下 measureVertical 的内部,即竖直布局的情况, 方法比较长, 这里我分段去分析一下:

    for (int i = 0; i < count; ++i) {    final View child = getVirtualChildAt(i);    ...    // 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).    // 遍历子元素并测量它们    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,             heightMeasureSpec, usedHeight);    // mTotalLength 是用来存储 LinearLayout 在竖直方向上的高度    final int childHeight = child.getMeasuredHeight();    final int totalLength = mTotalLength;           // 每测量一个子元素,mTotalLength 会保存它的高度以及它竖直方向上的 margin      mTotalLength = Math.max(totalLength, totalLength + childHeight +             lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));

    从上面一段代码可以看出来, 这里先遍历子元素, 然后执行 measureChildBeforeLayout 方法, 在方法内部会去执行 measureChildWithMargins 对子元素进行测量, 这个方法我们刚分析过。接着看 mTotalLength 则是用来存储 LinearLayout 在竖直方向上的高度, 它会保存每一个测量完的子元素的高度和它竖直方向上的 margin。

    在测量完子元素之后, LinearLayout 会对自己进行测量并保存尺寸, 继续看 measureVertical 方法中后面的代码:

    // 加上自己竖直方向上的 paddingmTotalLength += mPaddingTop + mPaddingBottom;int heightSize = mTotalLength;heightSize = Math.max(heightSize, getSuggestedMinimumHeight());int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);heightSize = heightSizeAndState & MEASURED_SIZE_MASK;...setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec,         childState), heightSizeAndState);

    对于竖直的 LinearLayout 在测量自己的尺寸时, 它水平方向上的测量过程会遵循 View 的测量过程, 而竖直方向的测量则有所不同, 然后执行 resolveSizeAndState 方法来生成竖直高度的 MeasureSpec ,即代码中的变量 heightSizeAndState , 我们看一下它的实现过程 :

    resolveSizeAndState( )

    /** * size: 是 mTotalLength, 即竖直方向上所有子元素的高度总和 * measureSpec: 父容器传过来的期望尺寸, 即剩余空间 */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);}

    可以看到, 如果 LinearLayout 的布局高度是 match_parent 或者 具体数值, 则它的测量过程和 View 是一致的, 高度是 specSize。如果布局高度是 wrap_content, 则它的高度是竖直方向左右子元素高度的总和, 但这个值仍不能大于 specSize

    (四)获取 View 的测量宽/高

    • 到这里 View 的测量流程就结束了,在三大流程中 measure 是最复杂的一个,在 measure 结束后就可以通过 getMeasuredWidth/Height() 正确的获得 View 的测量宽/高。但是据说在某些极端情况下,系统需要多次调用 measure 才能准备的测量出结果,所以一般比较稳妥的做法是在 onLayout 方法中去获取测量宽/高或者最终宽/高。

    现在有这样一个问题:怎样在 Activity 启动时,即在 onCreate 方法中获取 View 的宽高呢?
    如果直接在 onCreate 中调用 getMeasuredWidth/Height() 是不能正确获取它的尺寸值的, 而且同样在 onResume 和 onStart 中都是不准确的,因为你无法保证此时 View 的测量过程已经完成了,如果没有完成,得到的值则为0。

    1. Activity/View 的 onWindowFocusChanged(boolean hasFocus)
    onWindowFocusChanged 表示 View 已经初始化完毕了, 这时获取它的宽/高是没问题的。
    这个方法是当 Activity/View 得到焦点和失去焦点时都会调用一次, 在 Activity 中对应 onResume 和 onPause ,如果频繁的进行 onResume 和 onPause, 则 onWindowFocusChanged 也会被频繁的调用。

    public void onWindowFocusChanged(boolean hasFocus) {    super.onWindowFocusChanged(hasFocus);    if(hasFocus){        int width = view.getMeasuredWidth();        int height = view.getMeasuredHeight();    }}

    2. view.post(runnable):
    通过 post 将一个 runnable 消息投递到消息队列的底部,然后等待 Looper 调用此 runnable 的时候,View 已经初始化好了

    @Overrideprotected void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);     view.post(new Runnable(){         @Override         public void run(){            int width = view.getMeasuredWidth();            int height = view.getMeasuredHeight();          }     });}

    3. ViewTreeObserver
    ViewTreeObserver 的众多回调可以完成这个需求, 例如使用 OnGlobalLayoutListener 这个接口, 当 view 树的状态改变或者 view 树内部 view 的可见性改变, 都会回调 onGlobalLayout 方法。

    // 方法1:增加整体布局监听ViewTreeObserver vto = view.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){    @Override     public void onGlobalLayout() {        view.getViewTreeObserver().removeGlobalOnLayoutListener(this);             int height = view.getMeasuredHeight();         int width = view.getMeasuredWidth();     } });// 方法2:增加组件绘制之前的监听ViewTreeObserver vto =view.getViewTreeObserver();vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {   @Override    public boolean onPreDraw() {       int height = view.getMeasuredHeight();       int width = view.getMeasuredWidth();       }});

    4. view.measure(int widthMeasureSpec, int heightMeasureSpec)
    这是通过手动触发对 View 进行 measure 来得到 View 的宽/高的方法。需要根据 View 的 LayoutParams 情况来分别处理:

    • match_parent:无法测量宽/高,根据前面分析的 View 测量过程,此时构造它的 MeasureSpec 需要知道父容器的剩余控件,而此时我们无法获取,则理论上讲无法测出 View 的大小。

    • 具体的数值(dp / px):
      比如宽高都是200, 直接通过 MeasureSpec.makeMeasureSpec 手动构造它的宽和高尺寸, 然后传入 view.measure 方法触发测量 :

    int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(200, View.MeasureSpec.EXACTLY);int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(200, View.MeasureSpec.EXACTLY);view.measure(widthMeasureSpec, heightMeasureSpec);
    • wrap_content
    int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(1 << 30 - 1, View.MeasureSpec.AT_MOST);int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(1 << 30 - 1, View.MeasureSpec.AT_MOST);view.measure(widthMeasureSpec, heightMeasureSpec);

    1 << 30 - 1 就是30位 int 值的最大值, 也就是30个1。前面介绍 MeasureSpec 时说到 View 的尺寸用30位的int值表示,此时我们是用 View 理论上能支持的最大值去构造 MeasureSpec ,相当于给 View 一个足够的范围空间去完成自己的测量并保存自己的测量结果, 是可行的。

    • 有两个错误用法: 违背了系统的内部实现规范, 因为无法通过错误的 MeasureSpec 去得到合法的 SpecMode, 导致测量过程有错。
    int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1 , View.MeasureSpec.UNSPECIFIEDint heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(- 1, View.MeasureSpec.UNSPECIFIEview.measure(widthMeasureSpec, heightMeasureSpec);// 这个我自己在7.0版本的编译环境下已经编译不通过了,在 makeMeasureSpec // 方法的第一个参数需要传入 0 ~ 1073741823 范围的值, -1 不合法。
    view.measure(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);// measure 方法参数不合法

    看到这里, 三大流程中关于 measure 的知识点已经总结完了, 如果你觉得有不理解的地方或者有更好的见解还请提出来, 让我们共同学习一起成长。

    如果觉得收获,点个赞再走呗~