绘制流程(Measure Layout Draw)

上一篇文章描述了我们的在Activity中的onCreate中去setContentView,是怎样将布局显示在屏幕上的。setContentView都干了些什么 ,接着流程梳理,上篇结尾处说到了绘制的起始点,也就是ViewRootImpl的performTraversals()方法中的performMeasure、performLayout、performDraw。Android的绘制流程分为三步,第一步measure(测量),第二步layout(布局摆放),第三步draw(绘制)。很好理解这三个步骤,因为跟现实生活的场景很像,就像要布置一个新房子一样。

  1. 我们首先得知道这个屋子的大小,而且还要知道每个要放入屋子中的物品的大小。知道大小的过程就是Measure过程。
  2. 知道所有的大小尺寸后,根据他们的尺寸,我们才能去安排他们具体摆放在哪个位置。否则有些地方太小,物品太大,可能放不下。也有可能有的地方太大,物品太小,而浪费空间。所以根据物品大小,合理安排摆放位置。
  3. 在图纸上规划完所有物品的摆放之后,根据图纸上的位置,把真实的物品一个一个摆放在他们应该存在的位置上。

Measure(测量)

想要测量大小,最先应该测量的就应该是房子的大小吧。进入ViewRootImpl的performTraversals()方法,此方法超级长,有一段代码

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

看到两个获取宽高的的方法,都分别传入一个int类型的宽或者高的值,和一个对应的layoutParams的值。mWidth和mHeight此window的宽高,此处可以理解为屏幕的宽高。layoutParams为一个WindowParams,它的width和height都为 LayoutParams.MATCH_PARENT。

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

很明显,代码会走ViewGroup.LayoutParams.MATCH_PARENT这个case。调用了MeasureSpec类的makeMeasureSpec静态方法,返回了一个int值。第一个参数,第二个参数竟然是MeasureSpec类的一个常量。看来这个类很重要,有必要先要了解下这个类。

MeasureSpec

此类是View的一个内部类。它提供给子View去测量的实体,它包含mode与size两部分。它内部的执行都是32位的位操作,可以看做仅仅对一个int值进行操作,极大减小了内存的分配。32位的最高两位代表着mode部分,后30位代表着具体数值。mode有三种模式,所以完全可以用2位来表示。它们分别是:

  • UNSPECIFIED:此模式代表子View想要多大都行,父容器都不会干涉测量。这模式在自定义中很少用到。一般都是在系统控件中才用到,如ListView中。
  • EXACTLY:父容器提供一个精确的值给子View。
  • AT_MOST:子View的大小自己决定,但是最大不能超过父容器给定的值。
    它还包含三个常用方法:
  • makeMeasureSpec(int size,int mode):将传入的size和mode组装成一个MeasureSpec返回
  • getMode(int measureSpec):取出measureSpec中的mode值返回。
  • getSize(int measureSpec):取出measureSpec中的size值返回。

回到ViewRootImpl的getRootMeasureSpec方法的第一个case中,这方法只是将size为屏幕宽度,mode为MeasureSpec.EXACTLY组装成了一个measureSpec返回。接着执行performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);此方法中执行了mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); mView就是DecorView,所以进入measure()方法。发现直接到了View的measure()方法,因为此方法是被final修饰了,所以所有子类都不能重写此方法。那就查看此方法的逻辑,代码有一大部分是有关缓存的,其中执行了一句很重要的代码onMeasure(widthMeasureSpec, heightMeasureSpec); onMeasure()是可以被重写的,所以要看看重写后的逻辑。此时的view是DecorView,它的继承体系是这样的

绘制流程(Measure Layout Draw)_第1张图片

在DecorView中重写了onMeasure方法。在它的onMeasure中又调用了 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 进入到了FrameLayout的onMeasure中

        //......
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                //......
            }
        }

如果子view没有全部测量完毕或者当前的子view不是在Gone的状态下,就调用measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); 从方法名可以很轻易的看出,这个方法是测量children的方法(带margin),进入该方法

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

此方法是ViewGroup的方法,首先获取当前view的layoutParams,接着调用了getChildMeasureSpec方法返回了相应的MeasureSpec,那getChildMeasureSpec方法就是根据父容器提供的MeasureSpec,和父容器的padding值,和自己的margin值和自己的layoutParams来生成一个MeasureSpec。查看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中的mode和size。定义了resultSize 、resultMode 来进行计算,最后通过return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 将resultSize 和resultMode 组装成一个MeasureSpec返回。分别进入三个case中

  • MeasureSpec.EXACTLY:当父容器提供了一个精确的值给子View。由于我们在android:layout_width=" "中只能写match_parent,wrap_content。所以分这三种情况进行判断。
    • childDimension > 0:由于LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT定义的常量分别为-1,-2.所以只要childDimension >0就一定是定义了固定的数值。既然子View定义了固定的数值,那么resultSize就应该是它固定的值。resultMode就应该为MeasureSpec.EXACTLY,精确模式。
    • childDimension == LayoutParams.MATCH_PARENT:如果子View想要的是MATCH_PARENT,那么resultSize应该等于父容器能提供的大小。这也是一个精确的值,所以resultMode应该为MeasureSpec.EXACTLY,精确模式。
    • childDimension == LayoutParams.WRAP_CONTENT:如果子View只是想要包裹自己的内容。那现在是没有办法确定它里面内容的大小,所以只能确定不让子View超过父容器的大小。resultSize = size,resultMode为MeasureSpec.AT_MOST。
  • MeasureSpec.AT_MOST:父容器提供一个最大值。同样是分三种情况:
    • childDimension > 0:同样,如果子View设置的固定的值,那么resultSize就为它设定的值。resultMode应该就是精确的。
    • childDimension == LayoutParams.MATCH_PARENT:如果是想MATCH_PARENT,那就让resultSize等于父容器给的这个最大值。resultMode= MeasureSpec.AT_MOST。
    • childDimension == LayoutParams.WRAP_CONTENT:如果是要包裹内容,那么就让resultSize等于父容器能给的最大值,只要让他不超过这个值就可以了。所以resultMode = MeasureSpec.AT_MOST
  • MeasureSpec.UNSPECIFIED:为系统多次measure调用的。

回到ViewGroup的measureChildWithMargins方法中,现在获取到了要测量child的childWidthMeasureSpec和childHeightMeasureSpec,继续调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 此时又回到了view的measure方法,所以又执行了里面的onMeasure方法。此时的这个view就是decorview的子View,包含一个 的LinearLayout。所以又回去执行LinearLayout的onMeasure方法,在LinearLayout的onMeasure中又回去执行测量子View的方法。如此递归调用,直到调用onMeasure的view不是viewGroup的时候。他们最终都会走到view的onMeasure中

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

先看getSuggestedMinimumWidth方法,代码如下

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

就是如果没有背景,就返回我们设置的最小值,如果有背景,就返回最小值和背景的最大值,也就是提供一个默认的最小值而已,接着调用了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;
    }

MeasureSpec.AT_MOST和 MeasureSpec.EXACTLY时会返回measureSpec给定的值,基本上大多的时候也都会走到这里。将得到的宽高值传入setMeasuredDimension方法中,会调用setMeasuredDimensionRaw,在setMeasuredDimensionRaw中

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

终于对view的mMeasuredWidth 、mMeasuredHeight成员变量完成了赋值,并改变了标记。

此时回到FrameLayout的onMeasure方法中。执行完 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); 其实就是对该FrameLayout下的所有child完成了测量,此时就能通过getMeasuredWidth和getMeasuredHeight获取他们测量后的宽高了。执行完onMeasure的for循环后,FrameLayout的onMeasure又执行了

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

来完成对自己mMeasuredWidth、mMeasuredHeight的赋值。至此所有view的宽高全都测量出来了。
绘制流程(Measure Layout Draw)_第2张图片
测量流程

补充:在FrameLayout的onMeasure中,for循环中有一行childState = combineMeasuredStates(childState, child.getMeasuredState()); 得到一个childState的值,并且在setMeasuredDimension中的resolveSizeAndState中将childState传入其中,
查看View中的三个方法:

    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
    public final int getMeasuredState() {
        return (mMeasuredWidth&MEASURED_STATE_MASK)
                | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
                        & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
    }
    public final int getMeasuredHeightAndState() {
        return mMeasuredHeight;
    }

发现getMeasuredWidth方法返回并不是单纯的mMeasuredWidth ,而是掩码。其实mMeasuredWidth并不是单纯代表着宽度的数值。它的前8位代表着测量状态,它的后24位才代表着具体数值。所以getMeasuredWidth方法要返回具体数值要mMeasuredWidth & MEASURED_SIZE_MASK; 而单纯返回mMeasuredHeight的方法名的意思是返回测量后的高和状态。getMeasuredState把width和height的state分别封装到int中的前8位和16-24位。看下resolveSizeAndState的方法

    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

当measureSpec为AT_MOST的时候,也就是对应warp_content的场景,并且父容器提供的最大值小于了该类想要的值时,虽然我们依然给了他measureSpec中的值,但是加入了MEASURED_STATE_TOO_SMALL这个标记,标记测量的时候没有给到他相应的值。

Layout(布局)

所有要摆放物品的大小都已经测量完了,这时候就需要规划把它们具体摆放在哪了。查看ViewRootImpl的performLayout方法,host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); host是DecorView,由于此时DeocrView已经测量完毕,所以已经可以调用getMeasuredWidth、getMeasuredHeight来获取它的测量宽高了。进入View类的layout方法,首先会执行boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); 查看setOpticalFrame方法内部也是调用了setFrame方法。在setFrame中定义了一个boolean类型的changed变量,初始值为false。然后判断如果此View原来的left、right、 top 、bottom其中的任何一个和现在传入的四个值不同,就说明此view的布局要有所改变 ,这时将changed变量赋值为true。并将原来的成员变量进行相应的更新。比对原来的尺寸和现在的尺寸是否一样,如果不一样,执行了onSizeChanged()方法。这也是onSizeChanged方法回调的时机。在view的layout方法中,执行完boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);如果changed为true或者mPrivateFlags有需要重新layout的标记,执行onLayout(changed, l, t, r, b); 在view中onLayout是空实现,而onLayout在ViewGroup中的一个抽象方法,由继承的子类必须实现。因为每个具体的view,按什么规则来摆放自己view都会有不同的规则,所以这事view和ViewGroup不可能帮着去做。那就进入到DeocrView的onLayout方法中。它先调用了FrameLayout的onLayout方法,在这个方法中直接执行了layoutChildren(left, top, right, bottom, false /* no force left gravity */);

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                // 省略一大段代码:根据具体的逻辑来计算child应该摆放的位置值
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

此时又调用了view的layout方法,又会回到上面的逻辑,进入一个深度遍历,如果是ViewGroup,继续执行它的child的layout,直到全部view都执行完layout.

Draw(绘制)

所有的view都已经规划完了需要放在哪里,这时候就要把每个view都显示在他们需要显示的位置上。draw的起始点是在ViewRootImpl的performDraw方法中,执行了draw(fullRedrawNeeded); 关键代码如下:

        if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
                mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
            } else {
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
            }
        }

只留了两句最关键的代码,通过判断是否开启了硬件加速渲染,分别执行了mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty),在这两个方法中最终都执行了mView的draw(canvas)方法。这次又来到了View的draw方法中。这个方法有很清晰的6个步骤执行顺序。

  1. 绘制背景
  2. 如果有必要,保存当前图层
  3. 绘制View本身内容
  4. 绘制子view
  5. 如果有必要,绘制边缘,恢复图层
  6. 绘制view上装饰性的,比如滚动条
    其中2和5是可以跳过的。

绘制背景

执行drawBackground(canvas);代码很简单,就是将view的background绘制到canvas上。

绘制View本身内容

执行onDraw(canvas);,onDraw在view中为空实现,具体的实现需要在具体的类中分别实现。因为每个类要绘制的内容都是不一样的

绘制子view

执行了dispatchDraw(canvas); 在view中这个方法是一个空的实现,而在ViewGroup中有了具体的实现。这也很对,因为只有ViewGroup才需要绘制子View,所以才会去具体实现dispatchDraw()方法。在ViewGroup的dispatchDraw中代码很多,但是主要是调用了drawChild(canvas, transientChild, drawingTime);

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

来执行child的draw,进入遍历过程来执行view树的draw方法。

绘制view上装饰性

执行onDrawForeground(canvas);

执行完遍历过程后,view就绘制完成了

你可能感兴趣的:(绘制流程(Measure Layout Draw))