【朝花夕拾】Android自定義View篇之(一)View繪制流程


前言

       轉載請申明轉自【https://www.cnblogs.com/andy-songwei/p/10955062.html】謝謝!

       自定義View、多線程、網絡,被認為是Android開發者必須牢固掌握的最基礎的三大基本功。Android View的繪制流程原理又是學好自定義View的理論基礎,所以掌握好View的繪制原理是Android開發進階中無法繞過的一道坎。而關乎到原理性的東西往往又讓很多初學者感到難以下手,所以真正掌握的人並不多。本文采用非常淺顯的語言,從順着Android源碼的思路,對View的整個繪制流程進行近乎“地毯式搜索”般的方式,對其中的關鍵流程和知識點進行查證和分析,以圖讓初級程序員都能輕松讀懂。本文最大的特點,就是最大限度地向源碼要答案,從源碼中追流程的來龍去脈,在注釋中查功能的點點滴滴,所有的結論都盡量在源碼和注釋中找根據。

       為了能對其中的重難點分析透徹,文中貼出了大量的源碼依據以及源碼中的注釋,並對重要的注釋進行了翻譯和講解,所以文章會比較長。講解該知識點的文章普遍都非常長,所以希望讀者能夠秉承程序員吃苦耐勞的精神,攻克這個難關。本文中的源碼是基於API26的,即Android8.0系統版本,主要內容大致如下:

 

 

一、View繪制的三個流程

       我們知道,在自定義View的時候一般需要重寫父類的onMeasure()、onLayout()、onDraw()三個方法,來完成視圖的展示過程。當然,這三個暴露給開發者重寫的方法只不過是整個繪制流程的冰山一角,更多復雜的幕后工作,都讓系統給代勞了。一個完整的繪制流程包括measure、layout、draw三個步驟,其中:

     measure:測量。系統會先根據xml布局文件和代碼中對控件屬性的設置,來獲取或者計算出每個View和ViewGrop的尺寸,並將這些尺寸保存下來。

     layout:布局。根據測量出的結果以及對應的參數,來確定每一個控件應該顯示的位置。

     draw:繪制。確定好位置后,就將這些控件繪制到屏幕上。

 

二、Android視圖層次結構簡介  

       在介紹View繪制流程之前,咱們先簡單介紹一下Android視圖層次結構以及DecorView,因為View的繪制流程的入口和DecorView有着密切的聯系。

 

       咱們平時看到的視圖,其實存在如上的嵌套關系。上圖是針對比較老的Android系統版本中制作的,新的版本中會略有出入,還有一個狀態欄,但整體上沒變。我們平時在Activity中setContentView(...)中對應的layout內容,對應的是上圖中ViewGrop的樹狀結構,實際上添加到系統中時,會再裹上一層FrameLayout,就是上圖中最里面的淺藍色部分了。

       這里咱們再通過一個實例來繼續查看。AndroidStudio工具中提供了一個布局視察器工具,通過Tools > Android > Layout Inspector可以查看具體某個Activity的布局情況。下圖中,左邊樹狀結構對應了右邊的可視圖,可見DecorView是整個界面的根視圖,對應右邊的紅色框,是整個屏幕的大小。黃色邊框為狀態欄部分;那個綠色邊框中有兩個部分,一個是白框中的ActionBar,對應了上圖中紫色部分的TitleActionBar部分,即標題欄,平時咱們可以在Activity中將其隱藏掉;另外一個藍色邊框部分,對應上圖中最里面的藍色部分,即ContentView部分。下圖中左邊有兩個藍色框,上面那個中有個“contain_layout”,這個就是Activity中setContentView中設置的layout.xml布局文件中的最外層父布局,咱們能通過layout布局文件直接完全操控的也就是這一塊,當其被add到視圖系統中時,會被系統裹上ContentFrameLayout(顯然是FrameLayout的子類),這也就是為什么添加layout.xml視圖的方法叫setContentView(...)而不叫setView(...)的原因。

 

 

三、故事開始的地方

        如果對Activity的啟動流程有一定了解的話,應該知道這個啟動過程會在ActivityThread.java類中完成,在啟動Activity的過程中,會調用到handleResumeActivity(...)方法,關於視圖的繪制過程最初就是從這個方法開始的。

 

  1、View繪制起源UML時序圖

       整個調用鏈如下圖所示,直到ViewRootImpl類中的performTraversals()中,才正式開始繪制流程了,所以一般都是以該方法作為正式繪制的源頭。

圖3.1 View繪制起源UML時序圖

 

   2、handleResumeActivity()方法

       在這咱們先大致看看ActivityThread類中的handleResumeActivity方法,咱們這里只貼出關鍵代碼:

 1 //===========ActivityThread.java==========
 2 final void handleResumeActivity(...) {
 3     ......
 4     //跟蹤代碼后發現其初始賦值為mWindow = new PhoneWindow(this, window, activityConfigCallback);
 5     r.window = r.activity.getWindow(); 
 6        //從PhoneWindow實例中獲取DecorView  
 7     View decor = r.window.getDecorView();
 8     ......
 9     //跟蹤代碼后發現,vm值為上述PhoneWindow實例中獲取的WindowManager。
10     ViewManager wm = a.getWindowManager();
11     ......
12     //當前window的屬性,從代碼跟蹤來看是PhoneWindow窗口的屬性
13     WindowManager.LayoutParams l = r.window.getAttributes();
14     ......
15     wm.addView(decor, l);
16     ......
17 }

       上述代碼第8行中,ViewManager是一個接口,addView是其中定義個一個空方法,WindowManager是其子類,WindowManagerImpl是WindowManager的實現類(順便啰嗦一句,這種方式叫做面向接口編程,在父類中定義,在子類中實現,在Java中很常見)。第4行代碼中的r.window的值可以根據Activity.java的如下代碼得知,其值為PhoneWindow實例。

 1 //===============Activity.java=============
 2 private Window mWindow;
 3 public Window getWindow() {
 4    return mWindow;
 5 }
 6 
 7 final void attach(...){
 8    ......
 9    mWindow = new PhoneWindow(this, window, activityConfigCallback);
10    ......
11 }

 

3、兩個重要參數分析

       之所以要在這里特意分析handleResumeActivity()方法,除了因為它是整個繪制流程的最初源頭外,還有就是addView的兩個參數比較重要,它們經過一層一層傳遞后進入到ViewRootImpl中,在后面分析繪制中要用到。這里再看看這兩個參數的相關信息:

    (1)參數decor

 1 //==========PhoneWindow.java===========
 2 // This is the top-level view of the window, containing the window decor.
 3 private DecorView mDecor;
 4 ......
 5 public PhoneWindow(...){
 6    ......
 7    mDecor = (DecorView) preservedWindow.getDecorView();
 8    ......
 9 }
10 
11 @Override
12 public final View getDecorView() {
13    ......
14    return mDecor;
15 }

可見decor參數表示的是DecorView實例。注釋中也有說明:這是window的頂級視圖,包含了window的decor。

    (2)參數l

 1 //===================Window.java===================
 2 //The current window attributes.
 3     private final WindowManager.LayoutParams mWindowAttributes =
 4         new WindowManager.LayoutParams();
 5 ......
 6 public final WindowManager.LayoutParams getAttributes() {
 7         return mWindowAttributes;
 8     }
 9 ......
10 
11 
12 //==========WindowManager.java的內部類LayoutParams extends ViewGroup.LayoutParams=============
13 public LayoutParams() {
14             super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
15             ......
16         }
17 
18 
19 //==============ViewGroup.java內部類LayoutParams====================
20 public LayoutParams(int width, int height) {
21             this.width = width;
22             this.height = height;
23         }

該參數表示l的是PhoneWindow的LayoutParams屬性,其width和height值均為LayoutParams.MATCH_PARENT。

 

        在源碼中,WindowPhone和DecorView通過組合方式聯系在一起的,而DecorView是整個View體系的根View。在前面handleResumeActivity(...)方法代碼片段中,當Actiivity啟動后,就通過第14行的addView方法,來間接調用ViewRootImpl類中的performTraversals(),從而實現視圖的繪制。

 

四、主角登場 

   無疑,performTraversals()方法是整個過程的主角,它把控着整個繪制的流程。該方法的源碼有大約800行,這里咱們僅貼出關鍵的流程代碼,如下所示:
 1 // =====================ViewRootImpl.java=================
 2 private void performTraversals() {
 3    ......
 4    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 5    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);      
 6    ......
 7    // Ask host how big it wants to be
 8    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 9    ......
10    performLayout(lp, mWidth, mHeight);
11    ......
12    performDraw();
13 }

 上述代碼中就是一個完成的繪制流程,對應上了第一節中提到的三個步驟:

      1)performMeasure():從根節點向下遍歷View樹,完成所有ViewGroup和View的測量工作,計算出所有ViewGroup和View顯示出來需要的高度和寬度;

      2)performLayout():從根節點向下遍歷View樹,完成所有ViewGroup和View的布局計算工作,根據測量出來的寬高及自身屬性,計算出所有ViewGroup和View顯示在屏幕上的區域;

      3)performDraw():從根節點向下遍歷View樹,完成所有ViewGroup和View的繪制工作,根據布局過程計算出的顯示區域,將所有View的當前需顯示的內容畫到屏幕上。

咱們后續就是通過對這三個方法來展開研究整個繪制過程。

 

五、measure過程分析

       這三個繪制流程中,measure是最復雜的,這里會花較長的篇幅來分析它。本節會先介紹整個流程中很重要的兩個類MeasureSpec和ViewGroup.LayoutParams類,然后介紹ViewRootImpl、View及ViewGroup中測量流程涉及到的重要方法,最后簡單梳理DecorView測量的整個流程並鏈接一個測量實例分析整個測量過程。

 

  1、MeasureSpec簡介

       這里咱們直接上源碼吧,先直接通過源碼和注釋認識一下它,如果看不懂也沒關系,在后面使用的時候再回頭來看看。

 1 /**
 2      * A MeasureSpec encapsulates the layout requirements passed from parent to child.
 3      * Each MeasureSpec represents a requirement for either the width or the height.
 4      * A MeasureSpec is comprised of a size and a mode. There are three possible
 5      * modes:
 6      * <dl>
 7      * <dt>UNSPECIFIED</dt>
 8      * <dd>
 9      * The parent has not imposed any constraint on the child. It can be whatever size
10      * it wants.
11      * </dd>
12      *
13      * <dt>EXACTLY</dt>
14      * <dd>
15      * The parent has determined an exact size for the child. The child is going to be
16      * given those bounds regardless of how big it wants to be.
17      * </dd>
18      *
19      * <dt>AT_MOST</dt>
20      * <dd>
21      * The child can be as large as it wants up to the specified size.
22      * </dd>
23      * </dl>
24      *
25      * MeasureSpecs are implemented as ints to reduce object allocation. This class
26      * is provided to pack and unpack the &lt;size, mode&gt; tuple into the int.
27      */
28     public static class MeasureSpec {
29         private static final int MODE_SHIFT = 30;
30         private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
31         ......
32         /**
33          * Measure specification mode: The parent has not imposed any constraint
34          * on the child. It can be whatever size it wants.
35          */
36         public static final int UNSPECIFIED = 0 << MODE_SHIFT;
37 
38         /**
39          * Measure specification mode: The parent has determined an exact size
40          * for the child. The child is going to be given those bounds regardless
41          * of how big it wants to be.
42          */
43         public static final int EXACTLY     = 1 << MODE_SHIFT;
44 
45         /**
46          * Measure specification mode: The child can be as large as it wants up
47          * to the specified size.
48          */
49         public static final int AT_MOST     = 2 << MODE_SHIFT;
50         ......
51        /**
52          * Creates a measure specification based on the supplied size and mode.
53          *...... 
54          *@return the measure specification based on size and mode        
55          */
56         public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
57                                           @MeasureSpecMode int mode) {
58             if (sUseBrokenMakeMeasureSpec) {
59                 return size + mode;
60             } else {
61                 return (size & ~MODE_MASK) | (mode & MODE_MASK);
62             }
63             ......
64             
65         }
66         ......
67         /**
68          * Extracts the mode from the supplied measure specification.
69          *......
70          */
71         @MeasureSpecMode
72         public static int getMode(int measureSpec) {
73             //noinspection ResourceType
74             return (measureSpec & MODE_MASK);
75         }
76 
77         /**
78          * Extracts the size from the supplied measure specification.
79          *......
80          * @return the size in pixels defined in the supplied measure specification
81          */
82         public static int getSize(int measureSpec) {
83             return (measureSpec & ~MODE_MASK);
84         }
85         ......
86 }

 從這段代碼中,咱們可以得到如下的信息:

    1)MeasureSpec概括了從父布局傳遞給子view布局要求。每一個MeasureSpec代表了寬度或者高度要求,它由size(尺寸)和mode(模式)組成。

    2)有三種可能的mode:UNSPECIFIED、EXACTLY、AT_MOST

    3)UNSPECIFIED:未指定尺寸模式。父布局沒有對子view強加任何限制。它可以是任意想要的尺寸。(筆者注:這個在工作中極少碰到,據說一般在系統中才會用到,后續會講得很少)

    4)EXACTLY:精確值模式。父布局決定了子view的准確尺寸。子view無論想設置多大的值,都將限定在那個邊界內。(筆者注:也就是layout_width屬性和layout_height屬性為具體的數值,如50dp,或者設置為match_parent,設置為match_parent時也就明確為和父布局有同樣的尺寸,所以這里不要以為筆者搞錯了。當明確為精確的尺寸后,其也就被給定了一個精確的邊界)

    5)AT_MOST:最大值模式。子view可以一直大到指定的值。(筆者注:也就是其寬高屬性設置為wrap_content,那么它的最大值也不會超過父布局給定的值,所以稱為最大值模式)

    6)MeasureSpec被實現為int型來減少對象分配。該類用於將size和mode元組裝包和拆包到int中。(筆者注:也就是將size和mode組合或者拆分為int型數據)

    7)分析代碼可知,一個MeasureSpec的模式如下所示,int長度為32位置,高2位表示mode,后30位用於表示size

           

     8)UNSPECIFIED、EXACTLY、AT_MOST這三個mode的示意圖如下所示:

              

    9)makeMeasureSpec(int mode,int size)用於將mode和size打包成一個int型的MeasureSpec。

    10)getSize(int measureSpec)方法用於從指定的measureSpec值中獲取其size。

    11)getMode(int measureSpec)方法用戶從指定的measureSpec值中獲取其mode。

 

  2、ViewGroup.LayoutParams簡介

   該類的源碼及注釋分析如下所示。

 1 //============================ViewGroup.java===============================
 2 /**
 3      * LayoutParams are used by views to tell their parents how they want to be
 4      * laid out. 
 5      *......
 6      * <p>
 7      * The base LayoutParams class just describes how big the view wants to be
 8      * for both width and height. For each dimension, it can specify one of:
 9      * <ul>
10      * <li>FILL_PARENT (renamed MATCH_PARENT in API Level 8 and higher), which
11      * means that the view wants to be as big as its parent (minus padding)
12      * <li> WRAP_CONTENT, which means that the view wants to be just big enough
13      * to enclose its content (plus padding)
14      * <li> an exact number
15      * </ul>
16      * There are subclasses of LayoutParams for different subclasses of
17      * ViewGroup. For example, AbsoluteLayout has its own subclass of
18      * LayoutParams which adds an X and Y value.</p>
19      * ......
20      * @attr ref android.R.styleable#ViewGroup_Layout_layout_height
21      * @attr ref android.R.styleable#ViewGroup_Layout_layout_width
22      */
23     public static class LayoutParams {
24         ......
25 
26         /**
27          * Special value for the height or width requested by a View.
28          * MATCH_PARENT means that the view wants to be as big as its parent,
29          * minus the parent's padding, if any. Introduced in API Level 8.
30          */
31         public static final int MATCH_PARENT = -1;
32 
33         /**
34          * Special value for the height or width requested by a View.
35          * WRAP_CONTENT means that the view wants to be just large enough to fit
36          * its own internal content, taking its own padding into account.
37          */
38         public static final int WRAP_CONTENT = -2;
39 
40         /**
41          * Information about how wide the view wants to be. Can be one of the
42          * constants FILL_PARENT (replaced by MATCH_PARENT
43          * in API Level 8) or WRAP_CONTENT, or an exact size.
44          */
45         public int width;
46 
47         /**
48          * Information about how tall the view wants to be. Can be one of the
49          * constants FILL_PARENT (replaced by MATCH_PARENT
50          * in API Level 8) or WRAP_CONTENT, or an exact size.
51          */
52         public int height;
53         ......
54 }

 這對其中重要的信息做一些翻譯和整理:

    1)LayoutParams被view用於告訴它們的父布局它們想要怎樣被布局。(筆者注:字面意思就是布局參數)

    2)該LayoutParams基類僅僅描述了view希望寬高有多大。對於每一個寬或者高,可以指定為以下三種值中的一個:MATCH_PARENT,WRAP_CONTENT,an exact number。(筆者注:FILL_PARENT從API8開始已經被MATCH_PARENT取代了,所以下文就只提MATCH_PARENT)

    3)MATCH_PARENT:意味着該view希望和父布局尺寸一樣大,如果父布局有padding,則要減去該padding值。

    4)WRAP_CONTENT:意味着該view希望其大小為僅僅足夠包裹住其內容即可,如果自己有padding,則要加上該padding值。

    5)對ViewGroup不同的子類,也有相應的LayoutParams子類。 

    6)其width和height屬性對應着layout_width和layout_height屬性。

 

  3、View測量的基本流程及重要方法分析

       View體系的測量是從DecorView這個根view開始遞歸遍歷的,而這個View體系樹中包含了眾多的葉子view和ViewGroup的子類容器。這一小節中會從ViewRootImpl.performMeasure()開始,分析測量的基本流程。

     (1)ViewRootImpl.performMeasure()方法

       跟蹤源碼,進入到performMeasure方法分析,這里僅貼出關鍵流程代碼。

