原始碼解析Android中View的measure量算過程

孫群發表於2015-10-25

本文比較長,希望大家耐心讀完。

Android中的Veiw從記憶體中到呈現在UI介面上需要依次經歷三個階段:量算 -> 佈局 -> 繪圖,關於View的量算、佈局、繪圖的總體機制可參見博文《 Android中View的佈局及繪圖機制》。如果想了解layout佈局的細節,可參見博文《原始碼解析Android中View的layout佈局過程》。量算是佈局和繪圖的基礎,所以量算是很重要的一個環節。本文將從原始碼角度解析View的量算過程,這其中會涉及某些關鍵類以及關鍵方法。

對View進行量算的目的是讓View的父控制元件知道View想要多大的尺寸。


量算過程概述

如果要進行量算的View是ViewGroup型別,那麼ViewGroup會在onMeasure方法內會遍歷子View依次進行量算,本文重點說明非ViewGroup的View的量算過程,因為我們一旦瞭解了非ViewGroup的View的量算過程,ViewGroup的量算理解起來就要簡單許多,主要是ViewGroup在其內部對子View再依次執行量算。

  • 整個應用量算的起點是ViewRootImpl類,從它開始依次對子View進行量算,如果子View是一個ViewGroup,那麼又會遍歷該ViewGroup的子View依次進行量算。也就是說,量算會從View樹的根結點,縱向遞迴進行,從而實現自上而下對View樹進行量算,直至完成對葉子節點View的量算。

  • 那麼到底如何對一個View進行量算呢?Android通過呼叫View的measure()方法對View進行量算,讓該View的父控制元件知道該View想要多大的尺寸空間。

  • 具體來說,View的父控制元件ViewGroup會呼叫View的measure方法,ViewGroup會將一些寬度和高度的限制條件傳遞給View的measure方法。

  • 在View的measure方法會首先從成員變數中讀取以前快取過的量算結果,如果能找到該快取值,那麼就基本完事了,如果沒有找到快取值,那麼measure方法會執行onMeasure回撥方法,measure方法會將上述的寬度和高度的限制條件依次傳遞給onMeasure方法。onMeasure方法會完成具體的量算工作,並將量算的結果通過呼叫View的setMeasuredDimension方法儲存到View的成員變數mMeasuredWidth 和mMeasuredHeight中。

  • 量算完成之後,View的父控制元件就可以通過呼叫getMeasuredWidth、getMeasuredState、getMeasuredWidthAndState這三個方法獲取View的量算結果。

以上就是非ViewGroup型別的View量算的總體過程。


MeasureSpec簡介

上面我們提到ViewGroup在呼叫View的measure方法時,會傳入ViewGroup對View的寬度及高度的限制條件,這是合理的,例如ViewGroup的空間有限,它需要告訴子View要量算的尺寸的上限。

上面提到的尺寸的限制條件就是MeasureSpec,它可以通過一個Int型別的值來表示的,該Int值會同時包含兩種資訊:mode和size,即模式和尺寸。我們知道Java中Int型別的值是4個位元組的,Android會用第一個高位位元組儲存mode,然後用剩餘的三個位元組儲存size。

View有一個靜態內部類MeasureSpec,該類有幾個靜態方法以及靜態常量,我們可以用這些方法將mode和size打包成一個Int值或者是從一個Int值中解析出mode和size。

假設我們已有了一個包含MeasureSpec資訊的Int值measureSpec,那麼

  • 通過呼叫MeasureSpec.getSize(int measureSpec)即可從measureSpec解析出三個位元組所包含的尺寸size資訊,該方法返回Int型別,也就是說我們得到的size實際上就是對原有的measureSpec的高位位元組的8個二進位制位都設定為0,該方法的返回值size雖然也是4個位元組的Int值,但是已經完全不包含mode資訊。

  • 通過呼叫MeasureSpec.getMode(int measureSpec)即可從measureSpec解析出高位位元組所包含的模式mode資訊,該方法返回Int型別,也就是說我們得到的mode實際上對原有的measureSpec的低位的三個位元組的24個二進位制碼都設定為0,該方法的返回值mode雖然也是4個位元組的Int值,但是已經完全不包含size資訊。

