Android View的工作原理

一、绘制流程

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

绘制流程.png

二、measure过程

1、MeasureSpec

从上图可以了解到View在绘制过程中会调用到View的measure()方法,measure()方法接收两个参数:widthMeasureSpecheightMeasureSpec,分别用于确定视图的宽度和高度的规格。
MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(在某种测量模式下的规格大小)
SpecMode有三类:

  • UNSPECIFIED
    未指定模式,父容器不对View有任何限制,一般用于系统内部,开发过程中不太会用到。
  • EXACTLY
    精确模式,父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应LayoutParams中的match_parent和具体的数值这两种模式。
  • AT_MOST
    最大模式,父容器指定了一个可用大小,即SpecSize,View的大小不能大于这个值。它对应LayoutParams中的wrap_content。
子视图的MeasureSpec

widthMeasureSpecheightMeasureSpec这两个参数的值通常是由父视图传递给子视图,再经过计算得出来的,说明父视图会在一定程度上决定子视图的大小。观察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);
    }

其中childWidthMeasureSpec 与childHeightMeasureSpec 都是通过getChildMeasureSpec的计算得出的,并且与父容器的MeasureSpec和子元素本身的LayoutParams有关,再看看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);
    }

以上的代码可以用一个表格来表示:

普通View的MeasureSpce创建规则.png

总结如下:

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

最外层的根视图的widthMeasureSpec和heightMeasureSpec是在performTraversals()方法中获取到:

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

其中的lp.width和lp.height在创建ViewGroup实例的时候就被赋值为MATCH_PARENT了,getRootMeasureSpec的代码如下:

    private 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);  
            break;  
        }  
        return measureSpec;  
    }  

由此可见,当rootDimension等于MATCH_PARENT时,MeasureSpec的SpecMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT时,MeasureSpec的SpecMode就等于AT_MOST,当rootDimension为具体数值时,MeasureSpec的SpecMode就等于EXACTLY,与前面描述的一致。且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。

2、View的measure过程

View的measure过程由其measure方法来完成,而measure方法是一个final方法,这意味着子类不能重写此方法,而measure方法中调用的onMeasure方法才是真正去测量并设置View大小的地方,默认会调用getDefaultSize方法来获取视图的大小:

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

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

这里的MeasureSpec是由measure方法传递下来的,测量后调用setMeasuredDimension方法来设定测量后的大小,这样一次measure过程就结束了,这是系统的默认测量方式,实际上我们可以重写这个方法来改变测量方式,从而实现自定义View的测量。
值得注意的是,在重写onMeasure方法的时候,需要注意设置好View的warp_content情况,按照自身情况来测量出实际所需大小,否则在布局中使用wrap_content就相当于使用match_parent,从代码可以看出,如果View在布局中使用wrap_content,那么它的specMode是AT_MOST,则宽/高等于specSize,从上面的“普通View的MeasureSpce创建规则”表中可知,这种情况下View的specSize是parentSize,即父容器当前剩余空间大小,与使用match_parent效果一致。因此需要根据需求来判断解决这个问题,例如使用默认大小等。

3、ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程以外,还会去遍历调用所有子元素的measure方法,各个子元素再递归去执行这个过程。与View不同的是,ViewGroup是一个抽象类,并没有定义其测量的具体过程,毕竟不同ViewGroup的子类有不同的布局特性,如RelativeLayout和LinearLayout,因此需要子类自己去实现ViewGroup提供了一个叫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);
    }

measureChild与measureChildWithMargins不同的地方在于,measureChild没有测量自己的margin属性,而measureChildWithMargins有,当需要使用到margin属性时,还是需要使用measureChildWithMargins来测量。

4、测量结束

measure完成后,通过getMeasuredWidth/getMeasuredHeight方法就可以正确地获取到View的测量宽/高,但是在这种情形下,在onMeasure方法中拿到的测量宽/高很可能是不准确的,因为View需要多次measure才能确定自己的宽/高,前几次测量过程中,得出的测量结果可能与最终结果不一致,因此最好还是在onLayout方法中去获取View的测量宽/高或者最终宽/高。

三、layout过程

measure结束后,视图的大小就已经测量好了,接下来就是layout过程了。layout的作用是给视图进行布局的,也就是确定视图的位置。ViewRootd的performTraversals方法会在measure结束后继续执行,并调用layout方法来执行此过程:

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);  

layout方法接收四个参数,分别代表着相对于当前视图的父视图而言的左、上、右、下的坐标,在layout中会调用onLayout方法,但是,View的onLayout是一个空方法,因为View的位置应该由父视图ViewGroup来决定的,而ViewGroup中的onLayout方法是一个抽象方法,这是由于每个ViewGroup的布局方式不同,因此需要重写这个方法来确定子元素的位置。
layout结束后,就可以通过getWidth和getHeight来得到其最终宽/高:

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

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

四、draw过程

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

  • 绘制背景background.draw(canvas)
  • 绘制自己(onDraw)
  • 绘制children(disptchDraw)
  • 绘制装饰(onDrawScrollBars)
    首先绘制背景,其实就是在XML中通过android:background属性设置的图片或颜色,当然也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值;
    接下来是绘制自己,调用onDraw方法,使用画布来绘制自己的内容,自定义View的时候主要就是重写这一个方法;
    接下来是绘制children,调用disptchDraw来绘制所有的子元素;
    最后是绘制装饰,这一步的作用是对视图的滚动条进行绘制,每一个View其实都有滚动条,只是有些控件没有显示出来。

推荐阅读更多精彩内容