重新理解MeasureSpec

1 概述

網(wǎng)上有許多非常好的文章都在介紹MeasureSpec的測量規(guī)則,但是沒有介紹MeasureSpec的作用和應(yīng)用場景。

tekaEF.png

MeasureSpec是一個int,他將SpecMode和SpecSize封裝到了一起。

那么實際上MeasureSpec他是一個對:值和模式的一個封裝。

在這里,size和mode是成對出現(xiàn)的,他們一起作用。

MeasureSpec可以翻譯成:測量說明書。

他由手機(jī)屏幕的Window開始,將測量說明書生成并往下傳遞給DecorView,DecorView再生成自己的測量說明書,往下傳遞,不斷遞歸,每個ViewGroup都根據(jù)父View的測量說明書和自己的尺寸,生成自己的測量說明書并遞歸下去。

什么是測量說明書?由編寫測量說明書的一方(父View或者window)編寫測量說明書,告訴客戶(子View)要按照該測量說明書中的標(biāo)準(zhǔn)和規(guī)范來進(jìn)行測量操作,從而實現(xiàn)父View對子View尺寸限制。

試問,一個子View如何知道他的父View給他預(yù)留了多少尺寸?

答:父View通過調(diào)用子View的measure()方法,將父View留給子View的尺寸傳遞給子View。

2 MeasureSpec使用場景

MeasureSpec的使用場景分為兩個:

  1. child View接收到parent View為自己生成的MeasureSpec對象,在onMeasure(int widthMeasureSpec, int heightMeasureSpec)中提取出該對象中的數(shù)據(jù)并調(diào)用setMeasureDimension為自己設(shè)置measureWidth和measureHeight.

  2. parent View,即ViewGroup,這里先說下ViewGroup的onMeasure()方法的重寫套路:

    在收到自己的onMeasure()回調(diào)的時候:

    ①要先對自己所有的子View進(jìn)行測量,一般是遍歷所有子View并調(diào)用ViewGroup的measureChildWithMargins(),然后再調(diào)用child.getMeasureWidth方法取出測量值,然后要么累加所有子View的測量者(如LinearLayout),要么從中選出最大的那個(如FrameLayout)。

    ②根據(jù)業(yè)務(wù)邏輯和他的父View設(shè)置給自己的MeasureSpec,來對他自己調(diào)用setMeasureDimension。

    這里的第②點(diǎn)就和1.是一個東西,所以我們說的是①。

即②中,在測量所有的子View的時候,父View將為每個子View生成他專屬的MeasureSpec對象。

那么我們分別來看看這兩種使用場景中是如何使用MeasureSpec的。

按照MeasureSpec先創(chuàng)建后使用的順序,我們先看他的創(chuàng)建,后看他在子View中的使用

3 MeasureSpec的創(chuàng)建

3.1 Window為DecorView創(chuàng)建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方法里面,調(diào)用了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的視圖的尺寸,如手機(jī)屏幕大小。

而rootDimension參數(shù)是分別是DecorView的寬和高。而這里就是MATCH_PARENT,具體的定義要看創(chuàng)建DecorView的源碼的地方,對應(yīng)的是PhoneWindow類的installDecor()方法里。

通過這個getRootMeasureSpec()方法我們可以看到,創(chuàng)建的size和mode的對應(yīng)關(guān)系為:

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

這是window為DecorView創(chuàng)建MeasureSpec時,為后者創(chuàng)建的MeasureSpec的對應(yīng)的規(guī)則,舉一反三一下,ViewGroup為子View創(chuàng)建MeasureSpec的時候,也是用的這種規(guī)則來生成MeasureSpec對象。

3.2 ViewGroup為子View創(chuàng)建MeasureSpec

測量子View這件事都是發(fā)生在ViewGroup的onMeasure中,而ViewGroup是沒有重寫onMeasure的,這個規(guī)則他留給了他的子類去重寫,我們找到一個最簡單的子類: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調(diào)用的ViewGroup的方法:measureChildWithMargins

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    //獲取子View的LayoutParams
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //創(chuàng)建子View的寬的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    //創(chuàng)建子View的高的MeasureSpec
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    //將這里創(chuàng)建的MeasureSpec傳遞給子View,并讓子View進(jìn)行測量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

