Android View原理解析之測量流程(measure)

提示:本文的源碼均取自Android 7.0(API 24)

前言

自定義View是Android進階路線上必須攻克的難題,而在這之前就應該先對View的工作原理有一個系統的理解。本系列將分爲4篇博客進行講解,本文主要對View的測量流程進行講解。相關內容如下:

從View的角度看measure流程

在上一篇文章講到整個視圖樹(ViewTree)的根容器是DecorView,ViewRootImpl通過調用DecorView的measure方法開啓測量流程。measure是定義在View中的方法,我們就先從View的角度來看看測量過程中發生了什麼。

首先來看一下measure方法中的邏輯,關鍵代碼如下:

/**
 * This is called to find out how big a view should be. The parent
 * supplies constraint information in the width and height parameters.
 * 
 * 這個方法將調用onMeasure方法完成真正的測量工作
 * 因此View的派生類只需要也只能重寫onMeasure方法完成佈局邏輯
 */
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);

    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

    // ① 判斷是否需要執行測量過程
    if (forceLayout || needsLayout) { 
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        resolveRtlPropertiesIfNeeded();

        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            
        	// ② 調用onMeasure方法,將在onMeasure方法中真正地設置自身的大小
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            long value = mMeasureCache.valueAt(cacheIndex);
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

可以看到measure是被final修飾的,說明View的子類是無法重寫這個方法的,也就是說ViewGroup及其派生類調用的都是View中的measure方法。在這個方法中先是針對存在特殊邊界的情況,對MeasureSpec進行了調整。隨後在代碼①的位置判斷是否需要進行測量流程,最後在代碼②的位置調用onMeasure方法。接下來我們繼續看一下View#onMeasure方法,代碼如下:

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

在onMeasure方法中調用了setMeasuredDimension方法,這個方法用於設置View測量後的寬高。我們通過View#getMeasuredWidthView#getMeasuredHeight獲取的就是這個方法設置的值。這裏的寬高都是通過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 View的默認寬度/高度
 * @param measureSpec 父容器傳入的MeasureSpec
 * @return View最終的size(測量後的寬/高)
 */
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    // ① 對MeasureSpec進行解包
    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;
}

這個方法的邏輯很簡單,首先在代碼①的位置對傳入的MeasureSpec進行解包操作,獲取specSize和specMode。然後在代碼②的位置判斷測量模式(specMode),只有在測量模式爲MeasureSpec.UNSPECIFIED時使用傳入的默認大小,否則使用解包出來的specSize。這也說明默認情況下,View在測量模式爲AT_MOST或EXACTLY時都會直接使用MeasureSpec中的寬/高。UNSPECIFIED一般是系統內部使用的測量模式,所以大部分情況下這個方法都會返回從MeasureSpec解包出來的specSize。

另外,這個方法中使用的默認大小(size)是通過getSuggestedMinimumWidth和getSuggestedMinimumHeight獲得的,我們也來看一眼這兩個方法中都做了什麼:

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

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

}   

這兩個方法的邏輯是相似的,這裏僅分析getSuggestedMinimumWidth方法。首先會判斷View是否存在背景(無非就是各種Drawable),如果不存在就直接返回mMinWidth,對應着XML中的android:minWidth屬性;否則返回mMinWidth和背景最小寬度中的較大值。getMinimumWidth是Drawable中的方法,Drawable的子類都有自己的實現。

僅僅從View的角度來看,測量流程到此就結束了。因爲不需要測量子View的大小,只需要確定自身的大小就行了。由此可見,如果我們想要通過繼承View的方式實現自定義View,只需要重寫onMeasure方法,並在這個方法中根據不同的情況爲自己設置合適的寬/高,就可以保證測量流程正確進行。

從ViewGroup的角度看measure流程

ViewGroup需要承擔測量子View的責任,而View#measure方法又是無法被重寫的,那麼可以很自然地想到去ViewGroup#onMeasure方法中尋找相應的測量邏輯。但是當我們興致勃勃地在ViewGroup中尋覓時,會發現ViewGroup根本就沒有重寫onMeasure方法。

仔細想想也很正常,ViewGroup是一個抽象類,它的派生類們實現佈局的方式也是多種多樣,ViewGroup作爲父類是無法提供一個統一的測量方案的。當然啦,ViewGroup確實也提供了很多方便測量的方法,下面我們就來一個一個地認識它們:

首先來看看ViewGroup#measureChild方法:

/**
 * Ask one of the children of this view to measure itself, taking into
 * account both the MeasureSpec requirements for this view and its padding.
 * The heavy lifting is done in getChildMeasureSpec.
 *
 * @param child The child to measure
 * @param parentWidthMeasureSpec The width requirements for this view
 * @param parentHeightMeasureSpec The height requirements for this view
 */
protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
	// ① 獲取子View的LayoutParams
    final LayoutParams lp = child.getLayoutParams();

    // ② 生成測量子View需要的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    // ③ 調用子View的measure方法開始測量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

首先在代碼①的位置獲取子View的LayoutParams,裏面封裝着子View對父容器的期望,也就是告訴父容器自己期望的寬高。在代碼②的位置調用了getChildMeasure方法分別獲取子View寬高對應的MeasureSpec。這裏傳入了3個參數,分別是父容器的MeasureSpec、父容器的左右/上下內間距以及子View的LayoutParams中封裝的width/height。最後在代碼③的位置調用子View的measure方法開啓子View的測量流程。getChildMeasureSpec是一個非常重要的方法,接下來我們就來分析一下這個方法的邏輯:

/**
 * 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.
 *
 * 這個方法將根據父容器的MeasureSpec和子View LayoutParams中的寬/高
 * 爲子View生成最合適的MeasureSpec
 *
 * @param spec 父容器的MeasureSpec
 * @param padding 父容器的內間距(可能還會加上子View的外間距)
 * @param childDimension 子View的LayoutParams中封裝的width/height
 * @return 子View的MeasureSpec
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // ① 對父容器的MeasureSpec進行解包
	int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    // ② 減去間距(可以簡單認爲size就是父容器剩餘可用的空間)
    int size = Math.max(0, specSize - padding);

    // 記錄子View最終的大小和測量模式
    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;

    // ⑤ 父容器不對子View的大小作出限制
    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;
    }
    // ⑥ 將最終的size和mode打包爲子View需要的MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

這個方法裏的代碼比較多,但都是有跡可循的,咱們來一點一點捋一遍。首先在代碼①的位置,對父容器的MeasureSpec進行解包,獲取specSize和specMode。然後在代碼②的位置用specSize減去padding,其實就是獲取父容器剩餘的空間,並用resultSizeresultMode記錄子View最終的大小和測量模式。接下來,就需要依照具體的測量模式和子View的LayoutParams進行分析了。

在代碼③的位置,父容器的測量模式爲MeasureSpec.EXACTLY,也就是說父容器的大小已經確定了,那麼我們只需要參考子View的LayoutParams就好了:

  1. 如果LayoutParams中的寬/高是一個確定的值(比如20dp這樣的形式),即childDimension>0,那就說明這是一個有着強烈自我意識的子View,它知道自己想要多大的空間。系統將充分尊重子View的需求,resultSize就將被賦值爲子View聲明的寬/高(childDimension),測量模式也會被賦值爲MeasureSpec.EXACTLY。當然,如果子View聲明的寬/高大於父容器剩餘的空間,最終顯示的時候超出的部分是會被裁剪的;

  2. 如果LayoutParams中的寬/高是LayoutParams.MATCH_PARENT,說明子View想要和父容器一樣大,那就將父容器剩餘的空間(size)賦給resultSize就好了。此時子View的寬/高依舊是確定的,測量模式同樣會被賦值爲MeasureSpec.EXACTLY;

  3. 如果LayoutParams中的寬/高是LayoutParams.WRAP_CONTENT,說明子View自己也不清楚想要多大的空間,那父容器也無可奈何。此時會將resultSize賦值爲父容器剩餘的空間(size),並將測量模式賦值爲MeasureSpec.AT_MOST,也就是爲子View指定了一個最大可用的空間;

在代碼④的位置,父容器的測量模式爲MeasureSpec.AT_MOST,也就是說父容器只知道自己可以使用的最大空間,並不知道精確的大小,接下來結合子View的LayoutParams進行講解:

  1. 如果LayoutParams中的寬/高是一個確定的值(childDimension>0),那就將resultSize賦值爲子View聲明的寬/高(childDimension),測量模式也會被賦值爲MeasureSpec.EXACTLY;

  2. 如果LayoutParams中的寬/高是LayoutParams.MATCH_PARENT,說明子View想要和父容器一樣大。但是此時父容器也不確定自己有多大,所以只能將resultSize賦值爲父容器剩餘的空間(size),並將測量模式賦值爲MeasureSpec.AT_MOST,也就是爲子View指定了一個最大可用的空間;

  3. 如果LayoutParams中的寬/高是LayoutParams.WRAP_CONTENT,說明父容器和子View都不清楚自己想要多大的空間,那就直接將resultSize賦值爲父容器剩餘的空間(size),並將測量模式賦值爲MeasureSpec.AT_MOST,同樣爲子View指定了一個最大可用的空間。

可以看到在父容器的測量模式爲MeasureSpec.AT_MOST時,無論子View的LayoutParams使用WRAP_CONTENT還是MATCH_PARENT,結果都是一樣的。

在代碼⑤的位置,父容器的測量模式爲MeasureSpec.UNSPECIFIED,也就是不限制子View的大小。這一般是系統內部使用的測量模式,我們就不再重點講解了。只說明一下如果LayoutParams中的寬/高是一個確定的值,那就將resultSize賦值爲childDimension,測量模式也會被賦值爲MeasureSpec.EXACTLY。

getChildMeasureSpec是一個非常重要的方法,希望大家可以好好理解這個過程。

接下來,我們再來一起看看ViewGroup#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 被測量的子View(必須要有MarginLayoutParams)
 * @param parentWidthMeasureSpec 父容器的MeasureSpec(針對width)
 * @param widthUsed 在水平方向上被使用的額外空間(可能是被父容器或其他子View使用的空間)
 * @param parentHeightMeasureSpec 父容器的MeasureSpec(針對height)
 * @param heightUsed 在垂直方向上被使用的額外空間(可能是被父容器或其他子View使用的空間)
 */
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
	// ① 獲取子View的LayoutParams,並強制轉型爲MarginLayoutParams
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    // ② 生成測量子View需要的MeasureSpec
    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);
    
    // ③ 調用子View的measure方法開始測量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

