美文网首页
绘制流程(Measure Layout Draw)

绘制流程(Measure Layout Draw)

作者: youtianlong123 | 来源:发表于2017-05-25 13:25 被阅读0次

    上一篇文章描述了我们的在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,它的继承体系是这样的


    在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,包含一个 <ViewStub>和<FrameLayout>的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的宽高全都测量出来了。 测量流程

    补充:在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)

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