Android学习笔记---深入理解View#03

上一篇的结尾中,我们发现了View的绘制发生在ViewRootImplperformTraversals()中.而且在其中先后调用了performMeasure(),performLayout(),performDraw().
如此一来,我们又有了新的猎物了.就像美食一样,好吃的东西一定要仔细地品尝.在上主菜之前,我们先来点开胃菜.我们先来了解一下Android是怎样绘制View的.(官方的文档How Android Draws Views)

开胃菜(关于View绘制时需要知道的常识)

Activity接受焦点时,就会被要求绘制其布局.虽然Android Framework会处理这个过程,但Activity必须提供整个布局层级的根节点,因为需要知道从哪开始绘制.

Activity的整个布局被转换成了一棵树,绘制整个布局就相当于了遍历整颗树并把每个节点的View绘制出来.相应地,ViewGroup负责要求它的每一个child进行绘制,而View则负责绘制自己.由于树的遍历是有序的,所以父View绘制之前会先绘制其子View,而兄弟节点会按照在树中出现的顺序进行绘制.
绘制布局需要进行两个传递过程(pass process):分别是测量时的传递(measure pass)布局时的传递(layout pass).这里所说的传递指的是在view tree的各个节点之间的传递.

  1. measure passmeasure(int,int)中实现,而且它是一个从上到下的传递.在view tree中每个View节点都将它的尺寸规格向下传递给它的孩子,在整个传递过程结束时,每个节点都应该拥有了自己的测量值(尺寸大小).
  2. layout passlayout(int,int,int,int)中发生,它同样也是一个从上到下的传递.在传递过程中每个parent都需要根据在measure pass时得到的测量值在布局中放置它的所有children.

下面贴上一张普通的view tree的图.

Android学习笔记---深入理解View#03_第1张图片

measure()函数中,官方定下了一些规则,在函数执行完毕返回前必须要满足下面的条件:

  1. View(以及其后代节点)的getMeasuredWidth()(即mMeasuredWidth的值)getMeasuredHeight()(即mMeasuredHeight的值)的值必须已经设置.从函数名已经知道函数获取的是已经测量的宽高值,measure()函数结束就表明测量结束了,这一条规则理所当然.
  2. View测量后的宽高必须符合其父View所规定的大小.这一条规则可以保证当measure pass结束时,所有的parents能接受其所有children的测量值.这也很好理解,子View的大小总不能比其父布局还大吧,否则就没有意义了.

一个为parent的View可能会不止一次地对其children调用measure().因为如果parent使用未指定的尺寸测量它的每一个child得到各个child想要的大小,但如果所有children的(未加限制的)测量值的总和太大或太小,那就需要parent再次调用measure()重新测量,但这次的测量设置了相应的规则.(举个比喻,就像孩子们在分配糖果时,大家都对所分配的糖果不满意时,父母就会干涉并重新分配)

我要吃神户牛柳(深入measure过程)

吃过开胃菜后,再来品尝我们的主菜就会更加的美味.美味的食物通常都有独特的吃法,比如使用特定的餐具.我们的第一道菜(measure)就是神户牛柳,我们需要准备刀叉来用餐.那先准备一下我们的餐具吧.(与measure过程密切相关的两个类).

刀 (ViewGroup.LayoutParams)

先来说明一下ViewGroup.LayoutParams是干什么用的.View通过ViewGroup.LayoutParams来告诉它的parent它在布局中想被放在什么位置和想占多大.而基本的ViewGroup.LayoutParams只能表达View想占多宽和多高,可以通过下面的其中一种方式表达:

  • 一个确切的数值大小
  • MATCH_PARENT,表达View想要和它的parent一样大(去掉View的内边距)
  • WRAP_CONTENT,表达View只想要能将它的内容包裹的大小(加上View的内边距)

ViewGroup.LayoutParams只能表达View大小,但ViewGroup的子类的LayoutParams能表达View的位置.

叉 (MeasureSpec)

看过我的自定义View#02文章的同学可能会对MeasureSpec有所了解.MeasureSpecparent用来限制child的大小,在measure()的过程中,它作为参数,从view tree的根节点往下传递到它的子节点和其后代.它有下列3种模式:

  • UNSPECIFIED, 这种模式表明parent对它的child的大小没有限制,child可以告诉parent它自己所希望的尺寸.
  • EXACTLY, 这种模式表明parentchild设置了一个确切的值,child必须使用这个值,并且需要保证child的后代节点都要符合这个值的设置
  • AT_MOST, 这种模式表明parentchild设置了一个最大值,child可以是它想要的任何值,但child以及它的后代节点的尺寸大小都必须保证在这个最大值内.

既然MeasureSpec有相应的模式来限制View的尺寸,那用什么来表示限制尺寸的大小呢.MeasureSpec采用了一个32位的int值来代表模式和大小,高2位表示模式,低30位表示大小.

有了我们的餐具后,我们可以终于可以开动了.我们先从ViewRootImpl.performTraversals()中调用performMeasure()的地方开始,下面是该部分的代码:

.......
        if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
                    // 标注 1
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

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

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

                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    // 标注 3
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;
                    
                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    // 标注 4
                    if (measureAgain) {
                        if (DEBUG_LAYOUT) Log.v(TAG,
                                "And hey let's measure once more: width=" + width
                                + " height=" + height);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                    layoutRequested = true;
                }
            }
.......

上面的代码我做了4个标注,我们一个一个来看,先看标注1的代码.

// 标注 1
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

其中这里的mHeightmWidth分别是窗口(Activity)的宽高,lp为窗口(Window)的布局参数.childWidthMeasureSpecchildHeightMeasureSpec这两个变量其实从标注2的代码就很容易的看出它们是作为参数传进performMeasure(int,int)的.那我们就看看它们代表的是什么意思.

由于它们是从getRootMeasureSpec(int,int)获取的,从函数名可以看出这个函数是用来获取Root节点的MeasureSpec的(就是根节点在测量时给它的孩子节点所定下的尺寸大小的限制).但我们还是要看看这个函数的代码:

    /**
     * Figures out the measure spec for the root view in a window based on it's
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    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.
            // 如果布局参数要求MATCH_PARENT,那么就设置为窗口的大小,模式为EXACTLY,因为窗口(Activity)的大小固定
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            //如果布局参数为WRAP_CONTENT,就设置为AT_MOST模式,最大值为窗口大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            // 如果布局的参数为一个确切的值,那我们就让root view为该值,模式为EXACTLY
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

getRootMeasureSpec(int,int)的两个参数分别代表窗口的大小(windowSize)和窗口的布局参数的大小(rootDimension).官方的注释(我也做了相应的注释)已经写得很清楚了,这个函数通过window的布局参数来决定root viewMeasureSpec.
经过标注1的代码,我们获取到了root tree的根节点的MeasureSpec,这样就可以从树的根节点开始进行测量传递的过程了(在开胃菜中提到的measure pass).在对标注2这个最主要的代码部分进行分析前,我们先来分析后面的标注3标注4的代码.(好东西肯定要留到最后,反正我是这样想的)

                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    // 标注 3
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;
                     
                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    // 标注 4
                    if (measureAgain) {
                        if (DEBUG_LAYOUT) Log.v(TAG,
                                "And hey let's measure once more: width=" + width
                                + " height=" + height);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                    layoutRequested = true;

标注3首先获取到了测量后root view的宽高值,然后分别判断lp.horizontalWeightlp.verticalWeight的值是否大于0(即我们的布局参数在横向或纵向的weight被设置时,可以理解为在xml布局文件里设置了layout_weight属性).若设置了其中一个方向上的weight,那么标注4的代码都会执行,我们需要再调用一次performMeasure()进行测量(measure pass),但这次采用的是新的参数,把布局参数的weight考虑进去.

可能有同学会问,为什么第一次调用performMeasure()前不把weight考虑进去,测量完一次后才考虑这不让前面的工作都白费了吗?我也有相同的疑问,目前我还没找到一个准确的答案,但google这样写一定有它的道理,我在这里分享一下我的想法吧(不一定是正确的,如果错了希望大家能指正).

我是这样想的:第一次调用performMeasure()的时候并不知道weight是否设置了,因为我们通常设置layout_weight属性都是在子View中设置的,在子View测量完毕前,父布局并不知道它的所有子View的weight属性,而父布局的测量发生在子View测量结束后,所以我们可能需要进行两次的测量传递过程(measure pass).

好的,清楚了我们标注3,标注4的代码后,我们可以迎接我们的主角performMeasure()了,下面就是performMeasure()的代码.

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

原来代码中调用的是mView.measure(int,int),即调用了root viewmeasure(),既然如此,我们来看measure()的代码:

    /**
     * 

* This is called to find out how big a view should be. The parent * supplies constraint information in the width and height parameters. *

* *

* The actual measurement work of a view is performed in * {@link #onMeasure(int, int)}, called by this method. Therefore, only * {@link #onMeasure(int, int)} can and must be overridden by subclasses. *

* * * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * * @see #onMeasure(int, int) */ 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); } // Suppress sign extension for the low bytes long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -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; } // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension }

注释中也说了,真正的测量工作是发生在onMeasure(int,int)函数中的,并且说明了View的子类可以并必须重写onMeasure()来测量我们的View.这里的必须并不代表我们在自定义VIew的时候一定要重写onMeasure(),因为onMeasure()已经在View中实现了,在不重写的情况下会调用默认的实现.

既然注释中给我们指明了方向,那我们就来看看这个onMeasure().

神户牛的精华(onMeasure)

onMeasure()可以说是整个measure pass的核心部分,就像是神户牛的精华一样.那现在我们就来感受一下这神户牛的精华所带来的美味.
由于在view tree上不可能每个节点都是View节点(这里是叶子节点的意思),就像在"开胃菜"中给大家展现的图一样,在view tree中也会有ViewGroup节点,像FrameLayout,LinearLayout,RelativeLayout...,这些ViewGroup节点都相应的实现了自己的onMeasure().那么这就说明了ViewGroup.onMeasure()View.onMeasure()并不一样.既然测量传递的过程(measure pass)是从根节点开始的,那我们也从ViewGrouponMeasure()开始.这里我们使用的是FrameLayout的代码(其他的ViewGroup大家可以自己试着去分析).

提示:下面的代码可以先跳过,因为在后面会再提到.

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 第1部分
        int count = getChildCount();

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        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);
                    }
                }
            }
        }
        
        // 第2部分
        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        
        //第3部分
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

Part 1

