上一篇的結(jié)尾中,我們發(fā)現(xiàn)了View的繪制發(fā)生在ViewRootImpl
的performTraversals()
中.而且在其中先后調(diào)用了performMeasure(),performLayout(),performDraw()
.
如此一來,我們又有了新的獵物了.就像美食一樣,好吃的東西一定要仔細地品嘗.在上主菜之前,我們先來點開胃菜.我們先來了解一下Android是怎樣繪制View的.(官方的文檔How Android Draws Views)
開胃菜(關(guān)于View繪制時需要知道的常識)
當Activity
接受焦點時,就會被要求繪制其布局.雖然Android Framework
會處理這個過程,但Activity
必須提供整個布局層級的根節(jié)點,因為需要知道從哪開始繪制.
Activity
的整個布局被轉(zhuǎn)換成了一棵樹,繪制整個布局就相當于了遍歷整顆樹并把每個節(jié)點的View繪制出來.相應地,ViewGroup
負責要求它的每一個child
進行繪制,而View
則負責繪制自己.由于樹的遍歷是有序的,所以父View繪制之前會先繪制其子View,而兄弟節(jié)點會按照在樹中出現(xiàn)的順序進行繪制.
繪制布局需要進行兩個傳遞過程(pass process)
:分別是測量時的傳遞(measure pass)
和布局時的傳遞(layout pass)
.這里所說的傳遞指的是在view tree
的各個節(jié)點之間的傳遞.
-
measure pass
在measure(int,int)
中實現(xiàn),而且它是一個從上到下的傳遞.在view tree
中每個View節(jié)點都將它的尺寸規(guī)格向下傳遞給它的孩子,在整個傳遞過程結(jié)束時,每個節(jié)點都應該擁有了自己的測量值(尺寸大小). -
layout pass
在layout(int,int,int,int)
中發(fā)生,它同樣也是一個從上到下的傳遞.在傳遞過程中每個parent
都需要根據(jù)在measure pass
時得到的測量值在布局中放置它的所有children
.
下面貼上一張普通的view tree
的圖.
在measure()
函數(shù)中,官方定下了一些規(guī)則,在函數(shù)執(zhí)行完畢返回前必須要滿足下面的條件:
- View(以及其后代節(jié)點)的
getMeasuredWidth()(即mMeasuredWidth的值)
和getMeasuredHeight()(即mMeasuredHeight的值)
的值必須已經(jīng)設置.從函數(shù)名已經(jīng)知道函數(shù)獲取的是已經(jīng)測量的寬高值,measure()
函數(shù)結(jié)束就表明測量結(jié)束了,這一條規(guī)則理所當然. - View測量后的寬高必須符合其父View所規(guī)定的大小.這一條規(guī)則可以保證當
measure pass
結(jié)束時,所有的parents
能接受其所有children
的測量值.這也很好理解,子View的大小總不能比其父布局還大吧,否則就沒有意義了.
一個為parent
的View可能會不止一次地對其children
調(diào)用measure()
.因為如果parent
使用未指定的尺寸測量它的每一個child
得到各個child
想要的大小,但如果所有children
的(未加限制的)測量值的總和太大或太小,那就需要parent
再次調(diào)用measure()
重新測量,但這次的測量設置了相應的規(guī)則.(舉個比喻,就像孩子們在分配糖果時,大家都對所分配的糖果不滿意時,父母就會干涉并重新分配)
我要吃神戶牛柳(深入measure
過程)
吃過開胃菜后,再來品嘗我們的主菜就會更加的美味.美味的食物通常都有獨特的吃法,比如使用特定的餐具.我們的第一道菜(measure
)就是神戶牛柳,我們需要準備刀叉來用餐.那先準備一下我們的餐具吧.(與measure
過程密切相關(guān)的兩個類).
刀 (ViewGroup.LayoutParams)
先來說明一下ViewGroup.LayoutParams
是干什么用的.View通過ViewGroup.LayoutParams
來告訴它的parent
它在布局中想被放在什么位置和想占多大.而基本的ViewGroup.LayoutParams
只能表達View想占多寬和多高,可以通過下面的其中一種方式表達:
- 一個確切的數(shù)值大小
-
MATCH_PARENT
,表達View想要和它的parent
一樣大(去掉View的內(nèi)邊距) -
WRAP_CONTENT
,表達View只想要能將它的內(nèi)容包裹的大小(加上View的內(nèi)邊距)
ViewGroup.LayoutParams
只能表達View大小,但ViewGroup
的子類的LayoutParams
能表達View的位置.
叉 (MeasureSpec)
看過我的自定義View#02文章的同學可能會對MeasureSpec
有所了解.MeasureSpec
被parent
用來限制child
的大小,在measure()
的過程中,它作為參數(shù),從view tree
的根節(jié)點往下傳遞到它的子節(jié)點和其后代.它有下列3種模式:
-
UNSPECIFIED
, 這種模式表明parent
對它的child
的大小沒有限制,child
可以告訴parent
它自己所希望的尺寸. -
EXACTLY
, 這種模式表明parent
給child
設置了一個確切的值,child
必須使用這個值,并且需要保證child
的后代節(jié)點都要符合這個值的設置 -
AT_MOST
, 這種模式表明parent
給child
設置了一個最大值,child
可以是它想要的任何值,但child
以及它的后代節(jié)點的尺寸大小都必須保證在這個最大值內(nèi).
既然MeasureSpec
有相應的模式來限制View的尺寸,那用什么來表示限制尺寸的大小呢.MeasureSpec
采用了一個32位的int值來代表模式和大小,高2位表示模式,低30位表示大小.
有了我們的餐具后,我們可以終于可以開動了.我們先從ViewRootImpl.performTraversals()
中調(diào)用performMeasure()
的地方開始,下面是該部分的代碼:
.......
if (!mStopped || mReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
// 標注 1
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed! mWidth="
+ mWidth + " measuredWidth=" + host.getMeasuredWidth()
+ " mHeight=" + mHeight
+ " measuredHeight=" + host.getMeasuredHeight()
+ " coveredInsetsChanged=" + contentInsetsChanged);
// Ask host how big it wants to be
// 標注 2
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// Implementation of weights from WindowManager.LayoutParams
// We just grow the dimensions as needed and re-measure if
// needs be
// 標注 3
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
// 標注 4
if (measureAgain) {
if (DEBUG_LAYOUT) Log.v(TAG,
"And hey let's measure once more: width=" + width
+ " height=" + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
}
}
.......
上面的代碼我做了4個標注,我們一個一個來看,先看標注1
的代碼.
// 標注 1
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
其中這里的mHeight
和mWidth
分別是窗口(Activity)的寬高,lp
為窗口(Window)的布局參數(shù).childWidthMeasureSpec
和childHeightMeasureSpec
這兩個變量其實從標注2
的代碼就很容易的看出它們是作為參數(shù)傳進performMeasure(int,int)
的.那我們就看看它們代表的是什么意思.
由于它們是從getRootMeasureSpec(int,int)
獲取的,從函數(shù)名可以看出這個函數(shù)是用來獲取Root
節(jié)點的MeasureSpec
的(就是根節(jié)點在測量時給它的孩子節(jié)點所定下的尺寸大小的限制).但我們還是要看看這個函數(shù)的代碼:
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
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.
// 如果布局參數(shù)要求MATCH_PARENT,那么就設置為窗口的大小,模式為EXACTLY,因為窗口(Activity)的大小固定
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
//如果布局參數(shù)為WRAP_CONTENT,就設置為AT_MOST模式,最大值為窗口大小
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
// 如果布局的參數(shù)為一個確切的值,那我們就讓root view為該值,模式為EXACTLY
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
getRootMeasureSpec(int,int)
的兩個參數(shù)分別代表窗口的大小(windowSize
)和窗口的布局參數(shù)的大小(rootDimension
).官方的注釋(我也做了相應的注釋)已經(jīng)寫得很清楚了,這個函數(shù)通過window的布局參數(shù)來決定root view
的MeasureSpec
.
經(jīng)過標注1
的代碼,我們獲取到了root tree
的根節(jié)點的MeasureSpec
,這樣就可以從樹的根節(jié)點開始進行測量傳遞的過程了(在開胃菜中提到的measure pass
).在對標注2
這個最主要的代碼部分進行分析前,我們先來分析后面的標注3
和標注4
的代碼.(好東西肯定要留到最后,反正我是這樣想的)
// Implementation of weights from WindowManager.LayoutParams
// We just grow the dimensions as needed and re-measure if
// needs be
// 標注 3
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
// 標注 4
if (measureAgain) {
if (DEBUG_LAYOUT) Log.v(TAG,
"And hey let's measure once more: width=" + width
+ " height=" + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
標注3
首先獲取到了測量后root view
的寬高值,然后分別判斷lp.horizontalWeight
和lp.verticalWeight
的值是否大于0(即我們的布局參數(shù)在橫向或縱向的weight
被設置時,可以理解為在xml布局文件里設置了layout_weight
屬性).若設置了其中一個方向上的weight
,那么標注4
的代碼都會執(zhí)行,我們需要再調(diào)用一次performMeasure()
進行測量(measure pass
),但這次采用的是新的參數(shù),把布局參數(shù)的weight
考慮進去.
可能有同學會問,為什么第一次調(diào)用
performMeasure()
前不把weight
考慮進去,測量完一次后才考慮這不讓前面的工作都白費了嗎?我也有相同的疑問,目前我還沒找到一個準確的答案,但google這樣寫一定有它的道理,我在這里分享一下我的想法吧(不一定是正確的,如果錯了希望大家能指正).我是這樣想的:第一次調(diào)用
performMeasure()
的時候并不知道weight
是否設置了,因為我們通常設置layout_weight
屬性都是在子View中設置的,在子View測量完畢前,父布局并不知道它的所有子View的weight
屬性,而父布局的測量發(fā)生在子View測量結(jié)束后,所以我們可能需要進行兩次的測量傳遞過程(measure pass
).
好的,清楚了我們標注3,標注4
的代碼后,我們可以迎接我們的主角performMeasure()
了,下面就是performMeasure()
的代碼.
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);
}
}
原來代碼中調(diào)用的是mView.measure(int,int)
,即調(diào)用了root view
的measure()
,既然如此,我們來看measure()
的代碼:
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* </p>
*
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
*
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
*
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
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;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
注釋中也說了,真正的測量工作是發(fā)生在onMeasure(int,int)
函數(shù)中的,并且說明了View的子類可以并必須重寫onMeasure()
來測量我們的View.這里的必須并不代表我們在自定義VIew的時候一定要重寫onMeasure()
,因為onMeasure()
已經(jīng)在View中實現(xiàn)了,在不重寫的情況下會調(diào)用默認的實現(xiàn).
既然注釋中給我們指明了方向,那我們就來看看這個onMeasure()
.
神戶牛的精華(onMeasure
)
onMeasure()
可以說是整個measure pass
的核心部分,就像是神戶牛的精華一樣.那現(xiàn)在我們就來感受一下這神戶牛的精華所帶來的美味.
由于在view tree
上不可能每個節(jié)點都是View節(jié)點(這里是葉子節(jié)點的意思),就像在"開胃菜"
中給大家展現(xiàn)的圖一樣,在view tree
中也會有ViewGroup
節(jié)點,像FrameLayout,LinearLayout,RelativeLayout...
,這些ViewGroup
節(jié)點都相應的實現(xiàn)了自己的onMeasure()
.那么這就說明了ViewGroup.onMeasure()
與View.onMeasure()
并不一樣.既然測量傳遞的過程(measure pass
)是從根節(jié)點開始的,那我們也從ViewGroup
的onMeasure()
開始.這里我們使用的是FrameLayout
的代碼(其他的ViewGroup
大家可以自己試著去分析).
提示:下面的代碼可以先跳過,因為在后面會再提到.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 第1部分
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);
}
}
}
}
// 第2部分
// 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());
}
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
//第3部分
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
Part 1
雖然代碼不算很長,但為了方便,我們還是將它分為3個部分來分析吧(上面代碼注釋中所劃分的).先來第1部分
的代碼:
// 第1部分
int count = getChildCount();
// 編號1. 用于判斷是否需要對布局參數(shù)為MATCH_PARENT的子View進行重新測量
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) {
// 編號2. 對每個子View進行測量
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 編號3. 得到所有子View中最大的寬度(加上子View的外邊距)
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
// 編號4. 得到所有子View中最大的高度(加上子View的外邊距)
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
// 編號5. 得到子View的MeasureState
childState = combineMeasuredStates(childState, child.getMeasuredState());
// 編號6. 將布局參數(shù)為MATCH_PARENT的子View加入到`mMatchParentChildren`集合中
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
為了后面的分析方便,我在上面的代碼注釋中都給相應的語句進行了編號,我們先來分析簡單的.
編號1
的measureMatchParentChildren
是用于判斷FrameLayout是否需要對布局參數(shù)為MATCH_PARENT
的子View進行第二次測量.這里我們等分析過編號2
的代碼后再對這個變量進行解析(這里可以先留個疑問).
而編號3,編號4
的代碼就是為了得到FrameLayout
所有孩子中測量后的最大寬高(加上邊距),因為FrameLayout需要按照它的孩子中尺寸最大的寬高進行測量.
編號5
的代碼就是為了得到子View的MeasuredState
,這個對我們來說是個新的概念.既然這樣,我們就來看看child.getMeasuredState()
這個在View
類下的函數(shù)是個怎樣的函數(shù).
/**
* Return only the state bits of {@link #getMeasuredWidthAndState()}
* and {@link #getMeasuredHeightAndState()}, combined into one integer.
* The width component is in the regular bits {@link #MEASURED_STATE_MASK}
* and the height component is at the shifted bits
* {@link #MEASURED_HEIGHT_STATE_SHIFT}>>{@link #MEASURED_STATE_MASK}.
*/
// 將寬高的狀態(tài)位結(jié)合成在一個32位的int值并返回
// 寬度的狀態(tài)位在常規(guī)的位置
// 高度的狀態(tài)位在偏移后的位置
public final int getMeasuredState() {
return (mMeasuredWidth&MEASURED_STATE_MASK)
| ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
// 用于使高度的狀態(tài)位偏移的位數(shù)
public static final int MEASURED_HEIGHT_STATE_SHIFT = 16;
我把用到的變量也貼在了上面的代碼中.首先我們來了解一下什么是"寬高的狀態(tài)位".我們知道mMeasuredHeight
或mMeasuredWidth
都是32位的int值,但這個值并不是一個表示寬高的實際大小的值,而是一個由寬高的狀態(tài)和實際大小所組合的值.這里的高8位就表示狀態(tài)(STATE
),而低24位表示的是實際的尺寸大小(SIZE
),這個信息可以從它們相應的掩碼看出.
// 用于得出寬高的狀態(tài)位的掩碼
public static final int MEASURED_STATE_MASK = 0xff000000;
// 用于得出寬高的尺寸位的掩碼
public static final int MEASURED_SIZE_MASK = 0x00ffffff;
這就解析了為什么我們的getMeasuredHeight()
函數(shù)返回的是mMeasuredHeight & MEASURED_SIZE_MASK
.而getMeasuredHeightAndState()
返回的是mMeasuredHeight
.相應的關(guān)于寬度的函數(shù)也是一個道理.
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeightAndState() {
return mMeasuredHeight;
}
現(xiàn)在我們再來看getMeasuredState()
是怎樣將寬高的狀態(tài)位組合在一個int值中的.首先mMeasuredWidth & MEASURED_STATE_MASK
得到了寬度的狀態(tài)位,保存在高8位.然后通過(mMeasuredHeight >> MEASURED_HEIGHT_STATE_SHIFT)
和(MEASURED_STATE_MASK >> MEASURED_HEIGHT_STATE_SHIFT)
將高度和狀態(tài)掩碼都右移了16位,現(xiàn)在高度的狀態(tài)位在第8到第15位上,而MEASURED_STATE_MASK
變成了0x0000ff00
,接著將兩個移位后的數(shù)進行按位相與(&
)得到了高度的狀態(tài)位,保存在8-15位上.最后將處理后寬度和高度按位相或(|
)得到一個保存了寬度和高度的狀態(tài)位的int值.如下圖.
/**
* Merge two states as returned by {@link #getMeasuredState()}.
* @param curState The current state as returned from a view or the result
* of combining multiple views.
* @param newState The new view state to combine.
* @return Returns a new integer reflecting the combination of the two
* states.
*/
public static int combineMeasuredStates(int curState, int newState) {
return curState | newState;
}
回到編號5
的代碼,就是為了將所有子View的state
都結(jié)合在一起,這個有什么作用現(xiàn)在也不好講.先繼續(xù)看吧.
編號2
的代碼是將FrameLayout
中所有visibility
屬性不為GONE
的子View都進行測量(即在布局中占據(jù)位置的View),使用的是measureChildWithMargins()
.下面我們來看這個函數(shù)的代碼.
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure (需要測量的子View)
* @param parentWidthMeasureSpec The width requirements for this view
* (parent對子View寬度的要求(MeasureSpec))
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* (被parent或其他兄弟節(jié)點在布局的水平方向上使用了的尺寸大小)
* @param parentHeightMeasureSpec The height requirements for this view
* (parent對子View高度的要求(MeasureSpec))
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
* (被parent或其他兄弟節(jié)點在布局的垂直方向上使用了的尺寸大小)
*/
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);
}
像代碼中的注釋所寫的一樣,這個函數(shù)就是用來告訴child
需要進行測量.測量過程中需要遵循parent
的MeasureSpec
,還需要考慮將padding
和margin
的值.通過了getChildMeasureSpec()
得到了子View的MeasureSpec
后就可以調(diào)用子View的measure()
進行測量了.所以我們要來看看getChildMeasureSpec(int,int,int)
這個函數(shù),先來看看函數(shù)原型的文檔,因為我們要先搞清楚各個參數(shù)所代表的意義.
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view
* (對View的尺寸限制MeasureSpec)
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* (可以理解為父布局的padding值+View的margin值,即父布局中未使用的尺寸大小)
* @param childDimension How big the child wants to be in the current
* dimension
* (View希望在布局中的大小,即子View布局參數(shù)的寬高)
* @return a MeasureSpec integer for the child
*
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension)
這個函數(shù)是為了獲得當前View的MeasureSpec
以便于進行測量和傳遞給子View的.函數(shù)中主要是根據(jù)父布局的MeasureSpec
來創(chuàng)建View自己的MeasureView
.下面是相應的代碼.
代碼有點長,希望能完整地看一遍.但如果不想看也不要緊,就粗略地掃一眼吧.因為后面有圖片進行總結(jié),正所謂一圖勝千言啊!
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 分別獲取父布局`MeasureSpec`中的模式和尺寸
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 獲取父布局實際提供給View的尺寸大小(去除邊距)
// 即父布局最大的可用的大小
// 與0相比取最大值,以免尺寸大小為負值
int size = Math.max(0, specSize - padding);
// 當前View最終的尺寸大小和模式
int resultSize = 0;
int resultMode = 0;
// 根據(jù)父布局的模式來決定View的模式和尺寸
switch (specMode) {
// Parent has imposed an exact size on us
// 表示父布局的大小為確切的值
case MeasureSpec.EXACTLY:
// 由于`MATCH_PARENT`=-1,`WRAP_CONTENT`=-2,
// 所以childDimension >= 0 表示View的寬高布局參數(shù)為具體的值
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 表示View的布局參數(shù)為`MATCH_PARENT`,即View希望大小是父布局的最大的可以大小
// 模式與父布局一樣為EXACTLY
// 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.
// 表示View的布局參數(shù)為`WRAP_CONTENT`,那么說明View的大小不明確,需要由它的內(nèi)容決定
// 所以測量值的尺寸為父布局的最大的可以大小,模式為AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
// 表示父布局的大小不確定,需要由父布局的內(nèi)容決定
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
// 表明View的布局大小為確切的值
// 所以View的測量大小為布局參數(shù)的值,模式為EXACTLY
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.
// 表明View的布局參數(shù)為MATCH_PARENT
// 所以View的測量大小為父布局的可用大小,模式與父布局一樣為AT_MOST
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.
// View的布局參數(shù)為WRAP_CONTENT,即View的測量尺寸大小不確定,由其內(nèi)容決定
// 所以View的測量大小為父布局最大的可以大小,模式為AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
// 表示父布局對View的大小沒有限制,通常用在ListView等可滾動的控件中
// 這種情況下父布局會滿足View的所有要求
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;
}
// 根據(jù)最終的View的模式和尺寸生成View的MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
雖然上面的代碼不短,但代碼邏輯并不復雜,而且我已經(jīng)在代碼上做了比較明白的注釋了,如果還是不理解,那就請看圖.
函數(shù)的功能就是為了給View生成一個
MeasureSpec
類型的int,而這個值是由模式和大小合成的,而且它們的值由父布局MeasureSpec
的模式和View的布局大小共同決定.上圖就是一個決定View的Mode
和Size
的過程.這里有一點需要注意的,就是當父布局的
MeasureSpec
的模式為UNSPECIFIED
時,若View的布局大小不為一個具體的值那么resultSize
的大小就為0.這里決定resultSize
的值是下面的一條語句.
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
而這里的View.sUseZeroUnspecifiedMeasureSpec
可以在View中找到相應的信息.
/**
* Always return a size of 0 for MeasureSpec values with a mode of UNSPECIFIED
*/
static boolean sUseZeroUnspecifiedMeasureSpec = false;
public View(Context context) {
.....
// In M and newer, our widgets can pass a "hint" value in the size
// for UNSPECIFIED MeasureSpecs. This lets child views of scrolling containers
// know what the expected parent size is going to be, so e.g. list items can size
// themselves at 1/3 the size of their container. It breaks older apps though,
// specifically apps that use some popular open source libraries.
sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M;
......
}
就是說若當前的Android版本小于M
的話那sUseZeroUnspecifiedMeasureSpec
的值就為true
.所以在舊版本的Android中,resultSize
的值都為0.
現(xiàn)在我們回到編號1
的地方就可能對那句代碼有所理解了.
// 編號1. 用于判斷是否需要對布局參數(shù)為MATCH_PARENT的子View進行重新測量
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
我們現(xiàn)在對應著上面的View的MeasureSpec
生成圖來分析代碼.將流程反過來看,即從有編號的框開始看,我們可以看到在(1),(2),(4),(7)
4種情況下resultMode
的值為EXACTLY
.而它們的條件有3種是childDimension >= 0
即FrameLayout的布局參數(shù)layout_width
或layout_height
為實際的具體值.另一種情況就是FramLayout的布局參數(shù)為MATCH_PARENT
且它的父布局為EXACTLY
. 綜合的來講可以這么理解吧就是當FrameLayout的布局寬高不同時設置為具體的值,或不同時為MATCH_PARENT
那么measureMatchParentChildren
這個值就為true
.
再簡單的講就是如果FrameLayout的寬高只要有一個設置為WRAP_CONTENT
,那么該值就為true
.那么就需要對布局參數(shù)為MATCH_PARENT
的子View進行重新測量.因為WRAP_CONTENT
的情況下父布局的測量值受子View的影響.
能看到這里的同學真是不簡單啊,沒想到第1部分
講了這么久,(有的同學就可能會說:這分"神戶牛柳"的量也太多了吧,吃得有點撐啊!)這里篇幅確實有點長,但如果仔細看下來的話還是能學到不少的東西.大家可以先休息一下,待會再來繼續(xù)閱讀.我也在下面做了分割線幫大家標記位置.
Part 2
我們來繼續(xù)我們onMeasure()
的第2部分
代碼的分析吧.相信我,當你看完第1部分
的分析后,后面就會很有感覺.
// 第2部分
// 前面我們的maxWidth和maxHeight只是計算了子View的外邊距
// 但沒有計算FrameLayout的內(nèi)邊距,所以在這里加上
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
// 保證我們的`maxWidth`和`maxHeight`不會太小(至少要等于最小的建議值)
// 這里的最少建議值與背景有關(guān)
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());
}
// 標注
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
第2部分
的代碼是先是確定了maxWidth
和maxHeight
的值,給大家看一眼getSuggestedMinimumHeight()
的代碼吧,因為后面這個函數(shù)還會用到,我相信大家很容易就能理解.
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
我們主要還是看標注
的那句代碼,setMeasuredDimension()
就是將測量好的寬高值存儲下來供后面的布局過程使用.來看看代碼吧.
/**
* <p>This method must be called by {@link #onMeasure(int, int)} to store the
* measured width and measured height. Failing to do so will trigger an
* exception at measurement time.</p>
*
* @param measuredWidth The measured width of this view. May be a complex
* bit mask as defined by {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
* @param measuredHeight The measured height of this view. May be a complex
* bit mask as defined by {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
/**
* Sets the measured dimension without extra processing for things like optical bounds.
* Useful for reapplying consistent values that have already been cooked with adjustments
* for optical bounds, etc. such as those from the measurement cache.
*
* @param measuredWidth The measured width of this view. May be a complex
* bit mask as defined by {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
* @param measuredHeight The measured height of this view. May be a complex
* bit mask as defined by {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
*/
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
上面就是相關(guān)的代碼,也比較簡單,setMeasuredDimension()
中最后調(diào)用了setMeasuredDimensionRaw()
來設置mMeasuredWidth
與mMeasuredHeight
的值.其中關(guān)于Optical Bounds
有興趣的同學可以到Internet上搜索一下,或看看下圖.這里我們不作討論,可以跳過.
了解了
setMeasuredDimension()
后,那我們再來看看調(diào)用處給它傳進的兩個參數(shù)resolveSizeAndState(maxWidth,widthMeasureSpec,childState)
和resolveSizeAndState(maxWidth,widthMeasureSpec,childState)
.既然調(diào)用了resolveSizeAndState()
,那就看看它的代碼吧.
/**
* Utility to reconcile a desired size and state, with constraints imposed
* by a MeasureSpec. Will take the desired size, unless a different size
* is imposed by the constraints. The returned value is a compound integer,
* with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
* optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
* resulting size is smaller than the size the view wants to be.
*
* @param size How big the view wants to be.
* @param measureSpec Constraints imposed by the parent.
* @param childMeasuredState Size information bit mask for the view's
* children.
* @return Size information bit mask as defined by
* {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
// 防止View超出了限制的大小所做的處理
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
// 將尺寸大小值和狀態(tài)組合到一起
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
在第1部分
我們已經(jīng)讀過了不少的類似的代碼了,在這里我就不啰嗦了.主要還是講講resolveSizeAndState(maxHeight,heightMeasureSpec,childState<<MEASURED_HEIGHT_STATE_SHIFT)
這句代碼吧,為什么這里需要進行左移?如果前面有認真看的話就很容易理解,因為我們的childState
是存有寬高的狀態(tài)的組合值,我們的高度的狀態(tài)值存在第8-15位,所以這里需要將它左移16位(將狀態(tài)位放置在常規(guī)的位置).
Part 3
來到第3部分
了,這部分比較簡單,我們先來看看代碼.
//第3部分
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
// 寬度為總寬度減去父布局的Padding和View的Margin
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
// 第1部分已經(jīng)討論過
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
這里的代碼就是當FrameLayout有多于1個的子View的布局寬高為MATCH_PARENT
時(并且滿足第1部分
中的measureMatchParentChildren
為true
),即當FrameLayout的寬高設置存在WRAP_CONTENT
時,對子View進行重新的測量.
View的onMeasure()
到這里我們FrameLayout
的onMeasure()
已經(jīng)分析完畢了,既然我們已經(jīng)分析過了ViewGroup
的onMeasure()
,那View
的onMeasure()
我覺得也免不了,廢話不說趕緊上菜.
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
代碼還是很簡單的,就一句.雖然嵌套很多,但只有一個函數(shù)我們沒有見過,就是getDefaultSize()
,那就看看它是何方神圣.
/**
* 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
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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;
}
這個函數(shù)是決定View的MeasureSpec
的Size
的一種默認的方法.代碼非常的簡單,我就不再多說了.因為今天看這種代碼看得真的不少,我相信很多同學都快要看吐了.
不過到這里我很開心,因為我們這次的任務完成了,measure
的過程我們已經(jīng)分析完了.
最后的甜點(總結(jié))
牛柳吃完了,不知道大家能不能消化,所以最后給大家上個甜點吧.最后還是用圖說話,來總結(jié)一下measure
的整個流程.