自定義View之onMeasure()方法
前言
一個View從建立到被繪製到螢幕上,需要完成measure(測量)、layout(佈置)、draw(繪製)三個步驟,分別對應View中的measure()、layout()、draw()三個方法。網上關於這三個方法的原始碼解析文章有很多,而且一般情況下也不會去重寫它們(measure()方法還無法覆蓋),因此本文不打算將其作為重點。本文以及接下來的幾篇文章會詳細介紹和程式設計人員關係更大的onMeasure()、onLayout()與onDraw()的具體實現方法,以及過程中會涉及到的一些知識。
MeasureSpec
View中的onMeasure()方法是這樣的:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...}
- 1
可以看到,它的引數是widthMeasureSpec與heightMeasureSpec兩個int值。這兩個引數實質上是由View的靜態內部類MeasureSpec管理的兩個特殊的“物件”(並非真正的物件),包含了父view關於子view應當如何測量自身給出的“指示”。為了提升效率,Android系統採用位運算的方式,將模式SpecMode(2位)與尺寸SpecSize(30位)拼接成了一個int值,並傳遞這個int值作為測量時使用的引數。
MeasureSpec類的實現基本都是依靠位運算,沒什麼實質性內容。直接上一些結論:
(1)SpecMode分三種:UNSPECIFIED、EXACTLY、AT_MOST。UNSPECIFIED表示不指定具體測量模式,EXACTLY表示父View希望子view的尺寸取精確值(即等於SpecSize),AT_MOST表示父View希望子view的尺寸不超過SpecSize。
(2)使用MeasureSpec.getMode(int measureSpec)與MeasureSpec.getSize(int measureSpec)獲取SpecMode與SpecSize。
(3)使用MeasureSpec.makeMeasureSpec(int size,int mode)生成一個measureSpec值。
onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法
下面回到onMeasure()方法。顧名思義,這個方法是在該view需要測量自身時呼叫的。具體來說,當這個view的父view對其呼叫measure()方法時,onMeasure()方法會在過程中被呼叫。下面分別看看View與ViewGroup分別應當怎麼實現這個方法。
View的onMeasure()實現
view只需要根據自身情況,計算出自己的尺寸就可以了。步驟如下:
(1)使用MeasureSpec.getMode()與MeasureSpec.getSize()獲取父view要求的SpecMode與SpecSize。
(2)根據上面的引數確定自己的實際尺寸(width與height)。一般來說,如果SpecMode是EXACTLY,那麼直接取尺寸值=SpecSize即可。如果SpecMode是AT_MOST,那麼就需要根據自身特點計算出一個尺寸值,並保證最終尺寸值不超過SpecSize。當然了,你也可以完全無視父view的要求,自顧自地進行測量,不過這種方式顯然是不推薦的。
(3)使用setMeasuredDimension(int measuredWidth, int measuredHeight)設定最終測量尺寸。這個方法被呼叫之後,view的getMeasuredWidth()方法與getMeasuredHeight()方法才能生效(在之前呼叫會返回0)。
實際上,對於不那麼複雜的自定義view,View類提供的預設實現已經可以滿足大部分需求了。下面看看它是怎麼做的:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
getSuggestedMinimumWidth()與getSuggestedMinimumHeight()是根據view是否設定了BackgroundDrawable確定一個最小尺寸值。重點看一下getDefaultSize()方法:
public static int getDefaultSize(int size, int measureSpec) {
//size是view根據自身需求提供的一個尺寸值,measureSpec來自父view。
//最終尺寸值
int result = size;
//獲取父佈局要求的測量模式與測量尺寸
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//如果模式為UNSPECIFIED則不作限制
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//AT_MOST直接當做EXACTLY處理
case MeasureSpec.AT_MOST:
//如果模式為EXACTLY,則根據父佈局要求確定最終尺寸值
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
解釋見程式碼註釋。這裡需要記住的是,如果使用這個方法計算尺寸值的話,AT_MOST模式不會生效。AT_MOST一般是在view的layout_width與layout_height為WRAP_CONTENT時使用的。因此,如果想要自定義view支援WRAP_CONTENT屬性,就必須自己對AT_MOST的情況作出處理。
ViewGroup的onMeasure()實現
不同於View,ViewGroup需要負責子view的測量。具體來講,就是為子view提供合適的MeasureSpec,並呼叫子view的measure()(注意,不是onMeasure())方法。至於自身的尺寸,則需要結合更高一層的父view的指示以及子view的情況來確定。
為了給子view提供合適的MeasureSpec,ViewGroup中提供了一個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:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} 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
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 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);
}
英文註釋是自帶的,已經很詳細了。引數spec是高層view提供的MeasureSpec,padding為需要扣除的padding部分尺寸,childDimension為子view需求的尺寸(一般直接傳MarginLayoutParams.width)。實質上,這個方法就是綜合考慮了高層view的指示以及低層view的需求,分9種情況構建了一個合適的MeasureSpec。下面的圖來自Android View系統解析(下) ,任玉剛:
總結
關於onMeasure()方法的實現差不多就這些內容了。可以看出,MeasureSpec是父view與子view溝通的橋樑。實現onMeasure()方法的關鍵點就在於如何響應父view的MeasureSpec,以及如何為子view構建合適的MeasureSpec。