Android View measure layout draw 过程解析

版权声明:本文出自Jinguangyue的博客,转载必须注明出处。 http://blog.csdn.net/coderyue https://blog.csdn.net/CoderYue/article/details/80041595

感谢 Android 开发艺术探索

MeasureSpec

MeasureSpec 代表一个 32 位int值, 高2位代表 SpecMode, 低30位代表SpecSize, SpecMode 是指测量模式, 而 SpecSize是指在某种测量模式下的规格大小, 它的源码:

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

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * <ul>
         *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
         *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
         *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
         * </ul>
         *
         * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
         * implementation was such that the order of arguments did not matter
         * and overflow in either value could impact the resulting MeasureSpec.
         * {@link android.widget.RelativeLayout} was affected by this bug.
         * Apps targeting API levels greater than 17 will get the fixed, more strict
         * behavior.</p>
         *
         * @param size the size of the measure specification
         * @param mode the mode of the measure specification
         * @return the measure specification based on size and mode
         */
        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);
            }
        }

MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值避免过多的对象内存分配, 也提供了打包和解包方法, 这里要注意 MeasureSpec 所指的是 MeasureSpec 所代表的 int 值, 这并不是指 MeasureSpec 对象本身。

SpecMode 有三类, 如下:

UNSPECIFIED
父容器不对 View 有任何限制, 要多大给多大, 这种情况一般用于系统内部, 表示一种测量状态。

EXACTLY
父容器已经检测出 View 所需要的精确大小, 这个时候 View 的最终大小就是 SpecSize 所指的值, 它对应 Layoutparams 中的 match_parent 和具体的数值俩种模式。

AT_MOST
父容器指定了一个可用大小即 SpecSize, View 的大小不能大于这个值,具体多大要看 View 的具体实现, 它对应于 LayoutParams 中的 wrap_content.

MeasureSpec 和 layoutParams 的对应关系

在 View 测量的时候, 系统会将 LayoutParams 在父容器的约束下转换成对应的 MeasureSpec, 然后再根据这个 MeasureSpec 来确定 view 测量后的宽高,需要注意的是 MeasureSpec 不是唯一由 LayoutParams 来决定的, LayoutParams 需要和父容器一起才能确定 View 的 MeasureSpec, 从而进一步确定 View 的宽高, 另外对于顶级 View(就是 DecorView)和普通 View 来说, MeasureSpec 的转换有些不同, 对于 DecorView 它的 MeasureSpec 是由窗口的大小和自身的 LayoutParams 共同决定的, 对于普通的 View , 其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定, MeasureSpec 一旦确定后, onMeasure 中就可以确定 View 的测量宽/高。

对于 DecorView 来说, 在 ViewRootImpl 中的 measureHierarchy 中有如下一段代码, 显示了 MeasureSpec 的创建过程, 其中 desiredWindowHeight 是屏幕的高, desiredWindowWidth 是屏幕的宽, baseSize 我理解是对话框的宽度,baseSize 解释如下:首先尝试比屏幕宽度较小的宽度看看是否合适, 如果不合适再用屏幕宽度

            // On large screens, we don't want to allow dialogs to just
            // stretch to fill the entire width of the screen to display
            // one line of text.  First try doing the layout at a smaller
            // size to see if it will fit.
                if (baseSize != 0 && desiredWindowWidth > baseSize) {
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                .......

接下来看 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;
    }

上述代码比较明显, 根据它的 LayoutParams 中的宽高参数来划分。

LayoutParams.MATCH_PARENT: 精确模式,大小就是窗口大小;
LayoutParams.WRAP_CONTENT: 包裹内容模式, 大小不一定, 最大不能超过窗口大小;
固定大小, 精确模式: 大小为 Layoutparams 中所指定的大小。

对于普通的 View 来说, 这里指布局中的 View, View 的 Measure 过程由 ViewGroup 传递而来, 先看一下 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);
    }

