自定義View(一)View工作原理之測量 measure
在Android中,一個View繪製出來要經過三大流程,分別用measure來測量View的寬高,用layout來確定View在父容器中的位置,最終用draw將View繪製到螢幕上。本章節主要,通過自己的理解來講解一下第一個流程measure的相關知識點。
measure方法在View類中,在此方法中會 呼叫onMeasure方法,一般我們通過重寫onMeasure方法去自定義View的寬高。ViewGroup類中沒有實現onMeasure方法,所以在自定義ViewGroup的時候,我們要重寫onMeasure方法,在onMeasure方法中對其所有子元素進行measure過程,通過這種方式將測量行為傳遞子view,如此類推view的測量行為,從而完成整個View樹的測量工作。
講到view的measure行為,我們首先要一個重要的類MeasureSpec,這個類主要用來儲存測量模式SpecMode,規格大小SpecSize,通過一個32位的int值的高2位和低30去儲存它們的值。其中SpecMode有三種模式:UNSPECIFUED、EXACTLY、AT_MOST。其中我們主要理解EXACTLY和AT_MOST:
1. EXACTLY 表示父容器已經檢測出當前view的大小,就是view在SpecSize中所指定的值,它對應於LayoutParams中的match_parent和具體數值兩種模式
2. AT_MOST 表示當前view的大小不超過父容器的可用大小,它對應於LayoutParams中的wrap_content。
接下來我們解決一下ViewGroup的measure過程,雖然ViewGroup沒有實現onMeasure方法,但它提供了一些重要的方法用來測量child,先看一下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]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
從上述程式碼可以看出,ViewGroup會對每一個子元素進行measure,我們再看measureChild方法,程式碼如下
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上面程式碼中主要是通過呼叫getChildMeasureSpec方法來獲取子元素的測量值,然後通過child的measure方法將測量行為傳遞下去,我們看看getChildMeasureSpec方法的程式碼
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) { //父容器的測量模式
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY: //父容器精確模式
//子view是精確值
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
// 子view模式MATCH_PARENT,大小為父容器可用的最大空間
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
// 子view模式WRAP_CONTENT,大小為父容器可用的最大空間
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
//父容器AT_MOST模式
case MeasureSpec.AT_MOST:
if (childDimension >= 0) { //子view精確值
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
程式碼有點長,在這裡我們可以清晰地看到,child的MeasureSpec是怎麼來的,怎麼通過child的layoutparams值和父容器的MeasureSpec去計算得到當前child的MeasureSpec的,有一點要注意的是,預設的情況下,child的layoutparams值不管是match_parent還是wrap_content,其resultSize都是父容器可用空間的大小。
上面分析了ViewGroup的相關measure行為,主要是對子元素的測量,具體ViewGroup的大小該怎麼確定,這個要自定義的時候在onMeasure方法中計算,因為ViewGroup是一個view容器,所以它的寬高是很難有統一的計算方法,不同的情況計算方法又不一樣,所以ViewGroup沒有去實現onMeasure方法,留給繼承者去實現。
下面我們看一下View類的onMeasure方法,程式碼如下
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
可以看到,預設通過getDefaultSize去獲取view的測量大小,我們再看看getDefaultSize的程式碼
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
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;
}
我們只需要看AT_MOST和EXACTLY這兩種情況,所以其實此方法返回的就是specSize,而這個值就是view測量後的大小,因為specSize的值在AT_MOST的情形下,是等於父容器可用大小的,而如果在佈局中使用wrap_content,specSize大小和match_parent是一樣的,所以我們一般在自定義view的時候,對於warp_content的情形,都會給view指定一個自定義的大小,以免和match_parent一樣。
總結一下,通過上面的分析,我們知道view的measure流程大概是怎麼的一個過程,即我們首先會在ViewGroup的繼承類的onMeasure方法中,要分別去測量父容器的子view,具體怎麼通過測量出的child的寬高去計算ViewGroup的寬高,這個需要自己根據具體需求去實現。如果我們要子view中進行自定義的measure行為,就要重寫其onMeasure方法,根據需求是實現相關測量邏輯。整個測量流程是從ViewGroup到view的從上到下的行為,從而完成了我們整個view樹的測量工作。