當(dāng)創(chuàng)建子View的寬的MeasureSpec的時候,調(diào)用了getChildMeasureSpec方法

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    //用父View的尺寸,減去已經(jīng)使用了的尺寸(包括父view的padding,子View的marging和父View已經(jīng)使用了的尺寸)。
    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);
}

注意這里的參數(shù)padding,加上了父View的padding和子View的margin和父View已經(jīng)使用了的尺寸(如果是FrameLayout,就是0,如果是LinearLayout,則會累加,看ViewGroup的邏輯而定)。size變量則是父View的尺寸減去上述的padding,得出留給子View的剩余空間的大小。

那么這里給子View生成MeasureSpec的邏輯就是:

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

圖形化表示為:

tQgom6.png

這里有兩個點(diǎn)要注意一下:

  1. 當(dāng)父View的布局大小確定的時候,即EXACTLY的時候,那么生成的子View的MeasureSpec依然符合上面創(chuàng)建根布局的情況:

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

    而當(dāng)父View自己的布局大小都不確定的時候,即AT_MOST時(即父View也是用的WRAP_CONTENT,所以他才得到了AT_MOST的mode),子View的MATCH_PARENT其實就是要求和父View一樣的大,那父View不確定大小,子View自然也不確定大小了,即AT_MOST。

  2. 當(dāng)子View的尺寸是WRAP_CONTENT的時候,父View給子View生成的size是父View剩下的size。

為什么?因為父View在此時也不知道你子View有多大,那就把父View剩余的大小給子View,并告訴子View模式是AT_MOST,你子View最大不要超過我給你的這個大小,剩下的你盡管發(fā)揮。

4 MeasureSpec的使用

4.1 View的onMeasure

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

這里是用的getDefaultSize()方法來獲取最終的子View的寬高,并用setMeasuredDimension()設(shè)置到自己的屬性(稍后父View就可以獲取到子View給自己測量的大小了)。

getDefaultSize的兩個參數(shù)一個是獲取建議的最小寬度,一個是父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的測量模式。

從前面創(chuàng)建MeasureSpec知道,當(dāng)子View用了wrap_content的時候,父View就會給你生成AT_MOST的測量模式,但因為AT_MOST測量模式下也是用的父View返回的尺寸,這時父View返回的尺寸是父View剩下的尺寸。他的意思是:這些尺寸給你,但是這是你能使用的最大的尺寸,不要超過這個尺寸就行。

一般來說我們會在自定義View中重寫并判斷AT_MOST時,返回一個默認(rèn)的值。

比如這樣:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    //默認(rèn)尺寸,寫死或者根據(jù)業(yè)務(wù)邏輯計算得到
    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)
        //默認(rèn)尺寸
        val defaultWidth = 50
        val defaultHeight = 100
        //最終測量尺寸(注意,也計算了padding)
        val finalWidth = resolveSize(defaultWidth + paddingLeft + paddingRight, widthMeasureSpec)
        val finalHeight = resolveSize(defaultHeight + paddingTop + paddingBottom, heightMeasureSpec)
        //設(shè)置
        setMeasuredDimension(finalWidth, finalHeight)
}

resolveSize()方法內(nèi)部有更精細(xì)的判斷:

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是使用方,非創(chuàng)建方。

因此我們可以直接按照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());
    }
    //調(diào)用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在對自己測量的時候,也是調(diào)用的resolveSizeAndState這個方法來計算出最終的測量值的。

5 總結(jié)

對于MeasureSpec創(chuàng)建者ViewGroup, 根據(jù)子View的LayoutParams的width和heigth和自己的MeasureSpec來為子View創(chuàng)建出MeasureSpec。傳遞給子View,并讓子View根據(jù)該MeasureSpec設(shè)置對應(yīng)的測量寬高,然后父View再拿到子View測量寬高,將上述動作遍歷所有子View后,再對自己進(jìn)行測量,設(shè)置自己的寬高。

對于MeasureSpec使用者View,根據(jù)父View傳遞進(jìn)來個MeasureSpec,結(jié)合自身邏輯,計算出自己的寬高。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容