1. 程式人生 > >Android View原理解析之測量流程(measure)

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過程的邏輯,可能學習效果會更好一點。

參考資料