Android面試題(28)-android的view載入和繪製流程
View的載入流程
view佈局一直貫穿於整個android應用中,不管是activity還是fragment都給我們提供了一個view依附的物件,關於view的載入我們在開發中一直使用,在接下來的幾篇文章中將介紹在android中的載入機制和繪製流程並且對於基於android6.0的原始碼進行分析探討。這一部分先來分析一下activity中view的載入流程。
當我們開啟activity時候,在onCreate方法裡面都要執setContentView(R.layout.activity_main)方法,這個是幹什麼的呢?當然是載入佈局的,首先貼上原始碼:
@Override public voidsetContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); }
這個方法存在於AppCompatActivity類中,追蹤到最底層發現他是繼承自activity。AppCompatActivity 其實內部的實現原理也和之前的 ActionBarActivity 不同,它是通過 AppCompatDelegate 來實現的。AppCompatActivity 將所有的生命週期相關的回撥,都交由 AppCompatDelegate 來處理。
@NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; }
AppCompatDelegate:
AppCompatDelegate為了支援 Material Design的效果而設計,需要其內部的控制元件都具有自動著色功能,來實現這個極佳的視覺設計效果,但是原有的控制元件所不支援Material Design的效果,所以它只好將其需要的 UI 控制元件全部重寫一遍來支援這個效果。這些效果被放置在android.support.v7.widget包下;
但是開發者不可能一個一個的修改已經開發好的產品,這樣工作量是非常大的。因此,設計者用AppCompatDelegate以代理的方式自動為我們替換所使用的 UI 控制元件。注意到這裡有個getDelegate()方法,他返回的就是AppCompatDelegate,內部的處理是呼叫AppCompatDelegate的靜態方法create來獲取不同版本的代理。如下:
private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) { final int sdk = Build.VERSION.SDK_INT; if (BuildCompat.isAtLeastN()) { return new AppCompatDelegateImplN(context, window, callback); } else if (sdk >= 23) { return new AppCompatDelegateImplV23(context, window, callback); } else if (sdk >= 14) { return new AppCompatDelegateImplV14(context, window, callback); } else if (sdk >= 11) { return new AppCompatDelegateImplV11(context, window, callback); } else { return new AppCompatDelegateImplV9(context, window, callback); } }
這裡看到它通過不同版本的API做了區分判斷來做具體的實現, AppCompatDelegateImplVxx的類,都是高版本的繼承低版本的,最低支援到API9,而 AppCompatDelegateImplV9 中,就是通過 LayoutInflaterFactory 介面來實現 UI 控制元件替換的代理。
我們再來看看Activity中的setContentView()方法:
public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
這裡有一個getWindow()方法,它返回的是一個Window物件,我們在上一篇就說過,Window是一個抽象類,它只定義了管理螢幕的介面,真正實現它的是phoneWindow,所以我們直接看phoneWindow的setContentView,這個方法有點長,就不在貼上了,但是裡面有這麼一句
mWindow.getLayoutInflater().setPrivateFactory(this);
可以知道這裡使用getLayoutInflater方法設定佈局,追蹤到這個方法才發現返回的是一個LayoutInflater:
此時我們發現原來是用LayoutInflater的inflate方法載入佈局的它包括兩種型別的過載形式,一種是載入一個view的id,另一種是載入XmlPullParser。
inflate載入佈局原始碼分析
(1)首先根據id在layout中找到相應的xml佈局。原始碼如下:
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) { LayoutInflater factory = LayoutInflater.from(context); return factory.inflate(resource, root);}
(2)把找到的xml檔案使用pull解析,一步一解析,再次呼叫inflate的XmlPullParser的引數形式的方法。在這個解析過程中採用遞迴的形式一步步解析,解析到相關的view新增到佈局裡面,遞迴的使用createViewFromTag()建立子View,並通過ViewGroup.addView新增到parent view中,原始碼如下:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); if (DEBUG) { Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" + Integer.toHexString(resource) + ")"); } final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }
總結:
1.通過Activity的setContentView方法間接呼叫Phonewindow的setContentView(),在PhoneWindow中通過getLayoutInflate()得到LayoutInflate物件
2.通過LayoutInflate物件去載入View,主要步驟是
(1)通過xml的Pull方式去解析xml佈局檔案,獲取xml資訊,並儲存快取資訊,因為這些資料是靜態不變的
(2)根據xml的tag標籤通過反射建立View逐層構建View
(3)遞迴構建其中的子View,並將子View新增到父ViewGroup中;
四種xml檔案載入的常用方法:
1、使用view的靜態方法
View view=View.inflate(context, R.layout.child, null);
2、通過系統獲取
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view= inflater.inflate(R.layout.child, null);
3、通過LayoutInflater
LayoutInflater inflater = LayoutInflater.from(context);
View view= inflater.inflate(R.layout.child, null);
4、通過getLayoutInflater
View view=getLayoutInflater().inflate(R.layout.child, null);
在view載入結束之後,就開始繪製UI了,在這繪製的過程中如何繪製?執行了哪些方法步驟呢?
View的繪製流程
這一部分打算從四個方面來說:
1.View樹的繪製流程
2.mesure()方法
3.layout()方法
4.draw()方法
首先說說這個View樹的繪製流程:
說到這個流程,我們就必須先搞清楚這個流程是誰去負責的
實際上,view樹的繪製流程是通過ViewRoot去負責繪製的,ViewRoot這個類的命名有點坑,最初看到這個名字,翻譯過來是view的根節點,但是事實完全不是這樣,ViewRoot其實不是View的根節點,它連view節點都算不上,它的主要作用是View樹的管理者,負責將DecorView和PhoneWindow“組合”起來,而View樹的根節點嚴格意義上來說只有DecorView;每個DecorView都有一個ViewRoot與之關聯,這種關聯關係是由WindowManager去進行管理的;
那麼decorView與ViewRoot的關聯關係是在什麼時候建立的呢?答案是Activity啟動時,ActivityThread.handleResumeActivity()方法中建立了它們兩者的關聯關係,當建立好了decorView與ViewRoot的關聯後,ViewRoot類的requestLayout()方法會被呼叫,以完成應用程式使用者介面的初次佈局。也就是說,當Activity獲取到了使用者的觸控焦點時,就會請求開始繪製佈局,這也是整個流程的起點;而實際被呼叫的是ViewRootImpl類的requestLayout()方法,這個方法的原始碼如下:(ViewRootImpl原始碼是隱藏的,我在Android Studio通過普通方式無法獲取到,最後在android-sdk檔案中獲取的 F:\android_sdk\sources\android-26\android\view)
public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { // 檢查發起佈局請求的執行緒是否為主執行緒 checkThread(); mLayoutRequested = true; scheduleTraversals(); }}作者:absfree
連結:https://www.jianshu.com/p/060b5f68da79
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { // 檢查發起佈局請求的執行緒是否為主執行緒 checkThread(); mLayoutRequested = true; scheduleTraversals(); }}
作者:absfree
連結:https://www.jianshu.com/p/060b5f68da79
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { // 檢查發起佈局請求的執行緒是否為主執行緒 checkThread(); mLayoutRequested = true; scheduleTraversals(); }}
作者:absfree
連結:https://www.jianshu.com/p/060b5f68da79
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { // 檢查發起佈局請求的執行緒是否為主執行緒 checkThread(); mLayoutRequested = true; scheduleTraversals(); }}
作者:absfree
連結:https://www.jianshu.com/p/060b5f68da79
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }上面的方法中呼叫了scheduleTraversals()方法來排程一次完成的繪製流程,該方法會向主執行緒傳送一個“遍歷”訊息,最終會導致ViewRootImpl的performTraversals()方法被呼叫。下面,我們以performTraversals()為起點,來分析View的整個繪製流程。performTraversals()原始碼巨長,這裡就不貼上了,只需要記住,在裡面主要做了三件事
(1)是否重新計算檢視大小(mesure()方法)
(2)是否重新擺放檢視位置(layout()方法)
(3)是否重新繪製檢視(draw()方法)
接下來再看看這三個重要的方法:
(1)measure方法:
此階段的目的是計算出控制元件樹中的各個控制元件要顯示其內容的話,需要多大尺寸。起點是ViewRootImpl的measureHierarchy()方法,這個方法的原始碼如下:
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; if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(mTag, "Measuring " + host + " in display " + desiredWindowWidth + "x" + desiredWindowHeight + "..."); boolean goodMeasure = false; 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; } } } } if (!goodMeasure) { childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = true; } } if (DBG) { System.out.println("======================================"); System.out.println("performTraversals -- after measure"); host.debug(); } return windowSizeMayChange; }上面的程式碼中呼叫getRootMeasureSpec()方法來獲取根MeasureSpec,這個根MeasureSpec代表了對decorView的寬高的約束資訊。繼續分析之前,我們先來簡單地介紹下MeasureSpec的概念。
MeasureSpec是一個32位整數,由SpecMode和SpecSize兩部分組成,其中,高2位為SpecMode,低30位為SpecSize。SpecMode為測量模式,SpecSize為相應測量模式下的測量尺寸。View(包括普通View和ViewGroup)的SpecMode由本View的LayoutParams結合父View的MeasureSpec生成。
SpecMode的取值可為以下三種:
- EXACTLY: 對子View提出了一個確切的建議尺寸(SpecSize);
- AT_MOST: 子View的大小不得超過SpecSize;
- UNSPECIFIED: 對子View的尺寸不作限制,通常用於系統內部。
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); } }上面程式碼中的mView即為decorView,也就是說會轉向對View.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); } // Suppress sign extension for the low bytes long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; // Optimize layout by avoiding an extra EXACTLY pass when the view is // already measured as the correct size. In API 23 and below, this // extra pass is required to make LinearLayout re-distribute weight. 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(); int cacheIndex = forceLayout ? -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 }可以看出,這個方法接收的引數是父控制元件對其寬高的約束資訊,還有就是它使用了final聲明瞭,所以這個方法是不可以重寫的,那麼平時我們自定義View時,到底是如何去進行view的測量的呢?
在其中有這麼一句
onMeasure(widthMeasureSpec, heightMeasureSpec);所以,原來在measure方法中其實也是呼叫onMeasure方法去進行測量,所以平時我們自定義View時,只需要重寫onMeasure
就可以了;
對於decorView來說,實際執行測量工作的是FrameLayout的onMeasure()方法,該方法的原始碼如下:
@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(); ....... 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)); 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); } } }在上面的原始碼中,首先呼叫measureChildWithMargins()方法對所有子View進行了一遍測量,並計算出所有子View的最大寬度和最大高度。而後將得到的最大高度和寬度加上padding,這裡的padding包括了父View的padding和前景區域的padding。然後會檢查是否設定了最小寬高,並與其比較,將兩者中較大的設為最終的最大寬高。最後,若設定了前景影象,我們還要檢查前景影象的最小寬高。經過了以上一系列步驟後,我們就得到了maxHeight和maxWidth的最終值,表示當前容器View用這個尺寸就能夠正常顯示其所有子View(同時考慮了padding和margin)。而後我們需要呼叫resolveSizeAndState()方法來結合傳來的MeasureSpec來獲取最終的測量寬高,並儲存到mMeasuredWidth與mMeasuredHeight成員變數中。我們可以看到,容器View通過measureChildWithMargins()方法對所有子View進行測量後,才能得到自身的測量結果。也就是說,對於ViewGroup及其子類來說,要先完成子View的測量,再進行自身的測量(考慮進padding等)。
接下來我們來看下ViewGroup的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); }由以上程式碼我們可以知道,對於ViewGroup來說,它會呼叫child.measure()來完成子View的測量。傳入ViewGroup的MeasureSpec是它的父View用於約束其測量的,那麼ViewGroup本身也需要生成一個childMeasureSpec來限制它的子View的測量工作。這個childMeasureSpec就由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 = 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的LayoutParams生成子View的MeasureSpec的過程,** 子View的LayoutParams表示了子View的期待大小**。這個產生的MeasureSpec用於指導子View自身的測量結果的確定。
在上面的程式碼中,我們可以看到當ParentMeasureSpec的SpecMode為EXACTLY時,表示父View對子View指定了確切的寬高限制。此時根據子View的LayoutParams的不同,分以下三種情況:
- 具體大小(childDimension):這種情況下令子View的SpecSize為childDimension,即子View在LayoutParams指定的具體大小值;令子View的SpecMode為EXACTLY,即這種情況下若該子View為容器View,它也有能力給其子View指定確切的寬高限制(子View只能在這個寬高範圍內),若為普通View,它的最終測量大小就為childDimension。
- match_parent:此時表示子View想和父View一樣大。這種情況下得到的子View的SpecMode與上種情況相同,只不過SpecSize為size,即父View的剩餘可用大小。
- wrap_content: 這表示了子View想自己決定自己的尺寸(根據其內容的大小動態決定)。這種情況下子View的確切測量大小隻能在其本身的onMeasure()方法中計算得出,父View此時無從知曉。所以暫時將子View的SpecSize設為size(父View的剩餘大小);令子View的SpecMode為AT_MOST,表示了若子View為ViewGroup,它沒有能力給其子View指定確切的寬高限制,畢竟它本身的測量寬高還懸而未定。
當ParentMeasureSpec的SpecMode為AT_MOST時,我們也可以根據子View的LayoutParams的不同來分三種情況討論:
- 具體大小:這時令子View的SpecSize為childDimension,SpecMode為EXACTLY。
- match_parent:表示子View想和父View一樣大,故令子View的SpecSize為size,但是由於父View本身的測量寬高還無從確定,所以只是暫時令子View的測量結果為父View目前的可用大小。這時令子View的SpecMode為AT_MOST。
- wrap_content:表示子View想自己決定大小(根據其內容動態確定)。然而這時父View還無法確定其自身的測量寬高,所以暫時令子View的SpecSize為size,SpecMode為AT_MOST。
從上面的分析我們可以得到一個通用的結論,當子View的測量結果能夠確定時,子View的SpecMode就為EXACTLY;當子View的測量結果還不能確定(只是暫時設為某個值)時,子View的SpecMode為AT_MOST。
在measureChildWithMargins()方法中,獲取了知道子View測量的MeasureSpec後,接下來就要呼叫child.measure()方法,並把獲取到的childMeasureSpec傳入。這時便又會呼叫onMeasure()方法,若此時的子View為ViewGroup的子類,便會呼叫相應容器類的onMeasure()方法,其他容器View的onMeasure()方法與FrameLayout的onMeasure()方法執行過程相似。從這裡就可以看出,View的測量過程,其實就是遞迴去遍歷樹的過程;
下面會我們回到FrameLayout的onMeasure()方法,當遞迴地執行完所有子View的測量工作後,會呼叫resolveSizeAndState()方法來根據之前的測量結果確定最終對FrameLayout的測量結果並存儲起來。View類的resolveSizeAndState()方法的原始碼如下: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); }對於普通View(非ViewgGroup)來說,只需完成自身的測量工作即可。通過setMeasuredDimension()方法設定測量的結果,具體來說是以getDefaultSize()方法的返回值來作為測量結果。getDefaultSize()方法的原始碼如下:
public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec)