上一篇 Android DecorView 與 Activity 綁定原理分析 分析了在調用 setContentView 以後,DecorView 是如何與 activity 關聯在一塊兒的,最後講到了 ViewRootImpl 開始繪製的邏輯。本文接着上篇,繼續往下講,開始分析 view 的繪製流程。html
上文說到了調用 performTraversals 進行繪製,因爲 performTraversals 方法比較長,看一個簡化版:android
// ViewRootImpl 類 private void performTraversals() { // 這個方法代碼很是多,可是重點就是執行這三個方法 // 執行測量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 執行佈局(ViewGroup)中才會有 performLayout(lp, mWidth, mHeight); // 執行繪製 performDraw(); }
其流程具體以下:緩存
View的整個繪製流程能夠分爲如下三個階段:佈局
measure: 判斷是否須要從新計算 View 的大小,須要的話則計算;post
layout: 判斷是否須要從新計算 View 的位置,須要的話則計算;ui
draw: 判斷是否須要從新繪製 View,須要的話則重繪製。this
在介紹繪製前,先了解下 MeasureSpec。MeasureSpec 封裝了父佈局傳遞給子佈局的佈局要求,它經過一個 32 位 int 類型的值來表示,該值包含了兩種信息,高兩位表示的是 SpecMode
(測量模式),低 30 位表示的是 SpecSize
(測量的具體大小)。下面經過註釋的方式來分析來類:url
/** * 三種SpecMode: * 1.UNSPECIFIED * 父 ViewGroup 沒有對子View施加任何約束,子 view 能夠是任意大小。這種狀況比較少見,主要用於系統內部屢次measure的情形,
* 用到的通常都是能夠滾動的容器中的子view,好比ListView、GridView、RecyclerView中某些狀況下的子view就是這種模式。
* 通常來講,咱們不須要關注此模式。 * 2.EXACTLY * 該 view 必須使用父 ViewGroup 給其指定的尺寸。對應 match_parent 或者具體數值(好比30dp) * 3.AT_MOST * 該 View 最大能夠取父ViewGroup給其指定的尺寸。對應wrap_content * * MeasureSpec使用了二進制去減小對象的分配。 */ public class MeasureSpec { // 進位大小爲2的30次方(int的大小爲32位,因此進位30位就是要使用int的最高位和第二高位也就是32和31位作標誌位) private static final int MODE_SHIFT = 30; // 運算遮罩,0x3爲16進制,10進製爲3,二進制爲11。3向左進位30,就是11 00000000000(11後跟30個0) // (遮罩的做用是用1標註須要的值,0標註不要的值。由於1與任何數作與運算都得任何數,0與任何數作與運算都得0) private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 0向左進位30,就是00 00000000000(00後跟30個0) public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 1向左進位30,就是01 00000000000(01後跟30個0) public static final int EXACTLY = 1 << MODE_SHIFT; // 2向左進位30,就是10 00000000000(10後跟30個0) public static final int AT_MOST = 2 << MODE_SHIFT; /** * 根據提供的size和mode獲得一個詳細的測量結果 */ // 第一個return: // measureSpec = size + mode; (注意:二進制的加法,不是十進制的加法!) // 這裏設計的目的就是使用一個32位的二進制數,32和31位表明了mode的值,後30位表明size的值 // 例如size=100(4),mode=AT_MOST,則measureSpec=100+10000...00=10000..00100 // // 第二個return: // size &; ~MODE_MASK就是取size 的後30位,mode & MODE_MASK就是取mode的前兩位,最後執行或運算,得出來的數字,前面2位包含表明mode,後面30位表明size public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } /** * 得到SpecMode */ // mode = measureSpec & MODE_MASK; // MODE_MASK = 11 00000000000(11後跟30個0),原理是用MODE_MASK後30位的0替換掉measureSpec後30位中的1,再保留32和31位的mode值。 // 例如10 00..00100 & 11 00..00(11後跟30個0) = 10 00..00(AT_MOST),這樣就獲得了mode的值 public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } /** * 得到SpecSize */ // size = measureSpec & ~MODE_MASK; // 原理同上,不過此次是將MODE_MASK取反,也就是變成了00 111111(00後跟30個1),將32,31替換成0也就是去掉mode,保留後30位的size public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } }
順便提下 MATCH_PARENT 和 WRAP_CONTENT 這兩個表明的值,分別是 -1 和 -2。spa
/** * Special value for the height or width requested by a View. * MATCH_PARENT means that the view wants to be as big as its parent, * minus the parent's padding, if any. Introduced in API Level 8. */ public static final int MATCH_PARENT = -1; /** * Special value for the height or width requested by a View. * WRAP_CONTENT means that the view wants to be just large enough to fit * its own internal content, taking its own padding into account. */ public static final int WRAP_CONTENT = -2;
在 performTraversals 中,首先是要肯定 DecorView 的尺寸。只有當 DecorView 尺寸肯定了,其子 View 才能夠知道本身能有多大。具體是如何去肯定的,能夠看下面的代碼:.net
//Activity窗口的寬度和高度 int desiredWindowWidth; int desiredWindowHeight; ... //用來保存窗口寬度和高度,來自於全局變量mWinFrame,這個mWinFrame保存了窗口最新尺寸 Rect frame = mWinFrame; //構造方法裏mFirst賦值爲true,意思是第一次執行遍歷嗎 if (mFirst) { //是否須要重繪 mFullRedrawNeeded = true; //是否須要從新肯定Layout mLayoutRequested = true; // 這裏又包含兩種狀況:是否包括狀態欄 //判斷要繪製的窗口是否包含狀態欄,有就去掉,而後肯定要繪製的Decorview的高度和寬度 if (shouldUseDisplaySize(lp)) { // NOTE -- system code, won't try to do compat mode. Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else { //寬度和高度爲整個屏幕的值 Configuration config = mContext.getResources().getConfiguration(); desiredWindowWidth = dipToPx(config.screenWidthDp); desiredWindowHeight = dipToPx(config.screenHeightDp); } ... else{ // 這是window的長和寬改變了的狀況,須要對改變的進行數據記錄 //若是不是第一次進來這個方法,它的當前寬度和高度就從以前的mWinFrame獲取 desiredWindowWidth = frame.width(); desiredWindowHeight = frame.height(); /** * mWidth和mHeight是由WindowManagerService服務計算出的窗口大小, * 若是此次測量的窗口大小與這兩個值不一樣,說明WMS單方面改變了窗口的尺寸 */ if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) { if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame); //須要進行完整的重繪以適應新的窗口尺寸 mFullRedrawNeeded = true; //須要對控件樹進行從新佈局 mLayoutRequested = true; //window窗口大小改變 windowSizeMayChange = true; } } ... // 進行預測量 if (layoutRequested){ ... if (mFirst) { // 視圖窗口當前是否處於觸摸模式。 mAttachInfo.mInTouchMode = !mAddedTouchMode; //確保這個Window的觸摸模式已經被設置 ensureTouchModeLocally(mAddedTouchMode); } else { //六個if語句,判斷insects值和上一次比有什麼變化,不一樣的話就改變insetsChanged //insects值包括了一些屏幕須要預留的區域、記錄一些被遮擋的區域等信息 if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) { insetsChanged = true; } ... // 這裏有一種狀況,咱們在寫dialog時,會手動添加布局,當設定寬高爲Wrap_content時,會把屏幕的寬高進行賦值,給出儘可能長的寬度 /** * 若是當前窗口的根佈局的width或height被指定爲 WRAP_CONTENT 時, * 好比Dialog,那咱們仍是給它儘可能大的長寬,這裏是將屏幕長寬賦值給它 */ if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { windowSizeMayChange = true; //判斷要繪製的窗口是否包含狀態欄,有就去掉,而後肯定要繪製的Decorview的高度和寬度 if (shouldUseDisplaySize(lp)) { // NOTE -- system code, won't try to do compat mode. Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else { Configuration config = res.getConfiguration(); desiredWindowWidth = dipToPx(config.screenWidthDp); desiredWindowHeight = dipToPx(config.screenHeightDp); } } } } }
這裏主要是分兩步走:
若是是第一次測量,那麼根據是否有狀態欄,來肯定是直接使用屏幕的高度,仍是真正的顯示區高度。
若是不是第一次,那麼從 mWinFrame 獲取,並和以前保存的長寬高進行比較,不相等的話就須要從新測量肯定高度。
當肯定了 DecorView 的具體尺寸以後,而後就會調用 measureHierarchy 來肯定其 MeasureSpec :
// Ask host how big it wants to be windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight);
其中 host 就是 DecorView,lp 是 wm 在添加時候傳給 DecorView 的,最後兩個就是剛剛肯定顯示寬高 ,看下方法的具體邏輯 :
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { int childWidthMeasureSpec; int childHeightMeasureSpec; boolean windowSizeMayChange = false;boolean goodMeasure = false;
// 說明是 dialog if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { // On large screens, we don't want to allow dialogs to just // stretch to fill the entire width of the screen to display // one line of text. First try doing the layout at a smaller // size to see if it will fit. final DisplayMetrics packageMetrics = res.getDisplayMetrics(); res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true); int baseSize = 0;
// 獲取一個基本的尺寸 if (mTmpValue.type == TypedValue.TYPE_DIMENSION) { baseSize = (int)mTmpValue.getDimension(packageMetrics); } if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize + ", desiredWindowWidth=" + desiredWindowWidth);
// 若是大於基本尺寸 if (baseSize != 0 && desiredWindowWidth > baseSize) { childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured (" + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec) + " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
// 判斷測量是否準確 if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = true; } else { // Didn't fit in that size... try expanding a bit. baseSize = (baseSize+desiredWindowWidth)/2; if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": next baseSize=" + baseSize); childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured (" + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")"); if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { if (DEBUG_DIALOG) Log.v(mTag, "Good!"); goodMeasure = true; } } } } // 這裏就是通常 DecorView 會走的邏輯 if (!goodMeasure) { childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 與以前的尺寸進行對比,看看是否相等,不想等,說明尺寸可能發生了變化 if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = true; } } return windowSizeMayChange; }
上面主要主要作的就是來肯定父 View 的 MeasureSpec。可是分了兩種不一樣類型:
若是寬是 WRAP_CONTENT 類型,說明這是 dialog,會有一些針對 dialog 的處理,最終會調用 performMeasure 進行測量;
對於通常 Activity 的尺寸,會調用 getRootMeasureSpec MeasureSpec 。
下面看下 DecorView MeasureSpec 的計算方法:
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; }
該方法主要是根據 View 的 MeasureSpec 是根據寬高的參數來劃分的。
MATCH_PARENT :精確模式,大小就是窗口的大小;
WRAP_CONTENT :最大模式,大小不定,可是不能超過窗口的大小;
固定大小:精確模式,大小就是指定的具體寬高,好比100dp。
對於 DecorView 來講就是走第一個 case,到這裏 DecorView 的 MeasureSpec 就肯定了,從 MeasureSpec 能夠得出 DecorView 的寬高的約束信息。
當父 ViewGroup 對子 View 進行測量時,會調用 View 類的 measure
方法,這是一個 final 方法,沒法被重寫。ViewGroup 會傳入本身的 widthMeasureSpec
和 heightMeasureSpec
,分別表示父 View 對子 View 的寬度和高度的一些限制條件。尤爲是當 ViewGroup 是 WRAP_CONTENT 的時候,須要優先測量子 View,只有子 View 寬高肯定,ViewGroup 才能肯定本身到底須要多大的寬高。
當 DecorView 的 MeasureSpec 肯定之後,ViewRootImpl 內部會調用 performMeasure 方法:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { if (mView == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }
該方法傳入的是對 DecorView 的 MeasureSpec,其中 mView 就是 DecorView 的實例,接下來看 measure() 的具體邏輯:
/** * 調用這個方法來算出一個View應該爲多大。參數爲父View對其寬高的約束信息。 * 實際的測量工做在onMeasure()方法中進行 */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ......
// Suppress sign extension for the low bytes long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
// 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT標記,則強制從新佈局 // 好比調用View.requestLayout()會在mPrivateFlags中加入此標記 final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; 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); // 須要從新佈局 if (forceLayout || needsLayout) {
// first clears the measured dimension flag 標記爲未測量狀態
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
// 對阿拉伯語、希伯來語等從右到左書寫、佈局的語言進行特殊處理
resolveRtlPropertiesIfNeeded();
// 先嚐試從緩從中獲取,若forceLayout爲true或是緩存中不存在或是 // 忽略緩存,則調用onMeasure()從新進行測量工做 int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); . . . } 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;
}
// 若是自定義的View重寫了onMeasure方法,可是沒有調用setMeasuredDimension()方法就會在這裏拋出錯誤;
// 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 }
這裏要注意的是,這是一個 final 方法,不能被繼承。這個方法只在 View 類裏面。總結一下 measure()
都幹了什麼事:
調用 View.measure()
方法時 View 並非當即就去測量,而是先判斷一下要不要進行測量操做,若是不必,那麼 View 就不須要從新測量了,避免浪費時間資源
若是須要測量,在測量以前,會先判斷是否存在緩存,存在直接從緩存中獲取就能夠了,再調用一下 setMeasuredDimensionRaw
方法,將從緩存中讀到的測量結果保存到成員變量 mMeasuredWidth
和 mMeasuredHeight
中。
若是不能從 mMeasureCache
中讀到緩存過的測量結果,調用 onMeasure()
方法去完成實際的測量工做,而且將尺寸限制條件 widthMeasureSpec
和 heightMeasureSpec
傳遞給 onMeasure()
方法。關於 onMeasure()
方法,會在下面詳細介紹。
將結果保存到 mMeasuredWidth
和 mMeasuredHeight
這兩個成員變量中,同時緩存到成員變量 mMeasureCache
中,以便下次執行 measure()
方法時可以從其中讀取緩存值。
須要說明的是,View 有一個成員變量 mPrivateFlags
,用以保存 View 的各類狀態位,在測量開始前,會將其設置爲未測量狀態,在測量完成後會將其設置爲已測量狀態。
DecorView 是 FrameLayout 子類,這時候應該去看 FrameLayout 中的 onMeasure() 方法,代碼具體以下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 獲取子view的個數 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);
// mMeasureAllChildren 默認爲FALSE,表示是否所有子 view 都要測量,子view不爲GONE就要測量 if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 測量子view measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 獲取子view的佈局參數 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 記錄子view的最大寬度和高度 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());
// 記錄全部跟父佈局有着相同寬或高的子view if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } } // Account for padding too 子view的最大寬高計算出來後,還要加上父View自身的padding 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()); } // 肯定父 view 的寬高 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); 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;
// 若是子view的寬是MATCH_PARENT,那麼寬度 = 父view的寬 - 父Padding - 子Margin 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); } } }
FrameLayout 是 ViewGroup 的子類,後者有一個 View[] 類型的成員變量 mChildren,表明了其子 View 集合。經過 getChildAt(i) 能獲取指定索引處的子 View,經過 getChildCount() 能夠得到子 View 的總數。
在上面的源碼中,首先調用 measureChildWithMargins() 方法對全部子 View 進行了一遍測量,並計算出全部子View的最大寬度和最大高度。然後將獲得的最大高度和寬度加上padding,這裏的padding包括了父View的padding和前景區域的padding。而後會檢查是否設置了最小寬高,並與其比較,將二者中較大的設爲最終的最大寬高。最後,若設置了前景圖像,咱們還要檢查前景圖像的最小寬高。
通過了以上一系列步驟後,咱們就獲得了 maxHeight 和 maxWidth 的最終值,表示當前容器 View 用這個尺寸就可以正常顯示其全部子View(同時考慮了 padding 和 margin )。然後咱們須要調用 resolveSizeAndState() 方法來結合傳來的 MeasureSpec 來獲取最終的測量寬高,並保存到 mMeasuredWidth 與 mMeasuredHeight 成員變量中。
若是存在一些子 View 的寬或高是 MATCH_PARENT,那麼須要等父 View 的尺寸計算出來後,再來決定這些子 view 的寬高。
下面看看 measureChildWithMargins() 方法具體邏輯:
/** * 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 * @param parentWidthMeasureSpec The width requirements for this view * @param widthUsed Extra space that has been used up by the parent * horizontally (possibly by other children of the parent) * @param parentHeightMeasureSpec The height requirements for this view * @param heightUsed Extra space that has been used up by the parent * vertically (possibly by other children of the parent) */ 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 的 MeasureSpec,而後調用 child.measure() 來完成子 View 的測量。下面看看子 View 獲取 MeasureSpec 的具體邏輯:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 父 view 的 mode 和 size int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); // 去掉 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 = 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 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
該
方法清楚展現了普通 View 的 MeasureSpec 的建立規則,每一個 View 的 MeasureSpec 狀態量由其直接父 View 的 MeasureSpec 和 View 自身的屬性 LayoutParams (LayoutParams 有寬高尺寸值等信息)共同決定。
子 View 爲具體的寬/高
,那麼 View 的 MeasureSpec 都爲 LayoutParams 中大小。
子 View 爲 match_parent
,父元素爲精度模式(EXACTLY),那麼 View 的 MeasureSpec 也是精準模式他的大小不會超過父容器的剩餘空間。
子 View 爲 wrap_content
,無論父元素是精準模式仍是最大化模式(AT_MOST),View 的 MeasureSpec 老是爲最大化模式而且大小不超過父容器的剩餘空間。
父容器爲 UNSPECIFIED 模式主要用於系統屢次 Measure 的情形,通常咱們不須要關心。
總結爲下表:
View.measure() 代碼邏輯前面已經分析過了,最終會調用 onMeasuere 方法,下面看下 View.onMeasuere() 的代碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上面方法中調用了 方法中調用了 setMeasuredDimension()
方法,setMeasuredDimension()
又調用了 getDefaultSize()
方法。getDefaultSize()
又調用了getSuggestedMinimumWidth()
和 getSuggestedMinimumHeight(),
那反向研究一下,先看下 getSuggestedMinimumWidth()
方法 (getSuggestedMinimumHeight()
原理 getSuggestedMinimumWidth()
跟同樣)。
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
源碼很簡單,若是 View 沒有背景,就直接返回 View 自己的最小寬度 mMinWidth;
若是給 View 設置了背景,就取 View 自己的最小寬度 mMinWidth
和背景的最小寬度的最大值.
那麼 mMinWidth
是哪裏來的?搜索下源碼就能夠知道,View 的最小寬度 mMinWidth
能夠有兩種方式進行設置:
minWidth
屬性來爲 mMinWidth
賦值:case R.styleable.View_minWidth: mMinWidth = a.getDimensionPixelSize(attr, 0); break;
setMinimumWidth
方法爲 mMinWidth
賦值public void setMinimumWidth(int minWidth) { mMinWidth = minWidth; requestLayout(); }
下面看下 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; }
從註釋能夠看出,getDefaultSize()
這個測量方法並無適配 wrap_content
這一種佈局模式,只是簡單地將 wrap_content
跟 match_parent
等同起來。
到了這裏,咱們要注意一個問題:
getDefaultSize()
方法中 wrap_content
和 match_parent
屬性的效果是同樣的,而該方法是 View 的 onMeasure()
中默認調用的,也就是說,對於一個直接繼承自 View 的自定義 View 來講,它的 wrap_content 和 match_parent 屬性是同樣的效果,所以若是要實現自定義 View 的 wrap_content
,則要重寫 onMeasure()
方法,對 wrap_content
屬性進行處理。
如何處理呢?也很簡單,代碼以下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ super.onMeasure(widthMeasureSpec, heightMeasureSpec); //取得父ViewGroup指定的寬高測量模式和尺寸 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { //若是寬高都是AT_MOST的話,即都是wrap_content佈局模式,就用View本身想要的寬高值 setMeasuredDimension(mWidth, mHeight); }else if (widthSpecMode == MeasureSpec.AT_MOST) { //若是隻有寬度都是AT_MOST的話,即只有寬度是wrap_content佈局模式,寬度就用View本身想要的寬度值,高度就用父ViewGroup指定的高度值 setMeasuredDimension(mWidth, heightSpecSize); }else if (heightSpecMode == MeasureSpec.AT_MOST) { //若是隻有高度都是AT_MOST的話,即只有高度是wrap_content佈局模式,高度就用View本身想要的寬度值,寬度就用父ViewGroup指定的高度值 setMeasuredDimension(widthSpecSize, mHeight); } }
在上面的代碼中,咱們要給 View 指定一個默認的內部寬/高(mWidth
和 mHeight
),並在 wrap_content
時設置此寬/高便可。最後將在將寬高設置到 View 上:
// View 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); } private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }
這裏就是把測量完的寬高值賦值給 mMeasuredWidth
、mMeasuredHeight
這兩個 View 的屬性,而後將標誌位置爲已測量狀態。
子 View 測量完成之後,會計算 childState,看下 combineMeasuredStates 方法 :
public static int combineMeasuredStates(int curState, int newState) { return curState | newState; }
當前 curState 爲 0, newState 是調用 child.getMeasuredState() 方法獲得的,來看下這個方法的具體邏輯:
/** * 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}. */ public final int getMeasuredState() { return (mMeasuredWidth&MEASURED_STATE_MASK) | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT) & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT)); }
該方法返回一個 int 值,該值同時包含寬度的 state 以及高度的 state 信息,不包含任何的尺寸信息。
MEASURED_STATE_MASK 的值爲 0xff000000,其高字節的 8 位所有爲 1,低字節的 24 位所有爲 0。
MEASURED_HEIGHT_STATE_SHIFT 值爲 16。
將 MEASURED_STATE_MASK 與 mMeasuredWidth
作與操做以後就取出了存儲在寬度首字節中的 state 信息,過濾掉低位三個字節的尺寸信息。
因爲 int 有四個字節,首字節已經存了寬度的 state 信息,那麼高度的 state 信息就不能存在首位字節。MEASURED_STATE_MASK 向右移 16 位,變成了 0x0000ff00,這個值與高度值 mMeasuredHeight
作與操做就取出了 mMeasuredHeight
第三個字節中的信息。而 mMeasuredHeight
的 state 信息是存在首字節中,因此也得對mMeasuredHeight
向右移相同的位置,這樣就把 state 信息移到了第三個字節中。
最後,將獲得的寬度 state 與高度 state 按位或操做,這樣就拼接成一個 int 值,該值首個字節存儲寬度的 state 信息,第三個字節存儲高度的 state 信息。
這些都獲得以後,就能夠開始去計算父 View 的尺寸了:
// 肯定父 View 的寬高 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
下面開始看 resolveSizeAndState 具體邏輯:
// View 的靜態方法 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) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; case MeasureSpec.UNSPECIFIED: default: result = size; } return result | (childMeasuredState & MEASURED_STATE_MASK); }
這個方法的代碼結構跟前文提到的 getDefaultSize()
方法很類似,主要的區別在於 specMode
爲 AT_MOST 的狀況。咱們當時說 getDefaultSize()
方法是沒有適配wrap_content
這種狀況,而這個 resolveSizeAndState()
方法是已經適配了 wrap_content
的佈局方式,那具體怎麼實現 AT_MOST 測量邏輯的呢?有兩種狀況:
當父 ViewGroup 指定的最大尺寸比 View 想要的尺寸還要小時,會給這個父 ViewGroup 的指定的最大值 specSize
加入一個尺寸過小的標誌 MEASURED_STATE_TOO_SMALL,而後將這個帶有標誌的尺寸返回,父 ViewGroup 經過該標誌就能夠知道分配給 View 的空間過小了,在窗口協商測量的時候會根據這個標誌位來作窗口大小的決策。
當父 ViewGroup 指定的最大尺寸比沒有比 View 想要的尺寸小時(相等或者 View 想要的尺寸更小),直接取 View 想要的尺寸,而後返回該尺寸。
getDefaultSize()
方法只是 onMeasure()
方法中獲取最終尺寸的默認實現,其返回的信息比 resolveSizeAndState()
要少,那麼何時纔會調用 resolveSizeAndState()
方法呢? 主要有兩種狀況:
Android 中的大部分 ViewGroup 類都調用了 resolveSizeAndState()
方法,好比 LinearLayout 在測量過程當中會調用 resolveSizeAndState()
方法而非 getDefaultSize()
方法。
咱們本身在實現自定義的 View 或 ViewGroup 時,咱們能夠重寫 onMeasure()
方法,並在該方法內調用 resolveSizeAndState()
方法。
到此,終於把 View 測量過程講完了。
下一篇開始講 View 的 layout 和 draw 過程。