1 //=============ViewRootImpl.java==============
2 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
3        ......
4        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
5        ......
6 }

 這個mView是誰呢?跟蹤代碼可以找到給它賦值的地方:

1 //========================ViewRootImpl.java======================
2 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
3       ......
4       mView = view;
5       ......
6 
7       mWindowAttributes.copyFrom(attrs);
8       ......
9 }

       看到這里,是不是有些似曾相識呢?在第二節的繪制流程中提到過,這里setView的參數view和attrs是ActivityThread類中addView方法傳遞過來的,所以咱們這里可以確定mView指的是DecorView了。上述performMeasure()中,其實就是DecorView在執行measure()操作。如果您這存在“mView不是View類型的嗎,怎么會指代DecorView作為整個View體系的根view呢”這樣的疑惑,那這里就啰嗦一下,DecorView extends FrameLayout extends ViewGroup extends View,通過這個繼承鏈可以看到,DecorView是一個容器,但ViewGroup也是View的子類,View是所有控件的基類,所以這里View類型的mView指代DecorView是沒毛病的。

    (2)View.measure()方法

       盡管mView就是DecorView,但是由於measure()方法是final型的,View子類都不能重寫該方法,所以這里追蹤measure()的時候就直接進入到View類中了,這里貼出關鍵流程代碼:

 1 //===========================View.java===============================
 2 /**
 3      * <p>
 4      * This is called to find out how big a view should be. The parent
 5      * supplies constraint information in the width and height parameters.
 6      * </p>
 7      *
 8      * <p>
 9      * The actual measurement work of a view is performed in
10      * {@link #onMeasure(int, int)}, called by this method. Therefore, only
11      * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
12      * </p>
13      *
14      *
15      * @param widthMeasureSpec Horizontal space requirements as imposed by the
16      *        parent
17      * @param heightMeasureSpec Vertical space requirements as imposed by the
18      *        parent
19      *
20      * @see #onMeasure(int, int)
21      */
22 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
23       ......
24       // measure ourselves, this should set the measured dimension flag back
25       onMeasure(widthMeasureSpec, heightMeasureSpec);
26       ......
27 }

        這里面注釋提供了很多信息,這簡單翻譯並整理一下:

        1)該方法被調用,用於找出view應該多大。父布局在witdh和height參數中提供了限制信息;

        2)一個view的實際測量工作是在被本方法所調用的onMeasure(int,int)方法中實現的。所以,只有onMeasure(int,int)可以並且必須被子類重寫(筆者注:這里應該指的是,ViewGroup的子類必須重寫該方法,才能繪制該容器內的子view。如果是自定義一個子控件,extends View,那么並不是必須重寫該方法);

        3)參數widthMeasureSpec:父布局加入的水平空間要求;

        4)參數heightMeasureSpec:父布局加入的垂直空間要求。

       系統將其定義為一個final方法,可見系統不希望整個測量流程框架被修改。

    (3)View.onMeasure()方法

       在上述方法體內看到onMeasure(int,int)方法時,是否有一絲慰藉呢?終於看到咱們最熟悉的身影了,很親切吧!咱們編寫自定義View時,基本上都會重寫的方法!咱們看看其源碼:

 1 //===========================View.java===============================
 2 /**
 3      * <p>
 4      * Measure the view and its content to determine the measured width and the
 5      * measured height. This method is invoked by {@link #measure(int, int)} and
 6      * should be overridden by subclasses to provide accurate and efficient
 7      * measurement of their contents.
 8      * </p>
 9      *
10      * <p>
11      * <strong>CONTRACT:</strong> When overriding this method, you
12      * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
13      * measured width and height of this view. Failure to do so will trigger an
14      * <code>IllegalStateException</code>, thrown by
15      * {@link #measure(int, int)}. Calling the superclass'
16      * {@link #onMeasure(int, int)} is a valid use.
17      * </p>
18      *
19      * <p>
20      * The base class implementation of measure defaults to the background size,
21      * unless a larger size is allowed by the MeasureSpec. Subclasses should
22      * override {@link #onMeasure(int, int)} to provide better measurements of
23      * their content.
24      * </p>
25      *
26      * <p>
27      * If this method is overridden, it is the subclass's responsibility to make
28      * sure the measured height and width are at least the view's minimum height
29      * and width ({@link #getSuggestedMinimumHeight()} and
30      * {@link #getSuggestedMinimumWidth()}).
31      * </p>
32      *
33      * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
34      *                         The requirements are encoded with
35      *                         {@link android.view.View.MeasureSpec}.
36      * @param heightMeasureSpec vertical space requirements as imposed by the parent.
37      *                         The requirements are encoded with
38      *                         {@link android.view.View.MeasureSpec}.
39      *
40      * @see #getMeasuredWidth()
41      * @see #getMeasuredHeight()
42      * @see #setMeasuredDimension(int, int)
43      * @see #getSuggestedMinimumHeight()
44      * @see #getSuggestedMinimumWidth()
45      * @see android.view.View.MeasureSpec#getMode(int)
46      * @see android.view.View.MeasureSpec#getSize(int)
47      */
48     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
49         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
50                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
51     }

       函數體內也就一句代碼而已,注釋卻寫了這么一大堆,可見這個方法的重要性了。這里翻譯和整理一下這些注釋:

      1)測量該view以及它的內容來決定測量的寬度和高度。該方法被measure(int,int)(筆者注:就是前面提到過的那個方法)調用,並且應該被子類重寫來提供准確而且有效的對它們的內容的測量。

      2)當重寫該方法時,您必須調用setMeasuredDimension(int,int)來存儲該view測量出的寬和高。如果不這樣做將會觸發IllegalStateException,由measure(int,int)拋出。調用基類的onMeasure(int,int)方法是一個有效的方法。

      3)測量的基類實現默認為背景的尺寸,除非更大的尺寸被MeasureSpec所允許。子類應該重寫onMeasure(int,int)方法來提供對內容更好的測量。

      4)如果該方法被重寫,子類負責確保測量的高和寬至少是該view的mininum高度和mininum寬度值(鏈接getSuggestedMininumHeight()和getSuggestedMininumWidth());

      5) widthMeasureSpec:父布局加入的水平空間要求。該要求被編碼到android.view.View.MeasureSpec中。

      6)heightMeasureSpec:父布局加入的垂直空間要求。該要求被編碼到android.view.View.MeasureSpec中。

       注釋中最后提到了7個方法,這些方法后面會再分析。注釋中花了不少的篇幅對該方法進行說明,但讀者恐怕對其中的一些信息表示有些懵吧,比如MeasureSpec是什么,mininum高度和mininum寬度值是怎么回事等,MeasureSpec在本節的開頭介紹過,可以回頭再看看,其它的后面會作進一步的闡述,到時候咱們再回頭來看看這些注釋。

        注意:容器類控件都是ViewGroup的子類,如FrameLayout、LinearLayout等,都會重寫onMeasure方法,根據自己的特性來進行測量;如果是葉子節點view,即最里層的控件,如TextView等,也可能會重寫onMeasure方法,所以當流程走到onMeasure(...)時,流程可能就會切到那些重寫的onMeasure()方法中去。最后通過從根View到葉子節點的遍歷和遞歸,最終還是會在葉子view中調用setMeasuredDimension(...)來實現最終的測量。

    (4)View.setMeasuredDimension()方法

      繼續看setMeasuredDimension方法:

 1 /**
 2      * <p>This method must be called by {@link #onMeasure(int, int)} to store the
 3      * measured width and measured height. Failing to do so will trigger an
 4      * exception at measurement time.</p>
 5      *
 6      * @param measuredWidth The measured width of this view.  May be a complex
 7      * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
 8      * {@link #MEASURED_STATE_TOO_SMALL}.
 9      * @param measuredHeight The measured height of this view.  May be a complex
10      * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
11      * {@link #MEASURED_STATE_TOO_SMALL}.
12      */
13     protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
14         ......
15         setMeasuredDimensionRaw(measuredWidth, measuredHeight);
16     }

   這里需要重點關注注釋中對參數的說明:

       measuredWidth:該view被測量出寬度值。

       measuredHeight:該view被測量出的高度值。

      到這個時候才正式明確提到寬度和高度,通過getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),參數由widthMeasureSpec變成了measuredWidth,即由“父布局加入的水平空間要求”轉變為了view的寬度,measuredHeigh也是一樣。咱們先繼續追蹤源碼分析width的值:

 1 /**
 2      * Returns the suggested minimum width that the view should use. This
 3      * returns the maximum of the view's minimum width
 4      * and the background's minimum width
 5      *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
 6      * <p>
 7      * When being used in {@link #onMeasure(int, int)}, the caller should still
 8      * ensure the returned width is within the requirements of the parent.
 9      *
10      * @return The suggested minimum width of the view.
11      */
12     protected int getSuggestedMinimumWidth() {
13         return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
14     }

       這個方法是干嘛用的呢?注釋的翻譯如下:

      1)返回建議該view應該使用的最小寬度值。該方法返回了view的最小寬度值和背景的最小寬度值(鏈接android.graphics.drawable.Drawable#getMinimumWidth())之間的最大值。

      2)當在onMeasure(int,int)使用時,調用者應該仍然確保返回的寬度值在父布局的要求之內。

      3)返回值:view的建議最小寬度值。

      這其中提到的"mininum width“指的是在xml布局文件中該view的“android:minWidth"屬性值,“background's minimum width”值是指“android:background”的寬度。該方法的返回值就是兩者之間較大的那一個值,用來作為該view的最小寬度值,現在應該很容易理解了吧,當一個view在layout文件中同時設置了這兩個屬性時,為了兩個條件都滿足,自然要選擇值大一點的那個了。

 1 /**
 2      * Utility to return a default size. Uses the supplied size if the
 3      * MeasureSpec imposed no constraints. Will get larger if allowed
 4      * by the MeasureSpec.
 5      *
 6      * @param size Default size for this view
 7      * @param measureSpec Constraints imposed by the parent
 8      * @return The size this view should be.
 9      */
