1、簡介
組件在顯示到界面上要經過三個過程測量、布局、繪制。測量就是計算組件的大小,布局就是擺放組件的位置,而繪制就是把計算好的大小和位置繪制到畫布上。下面就在說說組件測量的那些事。組件的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
2、組件測量的3種類型
widthMeasureSpec和heightMeasureSpec是什么呢?他是一個測量規則,一個32位int值,前兩位表示測量的模式,低30位表示測量的大小。組件也提供對應的方法來獲取他們的值。
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int height = MeasureSpec.getSize(heightMeasureSpec);
通過size和mode也可以生成對應的測量規則
MeasureSpec.makeMeasureSpec(resultSize, resultMode);
測量模式有3種
EXACTLY
精確模式,當組件的長寬是具體值,如:
android:layout_width="200dp"
或是指定為match_parent(占據父view的大小),系統使用這個模式。
AT_MOST
最大值模式,當控件的長寬屬性為warp_content時,組件的大小隨自身的大小變化而變化,并且控件的尺寸不能超過父view允許最大尺寸。
UNSPECIFIED
不指定組件的大小,組件想多大就多大,通常在繪制自定義view時才會使用。
3、從源碼來看組件測量過程
先來看View源碼里的測量方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension方法接受測量后的長和寬,并賦值給組件,這樣組件就有長寬。那如何計算長寬呢,看下面的代碼:
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;
}
getDefaultSize方法接收兩個參數,一個是大小(系統傳入的是組件的最小寬度或高度getSuggestedMinimumWidth()),一個是測量的規則。
通過測量規則獲得測量的模式和指定的大小,
如果測量模式為MeasureSpec.UNSPECIFIED,則返回傳入的大小為組件大小,及最小寬高;
如果測量模式為MeasureSpec.AT_MOST或MeasureSpec.EXACTLY,則返回父視圖指定的大小。
再來看下ViewGroup中的測量實現,要實現ViewGroup的測量必須實現其子類的測量,但是ViewGroup中并沒有重寫onMeasure方法,需要具體實現類自己重寫,但是他提供一些方法來便于我們測量子視圖。
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);
}
}
}
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);
}
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
對子視圖的測量,實際上就是遍歷子視圖,然后調用他們measure方法,measure里會再調用onMeasure方法執行視圖的測量。這里的關鍵實際上就是計算出子視圖的測量規則measureSpec,及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;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
這個方法接收3個參數
spec,父視圖的測量規則,可以計算出父視圖的測量模式和測量大小
padding,用于計算扣除padding后的視圖大小
childDimension,視圖布局中定義的寬高大小
通過不同的模式來計算出子視圖的大小和模式從而轉換為測量規則
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:
...
break;
case MeasureSpec.AT_MOST:
...
break;
case MeasureSpec.UNSPECIFIED:
...
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
父視圖為精確模式
1、子視圖有定義具體的大小,resultSize為定義的具體大小,模式為MeasureSpec.EXACTLY
2、子視圖的childDimension為LayoutParams.MATCH_PARENT,resultSize為定義的具體大小,模式為MeasureSpec.EXACTLY
3、子視圖的childDimension為LayoutParams.WRAP_CONTEN,resultSize為定義的具體大小,模式為MeasureSpec.AT_MOST
其他兩種情況也是類似,源碼中已經描述的很清楚了。最后調用MeasureSpec.makeMeasureSpec(resultSize, resultMode);方法就獲得了子視圖的測量規則。
再回顧前面說的View的測量,就能確定具體的View的測量大小。視圖的整個測量過程就是這樣,關鍵還是在要理解清楚MeasureSpec測量規則的使用。
4、測量在實際開發中的應用
具有最大高度的滾動視圖
通過計算ScrollView子視圖的高度,超過設置的最大高度,則設置Scrollview的高度為最大高度,如果沒有超過,則按子視圖的高度來顯示scrollview。
具體的代碼如下:
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int mMaxHeight = height > maxHeight ? maxHeight : height;
View child = getChildAt(0);
int child_height = child.getHeight();
if (child_height == 0)
child_height = mMaxHeight;
else if (child_height > maxHeight)
child_height = maxHeight;
setMeasuredDimension(width, child_height);
}
5、注意事項
1、要區分getMeasuredHeight()與getHeight()的區別,getMeasuredHeight()是測量時的大小,組件有可能會測量多次,多次測量的大小有可能不一樣,getHeight()是要繪制的大小
2、視圖只有測量過后,getMeasuredHeight()方法才會有值,不然返回的是0