原始碼解析Android中View的measure量算過程
本文比較長,希望大家耐心讀完。
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佈局過程
相關文章
- android View measure過程原始碼解析AndroidView原始碼
- Android原始碼完全解析——View的Measure過程Android原始碼View
- 原始碼解析Android中View的layout佈局過程原始碼AndroidView
- Android View 原始碼解析(三) – View的繪製過程AndroidView原始碼
- Android View的Measure測量流程全解析AndroidView
- Android View 測量流程(Measure)完全解析AndroidView
- Android View繪製原始碼分析 MeasureAndroidView原始碼
- android view draw原始碼過程分析AndroidView原始碼
- Android中View的測量和佈局過程AndroidView
- Android繪製View的過程研究——計算View的大小AndroidView
- android中View.measure方法詳解AndroidView
- android apk安裝過程原始碼解析AndroidAPK原始碼
- Android中View的量算、佈局及繪圖機制AndroidView繪圖
- Android View 原始碼解析(一) - setContentViewAndroidView原始碼
- 原始碼解析.Net中Host主機的構建過程原始碼
- Android View的繪製過程AndroidView
- Android中父View和子view的點選事件的執行過程AndroidView事件
- Spark 原始碼系列(六)Shuffle 的過程解析Spark原始碼
- 初探Android的View繪製過程AndroidView
- Android 事件分發機制原始碼解析-view層Android事件原始碼View
- 從Chrome原始碼看DNS解析過程Chrome原始碼DNS
- Dubbo服務呼叫過程原始碼解析④原始碼
- 以太坊啟動過程原始碼解析原始碼
- Lucene原始碼解析--搜尋過程<二>原始碼
- 【Android原始碼】BroadcastReceiver的工作過程Android原始碼AST
- 【Android原始碼】Service的啟動過程Android原始碼
- 【Android原始碼】Service的繫結過程Android原始碼
- 【Android原始碼】View的建立流程Android原始碼View
- CardView原始碼解析-View陰影View原始碼
- 深入解析 Android 中 View 的工作原理AndroidView
- Dubbo原始碼解析之服務呼叫過程原始碼
- Dubbo原始碼解析之服務引入過程原始碼
- 看 Lumen 原始碼解析 Request 到 Response 過程原始碼
- SpringMVC原始碼解析(1)-啟動過程SpringMVC原始碼
- 原始碼解析Android中AsyncTask的工作原理原始碼Android
- Android中IntentService的使用及其原始碼解析AndroidIntent原始碼
- Spring Bean 的例項化過程原始碼解析SpringBean原始碼
- android view layout原始碼分析AndroidView原始碼