UI绘制流程分析(后篇)--完结篇

彻底搞懂UI绘制流程,看该系列就够了


前言

UI绘制流程分析(前篇)–App与Activity的启动 中我们详细讲解了在UI测量、布局以及绘制之前是要经过Application的创建、启动和Activity的创建和启动的这些流程的,然后就是布局文件xml的节点解析到Activity里的布局上,那么接下来就是对这些组件(View)进行测量、布局和绘制:

在这里插入图片描述

所以这也是接下来要讲解的知识点。


提示:以下是本篇文章正文内容

一、绘制流程

在上篇系列中可以知道,Activity里的组件是在onResume()方法里进行测量、布局和绘制的,所以我们一路跟踪,就在源码里发现测量的开始就是在performTraversals()方法里的performMeasure()方法开始:

private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;

        ...

        if (host == null || !mAdded)
            return;
        ...
         // Ask host how big it wants to be
         performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

         // Implementation of weights from WindowManager.LayoutParams
         // We just grow the dimensions as needed and re-measure if
         // needs be
         int width = host.getMeasuredWidth();
         int height = host.getMeasuredHeight();
         ...
                    

那么performMeasure()方法里的参数宽高又是怎么来的呢?一个组件进行performMeasure()方法去测量的时候,传进去的这两个参数肯定是要从它的父容器那里得到的,因为一个字控件的宽高,是要先去参考父容器才能最终决定得了,比如你子控件设置成了match_parent,那这时候肯定就是要取父容器的宽高嘛。所以我们往上找:

	...
	if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                    if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed!  mWidth="
                            + mWidth + " measuredWidth=" + host.getMeasuredWidth()
                            + " mHeight=" + mHeight
                            + " measuredHeight=" + host.getMeasuredHeight()
                            + " coveredInsetsChanged=" + contentInsetsChanged);

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

                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();

	...

可以看到,childWidthMeasureSpec和childHeightMeasureSpec是通过getRootMeasureSpec()方法所得,因为当前是一个Activity第一次启动的情况,也就是当前绘制的是布局DecorView,所以现在getRootMeasureSpec()方法传的mWidth和mHeight其实是整个屏幕的宽高(DecorView的父容器只能是参考整个屏幕的宽高了),lp则是DecorView的layoutParams,所以说控件的 childWidthMeasureSpec是通过屏幕宽(父容器)和控件本身的layoutParams布局参数来确定的。

childWidthMeasureSpec和childHeightMeasureSpec这两个值也要说说,它现在是控件在调用performMeasure()方法时所传进去的宽高规则值,因为我们现在是第一次进行测量,肯定是先从DecorView开始测量,那么也就是说当前控件DecorView在调用performMeasure()方法进行测量,那也就是说现在传进去的childWidthMeasureSpec和childHeightMeasureSpec就是屏幕的宽高规则值,所谓的宽高规则值是什么呢?

这个规则值是一个int类型值,我们都知道一个int等于4个字节,也就是有32位,那么此时这个childWidthMeasureSpec就有32位存储空间,而它的前2位存储的是测量模式,测量模式是什么意思,先来看看这个表:
在这里插入图片描述

1)UNSPECIFIED,表示未指定模式:表示当前组件大小可以随意大小,不受限制,但这种情况在实际开发中几乎用不上,所以可以不用深究它;

2)EXACTLY,表示精确模式:在这种模式下,尺寸的值是多少,那么这个组件的长或宽就是多少,对应布局文件中设置控件为 MATCH_PARENT 和确定的值。(MATCH_PARENT之所以也是精确模式是因为子控件设置了MATCH_PARENT后,那么它的宽高值就是父控件的宽高值,父控件的宽高值肯定是具体的值,即使是DecorView这种根布局,它的宽高值也是具体的,就是手机屏幕的宽高值)

3)AT_MOST,表示最大模式:也就是控件的宽高是在0和父组件给出的最大值之间,当前组件的长或宽最大只能为这么大,当然也可以比这个小。对应布局文件中设置控件为WRAP_CONETNT。

前2位存储的这个测量模式就是这三种情况值,而后30位则存储测量具体值,如果听不懂没关系,接下来从getRootMeasureSpec()方法里去分析:

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

我们还是以当前要测量的控件为DecorView来分析,所以windowSize表示手机屏幕宽高值,而rootDimension则是DecorView设置的模式(UNSPECIFIED、EXACTLY还是AT_MOST),然后根据DecorView设置的是哪种模式来走相应的逻辑,如果是match_parent,则measureSpec(DecorView最终的测量规则值,也就是它调用performMeasure方法时传进去的childWidthMeasureSpec值)为屏幕的值。如果DecorView设置的是wrap_content,则传进去的是屏幕给出的最大值。而其他情况(default)则是精确模式,DecorView自己设置的具体大小值。

