標籤:
本篇類比三個角色:Android 架構師-小福、Android 控制項開發工程師-小黑、 Android 開發工程師-小白,下面按照三個角色不同角度分析measure過程。
小福負責分享:
- measure的本質 - ok
- measure代碼流程 - 分析FrameLayout.onMeasure
- onMeasure方法與MeasureSpec - ok
- 提出問題
Android 架構師-小福的分享
一、Measure本質
小福:我今天分享是的measure架構設計相關的,先問一個問題,measure的本質是什嗎?
小黑:這個我知道,是Android系統建立UI介面的measure、layout、draw三步驟的第一步,主要用於測量視圖大小,更詳細點說是把“相對值”(WRAP_CONTENT, FILL_PARENT, MATCH_PARENT)轉換為具體指的過程。
小福:小黑說的對,再問一個問題,視圖大小指的是什嗎?
小白:視圖大小是在視圖在螢幕上顯示的大小,也就是開發的時候通過layout_width與layout_heigh設定的?
小福:小白說的只是其中一個作為開發人員的角度。Android系統設計中Canvas是無窮大的,假如一個螢幕的大小是320 * 480 ,但是layout_width="480px" , layout_heigh="800px",很明顯視圖的寬高大於實際螢幕大小。 問題來了,視圖的大小到底是螢幕上顯示的大小,還是視圖的實際大小(即使是超過了螢幕大小)?
小黑:具體視圖顯示大小是由開發人員設定,之後由我控制項開發工程師在onMeasure中決定,如果向小福說的尺寸,即使超過螢幕我可以決定是width= 320, heigh = 480 還是widt= 480, heigh = 800 ,決定權在我這裡,一會在我分享的時候會寫一個Demo來示範。 (視圖根據繪製大小不同分類:內容型視圖、圖形型視圖)
小白:Canvas是什嗎?小福:這個在之後分享draw過程的時候在詳細討論,可以笼統的理解為畫畫時使用的畫布。
二、Measure代碼流程
小福:先從源碼看下measure執行流程,看看這些過程中都做了些什麼。以下都是android.view.ViewRootImpl.java類中的源碼
public final class ViewRootImpl extends Handler implements ViewParent, View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { // 1 所有子視圖的requestLayout方法,最總都會觸發根視圖此方法 public void requestLayout() { checkThread(); // 需要重新布局 mLayoutRequested = true; scheduleTraversals(); } // 調度遍曆 public void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; ..... // 當前類繼承自Handler,發出一個空訊息,目的是加入Message隊列 sendEmptyMessage(DO_TRAVERSAL); } } @Override public void handleMessage(Message msg) { switch (msg.what) { ... case DO_TRAVERSAL: ... // 處理DO_TRAVERSAL訊息 performTraversals(); ... break;..... } } // 執行遍曆 private void performTraversals() { final View host = mView; int desiredWindowWidth; int desiredWindowHeight; int childWidthMeasureSpec; int childHeightMeasureSpec; ...... if (mLayoutRequested && !mStopped) { ...... childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); ...... // host是一個View對象 host.measure(childWidthMeasureSpec, childHeightMeasureSpec); ...... } ...... }}
注意:以上代碼中getRootMeasureSpec方法可以或者跟視圖中childWidthMeasureSpec與childHeightMeasureSpec,感興趣的可以自己看下desiredWindowWidth變數的賦值其擷取的是視窗的寬高。
上面的代碼一共分為5個步驟1 requestLayout() -> 2 scheduleTraversals() -> 3 handleMessage() -> 4 performTraversals() -> 5 host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- 介面中所有視圖執行requestLayout,重新布局請求會逐步向上傳遞,最終傳執行當前ViewRootImpl的requestLaout()
- 步驟1中會執行scheduleTraversals,其中發送一個空的訊息,把重新布局的請求通過Handler發送到主線程的MeassQueue等待執行(具體可以學習Handler)。
- 因為當前ViewRootImpl是繼承自Handler,所以直接尋找覆寫的handleMessage方法,因為傳遞的訊息是DO_TRAVERSAL,分支調用performTraversals
- performTraversals方法中調用host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- 因為host是View對象所以接下來需要查看View.measure方法,才能進一步分析measure流程
接著上面的measure流程的第五步走下去,以下是android.view.View.java檔案中的源碼:
public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Callback, AccessibilityEventSource { // 方法是final類型,說明不能被覆寫或者重載 public final void measure(int widthMeasureSpec, int heightMeasureSpec) { // 如果有重新請求標誌,或者寬高發生改變 if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { ...... // 真正執行測量視圖大小操作 // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); ...... // 添加重新請求子視圖布局標誌 mPrivateFlags |= LAYOUT_REQUIRED; } ...... } /** * Call this when something has changed which has invalidated the * layout of this view. This will schedule a layout pass of the view * tree. */ public void requestLayout() { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.REQUEST_LAYOUT); } // 添加重新請求布局標誌 mPrivateFlags |= FORCE_LAYOUT; mPrivateFlags |= INVALIDATED; if (mParent != null) { if (mLayoutParams != null) { mLayoutParams.resolveWithDirection(getResolvedLayoutDirection()); } if (!mParent.isLayoutRequested()) { mParent.requestLayout(); } } }}
上面代碼的measure流程可以分為4個步驟
1 measure與requestLayout -> 2 onMeasure
- measure方法是final類型,說明此方法不能被修改。其中判斷條件(mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT的值是在requestLayout 進行賦值的。只要測量的寬高等發生改變都會觸發第二步。
- 執行當前的onMeasure方法,通過Hierarchy Viewer等工具可以獲知根視圖是FrameLayout類型(這裡就不從源碼驗證了)
緊接著看下android.widget.FrameLayout類的onMeasure總都做了什嗎?
三、onMeasure方法與MeasureSpec 上面顯示的代碼中參數int widthMeasureSpec, int heightMeasureSpec都是通過MeasureSpec類進行統一處理。 MeasureSpec是一個android.view.View的內部類,封裝了從父類傳送到子類的布局要求資訊。每個MeasureSpec對象描述了空間的高度或寬度。 MeasureSpec由size和mode組成。
1. MeasureSpec的方法介紹:
| 類名.方法名 |
解釋 |
| MeasureSpec.getMode(int measureSpec) |
根據提供的測量值(格式)提模數式(上述三個模式之一) |
| MeasureSpec.getSize(int measureSpec) |
根據提供的測量值(格式)提取大小值 |
| MeasureSpec.makeMeasureSpec(int size,int mode) |
根據提供的大小值和模式建立一個測量值(格式) |
2. MeasureSpec有三種mode,分別說明並描述模式與layout參數值的對應關係
| 模式 |
翻譯 |
模式與Layout參數對應關係 |
模式描述 |
| UNSPECIFIED |
無限制 |
|
parent view不約束child view的大小 |
| AT_MOST |
最多的 |
wrap_content |
child view可以在parent view範圍內取值 |
| EXACTLY |
準確的 |
fill_parent(例如50dip) |
parent view為child view指定固定大小 |
3. MeasureSpec通過位運行從int類型的值中擷取mode與sieze更詳細的分析請查看《android中onMeasure初看,深入理解布局之一!》
四、提出問題為什麼布局需要父視圖與子視圖共同決定? 為什麼不直接設定寬和高?如果不這樣就像HTML那樣指定固定大小,這樣會造成一個過大把另外一個擠出去。因為HTML是具有整個頁面的控制權。而Android是拆分開的,這樣做保證了最末端的(介面開發)影響其他層級的布局,需要按照(控制項開發)的規則來。view.setWidth, view.setHeight ? 不是
需要通過measureChilde(view, width, height), 或者childView.measure();
還有哪些?View.getWidth(), View.getHight()
View.getMeasureWidth(), View.getMeasureHiegh()
一個未添加到視圖中的? 但是有時getMesureHeight 依然是返回0。為什嗎?
Android View measure (一) 流程分析