對於尺寸size,我們很好理解,比如表示某個寬度值或者表示某個高度值。那麼mode是什麼呢?

mode的取值有三種,分別是:

  • MeasureSpec.AT_MOST,即0x80000000,該值表示View最大可以取其父ViewGroup給其指定的尺寸,例如現在有個Int值widthMeasureSpec,ViewGroup將其傳遞給了View的measure方法,如果widthMeasureSpec中的mode值是AT_MOST,size是200,那麼表示View能取的最大的寬度是200。

  • MeasureSpec.EXACTLY,即0x40000000,該值表示View必須使用其父ViewGroup指定的尺寸,還是以widthMeasureSpec為例,如果其mode值是EXACTLY,size是200,那麼表示View的寬度必須是200,不多不少才行。

  • MeasureSpec.UNSPECIFIED,即0x00000000,該值表示View的父ViewGroup沒有給View在尺寸上設定限制條件,這種情況下View可以忽略measureSpec中的size,View可以取自己想要的值作為量算的尺寸。

更多資訊可參考API文件 android/view/View.MeasureSpec


measure方法

measure()的方法簽名是public final void measure(int widthMeasureSpec, int heightMeasureSpec)

當View的父控制元件ViewGroup對View進行量算時,會呼叫View的measure方法,ViewGroup會傳入widthMeasureSpec和heightMeasureSpec,分別表示父控制元件對View的寬度和高度的一些限制條件。

measure方法的原始碼如下所示:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //首先判斷當前View的layoutMode是不是特例LAYOUT_MODE_OPTICAL_BOUNDS
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        //LAYOUT_MODE_OPTICAL_BOUNDS是特例情況,比較少見
        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);
    }

    //根據widthMeasureSpec和heightMeasureSpec計算key值,我們在下面用key值作為鍵,快取我們量算的結果
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;

    //mMeasureCache是LongSparseLongArray型別的成員變數,
    //其快取著View在不同widthMeasureSpec、heightMeasureSpec下量算過的結果
    //如果mMeasureCache為空,我們就新new一個物件賦值給mMeasureCache
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

    //mOldWidthMeasureSpec和mOldHeightMeasureSpec分別表示上次對View進行量算時的widthMeasureSpec和heightMeasureSpec
    //執行View的measure方法時,View總是先檢查一下是不是真的有必要費很大力氣去做真正的量算工作
    //mPrivateFlags是一個Int型別的值,其記錄了View的各種狀態位
    //如果(mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT,
    //那麼表示當前View需要強制進行layout(比如執行了View的forceLayout方法),所以這種情況下要嘗試進行量算
    //如果新傳入的widthMeasureSpec/heightMeasureSpec與上次量算時的mOldWidthMeasureSpec/mOldHeightMeasureSpec不等,
    //那麼也就是說該View的父ViewGroup對該View的尺寸的限制情況有變化,這種情況下要嘗試進行量算
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {

        //通過按位操作,重置View的狀態mPrivateFlags,將其標記為未量算狀態
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        //對阿拉伯語、希伯來語等從右到左書寫、佈局的語言進行特殊處理
        resolveRtlPropertiesIfNeeded();

        //在View真正進行量算之前,View還想進一步確認能不能從已有的快取mMeasureCache中讀取快取過的量算結果
        //如果是強制layout導致的量算,那麼將cacheIndex設定為-1,即不從快取中讀取量算結果
        //如果不是強制layout導致的量算,那麼我們就用上面根據measureSpec計算出來的key值作為快取索引cacheIndex。
        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);

        //sIgnoreMeasureCache是一個boolean型別的成員變數,其值是在View的建構函式中計算的,而且只計算一次
        //一些老的App希望在一次layou過程中,onMeasure方法總是被呼叫,
        //具體來說其值是通過如下計算的: sIgnoreMeasureCache = targetSdkVersion < KITKAT;
        //也就是說如果targetSdkVersion的API版本低於KITKAT,即API level小於19,那麼sIgnoreMeasureCache為true

        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            //如果執行到此處,表示我們沒有從快取中找到量算過的尺寸或者是sIgnoreMeasureCache為true導致我們要忽略快取結果
            //此處呼叫onMeasure方法,並把尺寸限制條件widthMeasureSpec和heightMeasureSpec傳入進去
            //onMeasure方法中將會進行實際的量算工作,並把量算的結果儲存到成員變數中
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            //onMeasure執行完後,通過位操作,重置View的狀態mPrivateFlags,將其標記為在layout之前不必再進行量算的狀態
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            //如果執行到此處,那麼表示當前的條件允許View從快取成員變數mMeasureCache中讀取量算過的結果
            //用上面得到的cacheIndex從快取mMeasureCache中取出值,不必在呼叫onMeasure方法進行量算了
            long value = mMeasureCache.valueAt(cacheIndex);
            //一旦我們從快取中讀到值,我們就可以呼叫setMeasuredDimensionRaw方法將當前量算的結果到成員變數中
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        //如果我們自定義的View重寫了onMeasure方法,但是沒有呼叫setMeasuredDimension()方法,
        //那麼此處就會丟擲異常,提醒開發者在onMeasure方法中呼叫setMeasuredDimension()方法
        //Android是如何知道我們有沒有在onMeasure方法中呼叫setMeasuredDimension()方法的呢?
        //方法很簡單,還是通過解析狀態位mPrivateFlags。
        //setMeasuredDimension()方法中會將mPrivateFlags設定為PFLAG_MEASURED_DIMENSION_SET狀態,即已量算狀態,
        //此處就檢查mPrivateFlags是否含有PFLAG_MEASURED_DIMENSION_SET狀態即可判斷setMeasuredDimension是否被呼叫
        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和mOldHeightMeasureSpec儲存著最近一次量算時的MeasureSpec,
    //在量算完成後將這次新傳入的MeasureSpec賦值給它們
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

    //最後用上面計算出的key作為鍵,量算結果作為值,將該鍵值對放入成員變數mMeasureCache中,
    //這樣就實現了對本次量算結果的快取,以便在下次measure方法執行的時候,有可能將其從中直接讀出,
    //從而省去實際量算的步驟
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL);
}

上面的註釋對每行程式碼都進行了詳細的說明,如果大家仔細讀了的話,相信能一目瞭然,這裡根據上面的註釋簡單總結一下measure方法都幹了什麼事:

  • 首先,我們要知道並不是只要View的measure方法執行的時候View就一定要傻傻的真的去做量算工作,View也喜歡偷懶,如果View發現沒有必要去量算的話,那它就不會真的去做量算的工作。

  • 具體來說,View先檢視是不是要強制量算以及這次measure中傳入的MeasureSpec與上次量算的MeasureSpec是否相同,如果不是強制量算或者MeasureSpec與上次的量算的MeasureSpec相同,那麼View就不必真的去量算了。

  • 如果不滿足上述條件,View就考慮去做量算工作。但是在量算之前,View還想偷懶,它會以MeasureSpec計算出的key值作為鍵,去成員變數mMeasureCache中查詢是否快取過對應key的量算結果,如果能找到,那麼就簡單呼叫一下setMeasuredDimensionRaw方法,將從快取中讀到的量算結果儲存到成員變數mMeasuredWidth和mMeasuredHeight中。

  • 如果不能從mMeasureCache中讀到快取過的量算結果,那麼這次View就真的不能再偷懶了,只能乖乖地呼叫onMeasure方法去完成實際的量算工作,並且將尺寸限制條件widthMeasureSpec和heightMeasureSpec傳遞給onMeasure方法。關於onMeasure方法,我們會在下面詳細介紹。

  • 不論上面程式碼走了哪個判斷的分支,最終View都會得到量算的結果,並且將結果快取到成員變數mMeasureCache中,以便下次執行measure方法時能夠從其中讀取快取值。

  • 需要說明的是,View有一個成員變數mPrivateFlags,用以儲存View的各種狀態位,在量算開始前,會將其設定為未量算狀態,在量算完成後會將其設定為已量算狀態。


onMeasure方法

我們在上面提到,當View在measure方法中發現不得不進行實際的量算工作時,將會呼叫onMeasure方法,並且將尺寸限制條件widthMeasureSpec和heightMeasureSpec作為引數傳遞給onMeasure方法。View的onMeasure方法不是空方法,它提供了一個預設的具體實現。
onMeasure方法的程式碼如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //onMeasure呼叫了setMeasuredDimension方法,
    //setMeasuredDimension又需要呼叫getDefaultSize方法,
    //getDefaultSize又需要呼叫getSuggestedMinimumWidth和getSuggestedMinimumHeight方法
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

我們發現onMeasure方法中會呼叫setMeasuredDimension方法,setMeasuredDimension又需要呼叫getDefaultSize方法,getDefaultSize又需要呼叫getSuggestedMinimumWidth和getSuggestedMinimumHeight方法,即
setMeasuredDimension -> getDefaultSize -> getSuggestedMinimumWidth/Height

那我們就先研究getSuggestedMinimumWidth/Height,然後再依次研究getDefaultSize和setMeasuredDimension,這樣就能把onMeasure方法搞明白了。其實getSuggestedMinimumWidth和getSuggestedMinimumHeight的實現邏輯基本一樣,我們此處只研究getSuggestedMinimumWidth方法即可。


getSuggestedMinimumWidth方法

getSuggestedMinimumWidth用於返回View推薦的最小寬度,其程式碼如下所示:

protected int getSuggestedMinimumWidth() {
    //如果沒有給View設定背景,那麼就返回View本身的最小寬度mMinWidth
    //如果給View設定了背景,那麼就取View本身最小寬度mMinWidth和背景的最小寬度的最大值
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
  • 如果沒有給View設定背景,那麼就返回View本身的最小寬度mMinWidth

  • 如果給View設定了背景,那麼就取View本身最小寬度mMinWidth和背景的最小寬度的最大值

那你可能有疑問,View中儲存的最小寬度mMinWidth的值是從哪來的呢?實際上有兩種辦法給View設定最小寬度。

  • 第一種情況是,mMinWidth是在View的建構函式中被賦值的,View通過讀取XML中定義的minWidth的值來設定View的最小寬度mMinWidth,以下程式碼片段是View建構函式中解析minWidth的部分:

    //遍歷到XML中定義的minWith屬性
    case R.styleable.View_minWidth:
    //讀取XML中定義的屬性值作為mMinWidth,如果XML中未定義,則設定為0
    mMinWidth = a.getDimensionPixelSize(attr, 0);
    break;
  • 第二種情況是呼叫View的setMinimumWidth方法給View的最小寬度mMinWidth賦值,setMinimumWidth方法的程式碼如下所示:

    public void setMinimumWidth(int minWidth) {
        mMinWidth = minWidth;
        requestLayout();
    }

這樣我們就搞明白了getSuggestedMinimumWidth方法是怎麼執行的了,getSuggestedMinimumHeight方法與其邏輯完全一致,只不過是把寬度換成了高度,在此就不再贅述了。


getDefaultSize

我們在onMeasure方法中發現,onMeasure會執行以下兩行程式碼:getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)

我們已經研究了getSuggestedMinimumWidth/Height,知道其會返回View的最小寬度和高度,現在我們開始研究getDefaultSize方法。

Android會將View想要的尺寸以及其父控制元件對其尺寸限制資訊measureSpec傳遞給getDefaultSize方法,該方法要根據這些綜合資訊計算最終的量算的尺寸。

其原始碼如下所示:

public static int getDefaultSize(int size, int measureSpec) {
    //size表示的是View想要的尺寸資訊,比如最小寬度或最小高度
    int result = size;
    //從measureSpec中解析出specMode資訊
    int specMode = MeasureSpec.getMode(measureSpec);
    //從measureSpec中解析出specSize資訊,不要將specSize與上面的size變數搞混
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    //如果mode是UNSPECIFIED,表示View的父ViewGroup沒有給View在尺寸上設定限制條件
    case MeasureSpec.UNSPECIFIED:
        //此處當mode是UNSPECIFIED時,View就直接用自己想要的尺寸size作為量算的結果
        result = size;
        break;
    //如果mode是UNSPECIFIED,那麼表示View最大可以取其父ViewGroup給其指定的尺寸
    //如果mode是EXACTLY,那麼表示View必須使用其父ViewGroup指定的尺寸
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        //此處mode是UNSPECIFIED或EXACTLY時,View就用其父ViewGroup指定的尺寸作為量算的結果
        result = specSize;      
        break;
    }
    return result;
}

通過以上程式碼,我們就會發現View的父ViewGroup傳遞給View的限制條件measureSpec的作用在該方法中體現的淋漓盡致。

  • 首先根據measuredSpec解析出對應的specMode和specSize

  • 當mode是UNSPECIFIED時,View就直接用自己想要的尺寸size作為量算的結果

  • 當mode是UNSPECIFIED或EXACTLY時,View就用其父ViewGroup指定的尺寸作為量算的結果

最終,View會根據measuredSpec限制條件,得到最終的量算的尺寸。

這樣在onMeasure方法中,
當執行getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)時,我們就得到了最終量算到的寬度值;
當執行getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)時,我們就得到了最終量算到的高度值。


setMeasuredDimension

在前面我們研究onMeasure方法時就已經看到setMeasuredDimension會呼叫getDefaultSize方法,會將已經量算到的寬度值和高度值作為引數傳遞給setMeasuredDimension方法,我們研究一下該方法。

其原始碼如下所示:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        //layoutMode是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情況,我們不考慮
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    //最終呼叫setMeasuredDimensionRaw方法,將量算結果傳入進去
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

該方法會在開始判斷layoutMode是不是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情況,這種特例很少見,我們直接忽略掉。

setMeasuredDimension方法最後將量算的結果傳遞給方法setMeasuredDimensionRaw,我們再研究一下setMeasuredDimensionRaw這方法。


setMeasuredDimensionRaw

setMeasuredDimensionRaw接收兩個引數,分別是已經量算完成的寬度和高度。

其原始碼如下所示:

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    //將量算完成的寬度measuredWidth儲存到View的成員變數mMeasuredWidth中
    mMeasuredWidth = measuredWidth;
    //將量算完成的高度measuredHeight儲存到View的成員變數mMeasuredHeight中
    mMeasuredHeight = measuredHeight;
    //最後將View的狀態位mPrivateFlags設定為已量算狀態
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

我們發現,在該方法中做了三件事:

  • 將量算完成的寬度measuredWidth儲存到View的成員變數mMeasuredWidth中

  • 將量算完成的高度measuredHeight儲存到View的成員變數mMeasuredHeight中

  • 最後將View的狀態位mPrivateFlags設定為已量算狀態


量算完成的尺寸的state

至此,View的量算過程就完成了,但是View的父ViewGroup如何讀取到View量算的結果呢?

為此,View提供了三組方法,分別是:
1. getMeasuredWidth和getMeasuredHeight方法
2. getMeasuredWidthAndState和getMeasuredHeightAndState方法
3. getMeasuredState方法

有些人可能會納悶,只要有了第一組方法不就行了嗎?後面那兩組方法有啥用?

此處我們要再仔細研究一下View中儲存量算結果的成員變數mMeasuredWidth和mMeasuredHeight,下面的討論我們都只討論寬度,理解了寬度的處理方式,高度也是完全一樣的。

mMeasuredWidth是一個Int型別的值,其是由4個位元組組成的。

我們先假設mMeasuredWidth只儲存了量算完成的寬度資訊,而且View的父ViewGroup可以通過相關方法得到該值。但是存在這樣一種情況:View在量算時,父ViewGroup給其傳遞的widthMeasureSpec中的specMode的值是AT_MOST,specSize是100,但是View的最小寬度是200,顯然父ViewGroup指定的specSize不能滿足View的大小,但是由於specMode的值是AT_MOST,View在getDefaultSize方法中不得不妥協,只能含淚將量算的最終寬度設定為100。然後其父ViewGroup通過某些方法獲取到該View的量算寬度為100時,ViewGroup以為子View只需要100就夠了,最終給了子View寬度為100的空間,這就導致了在UI介面上View特別窄,使用者體驗也就不好。

Android為讓其View的父控制元件獲取更多的資訊,就在mMeasuredWidth上下了很大功夫,雖然是一個Int值,但是想讓它儲存更多資訊,具體來說就是把mMeasuredWidth分成兩部分:

  • 其高位的第一個位元組為第一部分,用於標記量算完的尺寸是不是達到了View想要的寬度,我們稱該資訊為量算的state資訊。
  • 其低位的三個位元組為第二部分,用於儲存實際的量算到的寬度。

由此我們可以看出Android真是物盡其用,一個變數能包含兩個資訊,這個有點類似於measureSpec的道理,但是二者又有不同:

  • measureSpec是將限制條件mode從ViewGroup傳遞給其子View。
  • mMeasuredWidth、mMeasuredHeight是將帶有量算結果的state標誌位資訊從View傳遞給其父ViewGroup。

那麼你可能會問,在本文中我們沒看到對mMeasuredWidth的高位位元組進行特殊處理啊?我們下面看一下View中的resolveSizeAndState方法。


resolveSizeAndState

resolveSizeAndState方法與getDefaultSize方法類似,其內部實現的邏輯是一樣的,但是又有區別,getDefaultSize僅僅返回最終量算的尺寸資訊,但resolveSizeAndState除了返回最終尺寸資訊還會有可能返回量算的state標誌位資訊。

resolveSizeAndState方法的原始碼如下所示:

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) {
                //當specMode為AT_MOST,並且父控制元件指定的尺寸specSize小於View自己想要的尺寸時,
                //我們就會用掩碼MEASURED_STATE_TOO_SMALL向量算結果加入尺寸太小的標記
                //這樣其父ViewGroup就可以通過該標記其給子View的尺寸太小了,
                //然後可能分配更大一點的尺寸給子View
                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);
}

當specMode為AT_MOST,並且父控制元件指定的尺寸specSize小於View自己想要的尺寸時,我們就會用掩碼MEASURED_STATE_TOO_SMALL向量算結果加入尺寸太小的標記,這樣其父ViewGroup就可以通過該標記其給子View的尺寸太小了,然後可能分配更大一點的尺寸給子View。

getDefaultSize方法只是onMeasure方法中獲取最終尺寸的預設實現,其返回的資訊比resolveSizeAndState要少,那麼什麼時候才會呼叫resolveSizeAndState方法呢? 主要有兩種情況:

  • Android中的許多layout類都呼叫了resolveSizeAndState方法,比如LinearLayout在量算過程中會呼叫resolveSizeAndState方法而非getDefaultSize方法。
  • 我們自己在實現自定義的View或ViewGroup時,我們可以重寫onMeasure方法,並在該方法內呼叫resolveSizeAndState方法。

getMeasuredXXX系列方法

