View 工作原理

1、 ViewRoot 和 DecorView 介绍

ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowMnager 类 和 DecorView 的纽带,View 的三大流程是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联,这个过程可参考如下源码:

    root = new ViewRootImpl(view.getContext(),display);
    root.setView(view,wparams,panelParentView);

view 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,经过 measure、layout 和 draw 三个过程才能将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来确定 View 在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上。
而 performTraversals 的流程如下:


View 工作原理_第1张图片
performTraversals 工作流程.png

由上图可知,performTraversals 会依次调用 performMeasure、performLayout 和 performDraw方法。这三个方法会完成顶级 View 的 measure、layout 和 draw 这三个流程。
其中,performMeasure 中会调用 measure 方法,在measure 方法中又会调用 onMeasure,然后在 onMeasure 才会对所用子元素进行 measure 过程,这时候,就从父容器传递到了子元素,也就是完成了一次 measure 过程,接着子元素会重复父容器的 measure 过程,如此反复直到完成整个 View 树的遍历。performLayout 和 performDraw 类似,唯一不同的是,performDraw 的传递 过程是在 draw 方法中,通过 dispatchDraw 来实现的。

measure 过程决定了 View 的宽高,Measure 完成之后,可以通过 getMeasureWdith 和 getMeasureHeight 方法获取到 View 测量后的宽高,一般情况下,这就是 View 最终的宽高。Layout 过程决定了 View 四个顶点的坐标和最终 View 的宽高,完成之后,可通过 getTop、getBottom、getLeft、getRight 来得到 View 四个顶点的位置,并且可以通过 getWidth 和 getHeight 来得到 View 最终的宽高。最后,Draw 方法决定了 View 的显示,只有 draw 方法完成后 View 的内容才能呈现在屏幕上。

DecorView 的结构:


View 工作原理_第2张图片
decorView.png

如图所示,DecorView 做为顶级 View,一般情况下它的内部都会包含一个竖直方向的 LinearLayout,在这个 LinearLayout 里面有上下两个部分(具体情况和 Android 版本及主题有关)。我们设置的布局就是下面的内容栏,其 id 为 content。我们可以通过:

  ViewGroup content = findViewById(R.android.id.content);

得到这个 content,然后我们可以通过 content.getChildAt(0) 得到我们设置的 View。我们还需要知道,DecorView 是一个 Fragment,View 的事件多要经过DecorView 才能传递给我们的 View。

2、理解 MeasureSpec

为了更好的理解 View 的测量过程,我们还要理解 MeasureSpec。MeasureSpec 决定了 View 的规格尺寸,但是 MeasureSpec 还受到父容器的影响。在测量的过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后在根据这个 measureSpec 来测量出 View 的宽高。

2.1 MeasureSpec

MeasureSpec 代表了一个 32 位的 int 值,高 2 位 代表 SpecMode,低 30 位代表 SpecSize。

  • SpecMode 是测量模式
  • SpecSize 是测量尺寸
    MeasureSpec:

/**
 * MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求
 * MeasureSpec由size和mode组成。
 * 三种Mode:
 * 1.UNSPECIFIED
 * 父不没有对子施加任何约束,子可以是任意大小(也就是未指定)
 * (UNSPECIFIED在源码中的处理和EXACTLY一样。当View的宽高值设置为0的时候或者没有设置宽高时,模式为UNSPECIFIED
 * 2.EXACTLY
 * 父决定子的确切大小,子被限定在给定的边界里,忽略本身想要的大小。
 * (当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的)
 * 3.AT_MOST
 * 子最大可以达到的指定大小
 * (当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸)
 * 
 * MeasureSpecs使用了二进制去减少对象的分配。
 */
public class MeasureSpec {
        // 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和倒数第二位也就是32和31位做标志位)
        private static final int MODE_SHIFT = 30;
        
        // 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)
        // (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
 
        // 0向左进位30,就是00 00000000000(00后跟30个0)
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        // 1向左进位30,就是01 00000000000(01后跟30个0)
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        // 2向左进位30,就是10 00000000000(10后跟30个0)
        public static final int AT_MOST     = 2 << MODE_SHIFT;
 
