重新理解MeasureSpec

1 概述

网上有许多非常好的文章都在介绍MeasureSpec的测量规则,但是没有介绍MeasureSpec的作用和应用场景。

tekaEF.png

MeasureSpec是一个int,他将SpecMode和SpecSize封装到了一起。

那么实际上MeasureSpec他是一个对:值和模式的一个封装。

在这里,size和mode是成对出现的,他们一起作用。

MeasureSpec可以翻译成:测量说明书。

他由手机屏幕的Window开始,将测量说明书生成并往下传递给DecorView,DecorView再生成自己的测量说明书,往下传递,不断递归,每个ViewGroup都根据父View的测量说明书和自己的尺寸,生成自己的测量说明书并递归下去。

什么是测量说明书?由编写测量说明书的一方(父View或者window)编写测量说明书,告诉客户(子View)要按照该测量说明书中的标准和规范来进行测量操作,从而实现父View对子View尺寸限制。

试问,一个子View如何知道他的父View给他预留了多少尺寸?

答:父View通过调用子View的measure()方法,将父View留给子View的尺寸传递给子View。

2 MeasureSpec使用场景

MeasureSpec的使用场景分为两个:

  1. child View接收到parent View为自己生成的MeasureSpec对象,在onMeasure(int widthMeasureSpec, int heightMeasureSpec)中提取出该对象中的数据并调用setMeasureDimension为自己设置measureWidth和measureHeight.

  2. parent View,即ViewGroup,这里先说下ViewGroup的onMeasure()方法的重写套路:

    在收到自己的onMeasure()回调的时候:

    ①要先对自己所有的子View进行测量,一般是遍历所有子View并调用ViewGroup的measureChildWithMargins(),然后再调用child.getMeasureWidth方法取出测量值,然后要么累加所有子View的测量者(如LinearLayout),要么从中选出最大的那个(如FrameLayout)。

    ②根据业务逻辑和他的父View设置给自己的MeasureSpec,来对他自己调用setMeasureDimension。

    这里的第②点就和1.是一个东西,所以我们说的是①。

即②中,在测量所有的子View的时候,父View将为每个子View生成他专属的MeasureSpec对象。

那么我们分别来看看这两种使用场景中是如何使用MeasureSpec的。

按照MeasureSpec先创建后使用的顺序,我们先看他的创建,后看他在子View中的使用

3 MeasureSpec的创建

3.1 Window为DecorView创建MeasureSpec

从最顶部开始:

ViewRootImpl.java

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                                 final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        }
    //...
    if (!goodMeasure) {
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
            windowSizeMayChange = true;
        }
    }
    //...
    return windowSizeMayChange;

这里的performMeasure方法里面,调用了DecorView的measure,至此MeasureSpec对象开始从ViewTreee顶部开始向下传递。

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

这里看下传递给DecorView的MeasureSpec是如何生成的:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

这里的windowSize是从WindowMananger获取到的Window的视图的尺寸,如手机屏幕大小。

而rootDimension参数是分别是DecorView的宽和高。而这里就是MATCH_PARENT,具体的定义要看创建DecorView的源码的地方,对应的是PhoneWindow类的installDecor()方法里。

通过这个getRootMeasureSpec()方法我们可以看到,创建的size和mode的对应关系为:

size mode
MATCH_PARENT MeasureSpec.EXACTLY
WRAP_CONTENT MeasureSpec.AT_MOST
具体值 MeasureSpec.EXACTLY

这是window为DecorView创建MeasureSpec时,为后者创建的MeasureSpec的对应的规则,举一反三一下,ViewGroup为子View创建MeasureSpec的时候,也是用的这种规则来生成MeasureSpec对象。

3.2 ViewGroup为子View创建MeasureSpec

测量子View这件事都是发生在ViewGroup的onMeasure中,而ViewGroup是没有重写onMeasure的,这个规则他留给了他的子类去重写,我们找到一个最简单的子类:FrameLayout,并截取部分他的onMeasure中的代码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    //...
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    //遍历子View
    for (int i = 0; i < count; i++) {
    final View child = getChildAt(i);
    if (mMeasureAllChildren || child.getVisibility() != GONE) {
        //测量子View
        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        //提取子View的measuredWidth和measuredHeight
        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);
        //...
    }
    //...
}

那么看他用来测量子View调用的ViewGroup的方法:measureChildWithMargins

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    //获取子View的LayoutParams
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //创建子View的宽的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    //创建子View的高的MeasureSpec
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    //将这里创建的MeasureSpec传递给子View,并让子View进行测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

当创建子View的宽的MeasureSpec的时候,调用了getChildMeasureSpec方法

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    //用父View的尺寸,减去已经使用了的尺寸(包括父view的padding,子View的marging和父View已经使用了的尺寸)。
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
            // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

            // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

            // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            //...
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

注意这里的参数padding,加上了父View的padding和子View的margin和父View已经使用了的尺寸(如果是FrameLayout,就是0,如果是LinearLayout,则会累加,看ViewGroup的逻辑而定)。size变量则是父View的尺寸减去上述的padding,得出留给子View的剩余空间的大小。

那么这里给子View生成MeasureSpec的逻辑就是:

父View剩余大小+父View的MeasureSpec+子View的尺寸 --> 子View的MeasureSpec

