Android View的工作原理

作者: 聽媽媽的话 | 来源:发表于2017-10-27 19:11 被阅读86次

    一、绘制流程

    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其实都有滚动条,只是有些控件没有显示出来。

    相关文章

      网友评论

        本文标题:Android View的工作原理

        本文链接:https://www.haomeiwen.com/subject/jhorpxtx.html