        /**
         * 根据提供的size和mode得到一个详细的测量结果
         */
        // measureSpec = size + mode;   (注意:二进制的加法,不是十进制的加法!)
        // 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值
        // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
        public static int makeMeasureSpec(int size, int mode) {
            return size + mode;
        }
 
        /**
         * 通过详细测量结果获得mode
         */
        // mode = measureSpec & MODE_MASK;
        // MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。
        // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
 
        /**
         * 通过详细测量结果获得size
         */
        // size = measureSpec & ~MODE_MASK;
        // 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
 
        /**
         * 重写的toString方法,打印mode和size的信息,这里省略
         */
        public static String toString(int measureSpec) {
            return null;
        }
}

代码中有详细注释,主要说一下 SpecMode:

  • UNSPECFIED
    父容器不对 View 有任何限制,要多大给多大,一般用于系统内部。
  • EXACTLY
    父容器已经检测出 View 所需要的大小,这时候 View 的最终大小就是 SpecSize 所指定的值。这个模式对应 LayoutParams 中的 match_parent和具体数值两种模式。
  • AT_MOST
    父容器制定了一个可用大小即 SpecSize,这个时候, View 的最终大小不能大于这个值,具体是多少要看 View 的具体实现,对于LayoutParams 中的 wrap_content。
2.2 MeasureSpec 和 LayoutParams 的对应关系

系统内部是通过 MeasureSpece 来进行 View 的测量,但是正常情况下我们使用 View 指定 MeasureSpec,尽管如此,我们给 View 设置 LayoutParams。在 View 测量的时候,系统会将 LayoutParams 在父容器的约束下转换成对应的 MeasureSpec,然后在根据这个 MeasureSpec 来确定 View 测量后的 宽高。需要注意的是 MeasureSpec 不是唯由 LayoutParams 决定的,LayoutParams 需要和父容器一起才能决定 View 的 MeasureSpec ,进而进一步决定 View 的宽高。另外,对于顶级 View (DecorView) 和 普通 View 来说,MeasureSpec 的转换过程略有不同。对于 DecorView,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来共同确定。
对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和 自身的 LayoutParams 来共同决定,MeasureSepc 一旦确定后,onMeasure 就可以确定 View 的测量的宽高。

对于 DecorView 来说,在 ViewRootImp 中的 measureHierachy 方法中有如下代码,它展示了 DecorView 的 MeasureSpec 的创建过程,其中 desiredWindowWidth 和 desiredWindowHeight 和屏幕尺寸:

    if (!goodMeasure) {
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }

getRootMeasureSpec 方法如下:

    /**
     * Figures out the measure spec for the root view in a window based on it's
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    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 中的宽/高参数来划分:

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

对于我们布局中的 View, View 的 measure 过程由 ViewGroup 传递而来,ViewGroup 的 measureChildWidthMargins 方法如下:

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

上述方法会对子元素进行 measure,在调用 子元素的 measure 方法前,会通过 getChildMeasureSpec 方法得到子元素的 MeasureSpec。具体来说,子元素的 MeasureSpec 的创建与父容器的 MeasureSpec 和子元素本身的 LayoutParams 有关,此外还和 View 的 margin 及 padding 有关,getChildMeasureSpec 方法如下:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        int size = Math.max(0, specSize - padding);
        int resultSize = 0;
        int resultMode = 0;
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } 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;
            }
            break;
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.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;
            } 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;
            }
            break;
        // Parent asked to see how big we want to be
        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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

这个方法的主要作用就是根据父容器的 MeasureSpec 结合 View 本身的 LayoutParams 来确定子元素的 MeasureSpec,参数 padding 为父容器已经占用的空间,因此子元素可用大小为父容器的尺寸减去 padding,具体代码如下:

        int specSize = MeasureSpec.getSize(spec);
        int size = Math.max(0, specSize - padding);

根据代码,普通 View 的创建规则如下:


View 工作原理_第3张图片
View 的 measureSpec 创建规则.png

主要规则如下:

  • 当View采用固定宽/高时,不管父容器的Measure是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams中的大小。
  • 当View的宽/高是match_parent时,如果父容器是精确模式,那么View也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
  • 当View的宽/高是wrap_content时,不管父容器是最大模式还是精确模式,View的模式总是最大模式,并且其大小不会超过父容器的剩余空间。
  • UNSPECIFIED模式主要用于系统内部多次Measure的情形,一般来说,不需要关注此模式。

根据上表,我们只要指定 父容器的 MeasureSpec 和子元素的 LayoutParams ,就可以快速的确定子元素的 MeasureSpec,有了 MeasureSpec 就可以进一步确定子元素测量后的大小。

3、View 的工作流程

View 的工作流程主要是 measure、layout、draw 三大流程,也就是测量,布局和绘制。
measure: 确定 View 的测量宽高。
layout: 确定 View 的最终宽高和四个顶点的位置。
draw: 将 View 绘制到屏幕上。

3.1 View 的measure 过程

View 的 measure 过程有 measure 方法来完成,measure 方法是一个 final 类型的方法,这意味着此类方法不能被子类重写,在 View 的 measure 方法中会去调用 View 的 onMeasure 方法,因此只需要看 onMeasure 的实现,View 的 onMeasure 方法:

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

由上可知,getDefaultSize 这个方法逻辑很简单,对我们来说,只需要 AT_MOST 和 EXACTLY 这两种情况。简单的理解,getDefaultSize 返回的大小就是 measureSpec 和 specSize,而这个 specSize 就是 View 测量后的大小,测量后的大小是在 layout 阶段决定的,这里要加以区分,但是几乎全部情况下 View 的测量大小和最终大小是相等的。
至于 UNSPECCIFIED 这种情况,一般用于系统内部的测量过程,在这种情况下, View 的大小为 getDefaultSize 的第一个参数 size,即宽高分别为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 这两个方法的返回值,源码如下:

  protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }

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

这里只分析,getSuggestedMinimum 方法的实现,从其代码可以看出,如果 View 没有设置背景,那么 View 的宽度为 mMinWidth,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值,如果这个属性不指定,那么 mMinWidth 则默认为 0,如果 View 指定了背景,则 View 的宽度为 max(mMinWidth,mBackgroudn.getMinimumWIdth())。
Drawable 的 getMinimumWidth 方法

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

从上面可以看出,getMinimumWidth 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则就返回 0。

getSuggestedMinimumWidth 的逻辑总结如下:如果 View 没有设置背景,那么返回 android:minWidth 这个属性所指定的值,这个值可以是 0,如何 View 设置了背景,则返回 android:minWidth 和背景的最小宽度这两者中的最大值。getSuttestedMinimumWidth 个 getSuggestedMinimumHeight 的返回值就是 View 在 UNSPECIFIED 情况下的测量宽高。
从 getDefaulSize 方法的实现来看,View 的宽高由 specSize 决定,所以我们可以得出结论:直接继承 View 的自定义控件需要重写onMeasure 方法并设置 wrap_content 的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent。如果 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽高等于 specSize,这种情况下, View 的 specSize 是 parentSize,而 parentSize 是父容器目前可以使用的大小,也就是父容器当前剩余的空间的大小,很显然, View 的宽高就等于父容器当前空间的大小,这种效果和在布局中使用 match_parent 完全一致,为了解决这个问题,在onMeasure 中会给 View 一个默认的宽高值:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int defaultWidth = 100;
        int defaultHeight = 100;
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(defaultWidth, defaultHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(defaultWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, defaultHeight);
        } else {
            widthSpecSize = Math.min(widthSpecSize, heightSpecSize);
            heightSpecSize = Math.min(widthSpecSize, heightSpecSize);
            setMeasuredDimension(widthSpecSize, heightSpecSize);
        }
    }


上面代码中,给 View 指定了一个默认的内部宽高值(mWidth 和 mHeight),并在 wrap_content 时设置这个宽高,对用非 wrap_content 情形,沿用系统的测量值即可。

ViewGroup 的 measure 过程

对于 ViewGroup 来说,除了完成自己的 measure 过程之外,还会遍历去调用所有子元素的 measure 方法,各个子元素在递归去执行这个过程。和 View 不同的是, 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);
            }
        }
    }

由上面的代码可知, ViewGroup 会对每个子元素进行 measure,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,接着将 MeasureSpec 直接传递给 View 的 measure 方法进行测量。

我们知道,ViewGroup 没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现,比如 LinearLayout 、Relativelayout 等,这是因为不同的 ViewGroup 子类有不同的布局特性,因此其细节各不相同。
LinearLayout 的 onMeasrue 方法如下:

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

直接看竖直布局的测量过程:

// 代码省略 ...
            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null || child.getVisibility() == View.GONE) {
                    continue;
                }
                final LayoutParams lp = (LayoutParams)child.getLayoutParams();
                final float childWeight = lp.weight;
  
 measureChildBeforeLayout(child,i,widhtMeasureSpec,0,heightMeasureSpec,totalWeight == 0 ? mTotalLenght : 0);

if( oldHeight != Integer.MIN_VALUE){\
  lp.height  = oldHeight;
}

                    final int childHeightMeasureSpec = 
               MeasureSpec.makeMeasureSpec(
                            Math.max(0, childHeight), MeasureSpec.EXACTLY);
                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                            lp.width);
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

                    // Child may now not fit in vertical dimension.
                    childState = combineMeasuredStates(childState, child.getMeasuredState()
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                }

                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }

其测量过程的代码较多,主要思想就是,系统会遍历子元素并对子元素执行 measureChildBeforeLayout 方法,这个方法内部会调用子元素的 measure 方法,这样各个子元素就开始依次进入 measure 过程,并且系统会通过 mTotalLength 这个变量来存储 LinearLayout 在竖直方向的初步高度,没测量一个子元素,mTotalLength 就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方法的 margin 等。当子元素测量完成后,LinearLayout 会测量自己的大小,源码大致如下:

    // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
    ...
   if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }

        maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

        if (matchWidth) {
            forceUniformWidth(count, heightMeasureSpec);
        }

View 的 measure 过程是三大流程中最复杂的一个, measure 完成之后,通过 getMeasuredWidht/height 方法就可以得到 View 的测量宽高。

现在考虑一个情况,当我们要在 onCreate 或者 onResume 中获取 View 的宽高时,这样是无法正确得到 View 的宽高信息的,因为 View 的 measure 过程和 Activity 的周期不是同步的,目前有四种方法解决这个问题:

  • Activity/View #onWindowFocusChanged
    onWindowFocusChanged 这个方法的含义是: View 已经初始化完毕了,宽高已经准备好了,这时候去获取宽高是没有问题的。需要注意的时,这个方法会被多次调用,具体来说就是,Activity 继续执行和暂停执行都会调用这个方法。
    其使用方法如下:

    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
                   int height = view.getMeasureHeight();  
                   int widht = view.getMeasureWidth();
        } else {
            Log.e(Tag, "onWindowFocusChanged:" + "false");
        }
    }

  • view.post(runnable)
    通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候,View 已经初始化好了,典型代码如下:
@Override
protected void onStart() {
    super.onStart();
    view.post(new Runnable() {
        @Override
        public void run() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}
  • ViewTreeObserver
    通过使用 ViewTreeObserver 的众多回调方法可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变的时候,onGlobalLayout方法将被回调,因此这是获取View的宽高一个很好的机会。值得注意的是,伴随着View树的状态改变等,onGlobalLayout会被多次调用。代码如下:
@Override
protected void onStart() {
    super.onStart();
    ViewTreeObserver observer = tv.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            tv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            int width = tv.getMeasuredWidth();
            int height = tv.getMeasuredHeight();
        }
    });
}
  • view.measure(int widthMeasureSpec,int heightMeasureSpce)
    通过手动对 View 进行 measure 来得到 View 的宽高,这种方法比较复杂,要分情况处理,根据 View 的 LayoutParams 来分:

(1) match_parent:
直接放弃,因为无法知道 parentSize,而 measure 过程需要知道这个值。

(2 )具体的数值(dp/dx):
比如宽高都是 100px,如下 measure:

int widthMeasureSpec = MeasureSpec.makeMeasureSpce(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpce(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);

(3) wrap_content
如下 measure:

int widthMeasureSpec = MeasureSpec.makeMeasureSpce((1 << 30) -1,MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpce((1 << 30) -1,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);

因为 View 的尺寸使用 30 为二进制表示,也就是说最大是 30 个1(即 2^30 - 1),也就是 (1 << 30) -1,在最大化模式下,我们用 View 理论上能支持的最大值去构造 MeasureSpec 是合理。

3.2 View 的 layout 过程

Layout 的作用 用来确定子元素的位置,当 ViewGroup 的位置确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用。layout 方法主要是确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置。
layout 方法如下:

  public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList listenersCopy =
                        (ArrayList)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、mBottom
这四个值,View 的四个顶点确定之后,那么 View 在父容器中的位置也就确定了,接着调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似,onLayout 的具体实现同样和具体布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法,接下来我们看一下 LinearyLayout 中 onLayout 是如何实现的:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

也就是 onLayout 的实现还是和布局有关,这里以垂直为例:

    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

这个方法的主要逻辑如下:首先遍历所有子元素并调用 setChildFrame 方法来为子元素指定对应的位置,其中 childTop 会逐渐增大,这就意味着后面的子元素会被放置到靠下的位置,这刚好符合垂直 LinearLayout 的特性,而下面的 setChildFrame 只是调用子元素的 layout 方法,子元素又会通过自己的 layout 方法来确定自己的位置,这样依次调用就确定了整个 View 树的 layout 过程。
setChildFrame 如下:

    private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }

setChildFrame 中的 width 和 height 实际上就是子元素的测量宽高,上面的代码如下:

                 final int childWidth = child.getMeasuredWidth();
                 final int childHeight = child.getMeasuredHeight();
                ....
                setChildFrame(child, childLeft + getLocationOffset(child), childTop,
                        childWidth, childHeight);

而在 layout 中,会通过 setFrame 去设置子元素的四个顶点的位置,在 setFrame中有如下赋值语句:


            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;

这样一来 子元素的位置就确定了。
现在说一下 View 的测量宽高和最终宽高有什么区别,也就是 View 的 getMeasureWidth 和 getWidth 有什么区别。先看一下 getWidth 和 getHeight 的方法的实现:

   public final int getWidth() {
        return mRight - mLeft;
    }

    public final int getHeight() {
        return mBottom - mTop;
    }

也就是 getWidth 和 getHeight 返回的是测量宽高,虽然在 View 的默认实现中,测量宽高和最终宽高是相等的,但是需要知道的是,测量宽高形成与 measure 过程,而最终宽高形成与 layout 过程,也就是赋值时机不一样,我们在日常开发中,可以认为这两个是相等的。
下面举例说明为什么是不相等的:

public void layout(int l,int t,int r,int b){
super.layout(l,t,r+100,b+100);
}

重写 layout 方法,导致最终宽高比测量的大 100px。

3.3 View 的 draw 过程

draw 过程比较简单,主要作用就是将 View 绘制到屏幕上面,View的 draw 过程如下:

  • 绘制背景 background.draw(canvas)。
  • 绘制自己 (onDraw)。
  • 绘制 children (dispatchDraw)。
  • 绘制装饰 (onDrawScrollBars)。
    draw 源码如下:
    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
        ...
    
    }

View 的绘制过程是通过 dispatchDraw 来实现的, dispatchDraw 会遍历调用所有子元素的 draw 方法,如此 draw 会一层一层的传递下去,View 有一个特殊的方法,setWillNotDraw,源码如下:

    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

也就是,一个 View 如果不需要绘制任何内容,那么设置这个标记位为 true 后,系统会相应的进行优化,View 默认没有启用这个标记位,ViewGroup 默认启用这个标记位。这个标记位的意义是:当我们自定义控件继承 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记以便于系统优化。相反,我们的 ViewGroup 需要绘制时,要手动关闭这个标记位。

具体参考 《Android 开发艺术探索》View 工作原理。

你可能感兴趣的:(View 工作原理)