10     public static int getDefaultSize(int size, int measureSpec) {
11         int result = size;
12         int specMode = MeasureSpec.getMode(measureSpec);
13         int specSize = MeasureSpec.getSize(measureSpec);
14 
15         switch (specMode) {
16         case MeasureSpec.UNSPECIFIED:
17             result = size;
18             break;
19         case MeasureSpec.AT_MOST:
20         case MeasureSpec.EXACTLY:
21             result = specSize;
22             break;
23         }
24         return result;
25     }

       通過本節開頭的介紹,您應該對MeasureSpec有了一個比較明確的認識了,再看看getDefaultSize(int size,int measureSpec)方法,就很容易理解了。正如其注釋中所說,如果父布局沒有施加任何限制,即MeasureSpec的mode為UNSPECIFIED,那么返回值為參數中提供的size值。如果父布局施加了限制,則返回的默認尺寸為保存在參數measureSpec中的specSize值。所以到目前為止,需要繪制的寬和高值就被確定下來了。只是,我們還需要明確這兩個值最初是從哪里傳過來的,后面我們還會順藤摸瓜,找到這兩個尺寸的出處。

       既然寬度值measuredWidth和高度值measuredHeight已經確定下來,我們繼續追蹤之前的setMeasuredDimension(int measuredWidth, int measuredHeight)方法,其內部最后調用了如下的方法:

 1 /**
 2      * ......
 3      * @param measuredWidth The measured width of this view.  May be a complex
 4      * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
 5      * {@link #MEASURED_STATE_TOO_SMALL}.
 6      * @param measuredHeight The measured height of this view.  May be a complex
 7      * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
 8      * {@link #MEASURED_STATE_TOO_SMALL}.
 9      */
10     private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
11         mMeasuredWidth = measuredWidth;
12         mMeasuredHeight = measuredHeight;
13         ......
14     }

       到目前為止,View中的成員變量mMeasureWidth和mMeasureHeight就被賦值了,這也就意味着,View的測量就結束了。前面講onMeasure()方法時介紹過,View子類(包括ViewGroup子類)通常會重寫onMeasure(),當閱讀FrameLayout、LinearLayout、TextView等重寫的onMeasure()方法時,會發現它們最終都會調用setMeasuredDimension() 方法,從而完成測量。這里可以對應上前面介紹View.onMeasure()時,翻譯注釋的第2)點以及setMeasuredDimension()方法的注釋說明。

    (5)getMeasureWidth()方法

       在View的onMeasure()方法的注釋中提到了該方法,這里順便也介紹一下。

1 //==================View.java==============
2 public static final int MEASURED_SIZE_MASK = 0x00ffffff;
3 /**
4  * ......
5  * @return The raw measured width of this view.
6  */
7 public final int getMeasuredWidth() {
8    return mMeasuredWidth & MEASURED_SIZE_MASK;
9 }

       獲取原始的測量寬度值,一般會拿這個方法和layout執行后getWidth()方法做比較。該方法需要在setMeasuredDimension()方法執行后才有效,否則返回值為0。

    (6)getMeasureHeight()方法

       在View的onMeasure()方法的注釋中提到了該方法,這里順便也介紹一下。

1 //==================View.java==============
2 /**
3   * ......
4   * @return The raw measured height of this view.
5   */
6 public final int getMeasuredHeight() {
7    return mMeasuredHeight & MEASURED_SIZE_MASK;
8 }

       獲取原始的測量高度值,一般會拿這個方法和layout執行后getHeight()方法做比較。該方法需要在setMeasuredDimension()方法執行后才有效,否則返回值為0。

 

  4、performMeasure()方法中RootMeasureSpec參數來源分析

       前面講到getDefaultSize(int size,int measureSpec)方法時提到過,要找到其中measureSpec的來源。事實上,根據View體系的不斷往下遍歷和遞歸中,前面流程中傳入getDefaultSize()方法中的值是根據上一次的值變動的,所以咱們需要找到最初參數值。根據代碼往回看,可以看到前文performTraversals()源碼部分第三行和第四行中,該參數的來源。咱們先看看傳入performMeasure(int,int)的childWidthMeasureSpec是怎么來的。

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);

       getRootMeasureSpec(int,int)方法的完整源碼如下所示:

 1 /**
 2      * Figures out the measure spec for the root view in a window based on it's
 3      * layout params.
 4      *
 5      * @param windowSize
 6      *            The available width or height of the window
 7      *
 8      * @param rootDimension
 9      *            The layout params for one dimension (width or height) of the
10      *            window.
11      *
12      * @return The measure spec to use to measure the root view.
13      */
14     private static int getRootMeasureSpec(int windowSize, int rootDimension) {
15         int measureSpec;
16         switch (rootDimension) {
17 
18         case ViewGroup.LayoutParams.MATCH_PARENT:
19             // Window can't resize. Force root view to be windowSize.
20             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
21             break;
22         case ViewGroup.LayoutParams.WRAP_CONTENT:
23             // Window can resize. Set max size for root view.
24             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
25             break;
26         default:
27             // Window wants to be an exact size. Force root view to be that size.
28             measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
29             break;
30         }
31         return measureSpec;
32     }

 照例先翻譯一下注釋

      1)基於window的layout params,在window中為root view 找出measure spec。(筆者注:也就是找出DecorView的MeasureSpec,這里的window也就是PhoneWindow了)

      2)參數windowSize:window的可用寬度和高度值。

      3)參數rootDimension:window的寬/高的layout param值。

      4)返回值:返回用於測量root view的MeasureSpec。    

       如果不清楚LayoutParams類,可以看看本節開頭的介紹。在getRootMeasureSpec(int,int)中,MeasureSpec.makeMeasureSpec方法在前面介紹MeasureSpec類的時候提到過,就是將size和mode組合成一個MeasureSpec值。這里我們可以看到ViewGroup.LayoutParam的width/height值和MeasureSpec的mode值存在如下的對應關系:

       我們再繼續看看windowSize和rootDimension的實際參數mWidth和lp.width的來歷。

 1 //===========================ViewRootImpl.java=======================
 2 ......
 3 final Rect mWinFrame; // frame given by window manager.
 4 ......
 5 private void performTraversals() {
 6     ......
 7     Rect frame = mWinFrame;
 8     ......
 9     mWidth = frame.width();
10     ......
11 }

       從源碼中對mWinFrame的注釋來看,是由WindowManager提供的,該矩形正好是整個屏幕(這里暫時還沒有在源碼中找到明確的證據,后續找到后再補上)。在文章【Android圖形系統(三)-View繪制流程】的“2.2 窗口布局階段”中有提到,WindowManagerService服務計算Activity窗口的大小,並將Activity窗口的大小保存在成員變量mWinFrame中。對Activity窗口大小計算的詳情,有興趣的可以閱讀一下大神羅升陽的博文【Android窗口管理服務WindowManagerService計算Activity窗口大小的過程分析】。

 1 //=================================ViewRootImpl.java================================
 2 ......
 3 final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
 4 ......
 5 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
 6     ......
 7     mWindowAttributes.copyFrom(attrs);
 8     ......
 9 }
10 private void performTraversals() {
11      ......
12      WindowManager.LayoutParams lp = mWindowAttributes;
13      ......     
14 }

       第5行setView方法,在上一節中講過,其中的參數就是ActivityThread類中傳過來的,attrs是PhoneWindow的LayoutParams值,在第三節中就專門講過這個參數,其width和height屬性值均為LayoutParams.MATCH_PARENT。結合getRootMeasureSpec(int windowSize, int rootDimension)方法,可以得出如下結果:

      

       此時,我們就得到了DecorView的MeasureSpec了,后面的遞歸操作就是在此基礎上不斷將測量要求從父布局傳遞到子view。

 

  5、ViewGroup中輔助重寫onMeasure的幾個重要方法介紹

        前面我們介紹的很多方法都是View類中提供的,ViewGroup中也提供了一些方法用於輔助ViewGroup子類容器的測量。這里重點介紹三個方法:measureChild(...)、measureChildWithMargins(...)和measureChildWithMargins(...)方法。

    (1)measureChild()方法和measureChildWithMargins()方法

 1 //================ViewGroup.java===============
 2 /**
 3      * Ask one of the children of this view to measure itself, taking into
 4      * account both the MeasureSpec requirements for this view and its padding.
 5      * The heavy lifting is done in getChildMeasureSpec.
 6      *
 7      * @param child The child to measure
 8      * @param parentWidthMeasureSpec The width requirements for this view
 9      * @param parentHeightMeasureSpec The height requirements for this view
10      */
11     protected void measureChild(View child, int parentWidthMeasureSpec,
12             int parentHeightMeasureSpec) {
13         final LayoutParams lp = child.getLayoutParams();
14 
15         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
16                 mPaddingLeft + mPaddingRight, lp.width);
17         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
18                 mPaddingTop + mPaddingBottom, lp.height);
19 
20         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
21     }

 

 1 //===================ViewGroup.java===================
 2 /**
 3      * Ask one of the children of this view to measure itself, taking into
 4      * account both the MeasureSpec requirements for this view and its padding
 5      * and margins. The child must have MarginLayoutParams The heavy lifting is
 6      * done in getChildMeasureSpec.
 7      *
 8      * @param child The child to measure
 9      * @param parentWidthMeasureSpec The width requirements for this view
10      * @param widthUsed Extra space that has been used up by the parent
11      *        horizontally (possibly by other children of the parent)
12      * @param parentHeightMeasureSpec The height requirements for this view
13      * @param heightUsed Extra space that has been used up by the parent
14      *        vertically (possibly by other children of the parent)
15      */
16     protected void measureChildWithMargins(View child,
17             int parentWidthMeasureSpec, int widthUsed,
18             int parentHeightMeasureSpec, int heightUsed) {
19         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
20 
21         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
22                 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
23                         + widthUsed, lp.width);
24         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
25                 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
26                         + heightUsed, lp.height);
27 
28         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
29     }

        對比這兩個方法可以發現,它們非常相似,從注釋上來看,后者在前者的基礎上增加了已經使用的寬高和margin值。其實它們的功能都是一樣的,最后都是生成子View的MeasureSpec,並傳遞給子View繼續測量,即最后一句代碼child.measure(childWidthMeasureSpec, childHeightMeasureSpec)。一般根據容器自身的需要來選擇其中一個,比如,在FrameLayout和LinearLayout中重寫的onMeasure方法中調用的就是后者,而AbsoluteLayout中就是間接地調用的前者。而RelativeLayout中,兩者都沒有調用,而是自己寫了一套方法,不過該方法和后者方法僅略有差別,但基本功能還是一樣,讀者可以自己去看看它們的源碼,這里就不貼出來了。

    (2)getChildMeasureSpec()方法

       前兩個方法中都用到了這個方法,它很重要,它用於將父布局傳遞來的MeasureSpec和其子view的LayoutParams,整合為一個最有可能的子View的MeasureSpec。

 1 //==================ViewGroup.java====================
 2  /**
 3      * Does the hard part of measureChildren: figuring out the MeasureSpec to
 4      * pass to a particular child. This method figures out the right MeasureSpec
 5      * for one dimension (height or width) of one child view.
 6      *
 7      * The goal is to combine information from our MeasureSpec with the
 8      * LayoutParams of the child to get the best possible results. For example,
 9      * if the this view knows its size (because its MeasureSpec has a mode of
10      * EXACTLY), and the child has indicated in its LayoutParams that it wants
11      * to be the same size as the parent, the parent should ask the child to
12      * layout given an exact size.
13      *
14      * @param spec The requirements for this view
15      * @param padding The padding of this view for the current dimension and
16      *        margins, if applicable
17      * @param childDimension How big the child wants to be in the current
18      *        dimension
19      * @return a MeasureSpec integer for the child
20      */
21     public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
22         int specMode = MeasureSpec.getMode(spec);
23         int specSize = MeasureSpec.getSize(spec);
24 
25         int size = Math.max(0, specSize - padding);
26 
27         int resultSize = 0;
28         int resultMode = 0;
29 
30         switch (specMode) {
31         // Parent has imposed an exact size on us
32         case MeasureSpec.EXACTLY:
33             if (childDimension >= 0) {
34                 resultSize = childDimension;
35                 resultMode = MeasureSpec.EXACTLY;
36             } else if (childDimension == LayoutParams.MATCH_PARENT) {
37                 // Child wants to be our size. So be it.
38                 resultSize = size;
39                 resultMode = MeasureSpec.EXACTLY;
40             } else if (childDimension == LayoutParams.WRAP_CONTENT) {
41                 // Child wants to determine its own size. It can't be
42                 // bigger than us.
43                 resultSize = size;
44                 resultMode = MeasureSpec.AT_MOST;
45             }
46             break;
47 
48         // Parent has imposed a maximum size on us
49         case MeasureSpec.AT_MOST:
50             if (childDimension >= 0) {
51                 // Child wants a specific size... so be it
52                 resultSize = childDimension;
53                 resultMode = MeasureSpec.EXACTLY;
54             } else if (childDimension == LayoutParams.MATCH_PARENT) {
55                 // Child wants to be our size, but our size is not fixed.
56                 // Constrain child to not be bigger than us.
57                 resultSize = size;
58                 resultMode = MeasureSpec.AT_MOST;
59             } else if (childDimension == LayoutParams.WRAP_CONTENT) {
60                 // Child wants to determine its own size. It can't be
61                 // bigger than us.
62                 resultSize = size;
63                 resultMode = MeasureSpec.AT_MOST;
64             }
65             break;
66 
67         // Parent asked to see how big we want to be
68         case MeasureSpec.UNSPECIFIED:
69             if (childDimension >= 0) {
70                 // Child wants a specific size... let him have it
71                 resultSize = childDimension;
72                 resultMode = MeasureSpec.EXACTLY;
73             } else if (childDimension == LayoutParams.MATCH_PARENT) {
74                 // Child wants to be our size... find out how big it should
75                 // be
76                 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
77                 resultMode = MeasureSpec.UNSPECIFIED;
78             } else if (childDimension == LayoutParams.WRAP_CONTENT) {
79                 // Child wants to determine its own size.... find out how
80                 // big it should be
81                 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
82                 resultMode = MeasureSpec.UNSPECIFIED;
83             }
84             break;
85         }
86         //noinspection ResourceType
87         return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
88     }

 咱們依然先翻譯和整理一下開頭的注釋:

      1)處理measureChildren的困難部分:計算出Measure傳遞給指定的child。該方法計算出一個子view的寬或高的正確MeasureSpec。

      2)其目的是組合來自我們MeasureSpec的信息和child的LayoutParams來得到最有可能的結果。比如:如果該view知道它的尺寸(因為它的MeasureSpec的mode為EXACTLY),並且它的child在它的LayoutParams中表示它想和父布局有一樣大,那么父布局應該要求該child按照精確的尺寸進行布局。

      3)參數spec:對該view的要求(筆者注:父布局對當前child的MeasureSpec要求)

      4)參數padding:該view寬/高的padding和margins值,如果可應用的話。

      5)參數childDimension:該child在寬/高上希望多大。

      6)返回:返回該child的MeasureSpec整數。

       如果明白了前文中對MeasureSpec的介紹后,這一部分的代碼應該就容易理解了,specMode的三種值,LayoutParams的width和height的三種值,以及和layout_width、layout_height之間的關對應關系,在文章的開頭已經介紹過了,不明白的可以再回頭復習一下。specMode和specSize分別是父布局傳下來的要求,size的值是父布局尺寸要求減去其padding值,最小不會小於0。代碼最后就是將重新得到的mode和size組合生成一個新的MeasureSpec,傳遞給子View,一直遞歸下去,該方法也在前面講過。本段代碼重難點就是這里新mode和新size值的確定,specMode和childDimension各有3種值,所以最后會有9種組合。如果對這段代碼看不明白的,可以看看筆者對這段代碼的解釋(width和height同理,這里以width為例):

  • 如果specMode的值為MeasureSpec.EXACTLY,即父布局對子view的尺寸要求是一個精確值,這有兩種情況,父布局中layout_width屬性值被設置為具體值,或者match_parent,它們都被定義為精確值。針對childDimension的值

          i)childDimension也為精確值時。它是LayoutParams中width屬性,是一個具體值,不包括match_parent情況,這個一定要和MeasureSpec中的精確值EXACTLY區別開來。此時resultSize為childDimension的精確值,resultMode理所當然為MeasureSpec.EXACTLY。這里不知道讀者會不會又疑問,如果子View的layout_width值比父布局的大,那這個結論還成立嗎?按照我們的經驗,似乎不太能理解,因為子view的寬度再怎么樣也不會比父布局大。事實上,我們平時經驗看到的,是最后布局后繪制出來的結果,而當前步驟為測量值,是有差別的。讀者可以自定義一個View,將父布局layout_width設置為100px,該自定義的子view則設置為200px,然后在子view中重寫的onMeasure方法中打印出getMeasuredWidth()值看看,其值一定是200。甚至如果子view設置的值超過屏幕尺寸,其打印值也是設置的值。

        ii)childDimension值為LayoutParams.MATCH_PARENT時。這個容易理解,它的尺寸和父布局一樣,也是個精確值,所以resultSize為前面求出的size值,由父布局決定,resultMode為MeasureSpec.EXACTLY。

        iii)childDimension值為LayoutParams.WRAP_CONTENT時。當子view的layout_width被設置為wrap_content時,即使最后我們肉眼看到屏幕上真正顯示出來的控件很小,但在測量時和父布局一樣的大小。這一點仍然可以通過打印getMeasuredWidth值來理解。所以一定不要被“經驗”所誤。所以resultSize值為size大小,resultMode為MeasureSpec.AT_MOST。

  • 如果specMode值為MeasureSpec.AT_MOST。其對應於layout_width為wrap_content,此時,我們可以想象到,子View對結果的決定性很大。

        i)childDimension為精確值時。很容易明確specSize為自身的精確值,specMode為MeasureSpec.EXACTLY。

        ii)childDimension為LayoutParams.MATCH_PARENT時。specSize由父布局決定,為size;specMode為MeasureSpec.AT_MOST。

        iii)childDimension為LayoutParams.WRAP_CONTENT時。specSize由父布局決定,為size;specMode為MeasureSpec.AT_MOST。

  • 如果specMode值為MeasureSpec.UNSPECIFIED。前面說過,平時很少用,一般用在系統中,不過這里還是簡單說明一下。這一段有個變量View.sUseZeroUnspecifiedMeasureSpec,它是用於表示當前的目標api是否低於23(對應系統版本為Android M)的,低於23則為true,否則為false。現在系統版本基本上都是Android M及以上的,所以這里該值我們當成false來處理。

        i)childDimension為精確值時。很容易明確specSize為自身的精確值,specMode為MeasureSpec.EXACTLY。

        ii)childDimension為LayoutParams.MATCH_PARENT時。specSize由父布局決定,為size;specMode和父布局一樣,為MeasureSpec.UNSPECIFIED。

        iii)childDimension為LayoutParams.WRAP_CONTENT時。specSize由父布局決定,為size;specMode和父布局一樣,為MeasureSpec.UNSPECIFIED。

       這個方法對理解測量時MeasureSpec的傳遞過程非常重要,並且需要記憶和理解的內容也不少,所以這里花的篇幅比較多。

 

       通過這一節,我們介紹了ViewGroup在測量過程中要用到的方法。通過這些方法,我們更加深入理解了測量過程中ViewGroup是如何測量子View的了。

 

  6、DecorView測量的大致流程

       前面我們提到過DecorView的繼承鏈:DecorView extends FrameLayout extends ViewGroup extends View。所以在這個繼承過程中一定會有子類重寫onMeasure方法,當DecorView第一次調用到measure()方法后,流程就開始切換到重寫的onMeasure()中了。我們按照這個繼承順序看看measure流程的相關源碼:

 1 //=============DecorView.java=============
 2 @Override
 3 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 4        ......
 5     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 6        ......
 7 }
 8 
 9 //=============FrameLayout.java=============
