在Android中,Veiw從記憶體中到呈現在UI介面上需要經過measure(測量)、layout(佈局)、draw(繪製)這樣一個過程。為什麼需要measure過程?因為在Android中View有自適應尺寸的機制,在用自適應尺寸來定義View大小的時候,View的真實尺寸還不能確定,這時候就需要根據View的寬高匹配規則,經過計算,得到具體的畫素值,measure過程就是幹這件事。
本文將從原始碼角度解析View的measure過程,這其中會涉及某些關鍵類以及關鍵方法。
MeasureSpec
MeasureSpec封裝了父佈局傳遞給子佈局的佈局要求,它通過一個32位int型別的值來表示,該值包含了兩種資訊,高兩位表示的是SpecMode
(測量模式),低30位表示的是SpecSize
(測量的具體大小)。下面通過註釋的方式來分析來類:
/**
* 三種SpecMode:
* 1.UNSPECIFIED
* 父ViewGroup沒有對子View施加任何約束,子view可以是任意大小。這種情況比較少見,主要用於系統內部多次measure的情形,用到的一般都是可以滾動的容器中的子view,比如ListView、GridView、RecyclerView中某些情況下的子view就是這種模式。一般來說,我們不需要關注此模式。
* 2.EXACTLY
* 該view必須使用父ViewGroup給其指定的尺寸。對應match_parent或者具體數值(比如30dp)
* 3.AT_MOST
* 該View最大可以取父ViewGroup給其指定的尺寸。對應wrap_content
*
* MeasureSpec使用了二進位制去減少物件的分配。
*/
public class MeasureSpec {
// 進位大小為2的30次方(int的大小為32位,所以進位30位就是要使用int的最高位和第二高位也就是32和31位做標誌位)
private static final int MODE_SHIFT = 30;
// 運算遮罩,0x3為16進位制,10進製為3,二進位制為11。3向左進位30,就是11 00000000000(11後跟30個0)
// (遮罩的作用是用1標註需要的值,0標註不要的值。因為1與任何數做與運算都得任何數,0與任何數做與運算都得0)
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 0向左進位30,就是00 00000000000(00後跟30個0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 1向左進位30,就是01 00000000000(01後跟30個0)
public static final int EXACTLY = 1 << MODE_SHIFT;
// 2向左進位30,就是10 00000000000(10後跟30個0)
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* 根據提供的size和mode得到一個詳細的測量結果
*/
// 第一個return:
// measureSpec = size + mode; (注意:二進位制的加法,不是十進位制的加法!)
// 這裡設計的目的就是使用一個32位的二進位制數,32和31位代表了mode的值,後30位代表size的值
// 例如size=100(4),mode=AT_MOST,則measureSpec=100+10000...00=10000..00100
//
// 第二個return:
// size & ~MODE_MASK就是取size 的後30位,mode & MODE_MASK就是取mode的前兩位,最後執行或運算,得出來的數字,前面2位包含代表mode,後面30位代表size
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* 獲得SpecMode
*/
// mode = measureSpec & MODE_MASK;
// MODE_MASK = 11 00000000000(11後跟30個0),原理是用MODE_MASK後30位的0替換掉measureSpec後30位中的1,再保留32和31位的mode值。
// 例如10 00..00100 & 11 00..00(11後跟30個0) = 10 00..00(AT_MOST),這樣就得到了mode的值
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* 獲得SpecSize
*/
// size = measureSpec & ~MODE_MASK;
// 原理同上,不過這次是將MODE_MASK取反,也就是變成了00 111111(00後跟30個1),將32,31替換成0也就是去掉mode,保留後30位的size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
複製程式碼
measure()
當View的父ViewGroup對View進行測量時,會呼叫View的measure
方法,ViewGroup會傳入widthMeasureSpec
和heightMeasureSpec
,分別表示父控制元件對View的寬度和高度的一些限制條件。原始碼分析該方法:
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下測量過的結果
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,這時候有可能找到相應的值,找到就返回對應索引;也可能找不到,找不到就返回-1
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//在快取中找不到相應的值或者需要忽略快取結果的時候,重新測量一次
//此處呼叫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()");
}
//到了這裡,View已經測量完了並且將測量的結果儲存在View的mMeasuredWidth和mMeasuredHeight中,將標誌位置為可以layout的狀態
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
//mOldWidthMeasureSpec和mOldHeightMeasureSpec儲存著最近一次測量時的MeasureSpec,
//在測量完成後將這次新傳入的MeasureSpec賦值給它們
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//最後用上面計算出的key作為鍵,測量結果作為值,將該鍵值對放入成員變數mMeasureCache中,
//這樣就實現了對本次測量結果的快取,以便在下次measure方法執行的時候,有可能將其從中直接讀出,
//從而省去實際測量的步驟
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
複製程式碼
上面的註釋已經一目瞭然,這裡再總結一下measure()
都幹了什麼事:
首先,呼叫
View.measure()
方法時View並不是立即就去測量,而是先判斷一下是否有必要進行測量操作,如果不是強制測量或者MeasureSpec
與上次的MeasureSpec
相同的時候,那麼View就不需要重新測量了.如果不滿足上面條件,View就考慮去做測量工作了.但在測量之前,View還想偷懶,如果能在快取中找到上次的測量結果,那直接從快取中獲取就可以了.它會以MeasureSpec計算出的key值作為鍵,去成員變數
mMeasureCache
中查詢是否快取過對應key的測量結果,如果能找到,那麼就簡單呼叫一下setMeasuredDimensionRaw
方法,將從快取中讀到的測量結果儲存到成員變數mMeasuredWidth
和mMeasuredHeight
中。如果不能從
mMeasureCache
中讀到快取過的測量結果,那麼這次View就真的不能再偷懶了,只能乖乖地呼叫onMeasure()
方法去完成實際的測量工作,並且將尺寸限制條件widthMeasureSpec
和heightMeasureSpec
傳遞給onMeasure()
方法。關於onMeasure()
方法,我們會在下面詳細介紹。不論上面程式碼走了哪個判斷的分支,最終View都會得到測量的結果,並且將結果儲存到
mMeasuredWidth
和mMeasuredHeight
這兩個成員變數中,同時快取到成員變數mMeasureCache
中,以便下次執行measure()
方法時能夠從其中讀取快取值。需要說明的是,View有一個成員變數
mPrivateFlags
,用以儲存View的各種狀態位,在測量開始前,會將其設定為未測量狀態,在測量完成後會將其設定為已測量狀態。
onMeasure()
上面我們提到,View的measure()
方法在需要進行實際的測量工作時會呼叫onMeasure()
方法.看下原始碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製程式碼
我們發現onMeasure()
方法中呼叫了setMeasuredDimension()
方法,setMeasuredDimension()
又呼叫了getDefaultSize()
方法.getDefaultSize()
又呼叫了getSuggestedMinimumWidth()
和getSuggestedMinimumHeight()
,那我們反向研究一下,先看下getSuggestedMinimumWidth()
方法(getSuggestedMinimumHeight()
原理getSuggestedMinimumWidth()
跟一樣).
getSuggestedMinimumWidth()
該方法返回View推薦的最小寬度,原始碼如下:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
複製程式碼
原始碼很簡單,如果View沒有背景,就直接返回View本身的最小寬度mMinWidth
;如果給View設定了背景,就取View本身的最小寬度mMinWidth
和背景的最小寬度的最大值.
那麼mMinWidth
是哪裡來的?搜尋下原始碼就可以知道,View的最小寬度mMinWidth
可以有兩種方式進行設定:
- 第一種是在View的構造方法中進行賦值的,View通過讀取XML檔案中View設定的
minWidth
屬性來為mMinWidth
賦值:
case R.styleable.View_minWidth:
mMinWidth = a.getDimensionPixelSize(attr, 0);
break;
複製程式碼
- 第二種是在呼叫View的
setMinimumWidth
方法為mMinWidth
賦值:
public void setMinimumWidth(int minWidth) {
mMinWidth = minWidth;
requestLayout();
}
複製程式碼
getDefaultSize()
知道了getSuggestedMinimumWidth()/getSuggestedMinimumHeight()
這兩個方法返回的是View的最小寬度/高度之後,我們將得到的最小寬度/高度值作為引數傳給getDefaultSize(int size, int measureSpec)
方法,看下原始碼:
public static int getDefaultSize(int size, int measureSpec) {
//size是傳進來的View自己想要的最小寬度/高度
int result = size;
//measureSpec是父ViewGroup給View的限制條件,解析出specMode和specSize
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//如果specMode為UNSPECIFIED,就表明父ViewGroup沒有對該View尺寸進行限制,直接取View自己想要的寬高
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//如果specMode為EXACTLY,表明父ViewGroup要求該View必須用父ViewGroup指定的尺寸(specSize),取父ViewGroup指定的寬高值
//如果specMode為AT_MOST,表明ViewGroup給該View指定了最大寬度/高度尺寸(specSize),取父ViewGroup指定的最大寬度/高度。
//這裡肯定有人有疑問了?為什麼specMode為AT_MOST是取View能到達的最大寬高值specSize,跟EXACTLY模式下的取值一模一樣,聯想到EXACTLY對應match_parent,AT_MOST對應wrap_content,那這樣wrap_content不就跟match_parent一樣的效果麼?是的,呼叫這個方法在測量的時候,wrap_content確實跟match_parent一樣的效果,這樣做有可能是Android還沒適配wrap_content而做的簡單處理,就像Recyclerview早期的版本就沒有適配wrap_content,導致wrap_content和match_parent一樣的效果,直到23.2.0版本才將match_parent和wrap_content區分開來。那適配了wrap_content的測量方法在哪裡呢?下文會講到。
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
複製程式碼
通過程式碼可以看到,父ViewGroup通過measureSpec
對View尺寸的限制作用已經體現出來了。最終通過該方法可以得到View在符合ViewGroup的限制條件下的預設尺寸,即
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
:獲得該View在符合ViewGroup的限制條件下的預設寬度值
getDefaultSize(getSuggestedMinimumHeight(), widthMeasureSpec)
:獲得該View在符合ViewGroup的限制條件下的預設高度值
從註釋可以看出,getDefaultSize()
這個測量方法並沒有適配wrap_content
這一種佈局模式,只是簡單地將wrap_content
跟match_parent
等同起來。
到了這裡,我們要注意一個問題,getDefaultSize()
方法中wrap_content
和match_parent
屬性的效果是一樣的,而該方法是View的onMeasure()
中預設呼叫的,也就是說,對於一個直接繼承自View的自定義View來說,它的wrap_content和match_parent屬性是一樣的效果,因此如果要實現自定義View的wrap_content
,則要重寫onMeasure()
方法,對wrap_content
屬性進行處理。如何處理呢?也很簡單,程式碼如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//取得父ViewGroup指定的寬高測量模式和尺寸
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
//如果寬高都是AT_MOST的話,即都是wrap_content佈局模式,就用View自己想要的寬高值
setMeasuredDimension(mWidth, mHeight);
}else if (widthSpecMode == MeasureSpec.AT_MOST) {
//如果只有寬度都是AT_MOST的話,即只有寬度是wrap_content佈局模式,寬度就用View自己想要的寬度值,高度就用父ViewGroup指定的高度值
setMeasuredDimension(mWidth, heightSpecSize);
}else if (heightSpecMode == MeasureSpec.AT_MOST) {
//如果只有高度都是AT_MOST的話,即只有高度是wrap_content佈局模式,高度就用View自己想要的寬度值,寬度就用父ViewGroup指定的高度值
setMeasuredDimension(widthSpecSize, mHeight);
}
}
複製程式碼
在上面的程式碼中,我們要給View指定一個預設的內部寬/高(mWidth
和mHeight
),並在wrap_content
時設定此寬/高即可。
setMeasuredDimension()
現在再來看下setMeasuredDimension()
這個方法,該方法將通過getDefaultSize()
得到的寬高值作為引數傳進去,看下原始碼都幹了些什麼:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
複製程式碼
該方法會在開始判斷layoutMode是不是LAYOUT_MODE_OPTICAL_BOUNDS
的特殊情況,這種特例很少見,我們直接忽略掉。
setMeasuredDimension()
方法最後將寬高值傳遞給方法setMeasuredDimensionRaw()
,我們再研究一下setMeasuredDimensionRaw()
這方法。
setMeasuredDimensionRaw()
該方法接受兩個引數,也就是測量完的寬度和高度,看原始碼:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
複製程式碼
看到了吧,這裡就是把測量完的寬高值賦值給mMeasuredWidth
、mMeasuredHeight
這兩個View的屬性,然後將標誌位置為已測量狀態。
View寬高尺寸值的state
至此,由父ViewGroup發起的向它內部的每個子View傳送measure命令,然後各個View根據ViewGroup給的限制條件測量出來的寬高尺寸已經存到View的mMeasuredWidth
、mMeasuredHeight
這兩個屬性當中。但ViewGroup怎麼知道他的子View是多大呢?View提供了以下三組方法:
getMeasuredWidth()
和getMeasuredHeight()
getMeasuredWidthAndState()
和getMeasuredHeightAndState()
getMeasuredState()
通過方法名稱可以猜出寬高的尺寸有state這個概念,我們再來研究View中儲存測量結果的屬性mMeasuredWidth
和mMeasuredHeight
,其實只要討論mMeasuredWidth
就可以了,mMeasuredHeight
一樣的道理。
mMeasuredWidth
是一個Int型別的值,其是由4個位元組組成的。
Android為讓其View的父控制元件獲取更多的資訊,就在mMeasuredWidth
上下了很大功夫,雖然是一個Int值,但是想讓它儲存更多資訊,具體來說就是把mMeasuredWidth
分成兩部分:
其高位的第一個位元組為第一部分,用於標記測量完的尺寸是不是達到了View想要的寬度,我們稱該資訊為測量的state資訊。
其低位的三個位元組為第二部分,用於儲存測量到的寬度。
一個變數能包含兩個資訊,這個有點類似於measureSpec
,但是二者又有不同:
measureSpec
是將限制條件從ViewGroup傳遞給其子View。mMeasuredWidth
、mMeasuredHeight
是將帶有測量結果的state標誌位資訊從View傳遞給其父ViewGroup。
那是在哪裡有對mMeasuredWidth
的第一個位元組進行處理呢?可以看到我們下面看一下View中的resolveSizeAndState()
方法。
resolveSizeAndState()
這是View一個很重要的測量方法,直接看原始碼:
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:
//當specMode為AT_MOST時,這時候specSize是父ViewGroup給該View指定的最大尺寸
if (specSize < size) {
//如果父ViewGroup指定的最大尺寸比View想要的尺寸還要小,這時候會使用MEASURED_STATE_TOO_SMALL這個掩碼向已經測量出來的尺寸specSize加入尺寸太小的標誌,然後將這個帶有標誌的specSize返回
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
//如果父控制元件指定最大尺寸沒有比View想要的尺寸小,這時候就放棄之前已經給View賦值的specSize,用View自己想要的尺寸就可以了。
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
複製程式碼
這個方法的程式碼結構跟前文提到的getDefaultSize()
方法很相似,主要的區別在於specMode
為AT_MOST的情況。我們當時說getDefaultSize()
方法是沒有適配wrap_content
這種情況,而這個resolveSizeAndState()
方法是已經適配了wrap_content
的佈局方式,那具體怎麼實現AT_MOST測量邏輯的呢?有兩種情況:
當父ViewGroup指定的最大尺寸比View想要的尺寸還要小時,會給這個父ViewGroup的指定的最大值
specSize
加入一個尺寸太小的標誌MEASURED_STATE_TOO_SMALL,然後將這個帶有標誌的尺寸返回,父ViewGroup通過該標誌就可以知道分配給View的空間太小了,在視窗協商測量的時候會根據這個標誌位來做視窗大小的決策。當父ViewGroup指定的最大尺寸比沒有比View想要的尺寸小時(相等或者View想要的尺寸更小),直接取View想要的尺寸,然後返回該尺寸。
getDefaultSize()
方法只是onMeasure()
方法中獲取最終尺寸的預設實現,其返回的資訊比resolveSizeAndState()
要少,那麼什麼時候才會呼叫resolveSizeAndState()
方法呢? 主要有兩種情況:
Android中的大部分layout類都呼叫了
resolveSizeAndState()
方法,比如LinearLayout在測量過程中會呼叫resolveSizeAndState()
方法而非getDefaultSize()
方法。我們自己在實現自定義的View或ViewGroup時,我們可以重寫
onMeasure()
方法,並在該方法內呼叫resolveSizeAndState()
方法。
getMeasureXXX系列方法
現在回過頭來看下以下三組方法:
getMeasuredWidth()
和getMeasuredHeight()
該組方法只返回測量結果的尺寸資訊,去除掉高位位元組的state資訊,以getMeasuredWidth()
為例,原始碼如下:
public final int getMeasuredWidth() {
// MEASURED_SIZE_MASK = 0x00ffffff,mMeasuredWidth與MEASURED_SIZE_MASK作與運算,
// 就能將mMeasuredWidth的最高位元組全部置0,從而去掉state資訊
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
複製程式碼
getMeasuredWidthAndState()
和getMeasuredHeightAndState()
該組方法返回測量結果同時包含尺寸和state資訊,以getMeasuredWidthAndState()
為例,原始碼如下:
public final int getMeasuredWidthAndState() {
//由於mMeasuredWidth完整包含了尺寸和state資訊,直接返回該資訊
return mMeasuredWidth;
}
複製程式碼
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位全部為1,低位元組的24位全部為0。
MEASURED_HEIGHT_STATE_SHIFT值為16。
將MEASURED_STATE_MASK與
mMeasuredWidth
做與操作之後就取出了儲存在寬度首位元組中的state資訊,過濾掉低位三個位元組的尺寸資訊。由於int有四個位元組,首位元組已經存了寬度的state資訊,那麼高度的state資訊就不能存在首位位元組。MEASURED_STATE_MASK向右移16位,變成了0x0000ff00,這個值與高度值
mMeasuredHeight
做與操作就取出了mMeasuredHeight
第三個位元組中的資訊。而mMeasuredHeight
的state資訊是存在首位元組中,所以也得對mMeasuredHeight
向右移相同的位置,這樣就把state資訊移到了第三個位元組中。最後,將得到的寬度state與高度state按位或操作,這樣就拼接成一個int值,該值首個位元組儲存寬度的state資訊,第三個位元組儲存高度的state資訊。
ViewGroup的measure過程
通過上面的介紹已經知道了單個View的測量過程,現在看下ViewGroup是怎樣測量的。
對於ViewGroup來說,除了完成自己的measure過程,還會遍歷去呼叫所有子元素的measure()
方法,各個子元素再遞迴去執行這個過程。和View不同的是,ViewGroup是一個抽象類,它提供了一個叫measureChildren()
的方法用於測量子元素,原始碼如下:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
//遍歷每個子元素,如果該子元素不是GONE的話,就去測量該子元素
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
複製程式碼
從上述程式碼來看,ViewGroup在measure時,會呼叫measureChild()
這個方法對每一個子元素進行測量,該方法原始碼如下:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//獲取child自身的LayoutParams屬性
final LayoutParams lp = child.getLayoutParams();
//根據父佈局的MeasureSpec,父佈局的padding和child的LayoutParams這三個引數,通過getChildMeasureSpec()方法計算出子元素的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//呼叫measure()方法測量child,前文已經解釋過這個方法,呼叫該方法之後會將view的寬高值儲存在mMeasuredWidth和mMeasuredHeight這兩個屬性當中,這樣child的尺寸就已經測量出來了
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製程式碼
很顯然,measureChild()
的思想就是取出子元素的LayoutParams,然後再通過getChildMeasureSpec()
方法來建立子元素的MeasureSpec,接著將MeasureSpec傳給View的measure()
方法來完成對子元素的測量。重點看下getChildMeasureSpec()
這個方法。
getChildMeasureSpec()
該方法是根據父容器的MeasureSpec、padding和子元素的LayoutParams屬性得到子元素的MeasureSpec,進而根據這個MeasureSpec來測量子元素。原始碼如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//取得SpecMode和SpecSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//子元素的可用大小為父容器的尺寸減去padding
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
//父容器是EXACTLY模式,表明父容器本身的尺寸已經是確定的了
case MeasureSpec.EXACTLY:
//childDimension是子元素的屬性值,如果大於等於0,就說明該子元素是指定寬/高尺寸的(比如20dp),
//因為MATCH_PARENT的值為-1,WRAP_CONTENT的值為-2,都是小於0的,所以大於等於0肯定是指定固定尺寸的。
//既然子元素都指定固定大小了,就直接取指定的尺寸,
//然後將子元素的測量模式定為EXACTLY模式,表明子元素的尺寸也確定了
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 如果子元素是MATCH_PARENT,也就是希望佔滿父容器的空間,那子元素的尺寸就取父容器的可用空間大小,模式也是EXACTLY,表明子元素的尺寸也確定了
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 如果子元素是WRAP_CONTENT,也就是寬/高希望能包裹自身的內容就可以了,
//但由於這時子元素自身還沒測量,無法知道自己想要多大的尺寸,
//所以這時就先取父容器給子元素留下的最大空間,模式為AT_MOST,表示子元素的寬/高不能超過該最大值
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器的尺寸還沒確定,但是不能超過最大值
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 子元素指定了大小,就取子元素的尺寸,模式為EXACTLY,表明該子元素確定了尺寸
// 這時父容器的限制對子元素來說是不起作用的,子元素的尺寸是可以超出了父容器的大小,超出的部分是顯示不出來的
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子元素是MATCH_PARENT,表明子元素希望佔滿父容器,
//但是父容器自身的大小還沒確定,也無法給子元素確切的尺寸,
//這時就先取父容器給子元素留下的最大空間,模式為AT_MOST,表示子元素不能超過該最大值
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子元素的尺寸只希望能包裹自身的內容就可以了,這時子元素還沒測量,無法知道具體尺寸,
// 就先取父容器給子元素留下的最大空間,模式為AT_MOST,表示子元素不能超過該最大值
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器沒有對子元素的大小進行約束
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子元素指定了大小,就取子元素的尺寸,模式為EXACTLY,表明該子元素確定了尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子元素想要佔滿父容器,先判斷下子元素是否需要取0,
// 如果不需要取0,就先取父容器給子元素留下的最大空間,模式為UNSPECIFIED,表示子元素並沒有受到約束
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子元素的尺寸只希望能包裹自身的內容就可以了,判斷下需不需要取0,
// 如果不需要就先取父容器給子元素留下的最大空間,模式為UNSPECIFIED,表示子元素並沒有受到約束
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//根據得到的大小和模式返回一個MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製程式碼
getChildMeasureSpec()
這個方法清楚展示了普通View的MeasureSpec的建立規則,每個View的MeasureSpec狀態量由其直接父View的MeasureSpec和View自身的屬性LayoutParams(LayoutParams有寬高尺寸值等資訊)共同決定。總結為下表:
View的佈局屬性��ViewGroup的MeasurSpec | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
非負具體值 | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
match_parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0/parentSize |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0/parentSize |
在得到View的MeasureSpec狀態後,將其與尺寸值通過makeMeasureSpec(int size,int mode)
方法結合在一起,就是最終傳給View的onMeasure(int, int)
方法的MeasureSpec值了。
檢視原始碼發現,ViewGroup並沒有定義其測量的具體過程方法,這是因為ViewGroup是一個抽象類,其測量過程的onMeasure()
方法需要各個子類去實現,比如LinearLayout、RelativeLayout、ListView等。為什麼ViewGroup不像View一樣對其onMeasure()
方法做統一的實現呢?那是因為不同的ViewGroup子類有不同的佈局特性,這導致它們的測量細節各不相同,因此ViewGroup無法做統一的實現。
需要注意的是,雖然View實現了onMeasure()
方法,但也只是一種預設實現,前面也提到過View的這種預設實現是不區分wrap_content
和match_parent
的,而View的子類如果需要支援區分實現這兩種佈局方式,就需要根據自身的特性自定義實現onMeasure()
方法,比如TextView、ImageView等就都實現了onMeasure()
方法,而且實現的方式各不相同,有興趣的同學可以去看下原始碼,這裡就不細講了。
DecorView和ViewRootImpl
本來關於View的measure過程到這裡已經介紹得七七八八了,但是為了更好的理解整個View樹結構的測量過程,這裡就先簡單提下DecorView和ViewRootImpl這兩個傢伙。
我們知道,Android介面上的View其實是一個View樹結構,而DecorView就是View樹的頂端,是檢視的頂級View,一般情況下它內部會包含一個豎直方向的LinearLayout,在這個LinearLayout裡面有上下兩個部分(具體情況和Android版本以及主題有關),上面是標題欄,下面是內容欄。我們在建立Activity時通過setContentView()
新增的佈局檔案其實就是被加到內容欄之中,而內容欄是一個id為content的FrameLayout,所以可以理解Activity指定佈局的方法不叫setView()
而叫setContentView()
了吧。
每一個Activity元件都有一個關聯的Window物件,用來描述一個應用程式視窗。每一個應用程式視窗內部又包含有一個View物件,用來描述應用程式視窗的檢視。在Activity建立完畢後,DecorView會被新增到Window中,之後我們才能在螢幕上看到應用程式的檢視效果。
而ViewRootImpl是連線WindowManager和DecorView的紐帶,控制元件的測量、佈局、繪製以及輸入事件的分發處理都由ViewRootImpl觸發。它是WindowManagerGlobal工作的實際實現者,因此它還需要負責與WMS互動通訊以調整視窗的位置大小,以及對來自WMS的事件(如視窗尺寸改變等)作出相應的處理。它呼叫了一個performTraversals()
方法使得View樹開始三大工作流程,然後使得View展現在我們面前。關鍵原始碼如下:
private void performTraversals() {
...
if (!mStopped || mReportNextDraw) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); // 1
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
}
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
}
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
}
...
}
複製程式碼
我們看到它裡面執行了三個方法,分別是performMeasure()
、performLayout()
、performDraw()
這三個方法,這三個方法分別完成DecorView的measure、layout、和draw這三大流程,其中performMeasure()
中會呼叫measure()
方法,在measure()
方法中又會呼叫onMeasure()
方法,在onMeasure()
方法中會對所有子元素進行measure過程,這個時候measure流程就從父容器傳遞到子元素中了,這樣就完成了一次measure過程。接著子元素會重複父容器的measure過程,如此反覆就實現了從DecorView開始對整個View樹的遍歷測量,measure過程就這樣完成了。同理,performLayout()
和performDraw()
也是類似的傳遞流程。針對performTraveals()
的大致流程,可以用以下流程圖來表示。
在performTraversals()
方法中,其實對於View樹的測量、佈局、繪製不是簡單地依次單次執行,以上的流程圖只是一個為了便於理解而簡化版的流程,真正的流程應該分為以下五個工作階段:
預測量階段:這是進入
performTraversals()
方法後的第一個階段,它會對View樹進行第一次測量。在此階段中將會計算出View樹為顯示其內容所需的尺寸,即期望的視窗尺寸。(呼叫measureHierarchy()
)佈局視窗階段:根據預測量的結果,通過
IWindowSession.relayout()
方法向WMS請求調整視窗的尺寸等屬性,這將引發WMS對視窗進行重新佈局,並將佈局結果返回給ViewRootImpl。(呼叫relayoutWindow()
)最終測量階段:預測量的結果是View樹所期望的視窗尺寸。然而由於在WMS中影響視窗布局的因素很多,WMS不一定會將視窗準確地佈局為View樹所要求的尺寸,而迫於WMS作為系統服務的強勢地位,View樹不得不接受WMS的佈局結果。因此在這一階段,
performTraversals()
將以視窗的實際尺寸對View樹進行最終測量。(呼叫performMeasure()
)佈局View樹階段:完成最終測量之後便可以對View樹進行佈局了。(呼叫
performLayout()
)繪製階段:這是performTraversals()的最終階段。確定了控制元件的位置與尺寸後,便可以對View樹進行繪製了。(呼叫
performDraw()
)
也就是說,實際上多了預測量階段和佈局視窗階段,這裡面還有很多可以講的,但本文主要是介紹View的measure過程,相關性不大的儘量少涉及,以免太過混亂。
預測量階段和最終測量階段都會至少完整測量一次View樹,這兩個階段的區別也只是引數不同而已。預測量階段用到了一個measureHierarchy()
方法,該方法傳入的引數desiredWindowWidth與desiredWindowHeight是期望的視窗尺寸。View樹本可以按照這兩個引數完成測量,但是measureHierarchy()
有自己的考量,即如何將視窗布局地儘可能地優雅。
這是針對將LayoutParams.width設定為了WRAP_CONTENT的懸浮視窗而言。如前文所述,在設定為WRAP_CONTENT時,指定的desiredWindowWidth是應用可用的最大寬度,如此可能會產生下面左圖所示的醜陋佈局。這種情況較容易發生在AlertDialog中,當AlertDialog需要顯示一條比較長的訊息時,由於給予的寬度足夠大,因此它有可能將這條訊息以一行顯示,並使得其視窗充滿了整個螢幕寬度,在橫屏模式下這種佈局尤為醜陋。
倘若能夠對可用寬度進行適當的限制,迫使AlertDialog將訊息換行顯示,則產生的佈局結果將會優雅得多,如圖下面右圖所示。但是,倘若不分清紅皁白地對寬度進行限制,當控制元件樹真正需要足夠的橫向空間時,會導致內容無法顯示完全,或者無法達到最佳的顯示效果。例如當一個懸浮視窗希望儘可能大地顯示一張照片時就會出現這樣的情況。
那麼measureHierarchy()
如何解決這個問呢?它採取了與View樹進行協商的辦法,即先使用measureHierarchy()
所期望的寬度限制嘗試對View樹進行測量,然後通過測量結果來檢查View樹是否能夠在此限制下滿足其充分顯示內容的要求。倘若沒能滿足,則measureHierarchy()
進行讓步,放寬對寬度的限制,然後再次進行測量,再做檢查。倘若仍不能滿足則再度進行讓步。
關鍵原始碼如下:
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
// 表示測量結果是否可能導致視窗的尺寸發生變化
boolean windowSizeMayChange = false;
//goodMeasure表示了測量是否能滿足View樹充分顯示內容的要求
boolean goodMeasure = false;
//測量協商僅發生在LayoutParams.width被指定為WRAP_CONTENT的情況下
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
//第一次協商。measureHierarchy()使用它最期望的寬度限制進行測量。
//這一寬度限制定義為一個系統資源。
//可以在frameworks/base/core/res/res/values/config.xml找到它的定義
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
// 寬度限制被存放在baseSize中
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//第一次測量。呼叫performMeasure()進行測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//View樹的測量結果可以通過mView的getmeasuredWidthAndState()方法獲取。
//View樹對這個測量結果不滿意,則會在返回值中新增MEASURED_STATE_TOO_SMALL位
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true; // 控制元件樹對測量結果滿意,測量完成
} else {
//第二次協商。上次的測量結果表明View樹認為measureHierarchy()給予的寬度太小,在此
//在此適當地放寬對寬度的限制,使用最大寬度與期望寬度的中間值作為寬度限制
baseSize = (baseSize+desiredWindowWidth)/2;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
//第二次測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 再次檢查控制元件樹是否滿足此次測量
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
// 控制元件樹對測量結果滿意,測量完成
goodMeasure = true;
}
}
}
}
if (!goodMeasure) {
//最終測量。當View樹對上述兩次協商的結果都不滿意時,measureHierarchy()放棄所有限制
//做最終測量。這一次將不再檢查控制元件樹是否滿意了,因為即便其不滿意,measurehierarchy()也沒
//有更多的空間供其使用了
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//如果測量結果與ViewRootImpl中當前的視窗尺寸不一致,則表明隨後可能有必要進行視窗尺寸的調整
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
// 返回視窗尺寸是否可能需要發生變化
return windowSizeMayChange;
}
複製程式碼
可以看到,measureHierarchy()
方法最終也是呼叫了performMeasure()
方法對View樹進行測量,只是多了協商測量的過程。
顯然,對於非懸浮視窗,即當LayoutParams.width被設定為MATCH_PARENT時,不存在協商過程,直接使用給定的desiredWindowWidth/Height進行測量即可。而對於懸浮視窗,measureHierarchy()
可以連續進行兩次讓步,從而導致View的onMeasure()
方法多次被呼叫。
這裡也看到,在View的measure過程中設定的MEASURED_STATE_TOO_SMALL標誌位就在測量協商過程中起作用了。
總結
看到了這裡,我們發現Android中View的measure過程是很巧妙的,知道如何利用以前測量過的資料,如果情況有變,那麼就呼叫onMeasure()
方法進行實際的測量工作。真正實現對View本身的測量就是在onMeasure()
中,在該方法中View要根據父ViewGroup給其傳遞進來的widthMeasureSpec和heightMeasureSpec,並結合View自身想要的尺寸,綜合考慮,計算出最終的寬度和高度,並儲存到相應的成員變數中,這才標誌著該View測量有效的完成了,如果沒有將值存入到成員變數中,View會丟擲異常。而且在成員變數中還儲藏著測量的狀態資訊state,該資訊表示了View對此次測量的結果是否滿意。而這個state資訊有可能會在ViewRootImpl在做視窗大小決策的時候提供反饋,從而達到最佳的顯示效果。