那么为了验证我是否说的对,就来看看这个MeasureSpec.makeMeasureSpec()方法,但在此之前,要先讲讲这个MeasureSpec类:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}
        
		public static final int UNSPECIFIED = 0 << MODE_SHIFT;

		public static final int EXACTLY     = 1 << MODE_SHIFT;

		public static final int AT_MOST     = 2 << MODE_SHIFT;
		...

一开始定义了一个int类型变量MODE_SHIFT为30,还有一个int类型变量MODE_MASK掩码为0x3 << MODE_SHIFT,0x3就是16进制的3,<< MODE_SHIFT则表示向左位移30位,移之前要把0x3(int类型32位大小— 00000000 00000000 00000000 00000011 )转为二进制的数,那MODE_MASK这个值最后就是11 000000 00000000 00000000 00000000,因为MeaSpec是32位的,所以现在左移30位后,变成前2位有效数字,然后后面30位为0。然后三种模式分别定义为:

...
		public static final int UNSPECIFIED = 0 << MODE_SHIFT;

		public static final int EXACTLY     = 1 << MODE_SHIFT;

		public static final int AT_MOST     = 2 << MODE_SHIFT;
...

对应的就是上表中描述的未指定模式、精确模式和最大模式。同理,这时候都要先将0、1、2这些十进制的数转成二进制再进行左移30位(因为<<这个符合是位运算,要转成二进制才能进行进制位移)。这里如果对于进制以及位运算不懂的同学可以先去搜搜这些知识点,因为都是计算机的基础知识,不是本文的重点,所以这里就不详细讲了,或者有疑问的欢迎大家一起讨论。
左移之后这三个值分别就是:

00 000000 00000000 00000000 00000000
01 000000 00000000 00000000 00000000
10 000000 00000000 00000000 00000000

那现在再回过头来看MeasureSpec.makeMeasureSpec()方法:

		public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

重点来看最后一句代码,可以看到,返回的值是用传进来的size(屏幕的宽值)和设置的测量模式作的运算而得,具体是size要跟取反的MODE_MASK掩码做与运算,也就是拿00 111111 11111111 11111111 11111111这个数跟size作与运算,所以此时得到的数是前2位为0,而后30位则是size的值;然后再来看符合为“|”的后半部分运算,是mode(DecorView的测量模式)和掩码MODE_MASK(11 000000 00000000 00000000 00000000)做与运算,得到的数则是前2位是测量模式,后30位的值为0;然后符合为“|”的两个部分得到的值作或运算,因此就能得到一个前2位是模式,后30位是size的32位值,也就是int类型的measureSpec,作为DecorView调用performMeasure测量方法里传进去的childWidthMeasureSpec值,也就是DecorView最终的测量规则值。

也就是说,一个控件在调用performMeasure测量方法时,它所传进去的测量规则值(宽和高)都是先要靠它的父布局的size以及控件本身设置的测量模式值来合成的,然后最终传到控件调用performMeasure测量方法里去。

既然我们能把测量模式和测量具体值通过这种进制运算来合并到,那要从measureSpec里去取出它的测量模式和测量具体值也是可以的,也就是逆运算嘛,所以接着看MeasureSpec,能发现它有getMode()和getSize()方法:

...
		@MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

       
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
...

逻辑都很好理解,其实就是对(size & ~MODE_MASK) | (mode & MODE_MASK)进行逆运算,如果觉得混乱,可以自己定义一个数字去验证。所以为什么为什么总说基础很重要,因为你如果不懂掩码、与运算、或运算、位移运算、进制这些基础知识,不了解它们的特性,你就没法深入底层去挖掘编程这一行。

分析完MeasureSpec后,我们再次回到performMeasure()方法里的测量逻辑里:

	...
	if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                    if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed!  mWidth="
                            + mWidth + " measuredWidth=" + host.getMeasuredWidth()
                            + " mHeight=" + mHeight
                            + " measuredHeight=" + host.getMeasuredHeight()
                            + " coveredInsetsChanged=" + contentInsetsChanged);

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

                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();

	...

现在通过getRootMeasureSpec()方法得到了DecorView的measureSpec了,childWidthMeasureSpec 和childHeightMeasureSpec ,即DecorView进行测量时传进去的测量规则宽高值,也就是调用performMeasure(),现在来看看该方法得详情:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

里面调用的是mView的measure()方法,也就是DecorView的measure()方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        ...
        
