前言
View作為整個app的顏值擔當,在Android體系中佔有重要的地位。深入理解Android View的繪製流程,對正確使用View來構建賞心悅目的外觀,以及用自定義View來設計理想中的酷炫效果等方面,有著極其重要的幫助作用,所以將View的繪製流程作為自定義View系列文章的第一篇。當然,View的繪製流程原理,在現實的工作中是成為高階工程師路上必須克服的障礙;在面試中,也是面試高階一點的職位,面試官幾乎一定會問的問題。總體來說,Android View的繪製流程原理,是一個Android程式設計師的基本內功之一。
本文最大的特點,就是最大限度地向原始碼要答案。從原始碼中追流程的來龍去脈,在註釋中查功能的點點滴滴,所有的結論都儘量在原始碼和註釋中找根據,關鍵的流程儘量說細緻,非重點的地方儘量簡略。所以讀者會看到,本文中貼了大量的原始碼,且基本都是和分析繪製流程相關的程式碼,同時附上了大量的原始碼註釋,以及對這些註釋的翻譯及整理。在每一個過程的最後,還根據原始碼的流程走向,繪製了一幅簡易的關鍵流程圖,以此來幫助讀者理解及加深印象。事實上,本文就是記錄的筆者從幾乎一無所知到追蹤原始碼來搞弄明白整個繪製流程的完整經歷的,中途遇到的一些困惑及疑難點都會花不少篇幅來解釋。當然僅僅學習本文還是不夠的,因為這方面的內容很多,僅僅一篇文章是不可能面面俱到的。正如其他的知名部落格文一樣,都有自己的側重點,有的側重根據示例來分析流程,有的會附著demo來驗證關鍵結論,本文中也會適當給出一些比較好的博文的連結,讀者可以去這些地方彌補本文的不足。最後,再囉嗦一句,本文的側重點是從原始碼中“順藤摸瓜”,帶領讀者“登堂入室”,希望能給讀者帶來一點幫助。另外,這裡說明一點,本文中的原始碼是基於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
//===================Window.java=================== //The current window attributes. private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams(); ...... public final WindowManager.LayoutParams getAttributes() { return mWindowAttributes; } ...... //==========WindowManager.java的內部類LayoutParams extends ViewGroup.LayoutParams============= public LayoutParams() { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); ...... } //==============ViewGroup.java內部類LayoutParams==================== public LayoutParams(int width, int height) { this.width = width; this.height = height; }
該參數列示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 <size, mode> 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是怎麼來的。
1 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 ...... }
第17行中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(二)】
結語
本文的篇幅比較長,能看完並且理解也是一件辛苦的事情,筆者學習及寫這篇部落格,也是花了將近半個月的業餘時間來完成的。但是要想超過別人,就是要做一件有一件辛苦但能夠成長的事情,時間長了,人與人之間的距離就拉開了。所以,真心希望本文能幫助您理解View的繪製流程,那筆者半個月來的辛苦也就沒有白費了。當然,本文肯定存在很多不足之處,希望筆者能不吝賜教,共同進步。