10 @Override
11 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
12   int count = getChildCount();
13   for (int i = 0; i < count; i++) {
14        final View child = getChildAt(i);
15        ......
16        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
17        ......             
18    }
19    ......
20    setMeasuredDimension(......)
21    ...... }

       第16行中measureChildWithMargins()方法是ViewGroup提供的方法,前面我們介紹過了。從上述FrameLayout中重寫的onMeasure方法中可以看到,是先把子view測量完成后,最后才去調用setMeasuredDimension(...)來測量自己的。事實上,整個測量過程就是從子view開始測量,然后一層層往上再測量父布局,直到DecorView為止的。

       可能到這里有些讀者會有個疑問,DecorView中onMeasure方法的參數值是從哪里傳過來的呢?呵呵,前面花了很大的篇幅,就在不斷地講它倆,這里再強調啰嗦一次:

1 //=====================ViewRootImpl.java=================
2 private void performTraversals() {
3    ......
4    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
5    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);      
6    ......
7 }

 如果還是不明白,回過頭去再看看這部分的說明吧,這里就不再贅述了。

 

  7、DecorView視圖樹的簡易measure流程圖

        到目前為止,DecorView的整個測量流程就接上了,從ViewRootImpl類的performTraversals()開始,經過遞歸遍歷,最后到葉子view測量結束,DecorView視圖樹的測量就完成了。這里再用一個流程圖簡單描述一下整個流程:

  

 

       在這一節的最后,推薦一篇博文,這里面有個非常詳細的案例分析,如何一步一步從DecorView開始遍歷,到整個View樹測量完成,以及如何測量出每個view的寬高值:【Android View的繪制流程:https://www.jianshu.com/p/5a71014e7b1b?from=singlemessage】Measure過程的第4點。認真分析完該實例,一定會對測量過程有個更深刻的認識。

 

六、layout過程分析

       當measure過程完成后,接下來就會進行layout階段,即布局階段。在前面measure的作用是測量每個view的尺寸,而layout的作用是根據前面測量的尺寸以及設置的其它屬性值,共同來確定View的位置。

  1、performLayout方法引出DecorView的布局流程

       測量完成后,會在ViewRootImpl類的performTraverserals()方法中,開始調用performLayout方法:

performLayout(lp, mWidth, mHeight);

       傳入該方法的參數我們在上一節中已經分析過了,lp中width和height均為LayoutParams.MATCH_PARENT,mWidth和mHeight分別為屏幕的寬高。

