---
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 完成**
> 
* 單個點擊事件就是從 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());
}
}
```
> 
### 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 則要使用者自己調用才會進行
:::
> 
### 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);
}
```
> 
## 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 是比較耗時的**
:::
> 
### 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 概念圖
>
* 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 中 寬、高 設置的數值==**
> 
```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 設定的數值**
> 
* **在測量 (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);
}
```
> 
:::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());
}
```
> 有問題的就是 黃色部分
> 
* 從 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 中是空實現
}
```
> 
:::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);
}
```
> 
* 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`