MeasureSpec——View測量過程中的建橋
一.什麼是MeasureSpec
在View工作的流程中,measure過程決定view的寬高,在view的measure過程中,MeasureSpec起到了至關重要的作用,它參與了Measure的測量過程。
我們知道,一個view的寬高有時受到父容器的影響,在測量工程中,系統會將view的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,再根據這個MeasureSpec測量出View的寬高,所以View很大程度上決定了view的尺寸規格。所以這個很抽象的MeasureSpec我們可以把它理解爲“策略規格書”。
二.探索MeasureSpec
先看下View的內部類MeasureSpec的內容:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec代表一個32位的int值,通過SpecMode和SpecSize組合成一個int來節約內存,其中高2位代表SpecMode,指測量模式,低30位代表SpecSize,指該測量模式下的規格大小。通過getMode和getSize可以得到其中的值。
通過讓0,1,2向左移位30定義了三個常量作爲SpecMode的三種類型:
EXACTLY
檢查出view的精確大小,此時view的寬高就等於SpecSize的寬高。
AT_MOST
父容器指定了一個可用大小的SpecSize,view寬高不能大於這個值。
用於wrapcontent時,內容包裹大小,但view大小不會超過父容器大小。
UNSPECIFIED
對view不做任何限制,不常出現,多用於scrollview這種要顯示內容遠大於可顯示區域的情況。
二.MeasureSpec與LayoutParams
舉個例子,這是自定義view的時候重寫onMeasure中的一小段:
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = getPaddingTop() + mBound.height() + getPaddingBottom();
if(heightMode==MeasureSpec.AT_MOST){
height = Math.min(height,heightSize);
}
}
可以看出,這裏通過條件判斷SpecMode對view的高進行了約束,一般情況下,我們可以給view設置LayoutParams,比如寬高或者wrapcontent,細心的同學會發現重寫onMeasure的時候我們會發現傳了兩個參數,寬高的MeasureSpec,這兩個參數是伴隨父容器傳下來的,它會將LayoutParams在父容器的約束下轉換成對應的MeasureSpec,然後根據這個MS來確定View的寬高。
對於一般的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定。
那對於頂級View(DecorView)來說沒有上層父容器了,所以其MeasureSpec由窗口的尺寸和自身的LayoutParams來決定。
三.MeasureSpec的創建過程
頂級view的MeasureSpec的創建過程與普通view有所不同,MeasureSpec由窗口的尺寸和自身的LayoutParams來決定,很好理解,父容器=屏幕。這裏代碼就不貼了。
總來就說就是跟屏幕尺寸有關:
- 如果設置matchparent:大小就是屏幕大小。
- wrapcontent:內容包裹,大小不能超過屏幕大小。
- 固定大小(android:layout_height=”100dp”):指定多大就是多大。
那麼對於普通View來說,measure過程由viewGroup傳遞過來,
viewGroup中的measureChildWithMargin方法:
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方法
}
注意child.measure傳的參數就是我們之前所說的重寫onMeasure方法得到的那兩個參數,通過getChildMeasureSpec方法(注意傳的參數)來得到子元素的MeasureSpec,所以解釋了爲什麼子元素MeasureSpec的創建與父容器的的MeasureSpec和子元素和子元素本身的LayoutParams有關,還與View的margin和padding有關:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//size爲子容器可用大小,爲父容器減去padding的大小
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
......//省略部分
}
上述measureChildWithMargin方法傳遞了參數爲父容器的MeasureSpec和子容器的padding=mPaddingTop+mPaddingBottom+lp.topMargin+lp.bottomMargin+ heightUsed(還有一組寬度),通過父容器的MeasureSpec結合view本身的LayoutParams來確定子元素的MeasureSpec。
那麼總結來說:
- view採用固定模式:
- 不管父容器的SpecMode是什麼,view的MeasureSpec都是EXACTLY,大小就是開發者設置的大小。
- View是matchparent:
- 父容器爲EXACTLY,那麼view也是EXACTLY,大小是父容器的剩餘空間。
- 父容器是AT_MOST,view也是AT_MOST,大小不能超過父容器的剩餘大小。
- view是wrapcontent:
- 不管父容器爲什麼,view的模式總是AT_MOST,大小不超過父容器的剩餘大小。
- view爲UNSPECIFIED:
- 理論沒有限制,可以無限大。