View 绘制体系知识梳理(3) - 绘制流程之 Measure 详解

一、测量过程的信使 - MeasureSpec

因为测量是一个从上到下的过程,而在这个过程当中,父容器有必要告诉子View它的一些绘制要求,那么这时候就需要依赖一个信使,来传递这个要求,它就是MeasureSpec.
MeasureSpec是一个32位的int类型,我们把它分为高2位和低30位。
其中高2位表示mode,它的取值为:

  • UNSPECIFIED(0) : The parent has not imposed any constraint on the child. It can be whatever size it wants.
  • EXACTLY(1) : 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.
  • AT_MOST(2) : The child can be as large as it wants up to the specified size.

30位表示具体的size

MeasureSpec是父容器传递给View的宽高要求,并不是说它传递的size是多大,子View最终就是多大,它是根据父容器的MeasureSpec和子ViewLayoutParams共同计算出来的。

为了更好的理解上面这段话,我们需要借助ViewGroup中的两个函数:

  • measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)
  • getChildMeasureSpec(int spec, int padding, int childDimension)
    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);
    }

    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) {
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

可以看到,在调用getChildMeasureSpec之前,需要考虑parentchild之间的间距,这包括parentpaddingchildmargin,因此,参与传递给childMeasureSpec的参数要考虑这么几方面:

  • 父容器的measureSpecpadding
  • Viewheightwidht以及margin

下面我们来分析getChildMeasureSpec的具体流程,它对宽高的处理逻辑都是相同的,根据父容器measureSpecmode,分成以下几种情况:

1.1 父容器的modeEXACTLY

这种情况下说明父容器的大小已经确定了,就是固定的值。

  • View指定了大小
    那么子Viewmode就是EXACTLYsize就是布局里面的值,这里就有疑问了,View所指定的宽高大于父容器的宽高怎么办呢?,我们先留着这个疑问。
  • ViewMATCH_PARENT
    View希望和父容器一样大,因为父容器的大小是确定的,所以子View的大小也是确定的,size就是父容器measureSpecsize - 父容器的padding - 子View``margin
  • ViewWRAP_CONTENT
    子容器只要求能够包裹自己的内容,但是这时候它又不知道它所包裹的内容到底是多大,那么这时候它就指定自己的大小就不能超过父容器的大小,所以modeAT_MOSTsize和上面类似。

1.2 父容器的modeAT_MOST

在这种情况下,父容器说明了自己最多不能超过多大,数值在measureSpecsize当中:

  • View指定大小
    同上分析。
  • ViewMATCH_PARENT
    View希望和父容器一样大,而此时父容器只知道自己不能超过多大,因此子View也就只能知道自己不能超过多大,所以它的modeAT_MOSTsize就是父容器measureSpecsize - 父容器的padding - 子View``margin
  • ViewWRAP_CONTENT
    子容器只要求能够包裹自己的内容,但是这时候它又不知道它所包裹的内容到底是多大,这时候虽然父容器没有指定大小,但是它指定了最多不能超过多少,这时候子View也不能超过这个值,所以modeAT_MOSTsize的计算和上面类似。

1.3 父容器的modeUNSPECIFIED

  • View指定大小
    同上分析。
  • ViewMATCH_PARENT
    View希望和父容器一样大,但是这时候父容器并没有约束,所以子View也是没有约束的,所以它的mode也为UNSPECIFIEDsize的计算和之前一致。
  • ViewWRAP_CONTENT
    View不知道它包裹的内容多大,并且父容器是没有约束的,那么也只能为UNSPECIFIED了,size的计算和之前一致。

二、测量过程的起点 - performTraversals()

介绍完了基础的知识,我们来从起点来整个看一下从View树的根节点到叶节点的整个测量的过程。
我们先直接说明结论,整个测量的起点是在ViewRootImplperformTraversals()当中

private void performTraversals() {
        ......
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        //...
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

上面的mView是通过setView(View view, WindowManager.LayoutParams attrs, View panelParentView)传进来的,那么这个view是什么时候传递进来的呢?
现在回忆一下,在ActivityThreadhandleResumeActivity中,我们调用了ViewManager.add(mDecorView, xxx),而这个方法最终会调用到WindowManagerGlobal的下面这个方法:

    public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
            root = new ViewRootImpl(view.getContext(), display);
        }
        //....
        root.setView(view, wparams, panelParentView);
    }