1 //=====================ViewRootImpl.java===================
2 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
3             int desiredWindowHeight) {
4    ......
5    final View host = mView;
6    ......
7    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
8    ......
9 }

       mView的值上一節也講過,就是DecorView,布局流程也是從DecorView開始遍歷和遞歸。

 

  2、layout方法正式啟動布局流程

       由於DecorView是一個容器,是ViewGroup子類,所以跟蹤代碼的時候,實際上是先進入到ViewGroup類中的layout方法中。

 1 //==================ViewGroup.java================
 2     @Override
 3     public final void layout(int l, int t, int r, int b) {
 4         if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
 5             if (mTransition != null) {
 6                 mTransition.layoutChange(this);
 7             }
 8             super.layout(l, t, r, b);
 9         } else {
10             // record the fact that we noop'd it; request layout when transition finishes
11             mLayoutCalledWhileSuppressed = true;
12         }
13     }

       這是一個final類型的方法,所以自定義 的ViewGroup子類無法重寫該方法,可見系統不希望自定義的ViewGroup子類破壞layout流程。繼續追蹤super.layout方法,又跳轉到了View中的layout方法。

 1 //=================View.java================
 2  /**
 3      * Assign a size and position to a view and all of its
 4      * descendants
 5      *
 6      * <p>This is the second phase of the layout mechanism.
 7      * (The first is measuring). In this phase, each parent calls
 8      * layout on all of its children to position them.
 9      * This is typically done using the child measurements
10      * that were stored in the measure pass().</p>
11      *
12      * <p>Derived classes should not override this method.
13      * Derived classes with children should override
14      * onLayout. In that method, they should
15      * call layout on each of their children.</p>
16      *
17      * @param l Left position, relative to parent
18      * @param t Top position, relative to parent
19      * @param r Right position, relative to parent
20      * @param b Bottom position, relative to parent
21      */
22     @SuppressWarnings({"unchecked"})
23     public void layout(int l, int t, int r, int b) {
24         ......
25         boolean changed = isLayoutModeOptical(mParent) ?
26                 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);  
27         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
28             onLayout(changed, l, t, r, b);
29             ......
30          }
31          ......
32 }

 先翻譯一下注釋中對該方法的描述:

      1)給view和它的所有后代分配尺寸和位置。

      2)這是布局機制的第二個階段(第一個階段是測量)。在這一階段中,每一個父布局都會對它的子view進行布局來放置它們。一般來說,該過程會使用在測量階段存儲的child測量值。

      3)派生類不應該重寫該方法。有子view的派生類(筆者注:也就是容器類,父布局)應該重寫onLayout方法。在重寫的onLayout方法中,它們應該為每一子view調用layout方法進行布局。

      4)參數依次為:Left、Top、Right、Bottom四個點相對父布局的位置。

 

  3、setFrame方法真正執行布局任務

       在上面的方法體中,我們先重點看看setFrame方法。至於setOpticalFrame方法,其中也是調用的setFrame方法。

 1 //=================View.java================
 2 /**
 3      * Assign a size and position to this view.
 4      *
 5      * This is called from layout.
 6      *
 7      * @param left Left position, relative to parent
 8      * @param top Top position, relative to parent
 9      * @param right Right position, relative to parent
10      * @param bottom Bottom position, relative to parent
11      * @return true if the new size and position are different than the
12      *         previous ones
13      * {@hide}
14      */
15     protected boolean setFrame(int left, int top, int right, int bottom) {
16         boolean changed = false;
17         ......
18         if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
19             changed = true;
20             ......
21             int oldWidth = mRight - mLeft;
22             int oldHeight = mBottom - mTop;
23             int newWidth = right - left;
24             int newHeight = bottom - top;
25             boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
26 
27             // Invalidate our old position
28             invalidate(sizeChanged);
29 
30             mLeft = left;
31             mTop = top;
32             mRight = right;
33             mBottom = bottom;
34             ......
35         }
36         return changed;
37  }

  注釋中重要的信息有:

      1)該方法用於給該view分配尺寸和位置。(筆者注:也就是實際的布局工作是在這里完成的)

      2)返回值:如果新的尺寸和位置和之前的不同,返回true。(筆者注:也就是該view的位置或大小發生了變化)

       在方法體中,從第27行開始,對view的四個屬性值進行了賦值,即mLeft、mTop、mRight、mBottom四條邊界坐標被確定,表明這里完成了對該View的布局。

 

  4、onLayout方法讓父布局調用對子view的布局

      再返回到layout方法中,會看到如果view發生了改變,接下來會調用onLayout方法,這和measure調用onMeasure方法類似。

 1 //============View.java============
 2 /**
 3      * Called from layout when this view should
 4      * assign a size and position to each of its children.
 5      *
 6      * Derived classes with children should override
 7      * this method and call layout on each of
 8      * their children.
 9      * @param changed This is a new size or position for this view
10      * @param left Left position, relative to parent
11      * @param top Top position, relative to parent
12      * @param right Right position, relative to parent
13      * @param bottom Bottom position, relative to parent
14      */
15     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
16     }

 先翻譯一下關鍵注釋:

      1)當該view要分配尺寸和位置給它的每一個子view時,該方法會從layout方法中被調用。

      2)有子view的派生類(筆者注:也就是容器,父布局)應該重寫該方法並且為每一個子view調用layout。

       我們發現這是一個空方法,因為layout過程是父布局容器布局子view的過程,onLayout方法葉子view沒有意義,只有ViewGroup才有用。所以,如果當前View是一個容器,那么流程會切到被重寫的onLayout方法中。我們先看ViewGroup類中的重寫:

1 //=============ViewGroup.java===========
2   @Override
3    protected abstract void onLayout(boolean changed,
4            int l, int t, int r, int b);

       進入到ViewGroup類中發現,該方法被定義為了abstract方法,所以以后凡是直接繼承自ViewGroup類的容器,就必須要重寫onLayout方法。 事實上,layout流程是繪制流程中必需的過程,而前面講過的measure流程,其實可以不要,這一點等會再說。

       咱們先直接進入到DecorView中查看重寫的onLayout方法。

1 //==============DecorView.java================
2  @Override
3  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
4      super.onLayout(changed, left, top, right, bottom);
5      ......
6 }

       DecerView繼承自FrameLayout,咱們繼續到FrameLayout類中重寫的onLayout方法看看。

 1 //================FrameLayout.java==============
 2     @Override
 3     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 4         layoutChildren(left, top, right, bottom, false /* no force left gravity */);
 5     }
 6 
 7     void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
 8         final int count = getChildCount();
 9         ......
10         for (int i = 0; i < count; i++) {
11              final View child = getChildAt(i);
12              if (child.getVisibility() != GONE) {
13                  final LayoutParams lp = (LayoutParams) child.getLayoutParams();
14 
15                  final int width = child.getMeasuredWidth();
16                  final int height = child.getMeasuredHeight();
17                  ......
18                  child.layout(childLeft, childTop, childLeft + width, childTop + height);
19             }
20     }

       這里僅貼出關鍵流程的代碼,咱們可以看到,這里面也是對每一個child調用layout方法的。如果該child仍然是父布局,會繼續遞歸下去;如果是葉子view,則會走到view的onLayout空方法,該葉子view布局流程走完。另外,我們看到第15行和第16行中,width和height分別來源於measure階段存儲的測量值,如果這里通過其它渠道賦給width和height值,那么measure階段就不需要了,這也就是我前面提到的,onLayout是必需要實現的(不僅會報錯,更重要的是不對子view布局的話,這些view就不會顯示了),而measure過程可以不要。當然,肯定是不建議這么做的,采用其它方式很實現我們要的結果。

 

  5、DecorView視圖樹的簡易布局流程圖

       如果是前面搞清楚了DecorView視圖樹的測量流程,那這一節的布局流程也就非常好理解了,咱們這里再簡單梳理一下:

 

 

七、draw過程分析

       當layout完成后,就進入到draw階段了,在這個階段,會根據layout中確定的各個view的位置將它們畫出來。該過程的分析思路和前兩個過程類似,如果前面讀懂了,那這個流程也就很容易理解了。

  1、從performDraw方法到draw方法

       draw過程,自然也是從performTraversals()中的performDraw()方法開始的,咱們從該方法追蹤,咱們這里僅貼出關鍵流程代碼,至於其它的邏輯,不是本文的重點,這里就先略過,有興趣的可以自行研究。

 1 //==================ViewRootImpl.java=================
 2 private void performDraw() {
 3       ......
 4       boolean canUseAsync = draw(fullRedrawNeeded);
 5       ......
 6 }
 7 
 8 private boolean draw(boolean fullRedrawNeeded) {
 9       ......
10       if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
11                         scalingRequired, dirty, surfaceInsets)) {
12                     return false;
13                 }
14       ......
15 }
16 
17 private boolean drawSoftware(......){
18       ......
19       mView.draw(canvas);
20       ......
21 }

       前面我們講過了,這mView就是DecorView,這樣就開始了DecorView視圖樹的draw流程了。

  2、DecorView樹遞歸完成“畫”流程

       DecorView類中重寫了draw()方法,追蹤源碼后進入到該部分。