首先在代碼①的位置獲取子View的LayoutParams,並強制轉型MarginLayoutParams,這也就說明使用這個方法的父容器要支持margin屬性(即父容器的LayoutParams要繼承MarginLayoutParams)。

後面兩步就和measureChild相似了,先在代碼②的位置使用getChildMeasureSpec方法生成測量子View需要的MeasureSpec。這裏和measureChild不同的是除了傳入父容器的內邊距之外,還傳入了子View的外邊距(margin)以及widthUsed/heightUsed。widthUsed和heightUsed是在水平/垂直方向上被使用的額外空間(可能是被父容器或其他子View使用的空間)。

最後在代碼③的位置調用子View的measure方法開始測量。其實看方法名也能明白這個方法和measureChild的區別,無非就是在測量的時候會考慮到外邊距的影響。當我們需要考慮子View的margin時,就可以使用這個方法進行測量。

最後我們再來看看ViewGroup#measureChildren方法:

/**
 * Ask all of the children of this view to measure themselves, taking into
 * account both the MeasureSpec requirements for this view and its padding.
 * We skip children that are in the GONE state The heavy lifting is done in
 * getChildMeasureSpec.
 *
 * @param widthMeasureSpec 父容器的MeasureSpec(針對width)
 * @param heightMeasureSpec 父容器的MeasureSpec(針對height)
 */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount; // 子View的數量
    final View[] children = mChildren; // 包含子View的數組
    for (int i = 0; i < size; ++i) { // 逐個對子View進行測量
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { // 只測量visibility不爲GONE的子View(提高效率)
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

可以看到,這個方法的目的是對父容器所有的子View進行測量,其實就是逐次對每個visibility不爲GONE的子View調用了measureChild方法。所以我們也就知道了,如果希望在測量過程中考慮子View的外間距的話,是不可以使用這個方法的。

到這裏,我們基本就把ViewGroup與測量流程相關的方法分析完了。仔細想來,似乎ViewGroup並沒有做出什麼實質性的測量工作。畢竟ViewGroup是一個抽象的父類,確實也不能決定具體的測量步驟。

如果通過繼承ViewGroup實現自定義View,就應該重寫onMeasure方法,並在這個方法中合理利用ViewGroup提供的measureChild、measureChildWithMargins、measureChildren和getChildMeasureSpec等方法完成對子View的測量工作,並通過setMeasuredDimension方法設置自身的寬高。

整體的流程圖

上面分別從View和ViewGroup的角度講解了測量流程,這裏再以流程圖的形式歸納一下整個measure過程,便於加深記憶:

小結

測量流程在三大流程中相對是比較複雜的,如果看完本文後依舊有些疑惑,不如打開AndroidStudio,沿着文章的脈絡親自探索一下整個measure過程的邏輯,可能學習效果會更好一點。

參考資料

https://blog.csdn.net/a553181867/article/details/51494058
https://blog.csdn.net/xmxkf/article/details/51490283
https://blog.csdn.net/lfdfhl/article/details/51347818

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章