...
}

这里面一开始是判断一些光学边界值,然后得到最终的widthMeasureSpec 和heightMeasureSpec值,然后就是获取测量缓存key:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
		// Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
        ...
}

这个缓存key就是一个long类型64位的值,它是通过上述的位计算之后,变成了一个装载着宽和高(widthMeasureSpec和heightMeasureSpec)的值。这样的话每次测量其实都是先从缓存里取这个measureSpec,这样就不用每次测量都重新测量一次,而是先判断有没有缓存,有就从缓存里取这个测量规则值就好,这样性能就不会耗损。而

取宽或高的时候则逆运算(右移回32位然后强转int也就是砍掉后32位)就可以得到:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
		...
		// Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
        ...

        if (forceLayout || needsLayout) {
            ...
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
        ...

}

通过key调用indexOfKey()取出缓存cacheIndex,然后根据cacheIndex判断,有缓存则直接根据cacheIndex取出对应的测量值,也就是通过(int) (value >> 32), (int) value逆运算去取出widthMeasureSpec和heightMeasureSpec,然后调用setMeasuredDimensionRaw()方法确定了组件最终的宽高值:

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

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

如果没有缓存则调用onMeasure()方法,看看它的详情:

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

先看看getSuggestedMinimumWith()方法:

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

这里意思其实就是如果该view有设置背景,则以背景宽高值为这个默认值,否则就为0。这个值随后就传到getDefaultSize()方法里执行,接下来看看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值。然后就是从measureSpec里取出对应的模式和测量值,根据测量模式的不同来得出最终的size值。

之所以要有这么一个默认值是因为当要测量的这个控件你没有在onMeasure()方法重写时定义你想要的宽高值时,就会走这个默认值逻辑,你此时就想着当DecorView里有子控件,那当它进行测量方法时,那就轮到它的子控件也进行测量,那此时这个子控件不再是一个ViewGroup容器组件了,而是一个View控件,然后你又没有在它的onMeasure()方法里定义处理最终的宽高值,那它走到这里肯定是需要一个具体的(默认)值。

执行完getDefaultSize()得到的值最终传到setMeasuredDimension()方法里:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

判断是否要算上边界值,然后确定最终的控件的宽高值。

那现在我们就要往回measure()方法继续分析:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
		...
		// Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
        ...

        if (forceLayout || needsLayout) {
            ...
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
        ...

}

没有缓存的话,自然就是要执行view的onMeasure()方法,我们是以DecorView为例子的,因此此时就是走DecorView的onMeasure()方法,而由于之前分析过DecorView是一个FrameLayout,所以我们来看看FrameLayout的onMeasure()方法,(如果不是DecorView,而是我们自己平时写自定义View,那此时走的就是我们重写的onMeasure()方法了):

	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    	...
		int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        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());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
        ...
	}

相信大家对FrameLayout都不陌生,它的特性是它里面的控件都是从左上角开始布局的,那对FrameLayout进行测量时,肯定就是需要遍历里面的子控件,然后测量每个子控件(调用measureChildWidthMargins()方法),得到子控件的宽高值后再计算margin值,得出每一个子控件的最终宽高值,然后就是在比较它们谁的高最大,然后FrameLayout的高就是这个值,比较它们谁的宽最大,FrameLayout的宽就是这个值。

我们看看子控件的测量方法measureChildWidthMargins()方法,传给它的参数是子控件child,以及父控件(此时是DecorView的MeasureSpec):

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

可以看到先调用这个子控件的getLayoutParams()得到它自己的LayoutParams,也就是在xml上定义的(match_parent、wrap_content还是具体值),然后就是将LayoutParams相关值传到getChildMeasureSpec()方法里调用,最终得到子控件的measureSpec:

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:
            ...
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

代码很长,但相信你看过前面分析MeasureSpec之后,这个方法会看得懂。

那么现在重新看这个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);
    }

现在可以知道,我们在写自定义View时,重写onMeasure()方法时的参数widthMeasureSpec和heightMeasureSpec为什么是控件自己本身的MeasureSpec,就是因为measureChildWithMargins()方法里它通过getChildMeasureSpec()方法已经转变了,将父控件传过来的MeasureSpec通过传到getChildMeasureSpec()方法里处理,然后最终得到子控件的MeasureSpec,之后就传到child.measure()方法里:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
		...
		// Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
        ...

        if (forceLayout || needsLayout) {
            ...
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
        ...

}

这样我们在调用onMeasure()方法时,这时候的widthMeasureSpec和heightMeasureSpec就是控件自己本身的了。然后就是周而复始,重复之前的步骤,这个子控件又会再一次去测量它自己,然后如果它里面有子控件的话,就又测量它自己:
继续measure—onMeasure—setMeasuredDimension。