現在我們再回過頭來看以下三組方法:

  • getMeasuredWidth和getMeasuredHeight方法
    該組方法只返回量算結果中的的尺寸資訊,去掉了高位位元組的state資訊,以getMeasuredWidth方法為例,其原始碼如下:

    public final int getMeasuredWidth() {
        //MEASURED_SIZE_MASK的值為0x00ffffff,用mMeasuredWidth與掩碼MEASURED_SIZE_MASK進行按位與運算,
        //可以將返回值中的高位位元組的8個bit位全置為0,從而去掉了高位位元組的state資訊
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

    MEASURED_SIZE_MASK的值為0x00ffffff,用mMeasuredWidth與掩碼MEASURED_SIZE_MASK進行按位與運算,可以將返回值中的高位位元組的8個bit位全置為0,從而去掉了高位位元組的state資訊

  • getMeasuredWidthAndState和getMeasuredHeightAndState方法
    該組方法返回的量算結果中同時包含尺寸和state資訊(如果state存在的話),以getMeasuredWidthAndState方法為例,其原始碼如下所示:

    public final int getMeasuredWidthAndState() {
        //該方法直接返回成員變數mMeasuredWidth,因為mMeasuredWidth本身已經包含了尺寸以及可能的state資訊
        return mMeasuredWidth;
    }

    該方法直接返回成員變數mMeasuredWidth,因為mMeasuredWidth本身已經包含了尺寸以及可能的state資訊

  • getMeasuredState方法
    該方法返回的Int值中同時包含寬度量算的state以及高度量算的state,不包含任何的尺寸資訊,其原始碼如下所示:

    public final int getMeasuredState() {
        //將寬度量算的state儲存在Int值的第一個位元組中,即高位首位元組
        //將高度量算的state儲存在Int值的第三個位元組中
        return (mMeasuredWidth&MEASURED_STATE_MASK)
                | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
                        & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
    }

    我們簡單分析一下以上程式碼:

  • 掩碼MEASURED_STATE_MASK的值為常量0xff000000,其高位位元組的8個bit位全為1,剩餘低位位元組的三個位元組的24個bit位全為0

  • MEASURED_HEIGHT_STATE_SHIFT的值為常量16

  • 當執行(mMeasuredWidth&MEASURED_STATE_MASK)時,將mMeasuredWidth與MEASURED_STATE_MASK進行按位與操作,該表示式的值高位位元組保留了量算後寬度的state,過濾掉了其低位三個位元組所儲存的寬度size

  • 由於我們已經用高位首位元組儲存了量算後寬度的state,所以高度的state就不能儲存在高位首位元組了。Android打算把它儲存在第三個位元組中。(mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)表示將mMeasuredHeight向右移16位,這樣高度的state位元組就從原來的第一個位元組右移動到了第三個位元組,由於高度的state向右移動了,所以其對應的掩碼也有相應移動。(MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT)表示state的掩碼也從第一個位元組右移16位到了第三個位元組,即掩碼從0xff000000變成了0x0000ff00。然後用右移後的state與右移後的掩碼執行按位與操作,這樣就在第三個位元組保留了高度的state資訊,並且過濾掉了第1、2、4位元組中的資訊,即將這三個位元組中的24個bit位置為0。

  • 最後,將我們得到的寬度的state與高度的state進行按位或操作,這樣就將寬度和高度的state都儲存在一個Int值中:第一個位元組儲存寬度的state,第三個位元組儲存高度的state。


總結

至此,View中量算的關鍵類以及方法我們基本都涉及到了,我們發現View的measure方法還是比較聰明的,知道如何偷懶利用以前量算過的資料,如果情況有變,那麼就呼叫onMeasure方法進行實際的量算工作,在onMeasure中,View要根據父ViewGroup給其傳遞進來的widthMeasureSpec和heightMeasureSpec,並結合View自身想要的尺寸,綜合考慮,計算出最終的量算的寬度和高度,並儲存到相應的成員變數中,這才標誌著該View量算有效的完成了,如果沒有將值存入到成員變數中,View會丟擲異常。在該成員變數中有可能也儲存了量算過程中的state資訊。由於View的measure已經實現了很多邏輯判斷,所以我們在自定義View或ViewGroup時,都不應該重寫measure方法,而應該重寫onMeasure方法,在其中實現我們自己的量算邏輯。

囉囉嗦嗦寫了將近一天,終於寫完了,希望本文對大家深入理解Android中的量算過程有所幫助,感謝大家耐心讀完本文。

相關閱讀:
Android相關博文整理彙總
Android中View的佈局及繪圖機制
原始碼解析Android中View的layout佈局過程

相關文章