由上圖可知,語法角度:子類可以重寫onMeasure,只能繼承View的measure,setMeasuredDimension方法。測量流程分爲兩種情況討論:容器控件ViewGroup,原始的View(非容器控件)。原始的View測量,只需要測量自己的寬高;而容器控件需要先測量所有的子View的寬高,然後再測量自己的寬高。看懂本篇文章,還需要大家自己先去研究下類View$MeasureSpec,相對比較簡單,本文不描述MeasureSpec相關知識。
二,源碼分析之View
先分析原始的View,打開View.java文件,查看measure方法:
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);
}
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 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("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
調用view.measure(w,h)來測量控件寬高,那麼這個方法是何時調用的呢?在該view的父控件測量自己寬高時調用。因爲該view所在父容器在測量自己寬高時,會先測量子view的寬高,最終都會調用child.measure(w,h),最後才測量自己的寬高。後面分析容器控件的測量流程時,會一目瞭然。
主要分析measure(w,h)的兩個關鍵點:
一,字段mPrivateFlags
1.1 字段mPrivateFlags在調用onMeasure(w,h)前,執行mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET,設置成員變量mPrivateFlags的MEASURED_DIMENSION_SET位設置爲0;
1.2 在onMeasure(w,h)執行完成後,判斷if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET)決定是否拋出IllegalStateException異常;
二,實際測量方法onMeasure(w,h),進入該方法查看源碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
進入setMeasuredDimension(w,h)查看源碼:
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;
}
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
setMeasuredDimension(w,h)是真正完成給view測量寬高,至於參數measuredWidth,measuredHeight是如何計算得來,下面會有分析。小結:測量一個view實際上是給字段mMeasuredWidth,mMeasuredHeight設置值,最後執行mPrivateFlags
|= PFLAG_MEASURED_DIMENSION_SET,將字段mPrivateFlags的EASURED_DIMENSION_SET位設置爲1。
mPrivateFlags更像是一個標誌位,在onMeasure測量前設置一個值,在onMeasure執行的最後設置一個值,測量完成後判斷mPrivateFlags的值。若前面沒有執行setMeasuredDimension(w,h)完成測量,那麼mPrivateFlags值則不會重新設置,判斷mPrivateFlags時會執行if語句中內容,拋出IllegalStateException異常。繼續分析前面提到:參數measuredWidth,measuredHeight是如何計算得來?只分析寬度(高度計算方式類似),分析這樣一段代碼:getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),於是進入方法getDefaultSize查看源碼:
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;
}
第二個參數measureSpec是父容器調用child.measure(w,h)傳入的參數,
measureSpec取決於父容器的measureSpec(爺爺容器給的建議值)和自身佈局參數LayoutParams(eg:控件寬高,外邊距,內邊距等),後面會具體分析。這裏只需要記住,measureSpec是父容器測量子View時給的建議值。這個建議值measureSpec配合第一個參數size,決定view的最終寬度,也就是getDefaultSize方法返回值。那麼size是什麼東西呢?查看getSuggestedMinimumWidth()源碼:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
mMinWidth的值取決於view的佈局參數android:minWidth="",如果沒有設置,則default爲0;mBackground.getMinimumWidth()返回該view的背景圖片需要最小寬度值。如果沒有背景圖片,則返回
mMinWidth,否則max(mMinWidth,mBackground.getMinimumWidth()取較大值。也就是說參數size:要麼取佈局參數中最小寬度(還可能爲0),要麼取背景圖片所需最小寬度。繼續回到getDefaultSize方法來分析寬度(高度計算方式類似),取出父控件給的建議值的測量大小specSize,測量模式specMode。
判斷specMode,1,如果是模式AT_MOST / EXACTLY,返回specSize;2,如果specMode是UNSPECIFIED,則父容器不對子view做任何限制,返回size。(UNSPECIFIED這種測量模式一般不做分析,不用管它)
注意,specMode是EXACTLY,說明父控件已經知道子view需要的精確值,那麼直接使用specSize容易理解;那specMode是AT_MOST時,說明父控件給的建議值是一個子view可以使用的最大值(<=specSize),爲什麼直接返回specSize呢?這裏肯定需要修改,因爲測量模式爲EXACTLY,說明子view寬度:要麼是match_parent,要麼是具體的值(100dp)。測量模式爲AT_MOST,說明子View寬度:只可能是wrap_content。在使用這個自定義的view時,不能讓match_parent和wrap_content的體現的效果一樣。
於是可以得出結論:在extends View的自定義控件中,需要重寫onMeasure(w,h),並單獨判斷MeasureSpec.getMode(w)爲AT_MOST時,返回一個寬度值(具體邏輯按需求來吧),高度同理!查看TextView源碼,onMeasure方法有對specMode爲AT_MOST進行處理。
原始的View(非容器控件)的測量,代碼流程圖大致如下,保存該圖片到本地可以清晰展示信息哦!
三,源碼分析之容器控件
接下來分析容器控件測量流程,前面說到容器控件(繼承ViewGroup)的測量過程:先測量所有子view,然後再測量容器控件本身。每種容器控件測量的細節不盡相同,但都遵循上面的方式。閱讀LinearLayout源碼時,發現裏面if條件判斷極爲噁心,於是本篇以FrameLayout爲例子分析容器控件的測量流程。首先貼上涉及的類,方法結構圖如下,可以看完後面分析回過頭來看此圖。
查看FrameLayout源碼,分析容器控件的測量過程:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
// ...code
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());
}
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
執行for循環遍歷一個存儲子View的對象數組,調用measureChildWithMargins方法,首先測量子View,該方法是從父類ViewGroup繼承過來。然後調用setMeasuredDimension方法測量容器控件自己的寬高,該方法從爺爺類View繼承過來。先來分析
measureChildWithMargins,查看源碼:
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);
}
參數child-->子View;參數parentWidthMeasureSpec,parentHeightMeasureSpec--容器控件的MeasureSpec;參數widthUsed,heightUsed-->容器控件中已被使用的寬/高的數值,這裏爲0。
執行getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft+mPaddingRight+ lp.leftMargin +lp.rightMargin+widthUsed, lp.width)獲取子View的寬度測量建議值,這個值最終要傳入到child.measure(w,h)中,開始測量子View。 從該方法參數可知:測量時,子View的MeasureSpec值由兩部分組成:(parentWidthMeasureSpec-->父控件的MeasureSpec),以及子View本身的佈局參數LayoutParams決定的。
其中LayoutParams中,涉及子view的寬高值,外邊距margin。至於mPaddingLeft 是指FrameLayout與其內容之間的距離,字段繼承於類View。
接下來分析如何合成子View的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 = 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);
}
參數spec是FrameLayout的MeasureSpec,就是子view的爺爺給爸爸的測量建議值;參數padding是子View的外邊距margin與FrameLayout的padding相加;參數childDimession是子view的寬/高值,由lp.width/lp.height得到。
當爸爸FrameLayout的測量模式specMode爲EXACTLY時,裏面還要分三種情況討論:
1,當childDimension >= 0(android:layout_width="50dp"),resultSize爲50dp,resultMode爲精確EXACTLY;
2,當childDimension == LayoutParams.MATCH_PARENT時,由於父容器specMode是精確的,子view又填充所有空間,那麼resultSize大小就爲size
= Math.max(0, specSize - padding),有具體數值,屬於精確模式Exactly;
3,當childDimension == LayoutParams.WRAP_CONTENT時,子view要小於等於size,於是大小爲size,模式爲AT_MOST。爸爸FrameLayout的測量模式爲AT_MOST,UNSPECIFIED的情況,就不再具體分析了。最後,使用MeasureSpec合併resultSize,resultMode。
至於,child.measure(w,h)的繼續分析,無非就是兩種情況:child如果是容器控件,則繼續重複上面的測量流程;如果child是一個原始的view,那就是進入文章前半部分的測量流程。
容器控件測量自己,調用方法setMeasuredDimension,由方法resolveSizeAndState的返回值,得到測量的寬高大小(不包含測量模式specMode)。這裏不再分析resolveSizeAndState方法,跟原始的view在測量時的getDefaultSize方法很類似,前面已經分析了getDefaultSize方法。注:子view的測量後寬高,影響到了容器控件測量自己,這也是爲什麼要先測量所有子view,然後才測量容器控件自己。
下面展示了代碼走向流程圖:
這裏,原始的view(非容器控件)與容器控件的測量流程分析完畢了。