1 //================DecorView.java==============
2 @Override
3 public void draw(Canvas canvas) {
4      super.draw(canvas);
5 
6      if (mMenuBackground != null) {
7          mMenuBackground.draw(canvas);
8      }
9 }

       從這段代碼來看, 調用完super.draw后,還畫了菜單背景,當然super.draw是咱們關注的重點,這里還做了啥咱們不用太關心。由於FrameLayout和ViewGroup都沒有重寫該方法,所以就直接進入都了View類中的draw方法了。

 1 //====================View.java===================== 
 2  /**
 3      * Manually render this view (and all of its children) to the given Canvas.
 4      * The view must have already done a full layout before this function is
 5      * called.  When implementing a view, implement
 6      * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
 7      * If you do need to override this method, call the superclass version.
 8      *
 9      * @param canvas The Canvas to which the View is rendered.
10      */
11     @CallSuper
12     public void draw(Canvas canvas) {
13        ......
14         /*
15          * Draw traversal performs several drawing steps which must be executed
16          * in the appropriate order:
17          *
18          *      1. Draw the background
19          *      2. If necessary, save the canvas' layers to prepare for fading
20          *      3. Draw view's content
21          *      4. Draw children
22          *      5. If necessary, draw the fading edges and restore layers
23          *      6. Draw decorations (scrollbars for instance)
24          */
25 
26         // Step 1, draw the background, if needed
27         int saveCount;
28 
29         if (!dirtyOpaque) {
30             drawBackground(canvas);
31         }
32 
33         // skip step 2 & 5 if possible (common case)
34         ......
35         // Step 3, draw the content
36         if (!dirtyOpaque) onDraw(canvas);
37 
38         // Step 4, draw the children
39         dispatchDraw(canvas);
40         ......
41         // Step 6, draw decorations (foreground, scrollbars)
42         onDrawForeground(canvas);45         ......
43     }

      這段代碼描述了draw階段完成的7個主要步驟,這里咱們先翻譯一下其注釋:

      1)手動渲染該view(以及它的所有子view)到給定的畫布上。

      2)在該方法調用之前,該view必須已經完成了全面的布局。當正在實現一個view是,實現onDraw(android.graphics.Cavas)而不是本方法。如果您確實需要重寫該方法,調用超類版本。

      3)參數canvas:將view渲染到的畫布。

      從代碼上看,這里做了很多工作,咱們簡單說明一下,有助於理解這個“畫”工作。

      1)第一步:畫背景。對應我我們在xml布局文件中設置的“android:background”屬性,這是整個“畫”過程的第一步,這一步是不重點,知道這里干了什么就行。

      2)第二步:畫內容(第2步和第5步只有有需要的時候才用到,這里就跳過)。比如TextView的文字等,這是重點,onDraw方法,后面詳細介紹。

      3)第三步:畫子view。dispatchDraw方法用於幫助ViewGroup來遞歸畫它的子view。這也是重點,后面也要詳細講到。

      4)第四步:畫裝飾。這里指畫滾動條和前景。其實平時的每一個view都有滾動條,只是沒有顯示而已。同樣這也不是重點,知道做了這些事就行。

       咱們進入onDraw方法看看

1 //=================View.java===============
2 /**
3      * Implement this to do your drawing.
4      *
5      * @param canvas the canvas on which the background will be drawn
6      */
7     protected void onDraw(Canvas canvas) {
8     }

 注釋中說:實現該方法來做“畫”工作。也就是說,具體的view需要重寫該方法,來畫自己想展示的東西,如文字,線條等。DecorView中重寫了該方法,所以流程會走到DecorView中重寫的onDraw方法。

1 //===============DocerView.java==============
2 @Override
3     public void onDraw(Canvas c) {
4         super.onDraw(c);
5       mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent,
6         mStatusColorViewState.view, mNavigationColorViewState.view);
7  }

      這里調用了onDraw的父類方法,同時第4行還畫了自己特定的東西。由於FrameLayout和ViewGroup也沒有重寫該方法,且View中onDraw為空方法,所以super.onDraw方法其實是啥都沒干的。DocerView畫完自己的東西,緊接着流程就又走到dispatchDraw方法了。

 1 //================View.java===============
 2 /**
 3      * Called by draw to draw the child views. This may be overridden
 4      * by derived classes to gain control just before its children are drawn
 5      * (but after its own view has been drawn).
 6      * @param canvas the canvas on which to draw the view
 7      */
 8     protected void dispatchDraw(Canvas canvas) {
 9 
10     }

       先看看注釋:被draw方法調用來畫子View。該方法可能會被派生類重寫來獲取控制,這個過程正好在該view的子view被畫之前(但在它自己被畫完成后)。

       也就是說當本view被畫完之后,就開始要畫它的子view了。這個方法也是一個空方法,實際上對於葉子view來說,該方法沒有什么意義,因為它沒有子view需要畫了,而對於ViewGroup來說,就需要重寫該方法來畫它的子view。

       在源碼中發現,像平時常用的LinearLayout、FrameLayout、RelativeLayout等常用的布局控件,都沒有再重寫該方法,DecorView中也一樣,而是只在ViewGroup中實現了dispatchDraw方法的重寫。所以當DecorView執行完onDraw方法后,流程就會切到ViewGroup中的dispatchDraw方法了。

 1 //=============ViewGroup.java============
 2  @Override
 3  protected void dispatchDraw(Canvas canvas) {
 4         final int childrenCount = mChildrenCount;
 5         final View[] children = mChildren;
 6         ......
 7         for (int i = 0; i < childrenCount; i++) {
 8             more |= drawChild(canvas, child, drawingTime);
 9             ......
10         }
11         ...... 
12  }

       從上述源碼片段可以發現,這里其實就是對每一個child執行drawChild操作。

 1 /**
 2      * Draw one child of this View Group. This method is responsible for getting
 3      * the canvas in the right state. This includes clipping, translating so
 4      * that the child's scrolled origin is at 0, 0, and applying any animation
 5      * transformations.
 6      *
 7      * @param canvas The canvas on which to draw the child
 8      * @param child Who to draw
 9      * @param drawingTime The time at which draw is occurring
10      * @return True if an invalidate() was issued
11      */
12     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
13         return child.draw(canvas, this, drawingTime);
14     }

先翻譯注釋的內容:

      1)畫當前ViewGroup中的某一個子view。該方法負責在正確的狀態下獲取畫布。這包括了裁剪,移動,以便子view的滾動原點為0、0,以及提供任何動畫轉換。

      2)參數drawingTime:“畫”動作發生的時間點。

       繼續追蹤源碼,進入到如下流程。

 1 //============View.java===========
 2 /**
 3      * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 4      *
 5      * This is where the View specializes rendering behavior based on layer type,
 6      * and hardware acceleration.
 7      */
 8     boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
 9       ......
10       draw(canvas);
11       ......
12 }

       注釋中說:該方法被ViewGroup.drawChild()方法調用,來讓每一個子view畫它自己。

       該方法中,又回到了draw(canvas)方法中了,然后再開始畫其子view,這樣不斷遞歸下去,直到畫完整棵DecorView樹。

 

  3、DecorView視圖樹的簡易draw流程圖

       針對上述的代碼追蹤流程,這里梳理了DecorView整個view樹的draw過程的關鍵流程,其中節點比較多,需要耐心分析。

      

       到目前為止,View的繪制流程就介紹完了。根節點是DecorView,整個View體系就是一棵以DecorView為根的View樹,依次通過遍歷來完成measure、layout和draw過程。而如果要自定義view,一般都是通過重寫onMeasure(),onLayout(),onDraw()來完成要自定義的部分,整個繪制流程也基本上是圍繞着這幾個核心的地方來展開的。

 

八、博文參考閱讀

       【Android View視圖層次

       【Android進階 - 視圖層級實時分析

       【Android視圖繪制流程完全解析,帶你一步步深入了解View(二)

       【Android圖形系統(三)-View繪制流程

       【Android View的繪制流程

       【Android View的繪制流程

 

結語

       本文的篇幅比較長,能看完並且理解也是一件辛苦的事情,筆者學習及寫這篇博客,也是花了將近半個月的業余時間來完成的。但是要想超過別人,就是要做一件有一件辛苦但能夠成長的事情,時間長了,人與人之間的距離就拉開了。所以,真心希望本文能幫助您理解View的繪制流程,那筆者半個月來的辛苦也就沒有白費了。當然,本文肯定存在很多不足之處,希望讀者能不吝賜教,共同進步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM