--- title: 'View 工作原理' disqus: kyleAlien --- View 工作原理 === ## OverView of Content [TOC] ## View 概述 ### [ViewRootImpl](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewRootImpl.java) & DecorView 關係 * ViewRootImpl 是用來連接 WindowMananger 和 DecorView 的樞紐,**View 的三大流程 (measure、layout、draw) 都是透過 ViewRootImpl 完成** > ![](https://i.imgur.com/3NxeIGI.png) * 單個點擊事件就是從 WindowManager 傳遞給 DecorView#ViewRootImpl 的 ! **==View 的繪製是由 ViewRootImpl 完成==** ### [Activity](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/Activity.java) & PhoneWindow 關係 1. AMS 通知 ActivityThread 創建 Activity 對象,並透過 Acitivty#attach 連結創建好的 Activity 對象 ```java= // ActivityThread.java public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal { @Override public Activity handleLaunchActivity(ActivityClientRecord r, PendingTransactionActions pendingActions, Intent customIntent) { ... 省略部分 final Activity a = performLaunchActivity(r, customIntent); ... 省略部分 return a; } private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ActivityInfo aInfo = r.activityInfo; ... 省略部分 ContextImpl appContext = createBaseContextForActivity(r); Activity activity = null; try { java.lang.ClassLoader cl = appContext.getClassLoader(); // 反射創建 Activity activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); ... } /* 省略 catch */ try { Application app = r.packageInfo.makeApplication(false, mInstrumentation); ... if (activity != null) { ... Window window = null; ... // @ 分析 attach (並假設這裡傳入的 window 是 null) activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback, r.assistToken, r.shareableActivityToken); ... r.activity = activity; ... } } /* 省略 catch */ return activity; } } ``` 2. Activity#**attach**:**創建 PhoneWindow 對象,並為 PhoneWindow 設定 WindowManager (好讓 PhoneWinodw 有辦法跟 WMS 通訊)** ```java= // Activity.java public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, /* 省略部分 interface */ { private Window mWindow; final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, /* 省略部分參數 */) { ... // 創建 PhoneWindow 實作類 mWindow = new PhoneWindow(this, window, activityConfigCallback); ... // @ Window(PhoneWindow) 與 WindowManager 產生關係 mWindow.setWindowManager( (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0); if (mParent != null) { mWindow.setContainer(mParent.getWindow()); } mWindowManager = mWindow.getWindowManager(); ... } } ``` :::info * DecorView 創建的時機 ? DecorView 並不會一開始就創建出來,它是懶加載,像是使用者在 Actiivty#onCreate 時呼叫 **setContentView 方法時才會創建** ::: 3. 透過 Activty#**setContentView** 方法觸發 PhoneWindow#installDecor 方法來創建 DecorView 對象 :::warning * Acitivty#setContentView 在底層是通過 **Window** 來完成 ::: ```java= // 某 Acitivity.java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // @ 分析 setContentView setContentView(R.layout.activity_lobby); } // ------------------------------------------------------------------------- // PhoneWindow.java public class PhoneWindow extends Window implements MenuBuilder.Callback { // 最終 setContentView 會在 PhoneWindow 實作 public void setContentView(int layoutResID) { if (mContentParent == null) { // 第一次加載布局才會創建 DecorView installDecor(); } ... } private void installDecor() { if (mDecor == null) { mDecor = generateDecor(-1); ... } ... } protected DecorView generateDecor(int featureId) { ... // 真正創建 DecorView 對象 return new DecorView(context, featureId, this, getAttributes()); } } ``` > ![](https://i.imgur.com/TS4NSKp.png) ### View 繪製流程 像是我們在使用 ViewRootImpl#`setView` 的這函數時,就會觸發 View 的 **異步刷新** ```java= // ViewRootImpl.java @Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } } ``` * View 的繪製流程入口,會從 ViewRootImpl#**scheduleTraversals** 方法被觸發開始,會透過 Choreographer 執行一個 Runnable 方法 (其實就是透過 Handler 發送一個任務) ```java= // ViewRootImpl.java final Choreographer mChoreographer; final TraversalRunnable mTraversalRunnable = new TraversalRunnable(); // @ scheduleTraversals void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; ... // @ 分析 mTraversalRunnable 這個 Member mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); ... } } ``` * TraversalRunnable 內單純就是執行 doTraversal 方法,這裡的 **重點在 ==performTraversals 方法==**,performTraversals 會觸發 performMeasure、performLayout、performDraw | 順序 | ViewRoot 觸發方法 | ViewRoot 執行方法 | ChildView 響應方法 | | -| -------- | -------- | -------- | | 1 | performMeasure | measure | onMeasure | | 2 | performLayout | layout | onLayout | | 3 | performDraw | draw | onDraw | ```java= // ViewRootImpl.java final class TraversalRunnable implements Runnable { @Override public void run() { // @ 分析 doTraversal 方法 doTraversal(); } } void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; ... // @ 查看 performTraversals 方法 performTraversals(); ... } } // performTraversals 方法很長,我們只關注重點部分 private void performTraversals() ... 省略部分 if (!mStopped || wasReportNextDraw) { if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || dispatchApplyInsets || updatedConfiguration) { ... // @ performMeasure 觸發 View#measure 方法 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... } } if (didLayout) { // @ performLayout 觸發 View#layout 方法 performLayout(lp, mWidth, mHeight); ... } if (!cancelDraw) { ... // @ performDraw 觸發 View#draw 方法 performDraw(); } } private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { if (mView == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { // 每個 View#measure 方法 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { mScrollMayChange = true; mInLayout = true; final View host = mView; if (host == null) { return; } try { // 每個 View#layout 方法 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); ... 省略部分 } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } mInLayout = false; } private void performDraw() { ... try { // 每個 View#draw 方法 boolean canUseAsync = draw(fullRedrawNeeded); if (usingAsyncReport && !canUseAsync) { mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null); usingAsyncReport = false; } } finally { mIsDrawing = false; Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } ``` * 從這邊可以看出 [**View**](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/View.java) 的繪製 ^1.^ 透過 Handler 添加繪製任務、並且 ^2.^ Measure、Layout、Draw 是有順序的、^3.^ **透過 View 中的 onMeasure、onLayout、onDraw 遞迴調用來完成 View 的遍歷,最終完成 View 樹的遍歷** :::success * View 樹遞迴調用 View#draw 會透過 dispatchDraw 自動調用 ViewGroup#dispatchDraw 方法進行繪製,但 layout、measure 則要使用者自己調用才會進行 ::: > ![](https://i.imgur.com/cApAKF7.png) ### DecorView 概述 * DecorView 是 Android SDK 為使用者提供的一個 ViewGroup (頂層 View),它又繼承於 FrameLayout; DecorView 一般情況下內部包有一個 Linearlayout,該 Linearlayout 又分為兩個重點部分 1. title_bar 2. content:我們一般在 Activity#onCreate 方法中使用 setContentView 就是對 content 添加 View ```java= // 可以透過以下方法取得 content @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_lobby); ViewGroup vg = (ViewGroup) findViewById(android.R.id.content); } ``` > ![](https://i.imgur.com/ilYwWac.png) ## measure 參數的決定 **先調用 View#measure 函數,measure 函數最終才會調用到 Child View 中的 onMeasure 函數** ```java= // View.java public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... if (forceLayout || needsLayout) { ... if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure 最後會調用 onMeasure onMeasure(widthMeasureSpec, heightMeasureSpec); } ... } ... } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ... } ``` * 而 measure 的兩個引數 (widthMeasureSpec、heightMeasureSpec) 分別是,**父視圖傳入的 Width、Height 大小,也就是 當前 View 可以在這個空間內發揮**; 接著我們會針對這個 Spec 研究(因為它會依照情狀不同而不同的變化,並非固定值) :::success * **onMeasure 是如何計算空間的機制** ? 我們知道 View 是樹結構,它透過深度為優先,**==遞迴== 整個 View**,才能計算出該 View 的真正空間大小,尤其是 **`wrap_content` 時就必須量到末端的 View 才能取得大小,所以 wrap_content 是比較耗時的** ::: > ![](https://i.imgur.com/hRCaAza.png) ### MeasureSpec 理解 * MeasureSpec 是 View 的靜態內部類,使用一個 int 儲存,這個 int 在 MeasureSpec 代表 **兩個功能** 1. **SIZE**:之後的的 30 bit 才是真正 ==Size== 2. **MODE**:最高的 2 個 bit 是 ==Mode== 標示位元 | MeasureSpec Mode | 功能 | Xml attr 對應 | | -------- | -------- | -------- | | UNSPECIFIED | Mode `00`,特殊數值,Child View 要多大給多大,可超出 Parent | 多是系統用 (e.g ListView) | | EXACTLY | Mode `01`,**確切數值**,並將數值存在後面 30 個位元,不可超出 Parent | 指定大小 (e.g `30dp` or `match_parent`) | | AT_MOST | Mode `10`,**未確定數值**,後面 30 位元大小指示參考值,最後測量完畢才能確定,但不可超出 Parent | `wrap_content` | :::info * ChildView 設定 `match_parent` 就一定是 EXACTLY ? 為啥 ChildView 都設定 `match_parent` 也是未確定數值 (AT_MOST) ? 這要配合 ParentView 一起看,有可能也是 (wrap_content) 未確定數值,那 ChildView 設定為 match_parent 也不是 `EXACTLY` ```xml= <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" tools:context=".MainActivity"> <!--Parent--> <TextView android:layout_width="match_parent" android:layout_height="match_parent"/> <!--Child--> </LinearLayout> ``` ::: * 測量過程中,系統會將依照 ParentView 的限制轉換成 MeasureSpec,再傳給 ChildView,ChildView 就要根據傳入的 MeasureSpec 做判斷 (之後會再說明) ```java= // View#MeasureSpec.java public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; ... 省略部分 // ParentView 不對 ChildView 有任何的限制,就算超出也沒關係 public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 固定寬高,但不超過 ParentView public static final int EXACTLY = 1 << MODE_SHIFT; // ChildView 要多大就給它多大(它也可以要很小,看它如何決定),但不超過 ParentView public static final int AT_MOST = 2 << MODE_SHIFT; // 將數值轉換為 MeasureSpec public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } } ``` * MeasureSpec 各種 Mode 概念圖 >![](https://i.imgur.com/Kxs4yaH.png) * ParentView 設定 `wrap_content` (AT_MOST) 時設定自身大小的時機 > 透過 setMeasuredDimension 方法設定自身大小 :::success * 先測量 ParentView 還是 ChildView ? 其實都是可以的,**這決定於 ViewGroup 的實現**,**^1.^ 大部份先測量 ChildView 最後才決定 ParentView**,像是 **^2.^ ViewPager 就是先決定 ParentView 大小** ```java= // 特例: ViewPager.java @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 透過 setMeasuredDimension 定義該 View 大小 // // 一開始就定義 Parent View 大小 setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec)); } ``` ::: ### LayoutParams 理解 * **如其名 Layout Parameters,它的主要功能是==儲存該 View 在 Xml 中 寬、高 設置的數值==** > ![](https://i.imgur.com/NrtPoCv.png) ```java= // ViewGroup#LayoutParams public static class LayoutParams { @Deprecated // API 8 以上以棄用 public static final int FILL_PARENT = -1; public static final int MATCH_PARENT = -1; public static final int WRAP_CONTENT = -2; /** * 以下為寬高,可以是 * 精確數值、MATCH_PARENT、 WRAP_CONTENT */ public int width; // 寬 public int height; // 高 public LayoutParams(Context c, AttributeSet attrs) { // 抓取 Xml 中設定的數值 TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout); setBaseAttributes(a, R.styleable.ViewGroup_Layout_layout_width, R.styleable.ViewGroup_Layout_layout_height); a.recycle(); } ... 以下省略 } ``` * 可依照需求在 ViewGroup 中繼承,並重新定義其功能,像是 [**ViewPager**](https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/com/android/internal/widget/ViewPager.java) 就有重新定義自己需要的 LayoutParams ```java= // ViewPager.java private static final int[] LAYOUT_ATTRS = new int[] { com.android.internal.R.attr.layout_gravity }; public static class LayoutParams extends ViewGroup.LayoutParams { public boolean isDecor; public int gravity; float widthFactor = 0.f; public LayoutParams() { super(FILL_PARENT, FILL_PARENT); } public LayoutParams(Context context, AttributeSet attrs) { super(context, attrs); final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); gravity = a.getInteger(0, Gravity.TOP); a.recycle(); } } ``` ### MeasureSpec & LayoutParams 關係 * 首先由上面我們可以知道 1. MeasureSpec 是程式內傳遞的數值,其中包含了 Mode + Size,**MeasureSpec 是被 ParentView 限制過後的數值** 2. LayoutParams 則是在 xml 內設定的數值,包含了 `match_parent`、`wrap_content`、確切數值,**LayoutParams 是使用者自己在 xml 設定的數值** > ![](https://i.imgur.com/maBHn84.png) * **在測量 (measure) 時,系統會將 xml 設定的 LayoutParams 轉換為 MeasureSpec**,再根據 MeasureSpec 來確定 View 測量後的寬高 :::danger * ChildView 的 MeasureSpec 並不是唯一由自身設定的 LayoutParams 決定,ChildView 的 MeasureSpec 數值必須要和 ParentView 的 MeasureSpec 一起決定 ::: 1. **DecorView** 是由:`窗口大小` + `自身 LayoutParams 決定` > [**ViewRootImpl**](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewRootImpl.java) 中的 measureHierarchy 方法,展示了 DecorView 的 MeasureSpec 創建過程。 > > 其中 desiredWindowWidth、desiredWindowHeight 就是螢幕寬高 ```java= // ViewRootImpl.java private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { // 其實這裡的 int 是指 MeasureSpec int childWidthMeasureSpec; int childHeightMeasureSpec; boolean goodMeasure = false; ... 省略部分 if (!goodMeasure) { // 螢幕寬 desiredWindowWidth childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); // 螢幕高 desiredWindowHeight childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); // @ 將 DecorView 算好的數值傳入 ChildView // (ChildView 可用的範圍 + Mode) // 呼叫 measure 方法,最後觸發 DecorView#onMeasure 方法 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = true; } } return windowSizeMayChange; } // 從這裡就可以看出是如何配合 `窗口大小` + `自身 LayoutParams 設定`, // 來創建 MeasureSpec private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; // 先判斷 自身 LayoutParams 設定 switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // MATCH_PARENT 對應 EXACTLY measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // WRAP_CONTENT 對應 AT_MOST measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // 固定 Size 對應 EXACTLY measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; } ``` 2. 一般 View 是由:`ParentView 的 MeasureSpec` + `自身 LayoutParams 決定` > 普通的 View 來說,View 的 measure 參數是由 [**ViewGroup**](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java) 決定後傳遞而來 ```java= // ViewGroup.java // 這裡傳入的 int 其實是指 MeasureSpec (也就是包含了 Mode + Size) protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; // 確定該 View 不是 Gone if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { // @ 查看 measureChild 方法 measureChild(child, widthMeasureSpec, heightMeasureSpec); } } } protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); // @ 查看 getChildMeasureSpec 方法 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); // 呼叫 ChildView#measure child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } public static int getChildMeasureSpec(int spec, int padding, int childDimension) { // 取得 ViewGroup#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; // 先判斷 ViewGroup 的 Mode switch (specMode) { case MeasureSpec.EXACTLY: // ParentView 是確切數值 or match_parent // 判斷 parent's LayoutParams if (childDimension >= 0) { // ChildView 是確切數值 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // ChildView 是確切數值 resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // ChildView 可自己決定大小 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.AT_MOST: // ParentView 是 warp_content if (childDimension >= 0) { // ChildView 是確切數值 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 由於 ParentView 自身大小都不確定,所以 ChildView 也不能確定 resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 由於 ParentView 自身大小都不確定,所以 ChildView 也不能確定 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // ChildView 是確切數值 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // ChildView 要多大有多大 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // ChildView 要多大有多大 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } ``` > ![](https://i.imgur.com/BUrItWt.png) :::success * 重點: 1. ViewGroup#measure 的目的是決定 ChildView 的 MeasureSpec 2. 只要提供 ParentView 的 MeasureSpec & ChildView 的 LayoutParams,ViewGroup 就可以決定 ChildView 的 MeasureSpec ::: ## View 繪製流程 View 的繪製流程主要分為 measure、layout、draw,接下來我們都會個別說明 (其中 measure 是最複雜的流程) ### [View](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/View.java) - measure 過程 * View 的 measure 方法中會去調用 View#onMeasure 方法,我們先來看看 View#onMeasure 方法預設的實現方式 ```java= // View.java @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 查看 @ getDefaultSize、getSuggestedMinimumWidth 方法 setMeasuredDimension( getDefaultSize( getSuggestedMinimumWidth(), widthMeasureSpec ), getDefaultSize( getSuggestedMinimumHeight(), heightMeasureSpec ) ); } ``` * 首先我們知道 setMeasuredDimension 方法是設定 View 的寬高;接著來看看 `getDefaultSize`、`getSuggestedMinimumWidth` 這兩個方法 1. **getDefaultSize**:透過 MeasureSpec 的 Mode 設定 size,預設都是取用 ParentView 給予的 size :::info * 從這邊可以看若是沒有特別設定 wrap_content,那 View 的大小就是最大值 (同 match_parent) ::: ```java= // View.java 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; // 預設 ChildView 若是 wrap_content、match_parent 就都是 最大 Size case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; } ``` 2. **getSuggestedMinimumWidth** 函數:取得建議的最小值 (這裡我們就不分寬、高), 如果有背景圖的話就取用背景大小、否則就取用 android:minWidth 這個屬性設定的值 如果有設定背景 & minWidth,則取用其中最的的數值 ```java= // View.java protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } ``` > 有問題的就是 黃色部分 > ![](https://i.imgur.com/EtHaItZ.png) * 從 View 對 measure 方法預設的算法,**ChildView#wrap_content 若是沒有設置就等同 match_parent**,而這個 default 值我們可以自己決定,**default 並沒有固定依據 !** (當然也可以照需求設定) ```java= // 自定義 View.java public class MyView extends View { // onMeasure 傳入的參數 // 是 ViewGroup 根據 View#LayoutParams、ViewGroup#MeasureSpec 共同決定 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int childWidthMode = MeasureSpec.getMode(widthMeasureSpec); int childWidthSize = MeasureSpec.getMode(widthMeasureSpec); int childHeightMode = MeasureSpec.getMode(heightMeasureSpec); int childHeightSize = MeasureSpec.getMode(heightMeasureSpec); int finalWidth = childWidthSize; int finalHeight = childHeightSize; if(childWidthMode == MeasureSpec.AT_MOST && childHeightMode == MeasureSpec.AT_MOST) { // Child 寬高都是 wrap_content finalWidth = 100; finalHeight = 25; } else if(childWidthMode == MeasureSpec.AT_MOST) { // Child 寬都是 wrap_content finalWidth = 100; } else if(childHeightMode == MeasureSpec.AT_MOST){ // Child 高都是 wrap_content finalHeight = 25; } setMeasuredDimension(finalWidth, finalHeight); } } ``` ### [ViewGroup](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java) - measure 過程 * ViewGroup 是個抽象類,它沒有去覆寫 onMeasure 方法,**所以測量要靠繼承者自己調用 ChildView 的 measure 方法**,不過他有提供一個 `measureChildren` 方法給繼承它的類用 (算是一種方便的工作) > getChildMeasureSpec 方法,上面有分析過了 ```java= // ViewGroup.java protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; // 該 View 要可見 if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { // @ 分析 measureChild 方法 measureChild(child, widthMeasureSpec, heightMeasureSpec); } } } protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); // 創建寬、高 MeasureSpec final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); // 呼叫 Child#measure 方法 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } ``` * 當然每個 ViewGroup 都可以有自己計算的方式,舉個例子 [**LinearLayout**](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/LinearLayout.java)#onMeasure 方法 (看它的 Vertical 測量 && LinearLayout wrap_content 的部分) 從下面可以看出,若是設定 Vertical & 是 wrap_content 的話,就會先調用 `measureChildBeforeLayout` 測量 ChildView 的大小,最終才決定 LinearLayout 的大小 ```java= // LinearLayout.java @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 主要分為兩種布局 if (mOrientation == VERTICAL) { // @ 查看 measureVertical 方法 measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } } void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { mTotalLength = 0; float totalWeight = 0; final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); ... for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); if (child == null) { mTotalLength += measureNullChild(i); continue; } if (child.getVisibility() == View.GONE) { i += getChildrenSkipCount(child, i); continue; } ... final LayoutParams lp = (LayoutParams) child.getLayoutParams(); totalWeight += lp.weight; // 指定高度 or match_parent if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) { final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); skippedMeasure = true; } else { // @ 主要看 wrap_content 的部分 ... final int usedHeight = totalWeight == 0 ? mTotalLength : 0; // 先 measure ChildView measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight); // 取得測量完的大小 final int childHeight = child.getMeasuredHeight(); ... final int totalLength = mTotalLength; // 最終遍歷完所有元素,就可以得到高度 mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); ... } } // 結束 for 迴圈 ... 省略部分 // 加上自身的 Padding mTotalLength += mPaddingTop + mPaddingBottom; int heightSize = mTotalLength; heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); ... 省略部分 // 加上自身的 Padding maxWidth += mPaddingLeft + mPaddingRight; maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); // 決定最終寬、高 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState); ... } // 先遞迴測量 ChildView 的大小 void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { // 調用 ViewGroup 的方法 measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); } ``` ### 取得 Measure 測量完畢 * 請先記住 Measure 完的寬高,並不等於 View 最終的寬高,最好是 **++等到 View 完成 onLayout 方法++ 在去取得寬高,那才是最終的寬高** > 只是大部分 Measure & Layout 完寬高相同 * Activity 的生命週期與 View 是不同步的,所以這裡提供幾個方法來取得 Measure 完後,測量出的數值 1. Activity/View#**onWindowFocusChanged**:該接口會被多次調用,調用的時機點是失去焦點 or 得到焦點時 (eg. onResume、onPause 時) ```java= // 自己的 Activity.java or 自定義 View.java @Override public void onWindowFocusChanged(boolean hasFocus) { if(hasFocus) { int width = appBarLayout.getMeasuredWidth(); int height = appBarLayout.getMinimumHeight(); } } ``` 2. View 內的 **post** 方法:通過 post 將 Runnable 添加到 View#Handler 中 ```java= // 自定義 View class MyView extends View { // 自訂一個消費接口 public void getMeasuredFinishView(Consumer<View> consumer) { post(() -> consumer.accept(this)); } } ``` 3. **ViewTreeObserver** 監聽:當 View 樹狀態改變時,onGlobalLayout 會被多次調用 ```java= public void startViewTreeObserver() { ViewTreeObserver observer = appBarLayout.getViewTreeObserver(); observer.addOnGlobalLayoutListener(() -> { int width = appBarLayout.getMeasuredWidth(); int height = appBarLayout.getMinimumHeight(); appBarLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this); }); } ``` 4. View#**onMeasure**:會依照 View#LayoutParams 區分 ```java= // 假設目前狀況是 ParentView 是 wrap_content 時 !! int measureWidth, measureHeight; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(widthMeasureSpec); ViewGroup.LayoutParams layoutParams = getLayoutParams(); if(layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) { // 1. 寬高是 matchParent // 會依照 ChildView 給測量的大小來決定 ParentView,所以一定不行! return; } // 2. 寬高是 確切數值 (eg. 100px) // 重點是 前面的第一個參數 if(widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) { measureWidth = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); measureHeight = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); } // 3. 寬高是 wrap_content // (1 << 30) - 1 取得 Size 最大數值 // 理論上可以支持最大值 !? 待測 !? if(widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.AT_MOST) { measureWidth = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST); measureHeight = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST); } } ``` ### [View](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/View.java)/ViewGroup - layout 過程 * **ViewGroup**#layout 是用來確定 ChildView 在 ViewGroup 中的位置,當 ViewGroup 的位置被確定後,ViewGroup 就會呼叫所有 ChildView#layout 方法 (**需要實現者自己調用**) ```java= // 偽代碼 @Override public final void layout(int l, int t, int r, int b) { for(View v: mChildView) { if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { v.layout(l, t, r, b); } } } ``` * **View**#layout 方法中有幾個重點 ```java= // View.java public void layout(int l, int t, int r, int b) { ... // 存下當前 上下左右的數值 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // @ 1. setFrame 設定新寬高 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // @ 呼叫自身的 onLayout 方法 onLayout(changed, l, t, r, b); ... } ... } ``` 1. 通過 `setFrame` 方法:來設定 View 上下左右的位置 (也就是對 mLeft、mTop、mBottom、mRight 初始化),這四個點確定後就確定了 View 在 ParentView 中的位置 ```java= // View.java protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; ... if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; ... int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); mLeft = left; mTop = top; mRight = right; mBottom = bottom; ... } return changed; } ``` 2. 調用自身的 `onLayout` 方法 ```java= // View.java protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // 在 View 中是空實現 } ``` > ![](https://i.imgur.com/cefrIIm.png) :::success * onMeasureWidth、onWidth 差異 (同樣 Hight 也是) 在哪呢 ? **主要差別在 ++設定的時機點不同++**,onMeasureWidth 是在 Measure#setMeasuredDimension 完成後就設定(較早),而 onWidth 是在 Layout#setFrame 後設定(較晚) ```java= // View.java public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; } public final int getMeasuredHeight() { return mMeasuredHeight & MEASURED_SIZE_MASK; } public final int getWidth() { return mRight - mLeft; } public final int getHeight() { return mBottom - mTop; } ``` 在日常開發中,最後使用 layout 過後的寬高 ::: ### [View](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/View.java) - draw 過程 * View#draw 是為了將 View 繪製到螢幕上,它的重點步驟如下 1. 繪製背景:drawBackground 2. 繪製自己:onDraw 3. 繪製 Child:dispatchDraw (繪製 View 的子 View) 4. 繪製裝飾 ```java= // View.java public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; ... // 繪製背景 drawBackground(canvas); final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content // 繪製自身 onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); drawAutofilledHighlight(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) // 繪製裝飾 onDrawForeground(canvas); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (isShowingLayoutBounds()) { debugDrawFocus(canvas); } return; } ... } ``` * View 繪製過程會通過 `dispatchDraw` 自動遍歷所有 ChildView,像是 [**ViewGroup**](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java) 就有實現這個函數,所以 ViewGroup 到 draw 方法時會自動執行所有 ChildView#draw 方法 透過遞迴呼叫完成繪製 ```java= // ViewGroup.java @Override protected void dispatchDraw(Canvas canvas) { final int childrenCount = mChildrenCount; final View[] children = mChildren; int flags = mGroupFlags; ... for (int i = 0; i < childrenCount; i++) { ... final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); // 目標 child final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } ... } protected boolean drawChild(Canvas canvas, View child, long drawingTime) { // 呼叫到 ChildView#draw 方法 return child.draw(canvas, this, drawingTime); } ``` > ![](https://i.imgur.com/WlqaJwN.png) * View 有一個特殊方法,setWillNotDraw 方法,如果一個 View 不需要繪製任何內容那就設定為 true,系統就會進行相應優化 ```java= // View.java public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); } ``` * ViewGroup 會自訂啟用這個標記,**若是 ViewGroup 需要繪製,那就需要手動開啟** ```java= // ViewGroup.java public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initViewGroup(); ... } private void initViewGroup() { // ViewGroup doesn't draw by default if (!isShowingLayoutBounds()) { // Debug 時才繪製 // 設定不 draw setFlags(WILL_NOT_DRAW, DRAW_MASK); } ... 省略部分 } ``` ## 自定義 View 清楚了上面基本 View 的繪製流程 (draw、layout、measure) 後,就可以自定義 View、ViewGroup ### 自定義分類 1. 繼承 View 重寫 onDraw、onMeasure 方法 > 1. 繪製 onDraw > 2. **自訂 wrap_content 時 default 的寬高** ```java= class MyView extends View { ... } ``` 2. 繼承 ViewGroup 重寫 onLayout、onMeasure 方法 > 需要處理 > > 1. **測量 measure**:onMeasure 接收到從父類限制的 MeasureWidth、MeasureHeight 後 (兩個參數),分析自身的 MeasureSpec & 考量 childView#LayoutParams 後透過 `setMeasuredDimension` 設定自身的寬高 > > > > 2. **布局 layout**:決定 ChildView 在該 ViewGroup 內的布局,呼叫 childView#layout 方法即可 ```java= class MyViewGroup extends ViewGroup { ... } ``` 3. 繼承 SDK 已經提供的 View (eg. ImageView、TextView... 等等) ```java= class MyViewGroup extends TextView { ... } ``` 4. 繼承 SDK 已經提供的 ViewGroup (eg. LinearLayout... 等等) ```java= class MyViewGroup extends LinearLayout { ... } ``` ### 自定義 View 注意事項 1. **自定義 View:讓 View 支持 `wrap_contnet`** 如果不在 View#onMeasure 中對 wrap_content (AT_MOST) 做處理,那預設就是如同 match_parent 大小 2. **最好能支持 Padding** * View:若要支持 Paddding 則需要在 View#onDraw 中進行操作,否則 padding 無法起到作用 * ViewGroup:要在 ViewGroup#onMeasure & ViewGroup#onLayout 中考慮 ParentView's Padding & ChildView's margin 問題 ! 3. 盡量不要在 View 中使用 Handler 因為 View 內部有提供類似 Handler 的 post 方法 4. View 中如果有 Thread or 動畫,**在 View#onDetachedFromWindow 時必須即時停止** | 方法 | 觸發時機 | | -------- | -------- | | **onAttachedToWindow** | Activity 啟動 or 添加 view | | **onDetachedFromWindow** | Activity 退出 or View 被 remove 時 | 5. 有滑動衝突時,要解滑動衝突 | 方法 | 觸發時機 | | -------- | -------- | | **dispatchTouchEvent** | 一定會被觸發,分發事件的源頭 | | **onInterceptEvent** | 該 View 判斷是否要攔截事件 | | **onTouchEvent** | 該 View 處理事件 | ## Appendix & FAQ :::info ::: ###### tags: `Android Framework`