---
# System prepended metadata

title: View 工作原理
tags: [Android Framework]

---

---
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`