图形化表示为:

tQgom6.png

这里有两个点要注意一下:

  1. 当父View的布局大小确定的时候,即EXACTLY的时候,那么生成的子View的MeasureSpec依然符合上面创建根布局的情况:

    size mode
    MATCH_PARENT MeasureSpec.EXACTLY
    WRAP_CONTENT MeasureSpec.AT_MOST
    具体值 MeasureSpec.EXACTLY

    而当父View自己的布局大小都不确定的时候,即AT_MOST时(即父View也是用的WRAP_CONTENT,所以他才得到了AT_MOST的mode),子View的MATCH_PARENT其实就是要求和父View一样的大,那父View不确定大小,子View自然也不确定大小了,即AT_MOST。

  2. 当子View的尺寸是WRAP_CONTENT的时候,父View给子View生成的size是父View剩下的size。

为什么?因为父View在此时也不知道你子View有多大,那就把父View剩余的大小给子View,并告诉子View模式是AT_MOST,你子View最大不要超过我给你的这个大小,剩下的你尽管发挥。

4 MeasureSpec的使用

4.1 View的onMeasure

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

这里是用的getDefaultSize()方法来获取最终的子View的宽高,并用setMeasuredDimension()设置到自己的属性(稍后父View就可以获取到子View给自己测量的大小了)。

getDefaultSize的两个参数一个是获取建议的最小宽度,一个是父View给的测量说明书。

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

看下getDefaultSize()方法

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

可以看到,如果父View给的测量模式是UNSPECIFIED,那就用getSuggestedMinimumWidth返回的大小。如果父View给的测量模式是AT_MOST或者EXACTLY,那就直接用父View给我们生成的大小。

这里要牵扯到一个自定义View的技巧:自定义View要重写onMeasure()方法来处理AT_MOST的测量模式。

从前面创建MeasureSpec知道,当子View用了wrap_content的时候,父View就会给你生成AT_MOST的测量模式,但因为AT_MOST测量模式下也是用的父View返回的尺寸,这时父View返回的尺寸是父View剩下的尺寸。他的意思是:这些尺寸给你,但是这是你能使用的最大的尺寸,不要超过这个尺寸就行。

一般来说我们会在自定义View中重写并判断AT_MOST时,返回一个默认的值。

比如这样:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    //默认尺寸,写死或者根据业务逻辑计算得到
    val defaultWidth = 50
    val defaultHeight = 100
    //取出父View给的测量模式
    val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
    val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
    //取出父View给的测量尺寸
    val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
    val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
    //最终测量尺寸
    val finalWidth = if (widthSpecMode == MeasureSpec.AT_MOST) defaultWidth else widthSpecSize
    val finalHeight = if (heightSpecMode == MeasureSpec.AT_MOST) defaultHeight else heightSpecSize
    //set
    setMeasuredDimension(finalWidth,finalHeight)
}

这种是手动计算的,还有一种是借助View自带的resolve()方法来去计算的。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //默认尺寸
        val defaultWidth = 50
        val defaultHeight = 100
        //最终测量尺寸(注意,也计算了padding)
        val finalWidth = resolveSize(defaultWidth + paddingLeft + paddingRight, widthMeasureSpec)
        val finalHeight = resolveSize(defaultHeight + paddingTop + paddingBottom, heightMeasureSpec)
        //设置
        setMeasuredDimension(finalWidth, finalHeight)
}

resolveSize()方法内部有更精细的判断:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

View对MeasureSpec是使用方,非创建方。

因此我们可以直接按照MeasureSpec的字面意思来直接理解:

SpecSize就是你父View给我指定的测量的大小。那么我拿到了这个大小我要怎么用呢?看你给我生成的测量模式SpecMode,如果是EXACTLY,那父View的意思就是直接让我用这个size作为最终我的测量大小就行了。如果是AT_MOST,父View传递给我的消息是,这个size不是让你作为最终的size的,我只是把我剩下的尺寸给你了,你不要超过这个尺寸即可。

4.2 FrameLayout的onMeasure

实际上我们想看的是ViewGroup在onMeasure中是如何使用MeasureSpec的,但是ViewGroup没有重写,直接沿用改的View的,因此找了个简单的FrameLayout的来看看。

@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;
    //遍历子View
    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();
            //取到所有子View中尺寸最大的那个尺寸
            maxWidth = Math.max(maxWidth,
                                child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                                 child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                    lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }
    
    // 计算FrameLayout自身的padding
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // 再次检查 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());
    }
    //调用resolveSizeAndState()方法去获取最终测量值
    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);
        }
    }
}

ViewGroup在对自己测量的时候,也是调用的resolveSizeAndState这个方法来计算出最终的测量值的。

5 总结

对于MeasureSpec创建者ViewGroup, 根据子View的LayoutParams的width和heigth和自己的MeasureSpec来为子View创建出MeasureSpec。传递给子View,并让子View根据该MeasureSpec设置对应的测量宽高,然后父View再拿到子View测量宽高,将上述动作遍历所有子View后,再对自己进行测量,设置自己的宽高。

对于MeasureSpec使用者View,根据父View传递进来个MeasureSpec,结合自身逻辑,计算出自己的宽高。

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