前言
從上一篇中。同Activity的布局加載了解了整個(gè)View樹加載的流程。最后是通過View的三大流程來實(shí)現(xiàn)布局的顯示的。那么我們這篇來講下布局的三大流程之一-->measure。
1.MeasureSpec
在講解測量之前我們要先清楚什么是MeasureSpec?MeasureSpaec可以理解為測量規(guī)格。在View.measure()中多次被用到。它是有一個(gè)32位的int值,高2位代表SpecMode(指測量模式),低30位代表SpecSize(在指定模式下的規(guī)格大小)。
他們對應(yīng)的二進(jìn)制值分別是:
UNSPECIFIED=00000000000000000000000000000000
EXACTLY =01000000000000000000000000000000
AT_MOST =10000000000000000000000000000000
由于最前面兩位代表模式,所以他們分別對應(yīng)十進(jìn)制的0,1,2;
在測量中,會(huì)根據(jù)子View的LayoutParames與父容器的MeasureSpec的規(guī)格來生成子View的MeasureSpec然后根據(jù)它來測量出View的寬/高。所以這個(gè)概念該是非常重要的。下面我來看下它的具體模式的含義。
| SpecMode | SpecSize |
|---|---|
| MeasureSpec.UNSPECIFIED | 不確定模式:子視圖View請求多大就是多大,父容器不限制其大小范圍,一般用于系統(tǒng)內(nèi)部 |
| MeasureSpec.EXACTLY | 精確模式,父容器已經(jīng)檢測View所需要的精確大小,View的最終大小就SpecSize所指定的值。對應(yīng)Layout中的match_parent和具體的數(shù)值兩種模式 |
| MeasureSpec.AT_MOST | 最大模式,父容器制定一個(gè)可用大小SpecSize,子View不能大于這個(gè)值。對應(yīng)LayoutParames中的warp_content |
2.View#measure()
measure測量分成兩種一種是原始View,那么通過measure方法就完成了其自己的測量,如果是ViewGroup,除了完成自己的測量完還要遍歷子元素的measure方法,各個(gè)子元素如果是View就測量自己,如果是ViewGroup就接著遍歷,最后都是調(diào)用View的measure。
因?yàn)閂iew是所有View與ViewGroup的老祖宗,那么我們先拋開ViewGroup先直接來了解下View.measure()方法:
int mOldWidthMeasureSpec = Integer.MIN_VALUE;
int mOldHeightMeasureSpec = Integer.MIN_VALUE;
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
..................
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
//如果上一次的測量規(guī)格和這次不一樣,則條件滿足,重新測量視圖View的大小
if (forceLayout || needsLayout) {
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
這段代碼比較簡單,首先判斷與上次測量的MeasureSpec是否相等,不等就重新測量??梢钥吹酱朔椒ㄕ{(diào)用了onMeasure()方法,并將傳來的值直接傳遞下去,那么就說明測量的主要的邏輯都在此方法中,我們跟往下走View#onMeasure():
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
通過注釋我們知道,參數(shù)中的MeasureSpec是父布局給我們傳遞過來的。這點(diǎn)我們要清楚。這段代碼看起來比較簡單,但是實(shí)際理解起來卻不容易。可以看到在onMeasure()只調(diào)用了setMeasuredDimension();就結(jié)束了了。那么我們就可以知道當(dāng)盜用此方法的時(shí)候就證明測量流程結(jié)束。那么我們來看下他里面參數(shù)分別是measuredWidth,measuredHeight。并通過getDefaultSize()方法來計(jì)算的,進(jìn)入此方法:
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view 這個(gè)view的默認(rèn)尺寸大小
* @param measureSpec Constraints imposed by the parent 這個(gè)參數(shù)是父容器提供的子View的MeasureSpec
* @return The size this view should be. 這個(gè)view在經(jīng)過此方法后返回的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;
}
該方法的作用是根據(jù)View默認(rèn)大小的寬高和父View傳遞的測量規(guī)格重新計(jì)算View的測量寬高。下面我們進(jìn)入getSuggestedMinimumWidth()方法看看是如何獲得View的尺寸大小的:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果View沒有設(shè)置背景那么返回mMinWidth(它對應(yīng)XML中的android:minWidth,如果不指定默認(rèn)為0),如果設(shè)置了背景就為Drawable的原始高度。
總結(jié): 在通常情況下我們沒有設(shè)置android:minWidth屬性,那么getDefaultSize()的返回值就為specSize(父容器提供的)那我們通過getDefaultSize()方法知道了,在自定義View的時(shí)候如果直接繼承View要重寫onMeasure()方法,否者warp_content和match_parent效果相同
sizeSpec大小是有父容器決定的,我們由上篇文章知道知道父容器DecorView的測量模式是MeasureSpec.EXACTLY,測量大小sizeSpec是整個(gè)屏幕的大小。
到這里我們就把View的繪制流程梳理完成了。下面我們就接著上篇講解從performTraversals()方法觸發(fā)查看view的三大流程。
3.ViewGroup測量
從上一篇文章我們知道頂級View(DecorView)繼承FrameLayout(ViewGroup)。那我們繼續(xù)上一篇的中performTraversals()方法中的performMeasure()測量這個(gè)方法看下:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
那么我們進(jìn)入到FrameLayout(ViewGroup),為了完全理清流程我們先來看下它父類ViewGroup#onMearsure()方法發(fā)現(xiàn)ViewGroup是一個(gè)抽象類,它里面沒有實(shí)現(xiàn)onMearsure(),這也能理解,因?yàn)閂iewGroup是所有空間容器的父類,具體的測量方式應(yīng)該是子類容器控件實(shí)現(xiàn)的。比如LinearLayout與RelativeLayout他們的方法都是不一樣的。但是它有一下兩個(gè)方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed)
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)
在measureChildren方法中會(huì)循環(huán)遍歷子View,然后調(diào)用measureChild()方法,通過對measureChild()與measureChildWithMargins()方法的比較發(fā)現(xiàn)兩者基本相同,只不過后者加入了邊距的運(yùn)算。不管那種方法最后都會(huì)調(diào)用child.measure(childWidthMeasureSpec, childHeightMeasureSpec);方法也就是View.measure(),之后就會(huì)走View#measure流程。那么我們現(xiàn)在回過頭來進(jìn)入FrameLayout#onMearsure()方法看他是如何實(shí)現(xiàn)的:
·
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//獲取容器下的所有子空間
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
//遍歷所有子控件,將子控件取出來
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//測量子控件
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
// //設(shè)置當(dāng)前FrameLayout測量結(jié)果,此方法的調(diào)用表示當(dāng)前View測量的結(jié)束。
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
......
上面講到在調(diào)用setMeasuredDimension()方法后就表示測量完成了,所以我們主要看它上面的代碼。首先取出容器下的所有子控件,然后調(diào)用 measureChildWithMargins();方法測量每個(gè)子控件。如下:
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);
}
上面的方法先取出子View的LayoutParams,然后通過getChildMeasureSpec()方法來得到子View的MeasureSpec,最后調(diào)用View.measure()完成測量。子View還是ViewGroup繼續(xù)走ViewGroup的測量,如果是子控件是View就會(huì)測量自己完成整個(gè)測量過程。那么我在跟蹤代碼看下getChildMeasureSpec()方法做了什么:
/
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
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:
//子View的寬/高是具體的值
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
//MeasureSpec.makeMeasureSpec--> 根據(jù)大小個(gè)模式生成一個(gè)MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
這個(gè)方法首先獲取了當(dāng)前DecorView容器的測量模式,然后減去傳進(jìn)來的padding參數(shù),得到一個(gè)子元素可用的大小size,代碼如下:
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);//(padding表示不可用的范圍,由上面的代碼可知padding=當(dāng)前容器(FrameLayout)的padding+子元素的margins)
有上一篇我們知道我們DecorView是match_parent,所以直接看MeasureSpec.EXACTLY:分支,其他分支是一樣的,通過觀察我們可以將上面MeasureSpec.EXACTLY:分支下的三個(gè)if語句總結(jié)如下:
- LayoutParams.MATCH_PARENT(精確模式): 當(dāng)子View寬/高為LayoutParams.MATCH_PARENT 大小就是父容器大小。
- LayoutParams.WRAP_CONTENT(最大模式): 大小不定,但是不能超過父容器的大小。
- 固定模式(比如100dp)(精確模式): 大小為LayoutParams指定大小。(這里注意如果你的大小超過窗口大小比如1200dp,那么你的大小就是1200dp,雖然窗口顯示不下,但是窗口會(huì)能顯示多少顯示多少)
總結(jié):
-
在 1.MeasurseSpec 中我們講到這么一句話在測量中,會(huì)根據(jù)子View的LayoutParames與父容器的MeasureSpec的規(guī)格來生成子View的MeasureSpec然后根據(jù)它來測量出View的寬/高。 通過上面的代碼和下面的總結(jié),現(xiàn)在我們在來理解這句話就很好理解了。
- measureChildWithMargins():子元素的MeasureSpec的創(chuàng)建與父容器的MeasureSpec和子元素本省的LayoutParames以及父容器的padding與子元素的margins決定。
- getChildMeasureSpec():通過傳入子元素LayoutParames(也就是XML中寬/高所具體定義的),來決定自己的MearsureSpec。
-
對于頂級View(即DecorView)和普通View來說MearsureSpec轉(zhuǎn)換過程略有不同,對于DecorView,其MearsureSpec是窗口尺寸和其自己的LayoutParames共同決定,對于普通View,MearsureSpec是由父容器的MearsureSpec和自身的LayoutParames共同決定。同時(shí)對于普通View針對不同的父容器和View本身不同的LayoutParames,View就可以有多重MeasureSpec具體不同參照下表:
圖片.png
普通View的MeasureSpec的創(chuàng)建規(guī)則 (此圖來自Android開發(fā)藝術(shù)探索)
-
4.頂級DecorView測量
對于好奇的小伙伴可能會(huì)問:上面提到DecorView的MearsureSpec是窗口尺寸和其自己的LayoutParames共同決定。那么是如何決定的呢?
其實(shí)在ViewRootImpl中的measureHierarchy中展示了MeasureSpec創(chuàng)建過程(此方法在performTraversals()被調(diào)用同時(shí)在三大流程之前):
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);//1139
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
getRootMeasureSpec()方法中的一個(gè)參數(shù)就是窗口的尺寸大小,第二個(gè)就是當(dāng)前View(頂級DecorView)的LayoutParames,他的方法如下:
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;
}
與getChildMeasureSpec的原理是一樣的具體看getChildMeasureSpec關(guān)于固定大小、精確模式、最大模式總結(jié)。我認(rèn)為在測量時(shí)先經(jīng)過ViewRootImpl#measureHierarchy方法測量出DecorView的
5.整個(gè)View三大流程之測量概括總結(jié)
上面我們把整個(gè)View的測量相關(guān)流程基本上都濾清了關(guān)于這些純概念源碼的東西看著乏味,也不容易理解,偏底層也沒有什么程序運(yùn)行效果。那么我用流程圖來梳理下整個(gè)流程:

結(jié)語
View的測量基本上就是這樣了。通過本章的學(xué)習(xí),我們應(yīng)該掌握測量的流程和里面重要的方法,這樣我們在自定義View的時(shí)候才會(huì)更的得心應(yīng)手。希望這篇文章對大家有所幫助。如果有錯(cuò)誤希望可以指出,覺得對你有所幫助就支持下,關(guān)注走一波!下篇View的布局(layout)見。
感謝
《Android開發(fā)藝術(shù)探索》
