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

系列文章传送门 (持续更新中..) :java

自定义控件(一) Activity的构成(PhoneWindow、DecorView)android

自定义控件(二) 从源码分析事件分发机制bash

自定义控件(四) 源码分析 layout 和 draw 流程ide


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

  • 在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值。post

SpecMode 的三种类型:

  • UNSPECIFIED: 父容器不对 view 有任何限制,view 要多大给多大。通常用于系统内部,能够不用特别关注学习

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

  • 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 的知识点已经总结完了, 若是你以为有不理解的地方或者有更好的看法还请提出来, 让咱们共同窗习一块儿成长。

若是以为收获,点个赞再走呗~

相关文章
相关标签/搜索