上述方法会对子元素进行 measure , 在调用子元素的 measure 方法之前会先通过 getChildMeasureSpec 方法来得到子元素的 MeasureSpec, 从上面源码中可以明显的看出子元素的 MeasureSpec 的创建与父元素的 MeasureSpec 和子元素本身的 LayoutParams 有关, 此外还和 View 的 margin 以及 padding 有关, 具体看 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 , 其 MeasureSpec 由父容器的 MeasureSpec 和 自身的 LayoutParams 共同决定, 当 View 采用固定宽高的时候, 不管父容器的 MeasureSpec 是什么, View 的 MeasureSpec 都是精准模式并且大小就是 LayoutParams 中的大小, 当 View 是 MATCH_PARENT 的时候, 如果父容器是标准模式, 那么 View 也是精准模式并且其大小是父容器的剩余空间, 如果父容器是 MATCH_PARENT 模式 那么 view 也是 MATCH_PARENT 并且 空间不会超过父容器的剩余空间, 当 View 是 WRAP_CONTENT 时, 不管父容器是精准模式还是最大模式, View 的模式总是最大化并且大小不能超过父容器的剩余空间。

View 工作流程

measure 过程要分是 view 和 ViewGroup, 因为 ViewGroup 除了要完成自己的测量过程外, 还会去遍历调用子 view 的 measure 方法。

View 的 measure 过程
View 的 measure 过程由其 measure 方法来完成,measure 方法是一个 final 类型的方法, final 方法不能被重写,在 View 的 measure 方法中会调用 onMeasure 方法, 主要看 onMeasure 的代码 :

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

getDefaultSize 返回的大小就是 MeasureSpec 中的 specSize, 而这个 specSize 就是 View 测量后的大小, 这里多次提到测量后的大小, 因为 View 的最终大小是在 layout 阶段确定的, 所以这里必须要加以区分, 但是几乎所有情况下 View 的测量大小和最终大小是相等的。

对于 UNSPECIFIED 情况, 代码中的 size 则是 getSuggestedMinimumWidth。 看下getSuggestedMinimumWidth :

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

这里可以看出如果 view 设置了背景, 那么就等于最小宽度和背景宽度较大的那个, 看下getMinimumWidth:

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

getMinimumWidth 返回 Drawable 的原始高度, 前提是 Drawable 由原始高度, Drawable 什么情况下有原始高度呢? ShapeDrawable 没有原始高度, BitmapDrawable 由原始高度就是图片尺寸。

总结一下 getSuggestedMinimumWidth 的逻辑, 如果 View 没有设置背景, 那么返回 android:minWidth 这个属性所指的值, 可以是0, 如果 view 设置了背景, 则返回 android:minWidth 和背景最小宽度这俩者中的最大值, getSuggestedMinimumWidth 的返回值就是 view 在 UNSPECIFIED 情况下的测量宽。

从 getDefaultSize 方法中看出, View 的宽高由 SpecSize 来决定,可以得出结论: 直接继承自定义 View 控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小, 否则在布局中使用 wrap_content 就相当于使用 match_parent 一样。 因为如果 view 是 wrap_content 模式, 那么它的 SpecMode 就是 AT_MOST 模式, 在这种模式下 它的宽高等于 SpecSize, 这种情况 SpecSize 等于 parentSize ,也就是剩余父空间的大小, 和 match_parent 完全一样, 那么如何处理这个问题呢:代码大体这样:

    private int mWidth;
    private int mHeight;

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth, mHeight);
        } else if(widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthMeasureSpec, heightSpecSize);
        } else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mHeight, widthSpecSize);
        }
    }

可以根据实际情况给 mWidth 和 mHeight 指定一个默认的宽高, 并且在 AT_MOST 模式时设置, 像 TextView, ImageView 里都针对 wrap_content 做了特殊的处理.

ViewGroup 的 measure 过程
对于 ViewGroup 来说, 除了完成自己的 measure 过程以外, 还会遍历去调用所有子元素的 measure 方法, 各个子元素再递归去执行这个过程, 和 View 不同的是, ViewGroup 是一个抽象类, 因此它没有重写 View 的 onMeasure 方法, 但是它提供了一个叫 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);
            }
        }
    }

可以看出来 ViewGroup 会对每一个子元素进行 measure, 这个方法的实现也很好理解,如下:

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 取出子元素的 LayoutParams, 然后通过 getChildMeasureSpec 创建子元素的 MeasureSpec ,接着调用 measure 进行测量。