也就是说,上面的mView也就是我们在setContentView当中渲染出来的mDecorView,也就是说它是整个View树的根节点,因为mDecorView是一个FrameLayout,所以它调用的是FrameLayoutmeasure方法。
那么这整个从根节点遍历完整个View树的过程是怎么实现的呢?
它其实就是依赖于measureonMeasure

  • 对于Viewmeasure是在它里面定义的,而且它是一个final方法,因此它的所有子类都没有办法重写该方法,在该方法当中,会调用onMeasure来设置最终测量的结果,对于View来说,它只是简单的取出父容器传进来的要求来设置,并没有复杂的逻辑。
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
    }
  • 对于ViewGroup,由于它是View的子类,因此它不可能重写measure方法,并且它也没有重写onMeasure方法。
  • 对于继承于View的控件,例如TextView,它会重写onMeasure,与View#onMeasure不同的是,它会考虑更多的情况来决定最终的测量结果。
  • 对于继承于ViewGroup的控件,例如FrameLayout,它同样会重写onMeasure方法,与继承于View的控件不同的是,由于ViewGroup可能会有子View,因此它在设置自己最终的测量结果之前,还有一个重要的任务:调用子Viewmeasure方法,来对子View进行测量,并根据子View的结果来决定自己的大小

因此,整个从上到下的测量,其实就是一个View树节点的遍历过程,每个节点的onMeasure返回时,就标志它的测量结束了,而这整个的过程是以Viewmeasure方法为纽带的:

  • 整个过程的起点是mDecorView这个根节点的measure方法,也就是performTraversals中的那句话。
  • 如果节点有子节点,也就是说它是继承于ViewGroup的控件,那么在它的onMeasure方法中,它并不会直接调用子节点的onMeasure方法,而是通过调用子节点measure方法,由于子节点不可能重写View#measure方法,因此它最终是通过View#measure来调用子节点重写的onMeasure来进行测量,子节点再在其中进行响应的逻辑处理。
  • 如果节点没有子节点,那么当它的onMeausre方法被调用时,它需要设置好自己的测量结果就行了。

对于measureonMeasure的区别,我们可以用一句简单的话来总结一下:measure负责进行测量的传递,onMeasure负责测量的具体实现

三、测量过程的终点 - onMeasure当中的setMeasuredDimension

上面我们讲到设置的测量结果,其实测量过程的最终目的是:通过调用setMeasuredDimension方法来给mMeasureHeightmMeasureWidth赋值
只要上面这个过程完成了,那么该ViewGroup/View/及其实现类的测量也就结束了,而setMeasuredDimension必须在onMeasure当中调用,否则会抛出异常,所以我们观察所有继承于ViewGroup/View的控件,都会发现它们最后都是调用上面说的那个方法。
前面我们已经分析过,measure只是传递的纽带,因此它的逻辑是固定的,我们直接看各个类的onMeasure方法就好。

3.1 ViewonMeasure

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

    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }

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

这里,我们会根据前面所说的,父容器传递进来measureSpec中的mode来给这两个变量赋值:

  • 如果modeUNSPECIFIED,那么说明父容器并不指望多个,因此子View根据自己的背景或者minHeight/minWidth属性来给自己赋值。
  • 如果是AT_MOST或者EXACTLY,那么就把它设置为父容器指定的size

3.2 ViewGrouponMeasure

由于ViewGroup的目的是为了容纳各子View,但是它并不确定子View应当如何排列,也就不知道该如何测量自己,因此它的onMeasure是没有任何意义的,所以并没有重写,而是应当由继承于它的控件来重写该方法。

3.3 继承于ViewGroup控件的onMeasure

为了方面,我们以DecorView为例,经过前面的分析,我们知道当我们在performTraversals中调用它的measure方法时,最终会回调到它对应的控件类型,也就是FrameLayoutonMeasure方法:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        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);
                    }
                }
            }
        }

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

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

我们可以看到,整个的onMeasure其实分为三步:

  • 遍历所有子View,调用measureChildWithMargins进行第一次子View的测量,在第一节中,我们也分析了这个方法,它最终也是调用子Viewmeasure方法。
  • 根据第一步的结果,调用setMeasuredDimension来设置自己的测量结果。
  • 遍历所有子View,根据第二步的结果,调用child.measure进行第二次的测量。

这也验证了第二节中的结论:父容器和子View的关联是通过measure进行关联的
同时我们也可以有一个新的结论,对于View树的某个节点,它的测量结果有可能并不是一次决定的,这是由于父容器可能需要依赖于子View的测量结果,而父容器的结果又可能会影响子View,但是,我们需要保证这个过程不是无限调用的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容