Android View 的工做流程和原理

前言

在平常開發中,咱們天天都在和各類 View 打交道,好比TextView,Button等,咱們直接拿過來就可使用,那麼 Android 是怎麼把 View 繪製到屏幕上呢,接下來咱們結合源碼來具體分析。android

在具體結合源碼分析前,先了解一個比較重要的概念 ViewRoot面試

ViewRoot

ViewRoot 對應於 ViewRootImpl 類,它是鏈接 WindowManager 和 根佈局 DecorView(看上圖) 的紐帶, View 的三大流程均是經過 ViewRoot 來完成的。在 ActivityThread 中,當 Activity 對象被建立完畢後,會將 DecorView 添加到 Window 中,同時會建立 ViewRootImpl 對象,並將 ViewRootImpl 對象和 DecorView 創建關聯。canvas

View 的繪製流程是從 ViewRoot 的 performTraversals 方法開始的,它通過 measure、layout 和 draw 三個過程才能最終將一個 View 繪製出來,其中 measure 用來測量 View 的寬和高,layout 用來肯定 View 在父容器中的放置位置,而 draw 則負責將 View 繪製在屏幕上。針對 performTraversals的大體流程以下:
小程序

performTraversals 會依次調用 performMeasure、performLayout 和 performDraw 三個方法,這三個方法分別完成頂級 View 的 measure、layout 和 draw 這三大流程,其中在 performMeasure 中會調用 measure 方法,在 measure 方法中又會調用 onMeasure 方法,在 onMeasure 方法中則會對全部的子元素進行 measure 過程,這個時候 measure 流程就從父容器傳遞到子元素中了,這樣就完成了一次 measure 過程。接着子元素會重複父容器的 measure 過程,如此反覆就完成了整個 View 樹的遍歷。同理,performLayout 和 performDraw 的傳遞流程和 performMeasure 是相似的,惟一不一樣的是,performDraw 的傳遞過程是在 draw 方法中經過 dispatchDraw 來實現的,不過這並無本質區別。性能優化

接下來結合源碼來分析這三個過程。架構

Measure 測量過程

這裏分兩種狀況,View 的測量過程和 ViewGroup 的測量過程。app

View 的測量過程

View 的 測量過程由其 measure 方法來完成,源碼以下:ide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            //省略代碼...
            
            if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

 

能夠看到 measure 方法是一個 final 類型的方法,這意味着子類不能重寫此方法。源碼分析

在 13 行 measure 中會調用 onMeasure 方法,這個方法是測量的主要方法,繼續看 onMeasure 的實現佈局

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
     getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension 方法的做用是設置 View 寬和高的測量值,咱們主要看 getDefaultSize 方法
是如何生成測量的尺寸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
}

 

能夠看到要獲得測量的尺寸須要用到 MeasureSpec,MeasureSpec 是什麼鬼呢,敲黑板了,重點來了。
MeasureSpec 決定了 View 的測量過程。確切來講,MeasureSpec 在很大程度上決定了一個 View 的尺寸規格。
來看 MeasureSpec 類的實現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        public static final int EXACTLY     = 1 << MODE_SHIFT;

        public static final int AT_MOST     = 2 << MODE_SHIFT;

        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 makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }

        public static String toString(int measureSpec) {
            //省略...
        }
    }

能夠看出 MeasureSpec 中有兩個主要的值,SpecMode 和 SpecSize, SpecMode 是指測量模式,而 SpecSize 是指在某種測量模式下的規格大小。

SpecMode 有三種模式:

  1. UNSPECIFIED
    不限制:父容器不對 View 有任何限制,要多大給多大,這種狀況比較少見,通常不會用到。

  2. EXACTLY
    限制固定值:父容器已經檢測出 View 所須要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應於 LayoutParams 中的 match_parent 和具體的數值這兩種模式。

  3. AT_MOST
    限制上限:父容器指定了一個可用大小即 SpecSize,View 的大小不能大於這個值,具體是什麼值要看不一樣 View 的具體實現。它對應於 LayoutParams 中的 wrap_content。

MeasureSpec 中三個主要的方法來處理 SpecMode 和 SpecSize

  1. makeMeasureSpec 打包 SpecMode 和 SpecSize
  2. getMode 解析出 SpecMode
  3. getSize 解析出 SpecSize

不知道童鞋們以前有沒有注意到 onMeasure 有兩個參數 widthMeasureSpec 和 heightMeasureSpec,那這兩個值從哪來的呢,這兩個值都是由父視圖通過計算後傳遞給子視圖的,說明父視圖會在必定程度上決定子視圖的大小,可是最外層的根視圖 也就是 DecorView ,它的 widthMeasureSpec 和 heightMeasureSpec 又是從哪裏獲得的呢?這就須要去分析 ViewRoot 中的源碼了,在 performTraversals 方法中調了 measureHierarchy 方法來建立 MeasureSpec 源碼以下:

1
2
3
4
5
6
7
8
9
10
11
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        
        childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                
      //省略代碼...          
}

裏面調用了 getRootMeasureSpec 方法生成 MeasureSpec,繼續查看 getRootMeasureSpec 源碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
       int measureSpec;
       switch (rootDimension) {

       case ViewGroup.LayoutParams.MATCH_PARENT:
           // Window can't resize. Force root view to be windowSize.
           measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
           break;
       case ViewGroup.LayoutParams.WRAP_CONTENT:
           // Window can resize. Set max size for root view.
           measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
           break;
       default:
           // Window wants to be an exact size. Force root view to be that size.
           measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
           break;
       }
       return measureSpec;
   }

經過上述代碼,DecorView 的 MeasureSpec 的產生過程就很明確了,具體來講其遵照以下規則,根據它的 LayoutParams 中的寬和高的參數來劃分。

  • LayoutParams.MATCH_PARENT:限制固定值,大小就是窗口的大小 windowSize
  • LayoutParams.WRAP_CONTENT:限制上限,大小不定,可是不能超過窗口的大小 windowSize
  • 固定大小:限制固定值,大小爲 LayoutParams 中指定的大小 rootDimension

對於 DecorView 而言, rootDimension 的值爲 lp.width 和 lp.height 也就是屏幕的寬和高,因此說 根視圖 DecorView 的大小默認老是會充滿全屏的。那麼咱們使用的 View 也就是 ViewGroup 中 View 的 MeasureSpec 產生過程又是怎麼樣的呢,在 ViewGroup 的測量過程當中會具體介紹。

先回頭看 getDefaultSize 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
       case MeasureSpec.AT_MOST:
       case MeasureSpec.EXACTLY:
           result = specSize;
           break;
       }
       return result;
   }

如今理解起來是否是很簡單呢,若是 specMode 是 AT_MOST 或 EXACTLY 就返回 specSize,這也是系統默認的行爲。以後會在 onMeasure 方法中調用 setMeasuredDimension 方法來設定測量出的大小,這樣 View 的 measure 過程就結束了,接下來看 ViewGroup 的 measure 過程。

ViewGroup 的測量過程

ViewGroup中定義了一個 measureChildren 方法來去測量子視圖的大小,以下所示

1
2
3
4
5
6
7
8
9
10
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];
           if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
               measureChild(child, widthMeasureSpec, heightMeasureSpec);
           }
       }
   }

從上述代碼來看,除了完成本身的 measure 過程之外,還會遍歷去全部在頁面顯示的子元素,
而後逐個調用 measureChild 方法來測量相應子視圖的大小

measureChild 的實現以下

1
2
3
4
5
6
7
8
9
10
11
protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

measureChild 的思想就是取出子元素的 LayoutParams,而後再經過 getChildMeasureSpec 來建立子元素的 MeasureSpec,接着將 MeasureSpec 直接傳遞給 View 的 measure 方法來進行測量。

那麼 ViewGroup 是如何建立來建立子元素的 MeasureSpec 呢,咱們繼續看 getChildMeasureSpec 方法源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面的代碼理解起來很簡單,爲了更清晰地理解 getChildMeasureSpec 的邏輯,這裏提供一個表,表中對 getChildMeasureSpec 的工做原理進行了梳理,表中的 parentSize 是指父容器中目前可以使用的大小,childSize 是子 View 的 LayoutParams 獲取的值,從 measureChild 方法中可看出

1
2
3
4
5
6
7
8
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

表以下:

普通 View 的 MeasureSpec 的建立規則普通 View 的 MeasureSpec 的建立規則

經過上表能夠看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就能夠快速地肯定出子元素的 MeasureSpec 了,有了 MeasureSpec 就能夠進一步肯定出子元素測量後的大小了。

至此,View 和 ViewGroup 的測量過程就告一段落了。來個小結。

MeasureSpec 的模式和生成規則
MeasureSpec 中 specMode 有三種模式:

  1. UNSPECIFIED
    不限制:父容器不對 View 有任何限制,要多大給多大,這種狀況比較少見,通常不會用到。

  2. EXACTLY
    限制固定值:父容器已經檢測出 View 所須要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應於 LayoutParams 中的 match_parent 和具體的數值這兩種模式。

  3. AT_MOST
    限制上限:父容器指定了一個可用大小即 SpecSize,View 的大小不能大於這個值,具體是什麼值要看不一樣 View 的具體實現。它對應於 LayoutParams 中的 wrap_content。

生成規則:

  1. 對於普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定。
  2. 對於不一樣 ViewGroup 中的不一樣 View 生成規則參照上表。

MeasureSpec 測量過程:
measure 過程主要就是從頂層父 View 向子 View 遞歸調用 view.measure 方法,measure 中調 onMeasure 方法的過程。

說人話呢就是,視圖大小的控制是由父視圖、佈局文件、以及視圖自己共同完成的,父視圖會提供給子視圖參考的大小,而開發人員能夠在 XML 文件中指定視圖的大小,而後視圖自己會對最終的大小進行拍板。

那麼測量事後,怎麼獲取 View 的測量結果呢
通常狀況下 View 測量大小和最終大小是同樣的,咱們可使用 getMeasuredWidth 方法和 getMeasuredHeight 方法來獲取視圖測量出的寬高,可是必須在 setMeasuredDimension 以後調用,不然調用這兩個方法獲得的值都會是0。爲何要說是通常狀況下是同樣的呢,在下文介紹 Layout 中會具體介紹。

Layout 佈局過程

測量結束後,視圖的大小就已經測量好了,接下來就是 Layout 佈局的過程。上文說過 ViewRoot 的 performTraversals 方法會在 measure 結束後,執行 performLayout 方法,performLayout 方法則會調用 layout 方法開始佈局,代碼以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
           int desiredWindowHeight) {
       mLayoutRequested = false;
       mScrollMayChange = true;
       mInLayout = true;

       final View host = mView;
       if (host == null) {
           return;
       }
       if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
           Log.v(mTag, "Laying out " + host + " to (" +
                   host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
       }
try {
   host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
   //...省略代碼
   } finally {
           Trace.traceEnd(Trace.TRACE_TAG_VIEW);
   }
   mInLayout = false;

View 類中 layout 方法實現以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

layout 方法接收四個參數,分別表明着左、上、右、下的座標,固然這個座標是相對於當前視圖的父視圖而言的,而後會調用 setFrame 方法來設定 View 的四個頂點的位置,即初始化 mLeft、mRight、mTop、mBottom 這四個值,View 的四個頂點一旦肯定,那麼 View 在父容器中的位置也就肯定了,接着會調用 onLayout 方法,這個方法的用途是父容器肯定子元素的位置,和 onMeasure 方法相似

onLayout 源碼以下:

1
2
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

納尼,怎麼是個空方法,沒錯,就是一個空方法,由於 onLayout 過程是爲了肯定視圖在佈局中所在的位置,而這個操做應該是由佈局來完成的,即父視圖決定子視圖的顯示位置,咱們繼續看 ViewGroup 中的 onLayout 方法

1
2
@Override  
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

能夠看到,ViewGroup 中的 onLayout 方法居然是一個抽象方法,這就意味着全部 ViewGroup 的子類都必須重寫這個方法。像 LinearLayout、RelativeLayout 等佈局,都是重寫了這個方法,而後在內部按照各自的規則對子視圖進行佈局的。因此呢咱們若是要自定義 ViewGroup 那麼就要重寫 onLayout 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class TestViewGroup extends ViewGroup {

    public TestViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
        }
    }
}

xml 中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <com.will.testdemo.customview.TestViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_bright"
        >

        <TextView
            android:id="@+id/tv_hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimaryDark"
            android:text="@string/hello_world"
            android:textColor="@android:color/white"
            android:textSize="15sp"
            />

    </com.will.testdemo.customview.TestViewGroup>

  </LinearLayout>

顯示效果以下:

不知道童鞋們發現了沒,我給自定義的 ViewGroup 設置了背景色,看效果貌似佔滿全屏了,但是我在 xml 中設置的 wrap_content 啊,這是什麼狀況,咱們回頭看看 ViewGroup 中 View 的 MeasureSpec 的建立規則
普通 View 的 MeasureSpec 的建立規則普通 View 的 MeasureSpec 的建立規則