ViewGroup 并没有定义其测量的具体过程, 这是因为 ViewGroup 是一个抽象类, 其测量的过程 onMeasure 方法需要每个子类自己去实现, 比如 LinearLayout,RelativeLayout 等, 为什么 ViewGroup 不像 View 一样对 onMeasure 做统一的实现呢? 因为不同的 ViewGroup 子类有不同的布局特性, 这导致他们的测量细节各不相同, 比如 LinearLayout 和 RelativeLayout 的布局特性差别就太大了, 所以无法统一实现, 下面分析一下 LinearLayout 的 measure 过程。

LinearLayout 的 onMeasure 方法:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

下面看 measureVertical 代码很长, 主要看:

        final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    // The heightMode is either UNSPECIFIED or AT_MOST, and
                    // this child is only laid out using excess space. Measure
                    // using WRAP_CONTENT so that we can find out the view's
                    // optimal height. We'll restore the original height of 0
                    // after measurement.
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    // Restore the original height and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

从代码看出系统会遍历族元素并对每个子元素执行 measureChildBeforeLayout 方法, 进入这个方法可知:

 void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }

接下来又看到了熟悉的代码, measureChildWithMargins 方法里会调用子元素的 measure, 并且系统会通过 mTotalLength 变量来存储 LinearLayout 在竖直方向上的高度, 每测量一个子元素, mTotalLength 都会增加, 增加的部分主要包括了子元素的高度以及子元素在竖直方向上的 margin 等, 子元素测量完毕后,LinearLayout 会测量自己的大小。代码如下:

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

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

整个计算过程可以详细看源码,当子元素测量完毕后, LinearLayout 会根据子元素的情况来测量自己的大小, 针对竖直的 LinearLayout 而言, 它在水平方向的测量过程遵循 View 的测量过程, 在竖直方向的测量过程则和 View 有所不同, 具体来说如果布局采用 match_parent 的话, 那么测量过程和 View 一致, 也就是高度位 SpecSize , 如果它的布局中高度采用的是 wrap_content,那么它的高度是所有子元素所占用的高度总和, 但是仍然不能超过父容器的剩余空间, 当然它的最终高度还要考虑其在竖直方向上的 padding。

View 的 measure 过程是三个流程中最复杂的, measure 过程以后,通过 getMeasureWidth 就能获取到 测量宽度了,在某些情况下, 系统可能要多次调用 measure 才能确定最终的测量宽高, 在这种情况下, 在 onMeasure 里拿到的测量宽高实际上并不准确, 所以最好在 onLayout 方法里获取宽高才最准确。

在实际情况中我们会用到 View 的宽高, 比如在 Activity 的某一个声明周期内去获取 view 的宽高, 在 onCreate , onResume 等方法中获取到的都是0, 因为 View 的测量过程并不是和 Activity 的声明周期同步执行的, 那么有什么方法能够解决这个问题呢:

Activity , View 的onWindowFocusChanged

onWindowFocusChanged 这个方法的含义是 View 已经初始化完毕了, 宽高已经准备好了, 这个时候去获取肯定是没问题的, 需要注意的就是这个方法可能会调用多次, 在 Activity 继续执行和暂停执行的时候都会调用, 频繁的 onResume 和 onPause 也会多次调用 , 所以可以这样写:

     @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if(hasWindowFocus){
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }

view.post
通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候, View 也已经初始化好了, 代码如下:


        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight(); 
            }
        });

ViewTreeObserver
使用 ViewTreeObserver 的回调接口可以完成这个功能, 比如使用 addOnGlobalLayoutListener 接口, 当 view 树的状态发生改变或者 View 树内部的 view 的可见性发生改变时, onGlobalLayout 都会被调用, 需要注意的是, onGlobalLayout 方法可能被调用多次, 代码如下:

 view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });

view.measure
可以通过手动调用 measure 来得到 view 的宽高, 不过要分情况, 要根据 Layoutparams 来分:

match_parent
这种情况无法 measure 出具体的宽高, 根据 View 的 measure 过程, 构造此种 MeasureSpec 需要知道 prarentSize, 即父容器的剩余空间, 而这个时候我们无法知道 parentSize 的大小, 所以在 match_parent 我们不能手动测量出宽高

具体的数值 dp/px
比如宽高都是 100px ,如下 measure:

        int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
        view.measure(widthMeasureSpec, heightMeasureSpec);

wrap_content
如下:

        int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
        view.measure(widthMeasureSpec, heightMeasureSpec);

通过分析 MeasureSpec 的实现可知, View 的尺寸使用30位二进制表示, 也就是说最大的 30个1, 也就是 (1 << 30) - 1, 在 wrap_content 模式下, 利用 view 支持的最大值去构造 MeasureSpec 是合理的。

网上的错误做法:

view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

layout过程

Layout方法是去顶子元素的位置, 当 ViewGroup 的位置确定后, 会在 onLayout 方法中 遍历所有的子元素并调用其 layout 方法, 在 layout 方法中 onLayout 又会被调用, 看下源码:

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

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

layout 方法流程是: 首先通过 setFrame 设定 View 四个顶点的位置, View 四个定点一旦确定, 那么 View 在父容器中的位置也就确定了, 接着会调用 onLayout 方法, 这个方法的用途是父容器确定子元素的位置, 和 onMeasure 类似, onLayout 的具体实现同样和具体的布局有关, 所以 View 和 ViewGroup 都没有真正的实现 onLayout 方法, 看一下 LinearLayout 的 onLayout 方法, 如下:


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

LinearLayout 中 onLayout 的实现逻辑和 onMeasure 类似, 这里选择 layoutVertical 看看它的实现:

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }

在源码内部可以看到, 此方法会遍历所有子元素并调用 setChildFrame 方法来为子元素指定对应的位置, 其中 childTop 会逐渐增大, 这就意味着后面的子元素会被放置在靠下的位置, 这也符合 LinearLayout 的特性, 至于 setChildFrame 它只是调用了子元素的 layout 方法。 这样父元素完成自己的定位后通过 onLayout 方法又会调用子元素的 layout 方法, 子元素又会通过自己的 layout 方法确定自己的位置, 如此反复, setChildFrame 实现如下:

private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }

我们通过源码注意到, setChildFrame 中的 width 和 height 实际上就是子元素的测量宽高, 从下面的代码可以看出这一点:

                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                setChildFrame(child, childLeft + getLocationOffset(child), childTop,
                        childWidth, childHeight);

而在 layout 中会通过 setFrame 去设置子元素的四个定点的位置, 在 setFrame 中:

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;

那么 View 的测量宽高和最终宽高到底由什么区别, 这个问题可以具体位: View 的 getMeasure 和 getWidth 这俩个方法的区别,看下代码:

    public final int getWidth() {
        return mRight - mLeft;
    }

从源码中和mLeft, mTop 等等的复制过程来看, getWidth 方法的返回值刚好就是 View 的测量宽度, 在 View 的默认实现中饭, View 的测量宽高和最终宽高是相等的, 只不过测量宽高形成在 measure 过程, 而最终宽高在 layout 过程, 也就是说赋值时机不同, 测量宽高的时机会相对早一些,正常的日常开发中我们可以认为他们就是相等的,但是也会存在不一致的情况, 比如如果重写 View 的 layout 方法:

    @Override
    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r+100, b+100);
    }

上面代码会导致在任何情况下 View 的最终宽高总是比测量宽高大 100px, 这样肯定没有实际意义了, 不过这样也说明测的宽高不等于最终宽高, 实际应该没有这样蛋疼得操作吧。

draw 过程

draw 过程就比较简单了, 它的作用是将 View 绘制到屏幕上面, View 的绘制过程遵循如下几步,
绘制背景 background.draw(canvas)
绘制自己(onDraw)
绘制孩子 diapatchDraw
绘制装饰 onDrawScrollBars

看 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)
         */

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

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
}

View 的绘制过程的传递是通过 dispatchDraw 来实现的, dispatchDraw 会遍历所有调用子元素的 draw 方法, 如此 draw 事件就一层层的传递的下去, View 有一个特殊的方法 setWillNotDraw, 先看一下它的源码:

  /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

从 setWillNotDraw 方法注释中可以看出, 如果一个 View 不需要绘制任何内容, 那么设置这个标记位 true 以后, 系统会进行相应的优化, 默认情况下, View 没有启用这个优化标记位, 但是 ViewGroup 会默认启用, 这个标志位的实际意义是: 当我们自定义控件继承 ViewGroup 并且本身不具备绘制功能时, 就可以开启这个标志位从而便于系统进行后续的优化, 当然, 当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时, 我们需要显示的关闭 willNotDraw 标记位。

猜你喜欢

转载自blog.csdn.net/CoderYue/article/details/80041595