最后我们再次回到FrameLayout的onMeasure()方法:

	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    	...
		int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        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());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
        ...
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        ...
	}

可以看到,最终调用setMeasuredDimension()设置自己的最终宽高值。

以上是FrameLayout测量时的逻辑,而其他容器布局进行测量的逻辑又是不一样,比如LinearLayout布局的宽和高就是遍历每个子控件时根据设置的不同方向,则决定它们的高或者宽是累加来得到,这里就不作详细述说,大家感兴趣可以自行研究。

二、布局流程

分析完performMeasure()测量流程之后,就是布局流程也就是performLayout()方法:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;
        if (host == null) {
            return;
        }
        ...

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

            mInLayout = false;
        ...
}

将DecorView对象mView赋值给host后,因为得益于测量后,知道了host的宽高值,这样在布局的时候,根据宽高值就可以计算出top,left,right和bottom四个坐标值(通过getMeasuredWidth()和getMeasuredHeight()直接获取宽高,,然后计算四个点),这样就可以布局我们的控件。所以我们来看看layout()方法详情:

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            ...
        ...
}

将四个坐标值传到了 onLayout()方法:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

最终是要看你的容器布局里的真正重写的onLayout()方法,还是以FrameLayout为例,看看它的onLayout()方法:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

继续跟踪layoutChildren()方法:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
       ...

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                ...

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    ...
                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
}

这里面的逻辑并不复杂,结合FrameLayout的特性,我们就知道它是遍历它里面所有子控件,然后得到它们的宽高值(因为它们之前经历过测量这个环节,因此getMeasuredWidth()可得),然后计算出它们的四个点的坐标值,最后根据设置的不同摆放方向Gravity来摆放它们。调用child.layout()重复以上的逻辑,也就是子View如果也是容器布局,那就是又去摆放它里面的子控件…

跟测量一样,其他容器的布局流程又会根据它特性的不一样而不同,这个大家看着源码来分析就好。

三、绘制流程

绘制流程其实也不复杂,来看看performDraw()方法:

private void performDraw() {
        if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
            return;
        } else if (mView == null) {
            return;
        }

        ...

        try {
            boolean canUseAsync = draw(fullRedrawNeeded);

其实就是调用了draw()方法:

private boolean draw(boolean fullRedrawNeeded) {
        Surface surface = mSurface;
        if (!surface.isValid()) {
            return false;
        }

        if (DEBUG_FPS) {
            trackFPS();
        }
        ...
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
                }
         ...       
}

绘制第一步首先是要获取到画板Surface,这样才能通过它获取到我们的Canvas,最后就是调用drawSoftware()方法,它就是绘制流程,看看里面的详情:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

        // Draw with software renderer.
        final Canvas canvas;
        ...
		try {
            dirty.offset(-dirtyXOffset, -dirtyYOffset);
            final int left = dirty.left;
            final int top = dirty.top;
            final int right = dirty.right;
            final int bottom = dirty.bottom;

            canvas = mSurface.lockCanvas(dirty);
            ...
         ...
            
}

调用Surface的lockCanvas()方法去构造画布对象canvas,这样后面就把canvas传到view的draw()方法里:

...
		try {
                canvas.translate(-xoff, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;

                mView.draw(canvas);

                drawAccessibilityFocusedDrawableIfNeeded(canvas);
            }
...

来看看draw()方法详情:

public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
        ...
}

我们通过注释里的6点说明就可以知道绘制的流程就是这6步:
1)绘制背景
2)如果需要,保存图层
3)绘制View内容
4)绘制子控件
5)如果需要,恢复图层
6)画装饰(滚动条等)

由此可以知道绘制是控件自己本身的事情,而不是由父布局去画它的:

...
		// Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
...

第三步就是View的onDraw()方法:

protected void onDraw(Canvas canvas) {
    }

这里值得一提的是容器没有onDraw方法,这点要注意,但绘制背景也是跟View一样,是在第一步进行:

private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();

        // Attempt to use a display list if requested.
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mThreadedRenderer != null) {
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

            final RenderNode renderNode = mBackgroundRenderNode;
            if (renderNode != null && renderNode.isValid()) {
                setBackgroundRenderNodeProperties(renderNode);
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
                return;
            }
        }

        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

整个绘制流程还是很清晰的,至于说绘制的底层又是怎样进行的,这就是安卓底层的图形渲染原理,这个涉及到系统底层,以后有机会再讲解,欢迎大家关我公号:Pingred

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值