從表中可看出由於 ViewGroup 的父佈局設置的 match_parent 也就是限制固定值模式,而 ViewGroup 設置的 wrap_content,那麼最後 ViewGroup 使用的是 父佈局的大小,也就是窗口大小 parentSize,那麼若是咱們給 ViewGroup 設置固定值就會使用 咱們設置的值,來改下代碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <com.will.testdemo.customview.TestViewGroup
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:background="@android:color/holo_blue_bright"
        >

        <TextView
            android:id="@+id/tv_hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimaryDark"
            android:text="@string/hello_world"
            android:textColor="@android:color/white"
            android:textSize="15sp"
            />

    </com.will.testdemo.customview.TestViewGroup>

</LinearLayout>

效果以下:

表中的其餘狀況,建議童鞋們本身寫下代碼,會理解的更好。

以前說過,通常狀況下 View 測量大小和最終大小是同樣的,爲何呢,由於最終大小在 onLayout 中肯定,咱們來改下代碼:

1
2
3
4
5
6
7
@Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
       if (getChildCount() > 0) {
           View childView = getChildAt(0);
           childView.layout(0, 0, childView.getMeasuredWidth()+100, childView.getMeasuredHeight()+200);
       }
   }

顯示效果

沒錯,onLayout 就是這麼任性,因此要獲取 View 的真實大小最好在 onLayout 以後獲取。那麼如何來獲取 view 的真實大小呢,能夠經過下面的代碼來獲取

1
2
3
4
tv_hello.post(Runnable {
       log(" getMeasuredWidth() = ${tv_hello.measuredWidth}")
       log(" getWidth() = ${tv_hello.width}")
}

打印以下:

1
2
01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog:  getMeasuredWidth() = 239
01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog:  getWidth() = 339

能夠看到實際高度和測試的高度是不同的,由於咱們在 onLayout 中作了修改。

由於 View 的繪製過程和 Activity 的生命週期是不一樣步的,因此咱們可能在 onCreate 中獲取不到值。這裏提供幾種方法來獲取

1.Activity 的 onWindowFocusChanged 方法

1
2
3
4
5
6
override fun onWindowFocusChanged(hasFocus: Boolean) {
       super.onWindowFocusChanged(hasFocus)
       if (hasFocus){
           //獲取 view 的大小
       }
   }

2.view.post(runnable) 也就是我上面使用的方法
3.ViewTreeObserver 這裏童鞋們搜索下就能夠找到使用方法,篇幅較長就不舉例子了

Draw 繪製過程

肯定了 View 的大小和位置後,那就要開始繪製了,Draw 過程就比較簡單,它的做用是將 View 繪製到屏幕上面。View 的繪製過程遵循以下幾步:

  1. 繪製背景 background.draw (canvas)
  2. 繪製本身(onDraw)
  3. 繪製 children(dispatchDraw)
  4. 繪製裝飾(onDrawScrollBars)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        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
            if (!dirtyOpaque) 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 (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
        
        //省略代碼..
}

View 的繪製過程的傳遞是經過 dispatchDraw 實現的,dispatchdraw 會遍歷調用全部子元素的 draw 方法,如此 draw 事件就一層一層的傳遞下去。和 Layout 同樣 View 是不會幫咱們繪製內容部分的,所以須要每一個視圖根據想要展現的內容來自行繪製,重寫 onDraw 方法。具體可參考 TextView 或者 ImageView 的源碼。

最後

View 的工做流程和原理到這就分析完了,難點主要是 MeasureSpec 測量過程,須要童鞋們認真揣摩。

最後給你們分享一份很是系統和全面的Android進階技術大綱及進階資料,及面試題集

想學習更多Android知識,請加入Android技術開發交流 7520 16839

進羣與大牛們一塊兒討論,還可獲取Android高級架構資料、源碼、筆記、視頻

包括 高級UI、Gradle、RxJava、小程序、Hybrid、移動架構、React Native、性能優化等全面的Android高級實踐技術講解性能優化架構思惟導圖,和BATJ面試題及答案!

羣裏免費分享給有須要的朋友,但願可以幫助一些在這個行業發展迷茫的,或者想系統深刻提高以及困於瓶頸的

朋友,在網上博客論壇等地方少花些時間找資料,把有限的時間,真正花在學習上,因此我在這免費分享一些架構資料及給你們。但願在這些資料中都有你須要的內容。

相關文章
相關標籤/搜索