MeasureSpec
基本概念
MeasureSpec參與了View的measure過程,系統(tǒng)根據(jù)MeasureSpec來測量出View的測量寬/高
- 對于DecorView,其MeasureSpec是由窗口尺寸的大小和自身的LayoutParams來共同確定的
- 對于普通的View,是由父容器的MeasureSpec和自身的LayoutParams共同決定的
一個MeasureSpec由MeasureMode和MeasureSize組成,分別指測量模式和規(guī)格大小。
public static class MeasureSpec {
//偏移量
private static final int MODE_SHIFT = 30;
//1100 0000 0000 0000 0000 0000 0000 0000
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;
//合并SpecSize和SpecMode生成MeasureSpec
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) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
從上面的源碼可以看到MeasureSpec是一個32位int值,高兩位代表SpecMode,低30位代表SpecSize。SpecMode有三種模式:
- UNSPECIFIED:父容器不對View有任何的限制,要多大給多大,這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量的狀態(tài)。
- EXACTLY:父容器已經(jīng)檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize指定的值。他對應(yīng)于LayoutParams中的match_parent和具體的數(shù)值這兩種情況。
- AT_MOST:父容器指定了一個可用大小,View的大小不能超過父容器的SpecSize。具體的值要看不同View的具體實現(xiàn),它對應(yīng)于LayoutParams中的wrap_content。
DecorView的MeasureSpec創(chuàng)建過程
在ViewRootImpl中的measureHierarchy方法中有如下一段代碼:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
其中傳入的參數(shù)desiredWindowWidth、desiredWindowHeight是屏幕的寬度和高度,接著再看下getRootMeasureSpec方法的實現(xiàn):
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
根據(jù)LayoutParams中的寬/高的參數(shù)來劃分:
- LayoutParams.MATCH_PARENT:精確模式,大小就是窗口的大??;
- LayoutParams.WTAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小。
- 固定大?。壕_模式,大小為LayoutParams中指定的大小。
普通View的MeasureSpec創(chuàng)建過程
這里的View指的是我們布局中的View,View的measure過程由ViewGroup傳遞而來,先看下ViewGroup的measureChildWithMargins方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//獲取子元素的布局參數(shù)
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);
}
從上面的代碼看出,子元素的MeasureSpec的創(chuàng)建與父容器的MeasureSpec和子元素本身的LayoutParams有關(guān)。得到子元素MeasureSpec的具體情況可以看下getChildMeasureSpec()方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//獲取子view的真實寬度
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 若當前view的SpecMode是EXACTLY
case MeasureSpec.EXACTLY:
//寬/高是固定的數(shù)據(jù)
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) {
//如果設(shè)置了sUseZeroUnspecifiedMeasureSpec,大小就是0
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;
}
//合并得到MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述代碼主要作用是根據(jù)父容器的MeasureSpec同時結(jié)合View本身的LayoutParams來確定子元素的MeasureSpec。
| child布局參數(shù)/父元素Mode | EXACTLY | AT_MOST | USPECIFIED |
|---|---|---|---|
| dp\px | EXACTLY childSize |
EXACTLY childSize |
EXACTLY childSize |
| match_parent | EXACTLY parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
| wrap_content | AT_MOST parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
總結(jié)
- Activity對象創(chuàng)建完畢后,會將DecorView添加到Window中,同時創(chuàng)建ViewRootImpl對象與DecorView創(chuàng)建聯(lián)系
- View的繪制流程從ViewRootImpl的PerformTraversals()方法開始。performTraversals()先計算出窗口的大小,再通過getRootMeasure()方法,計算出DecorView的寬高,傳入performMeasure()方法中
private void performTraversals() {
...
if (!mStopped) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
// 獲取測量規(guī)格,mWidth 和 mHeight 當前視圖 frame 的大小
// lp是WindowManager.LayoutParams
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
...
}
}
- 開始view的measure,起作用的其實是onMeasure()方法
View的measure過程
View的measure方法中回去調(diào)用View的onMeasure方法,如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension()會設(shè)置View寬高的測量值
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這兩種情況時,返回的值都是measureSpec的specSize,specSize是測量后的大小。
UNSPECIFIED這種情況,一般用于系統(tǒng)內(nèi)部的測量,在這種情況下,View的大小為getDefaultSize的第一個參數(shù)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());
}
這里判斷了View是否設(shè)置了背景,若設(shè)置了背景,View的寬度為mMinWidth。mMinWidth對應(yīng)于android:minWidth這個屬性所指的值,若不設(shè)置默認為0。如果View設(shè)置了背景,則View寬度為max(mMinHeight, mBackground.getMinimumHeight()):
public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight();
return intrinsicHeight > 0 ? intrinsicHeight : 0;
}
//返回drawable內(nèi)在高度,如果drawable沒有內(nèi)在高度返回-1
public int getIntrinsicHeight() {
return -1;
}
根據(jù)上述代碼,得到getMinimumHeight()返回的就是drawable的原始高度,前提是drawable有原始高度,沒有就返回0。例如:ShapeDrawable無原始寬/高,BitmapDrawable有原始寬/高。
wrap_content和自定義問題
從getDefaultSize()方法來看,若按照onMeasure的默認方法實現(xiàn),當寬/高設(shè)置成wrap_content或match_parent時,最后的結(jié)果都是specSize,也就是兩種情況下都是match_parent的效果。所以在自定義view下的時候需要對wrap_content進行處理。
在自定義view的時候,我們通過判定寬高是否是wrap_content來給寬/高是wrap_content的情況設(shè)定一個默認的內(nèi)部寬/高。像TextView、ImageView都對這里做了處理。
View的Measure過程總結(jié)
- 從ViewGroup#measureChildWidthMargins開始,計算View的MeasureSpec
- measureChildWidthMargins()調(diào)用child.measure()開始測量過程
- View#measure()調(diào)用onMeasure()
- onMeasure()方法測量View寬高
- 根據(jù)是否有背景圖來確定View的默認大小
- 根據(jù)SpecMode來確定View測量后的大小是SpecSize還是默認大小
- onMeasure()測量后的數(shù)據(jù)給全局變量mMeasuredWidth和mMeasuredHeight
ViewGroup的measure過程
對于ViewGroup來說,除了完成自己的measure過程外,還會遍歷去調(diào)用所有子元素的measure方法,各個子元素再去遞歸執(zhí)行這個過程。
ViewGroup#measureChildren
遍歷子view調(diào)用measureChild()
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];
//當child不是GONE時,就去測量
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
ViewGroup#measureChild()
- 前面提到過的measureChildWithMargins()中的getChildMeasureSpec(),開始測量
- 調(diào)用子view向下遞歸測量
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);
}
ViewGroup沒有定義測量的具體過程,因為ViewGroup是一個抽象類,其測量過程的onMeasure方法需要各個子類去具體實現(xiàn)。
獲得View寬高的四種方法
在實際操作中我們可能回去獲取某個view的寬和高,但是在onCreate() onResume()方法中一般是獲取不到的,因為View的measure過程和Activity生命周期方法不是同步執(zhí)行的。
1.Activity/View#onWindowFocusChanged
onWindowFocusChanged這個方法的含義是:View已經(jīng)初始化完畢了,寬高已經(jīng)準備好了。當Activity窗口得到焦點和失去焦點是均會被調(diào)用一次,當頻繁進行onPause和onPause,會被頻繁調(diào)用。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
2.view.post(runnable)
通過post可以將一個runnable投遞到消息隊列的尾部,然后等待Looper調(diào)用此runnable的時候,View也已經(jīng)初始化好了
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
3.ViewTreeObserver
可以通過ViewTreeObserver添加監(jiān)聽來監(jiān)聽View的各種動態(tài)。onGlobalLayout()會在View樹發(fā)生改變或是View樹內(nèi)部的View的可見性發(fā)生改變時調(diào)用。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
4.手動measure(view.measure(int widthMeasureSpec,int heightMeasureSpec))
對view進行measure得到view的寬 高,需要根據(jù)view的LayoutParams來分:
match_parent
無法measure出具體寬高。因為要測量出當前view尺寸,需要知道父容器的屬于空間,無法知道parentSize。
wrap_content
((1 << 30) - 1)是specMode支持的最大值
int widthSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
具體數(shù)值
LayoutParams lp = view.getLayoutParams();
int widthSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(lp.height, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);