Android coder进阶android技术专栏爱上Android

Android View的工作原理

2017-10-27  本文已影响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有三类:

子视图的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

总结如下:

根视图的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的绘制过程遵循如下几步:

上一篇下一篇

猜你喜欢

热点阅读