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#getMeasuredWidth
和View#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,其實就是獲取父容器剩餘的空間,並用resultSize和resultMode記錄子View最終的大小和測量模式。接下來,就需要依照具體的測量模式和子View的LayoutParams進行分析了。
在程式碼③的位置,父容器的測量模式為MeasureSpec.EXACTLY
,也就是說父容器的大小已經確定了,那麼我們只需要參考子View的LayoutParams就好了:
-
如果LayoutParams中的寬/高是一個確定的值(比如20dp這樣的形式),即childDimension>0,那就說明這是一個有著強烈自我意識的子View,它知道自己想要多大的空間。系統將充分尊重子View的需求,resultSize就將被賦值為子View宣告的寬/高(childDimension),測量模式也會被賦值為MeasureSpec.EXACTLY。當然,如果子View宣告的寬/高大於父容器剩餘的空間,最終顯示的時候超出的部分是會被裁剪的;
-
如果LayoutParams中的寬/高是
LayoutParams.MATCH_PARENT
,說明子View想要和父容器一樣大,那就將父容器剩餘的空間(size)賦給resultSize就好了。此時子View的寬/高依舊是確定的,測量模式同樣會被賦值為MeasureSpec.EXACTLY; -
如果LayoutParams中的寬/高是
LayoutParams.WRAP_CONTENT
,說明子View自己也不清楚想要多大的空間,那父容器也無可奈何。此時會將resultSize賦值為父容器剩餘的空間(size),並將測量模式賦值為MeasureSpec.AT_MOST,也就是為子View指定了一個最大可用的空間;
在程式碼④的位置,父容器的測量模式為MeasureSpec.AT_MOST
,也就是說父容器只知道自己可以使用的最大空間,並不知道精確的大小,接下來結合子View的LayoutParams進行講解:
-
如果LayoutParams中的寬/高是一個確定的值(childDimension>0),那就將resultSize賦值為子View宣告的寬/高(childDimension),測量模式也會被賦值為MeasureSpec.EXACTLY;
-
如果LayoutParams中的寬/高是
LayoutParams.MATCH_PARENT
,說明子View想要和父容器一樣大。但是此時父容器也不確定自己有多大,所以只能將resultSize賦值為父容器剩餘的空間(size),並將測量模式賦值為MeasureSpec.AT_MOST,也就是為子View指定了一個最大可用的空間; -
如果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過程的邏輯,可能學習效果會更好一點。