第四章 View 的工作原理

4.1 ViewRoot 与 DecorView

ViewRoot 对应于 ViewRootImpl 类,是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均通过 ViewRoot 完成,在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将这两个关联。

View 的三大绘制流程是从 ViewRoot 的 performTraversals 方法开始的。经过 measure 、layot 、draw 三个过程才最终显示出来。具体过程在 4.3 将会详细讲解。

DecorView 为顶级 View,其是 WindowManager 持有的 所有 View 的 顶级父容器。在 Activity 中,通常其会包含一个 LinearLayout,并分为两部分,其中上面为 ActionBar,下部分为一个 Id 为 android.R.id.content 的 FrameLayout,当我们调用 setContentView 方法时,设置的 View 其实是作为 FrameLayout 的 子 View。

其中 ActionBar 部分可通过设置主题为 noActionbar 来去除。

4.2 MeasureSpec

4.2.1 MeasureSpec

MeasureSpec 类似一种测量规格,当父布局进行 measure 时,会构造一个 MeasureSpec 对象,其中包含着一些信息,并传送到 子 View中,子 View 在根据 父布局 传进来的该对象来测量自己的 长宽,下面将具体介绍其包含的信息。

MeasureSpec 是一个 32 位的 int 值,其中高二位代表 SpecMode,即测量模式,低 30 位表示 SpecSize,指前面测量模式下的大小,以下为源码中的部分定义:

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

主要为将 size 与 mode 包装为一个 MeasureSpec 的 makeMeasureSpec 方法,与解包获取 mode 与 size 的方法。

同个上面代码可以看出 mode 分为三种,分别为:

  • UNSPECTIFIED:父容器不对 View 有任何限制,子 View 的可以随意指定自己的大小。
  • EXACTLY:父容易已经测量出子 View 的大小,此时的大小为 size 值,主要用于当 子 View 的长宽为 MATCH_PARENT 时的测量,此时 size 值就为父容器自身的长宽。
  • AT_MOST:父容器指定了一个最大长宽,此时 size 中的值为父容器为孩子指定的最大值,主要用于当子 View 的长宽为 wrap_content 时的测量,通常情况下最大值为父容器自身的长宽。

4.2.2 MeasureSpec 与 LayoutParams 的关系

当一个 View 进行 measure 过程时,其父容器会根据其 LayoutParams 生成一个 MeasureSpec ,并作为子 View onMeasure 的依据。因此,决定 MeasureSpec 的因素有两个,一是 父容器,二是 LayoutParams,接下来将介绍 DecorView 与 普通 ViewGroup (安卓中常见 Layout,默认实现) 情况下 MeasureSpec 的生成规则,值得注意的是,这里介绍的是默认实现,如果重写了相关方法,则可能会有出入,同时也可以通过重写相关方法来根据具体业务写入新的生成规则。

DecorView 中的 MeasureSpec 生成规则主要与 getRootMeasureSpec 方法有关,以下为源码:

private static int getRootMeasureSpec(int windowSize, int rootDimension){
    int measureSpec;
    switch(rootDimension){
        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            
    }
    return measureSpec;
}

可以很明确的看出, DecorView 的 MeasureSpec 产生规则主要按照 LayoutParams 中宽高来划分:

  • LayoutParams.MATCH_PARENT 对应 EXACTLY 模式,size 为 窗口大小
  • LayoutParams.WRAP_CONTENT 对应 AT_MOST 模式,size 为窗口大小
  • 固定大小同样对应 EXACTLY 模式,size 为该大小

接下来来分析普通 ViewGroup 的生成规则,普通 ViewGroup 与 DecorView 是不同的,因为 ViewGroup 存在多重嵌套,可能父 VireGroup 本身的大小也尚未确定,这里主要来看看 ViewGroup 中的 measureChildWithMargins 方法:

protected void measureChildWithMargins(View child,
                                       int parentWidthMeasureSpec, int widthUsed,
                                       int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    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);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

其中最后一行 child.measure 为调用孩子的 measure 过程,可以看出其 MeasureSpec 生成主要为调用方法 getChildMeasureSpec 方法,该方法代码较多,可以自行查看,这里直接给出规则。

这里的 MeasureSpec 主要由两个部分决定,一是父容器自身的 MeasureSpec,二是孩子的 LayoutParams,下表中给出其生成规则:

LayoutParamsEXACTLYAT_MOSTUNSPECTIFIED
具体尺寸EXACTLY childSizeEXACILY childSizeEXACILY childSize
match_parentEXACTLY parentSizeAT_MOST parentSizeUNSPECTIFIED 0
wrap_contentAT_MOST parentSizeAT_MOST parentSizeUNSPECTIFIED 0

4.3 View 的工作流程

View 的工作流程主要指 measure、layout、draw 三个过程,即测量、布局、绘制。对于一个 ViewGroup,在完成自己相应过程的同时,它也会递归调用其孩子的对应过程,最终完成孩子的绘制。

4.3.1 measure 过程

View 的 measure 过程

View 的 measure 过程是在其 measure 方法中完成的,该方法不可重写。在此方法中会调用 onMeasure 方法,来看看 onMeasure 方法的源码:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

可以看到这个方法的默认实现主要与三个方法有关:

  • setMeasureedDimension 该方法主要为设置测量结果,测量结果是宽高的像素值,在自定义 View 中,如果我们需要自定义 onMeasure 过程,一般同样需要调用这个方法。
  • getDefaultSize 与 getSuggestedMinimumXXX 这两个方法与其默认实现有关,我们分别来看。

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

该方法还是比较简单的,直接根据逻辑走即可。

getSuggestedMinimumXXX ( 以 Width 为例 )

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

可以看到,这个值与 最小宽度以及背景宽度有关。

通过源码可以看出,当给一个默认 View 设置 wrap_content 时,其大小会占满父布局,实际上,我们通常使用的 TextView ImageView 等都对该过程进行了重写。

VioewGroup 的 measure 过程

对于 ViewGroup 来说,它除了完成自己的 measure,还会调用子元素的 measure 方法。ViewGroup 是一个抽象类,它没有重写 View 的 onMeasure 方法,但是它提供了一个 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);
        }
    }
}

这个还是比较好理解的,主要是遍历所有 VISIBILITY 不为 GONE 的孩子,并调用 measureChild 方法。

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

该方法还是比较好理解的,主要是获取孩子的 LayoutParams,然后调用 getChildMeasureSpec 方法构造 MeasureSpec,然后调用孩子的 measure 方法,这里 getChildMeasureSpec 效果在之前已经有介绍。

ViewGroup 的测量过程 onMeasure 需要各自子类具体去实现,因为对于不同的 Layout,其特性不同,因此测量规则也不同。值得注意的是,有的 Layout 因为需要,会对孩子进行多次测量,例如第一次测量为 UNSPECIFIED,获取孩子的大小,然后根据孩子的大小设置自己的大小后在使用 AT_MOST 模式进行二次测量。

4.3.2 layout 过程

该过程的作用主要为确认一个 View 在父容器中的位置,包括左上顶点与右下顶点的坐标共四个数分别是 top,left,bottom,right 。该过程比较简单。首先,layout 方法会被调用,该方法中会调用 setFrame 来设置上面的四个参数。同时会调用 onLayout 方法。值得注意的是,onLayout 在 view 与 viewGroup 中都为空方法。其中第一个参数为是否改变:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

4.3.3 draw 过程

该过程也比较简单,它的作用是将 View 绘制到屏幕上,绘制过程遵循如下几步:

  1. 绘制背景 background.draw(canvas)
  2. 绘制自己 onDraw
  3. 绘制孩子 dispatchDraw
  4. 绘制装饰 onDrawScrollBars

值得注意的是,在绘制自己的时候,会判断一个 willNotDraw 变量是否为 false,只有该变量为 false 时才会调用 onDraw 方法。对于普通的 View,其默认值为 false,但对于 ViewGroup,其默认值为 true,如果我们需要在 Layout 中绘制自己,则需要显式将其值设置为 false 。

4.4 自定义 View

4.4.1 自定义 View 分类

  1. 继承 View 重写 onDraw 方法
  2. 继承 ViewGroup 派生特殊的 Layout
  3. 继承特定的 View
  4. 继承特定的 ViewGroup

4.4.2 自定义 View 规则

  1. 让 View 支持 wrap_content : 默认 View 的实现中,对于 AT_MOST 的模式,是直接占满父布局的。
  2. 如果有必要,让 View 支持 padding,让 ViewGroup 支持设置孩子的 padding 与 margin
  3. 尽量不要在 View 中使用 Handler,View 自带 POST 方法
  4. View 中如果有线程线程或者动画,需要及时停止,可在 onDetachedFromWindow 中停止
  5. View 带有滑动嵌套情形时,需要处理好滑动冲突