虽然代码不算很长,但为了方便,我们还是将它分为3个部分来分析吧(上面代码注释中所划分的).先来第1部分的代码:

        // 第1部分
        int count = getChildCount();
        // 编号1. 用于判断是否需要对布局参数为MATCH_PARENT的子View进行重新测量
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        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) {
                // 编号2. 对每个子View进行测量
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                // 编号3. 得到所有子View中最大的宽度(加上子View的外边距)
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                // 编号4. 得到所有子View中最大的高度(加上子View的外边距)        
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                // 编号5. 得到子View的MeasureState
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                // 编号6. 将布局参数为MATCH_PARENT的子View加入到`mMatchParentChildren`集合中
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

为了后面的分析方便,我在上面的代码注释中都给相应的语句进行了编号,我们先来分析简单的.
编号1measureMatchParentChildren是用于判断FrameLayout是否需要对布局参数为MATCH_PARENT的子View进行第二次测量.这里我们等分析过编号2的代码后再对这个变量进行解析(这里可以先留个疑问).
编号3,编号4的代码就是为了得到FrameLayout所有孩子中测量后的最大宽高(加上边距),因为FrameLayout需要按照它的孩子中尺寸最大的宽高进行测量.
编号5的代码就是为了得到子View的MeasuredState,这个对我们来说是个新的概念.既然这样,我们就来看看child.getMeasuredState()这个在View类下的函数是个怎样的函数.

    /**
     * Return only the state bits of {@link #getMeasuredWidthAndState()}
     * and {@link #getMeasuredHeightAndState()}, combined into one integer.
     * The width component is in the regular bits {@link #MEASURED_STATE_MASK}
     * and the height component is at the shifted bits
     * {@link #MEASURED_HEIGHT_STATE_SHIFT}>>{@link #MEASURED_STATE_MASK}.
     */
     // 将宽高的状态位结合成在一个32位的int值并返回
     // 宽度的状态位在常规的位置
     // 高度的状态位在偏移后的位置
    public final int getMeasuredState() {
        return (mMeasuredWidth&MEASURED_STATE_MASK)
                | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
                        & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
    }
    // 用于使高度的状态位偏移的位数
    public static final int MEASURED_HEIGHT_STATE_SHIFT = 16;

我把用到的变量也贴在了上面的代码中.首先我们来了解一下什么是"宽高的状态位".我们知道mMeasuredHeightmMeasuredWidth都是32位的int值,但这个值并不是一个表示宽高的实际大小的值,而是一个由宽高的状态和实际大小所组合的值.这里的高8位就表示状态(STATE),而低24位表示的是实际的尺寸大小(SIZE),这个信息可以从它们相应的掩码看出.

    // 用于得出宽高的状态位的掩码
    public static final int MEASURED_STATE_MASK = 0xff000000;
    // 用于得出宽高的尺寸位的掩码
    public static final int MEASURED_SIZE_MASK = 0x00ffffff;

这就解析了为什么我们的getMeasuredHeight()函数返回的是mMeasuredHeight & MEASURED_SIZE_MASK.而getMeasuredHeightAndState()返回的是mMeasuredHeight.相应的关于宽度的函数也是一个道理.

    public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }
    public final int getMeasuredHeightAndState() {
        return mMeasuredHeight;
    }

现在我们再来看getMeasuredState()是怎样将宽高的状态位组合在一个int值中的.首先mMeasuredWidth & MEASURED_STATE_MASK得到了宽度的状态位,保存在高8位.然后通过(mMeasuredHeight >> MEASURED_HEIGHT_STATE_SHIFT)(MEASURED_STATE_MASK >> MEASURED_HEIGHT_STATE_SHIFT)将高度和状态掩码都右移了16位,现在高度的状态位在第8到第15位上,而MEASURED_STATE_MASK变成了0x0000ff00,接着将两个移位后的数进行按位相与(&)得到了高度的状态位,保存在8-15位上.最后将处理后宽度和高度按位相或(|)得到一个保存了宽度和高度的状态位的int值.如下图.

Android学习笔记---深入理解View#03_第2张图片

    /**
     * Merge two states as returned by {@link #getMeasuredState()}.
     * @param curState The current state as returned from a view or the result
     * of combining multiple views.
     * @param newState The new view state to combine.
     * @return Returns a new integer reflecting the combination of the two
     * states.
     */
    public static int combineMeasuredStates(int curState, int newState) {
        return curState | newState;
    }

回到编号5的代码,就是为了将所有子View的state都结合在一起,这个有什么作用现在也不好讲.先继续看吧.

编号2的代码是将FrameLayout中所有visibility属性不为GONE的子View都进行测量(即在布局中占据位置的View),使用的是measureChildWithMargins().下面我们来看这个函数的代码.

    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure (需要测量的子View)
     * @param parentWidthMeasureSpec The width requirements for this view 
     *      (parent对子View宽度的要求(MeasureSpec))
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     *        (被parent或其他兄弟节点在布局的水平方向上使用了的尺寸大小)
     * @param parentHeightMeasureSpec The height requirements for this view
     *      (parent对子View高度的要求(MeasureSpec))
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     *        (被parent或其他兄弟节点在布局的垂直方向上使用了的尺寸大小)
     */
    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);
    }

像代码中的注释所写的一样,这个函数就是用来告诉child需要进行测量.测量过程中需要遵循parentMeasureSpec,还需要考虑将paddingmargin的值.通过了getChildMeasureSpec()得到了子View的MeasureSpec后就可以调用子View的measure()进行测量了.所以我们要来看看getChildMeasureSpec(int,int,int)这个函数,先来看看函数原型的文档,因为我们要先搞清楚各个参数所代表的意义.

    /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     *      (对View的尺寸限制MeasureSpec)
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     *        (可以理解为父布局的padding值+View的margin值,即父布局中未使用的尺寸大小)
     * @param childDimension How big the child wants to be in the current
     *        dimension
     *        (View希望在布局中的大小,即子View布局参数的宽高)
     * @return a MeasureSpec integer for the child
     * 
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension)

这个函数是为了获得当前View的MeasureSpec以便于进行测量和传递给子View的.函数中主要是根据父布局的MeasureSpec来创建View自己的MeasureView.下面是相应的代码.

代码有点长,希望能完整地看一遍.但如果不想看也不要紧,就粗略地扫一眼吧.因为后面有图片进行总结,正所谓一图胜千言啊!

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 分别获取父布局`MeasureSpec`中的模式和尺寸
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        // 获取父布局实际提供给View的尺寸大小(去除边距)
        // 即父布局最大的可用的大小
        // 与0相比取最大值,以免尺寸大小为负值
        int size = Math.max(0, specSize - padding);
        
        // 当前View最终的尺寸大小和模式
        int resultSize = 0;
        int resultMode = 0;
        
        // 根据父布局的模式来决定View的模式和尺寸
        switch (specMode) {
        // Parent has imposed an exact size on us
        // 表示父布局的大小为确切的值
        case MeasureSpec.EXACTLY:
            // 由于`MATCH_PARENT`=-1,`WRAP_CONTENT`=-2,
            // 所以childDimension >= 0 表示View的宽高布局参数为具体的值
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 表示View的布局参数为`MATCH_PARENT`,即View希望大小是父布局的最大的可以大小
                // 模式与父布局一样为EXACTLY
                // 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.
                // 表示View的布局参数为`WRAP_CONTENT`,那么说明View的大小不明确,需要由它的内容决定
                // 所以测量值的尺寸为父布局的最大的可以大小,模式为AT_MOST
                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
                // 表明View的布局大小为确切的值
                // 所以View的测量大小为布局参数的值,模式为EXACTLY
                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.
                // 表明View的布局参数为MATCH_PARENT
                // 所以View的测量大小为父布局的可用大小,模式与父布局一样为AT_MOST
                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.
                // View的布局参数为WRAP_CONTENT,即View的测量尺寸大小不确定,由其内容决定
                // 所以View的测量大小为父布局最大的可以大小,模式为AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        // 表示父布局对View的大小没有限制,通常用在ListView等可滚动的控件中
        // 这种情况下父布局会满足View的所有要求
        
        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;
        }
        // 根据最终的View的模式和尺寸生成View的MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

虽然上面的代码不短,但代码逻辑并不复杂,而且我已经在代码上做了比较明白的注释了,如果还是不理解,那就请看图.

Android学习笔记---深入理解View#03_第3张图片

函数的功能就是为了给View生成一个 MeasureSpec类型的int,而这个值是由模式和大小合成的,而且它们的值由父布局 MeasureSpec的模式和View的布局大小共同决定.上图就是一个决定View的 ModeSize的过程.
这里有一点需要注意的,就是当父布局的 MeasureSpec的模式为 UNSPECIFIED时,若View的布局大小不为一个具体的值那么 resultSize的大小就为0.这里决定 resultSize的值是下面的一条语句.

resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

而这里的View.sUseZeroUnspecifiedMeasureSpec可以在View中找到相应的信息.

    /**
     * Always return a size of 0 for MeasureSpec values with a mode of UNSPECIFIED
     */
    static boolean sUseZeroUnspecifiedMeasureSpec = false;
    public View(Context context) {
    .....
            // In M and newer, our widgets can pass a "hint" value in the size
            // for UNSPECIFIED MeasureSpecs. This lets child views of scrolling containers
            // know what the expected parent size is going to be, so e.g. list items can size
            // themselves at 1/3 the size of their container. It breaks older apps though,
            // specifically apps that use some popular open source libraries.
            sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M;
    ......
    }

就是说若当前的Android版本小于M的话那sUseZeroUnspecifiedMeasureSpec的值就为true.所以在旧版本的Android中,resultSize的值都为0.
现在我们回到编号1的地方就可能对那句代码有所理解了.

        // 编号1. 用于判断是否需要对布局参数为MATCH_PARENT的子View进行重新测量
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;

我们现在对应着上面的View的MeasureSpec生成图来分析代码.将流程反过来看,即从有编号的框开始看,我们可以看到在(1),(2),(4),(7)4种情况下resultMode的值为EXACTLY.而它们的条件有3种是childDimension >= 0即FrameLayout的布局参数layout_widthlayout_height为实际的具体值.另一种情况就是FramLayout的布局参数为MATCH_PARENT且它的父布局为EXACTLY. 综合的来讲可以这么理解吧就是当FrameLayout的布局宽高不同时设置为具体的值,或不同时为MATCH_PARENT那么measureMatchParentChildren这个值就为true.
再简单的讲就是如果FrameLayout的宽高只要有一个设置为WRAP_CONTENT,那么该值就为true.那么就需要对布局参数为MATCH_PARENT的子View进行重新测量.因为WRAP_CONTENT的情况下父布局的测量值受子View的影响.

能看到这里的同学真是不简单啊,没想到第1部分讲了这么久,(有的同学就可能会说:这分"神户牛柳"的量也太多了吧,吃得有点撑啊!)这里篇幅确实有点长,但如果仔细看下来的话还是能学到不少的东西.大家可以先休息一下,待会再来继续阅读.我也在下面做了分割线帮大家标记位置.


Part 2

我们来继续我们onMeasure()第2部分代码的分析吧.相信我,当你看完第1部分的分析后,后面就会很有感觉.

        // 第2部分
        // 前面我们的maxWidth和maxHeight只是计算了子View的外边距
        // 但没有计算FrameLayout的内边距,所以在这里加上
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        // 保证我们的`maxWidth`和`maxHeight`不会太小(至少要等于最小的建议值)
        // 这里的最少建议值与背景有关
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        // 上面保证了背景的宽高值,下面保证前景对的宽高值
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

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

第2部分的代码是先是确定了maxWidthmaxHeight的值,给大家看一眼getSuggestedMinimumHeight()的代码吧,因为后面这个函数还会用到,我相信大家很容易就能理解.

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

我们主要还是看标注的那句代码,setMeasuredDimension()就是将测量好的宽高值存储下来供后面的布局过程使用.来看看代码吧.

    /**
     * 

This method must be called by {@link #onMeasure(int, int)} to store the * measured width and measured height. Failing to do so will trigger an * exception at measurement time.

* * @param measuredWidth The measured width of this view. May be a complex * bit mask as defined by {@link #MEASURED_SIZE_MASK} and * {@link #MEASURED_STATE_TOO_SMALL}. * @param measuredHeight The measured height of this view. May be a complex * bit mask as defined by {@link #MEASURED_SIZE_MASK} and * {@link #MEASURED_STATE_TOO_SMALL}. */ 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); } /** * Sets the measured dimension without extra processing for things like optical bounds. * Useful for reapplying consistent values that have already been cooked with adjustments * for optical bounds, etc. such as those from the measurement cache. * * @param measuredWidth The measured width of this view. May be a complex * bit mask as defined by {@link #MEASURED_SIZE_MASK} and * {@link #MEASURED_STATE_TOO_SMALL}. * @param measuredHeight The measured height of this view. May be a complex * bit mask as defined by {@link #MEASURED_SIZE_MASK} and * {@link #MEASURED_STATE_TOO_SMALL}. */ private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }

上面就是相关的代码,也比较简单,setMeasuredDimension()中最后调用了setMeasuredDimensionRaw()来设置mMeasuredWidthmMeasuredHeight的值.其中关于Optical Bounds有兴趣的同学可以到Internet上搜索一下,或看看下图.这里我们不作讨论,可以跳过.

Android学习笔记---深入理解View#03_第4张图片

了解了 setMeasuredDimension()后,那我们再来看看调用处给它传进的两个参数 resolveSizeAndState(maxWidth,widthMeasureSpec,childState)resolveSizeAndState(maxWidth,widthMeasureSpec,childState).既然调用了 resolveSizeAndState(),那就看看它的代码吧.

    /**
     * Utility to reconcile a desired size and state, with constraints imposed
     * by a MeasureSpec. Will take the desired size, unless a different size
     * is imposed by the constraints. The returned value is a compound integer,
     * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
     * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
     * resulting size is smaller than the size the view wants to be.
     *
     * @param size How big the view wants to be.
     * @param measureSpec Constraints imposed by the parent.
     * @param childMeasuredState Size information bit mask for the view's
     *                           children.
     * @return Size information bit mask as defined by
     *         {@link #MEASURED_SIZE_MASK} and
     *         {@link #MEASURED_STATE_TOO_SMALL}.
     */
    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) {
                    // 防止View超出了限制的大小所做的处理
                    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);
    }

第1部分我们已经读过了不少的类似的代码了,在这里我就不啰嗦了.主要还是讲讲resolveSizeAndState(maxHeight,heightMeasureSpec,childState<这句代码吧,为什么这里需要进行左移?如果前面有认真看的话就很容易理解,因为我们的childState是存有宽高的状态的组合值,我们的高度的状态值存在第8-15位,所以这里需要将它左移16位(将状态位放置在常规的位置).

Part 3

来到第3部分了,这部分比较简单,我们先来看看代码.

        //第3部分
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    // 宽度为总宽度减去父布局的Padding和View的Margin
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    // 第1部分已经讨论过
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }

这里的代码就是当FrameLayout有多于1个的子View的布局宽高为MATCH_PARENT时(并且满足第1部分中的measureMatchParentChildrentrue),即当FrameLayout的宽高设置存在WRAP_CONTENT时,对子View进行重新的测量.

View的onMeasure()

到这里我们FrameLayoutonMeasure()已经分析完毕了,既然我们已经分析过了ViewGrouponMeasure(),那ViewonMeasure()我觉得也免不了,废话不说赶紧上菜.

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

代码还是很简单的,就一句.虽然嵌套很多,但只有一个函数我们没有见过,就是getDefaultSize(),那就看看它是何方神圣.

    /**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    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;
    }

这个函数是决定View的MeasureSpecSize的一种默认的方法.代码非常的简单,我就不再多说了.因为今天看这种代码看得真的不少,我相信很多同学都快要看吐了.
不过到这里我很开心,因为我们这次的任务完成了,measure的过程我们已经分析完了.

最后的甜点(总结)

牛柳吃完了,不知道大家能不能消化,所以最后给大家上个甜点吧.最后还是用图说话,来总结一下measure的整个流程.

Android学习笔记---深入理解View#03_第5张图片
measure

你可能感兴趣的:(Android学习笔记---深入理解View#03)