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

系列文章传送门 (持续更新中..) :
自定义控件(一) 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.java
public 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 方法中后面的代码:

// 加上自己竖直方向上的 padding
mTotalLength += 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 已经初始化好了

@Override
protected 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.UNSPECIFIED
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(- 1, View.MeasureSpec.UNSPECIFIE
view.measure(widthMeasureSpec, heightMeasureSpec);

// 这个我自己在7.0版本的编译环境下已经编译不通过了,在 makeMeasureSpec 
// 方法的第一个参数需要传入 0 ~ 1073741823 范围的值, -1 不合法。
view.measure(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
// measure 方法参数不合法

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值