美文网首页Android开发Android知识Android进阶之路
Android学习笔记---深入理解View#03

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

作者: Kenny_Ho | 来源:发表于2016-09-22 20:43 被阅读498次

    上一篇的结尾中,我们发现了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的图.

    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()的代码:

        /**
         * <p>
         * This is called to find out how big a view should be. The parent
         * supplies constraint information in the width and height parameters.
         * </p>
         *
         * <p>
         * 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.
         * </p>
         *
         *
         * @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值.如下图.

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

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


    函数的功能就是为了给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()就是将测量好的宽高值存储下来供后面的布局过程使用.来看看代码吧.

        /**
         * <p>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.</p>
         *
         * @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上搜索一下,或看看下图.这里我们不作讨论,可以跳过.


    了解了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<<MEASURED_HEIGHT_STATE_SHIFT)这句代码吧,为什么这里需要进行左移?如果前面有认真看的话就很容易理解,因为我们的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的整个流程.

    measure

    相关文章

      网友评论

        本文标题:Android学习笔记---深入理解View#03

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