performTraversals 會依次調(diào)用 performMeasure、performLayout 和 performDraw 三個方法,這三個方法分別完成頂級 View 的 measure、layout 和 draw 這三大流程,其中在 performMeasure 中會調(diào)用 measure 方法,在 measure 方法中又會調(diào)用 onMeasure 方法,在 onMeasure 方法中則會對所有的子元素進行 measure 過程,這個時候 measure 流程就從父容器傳遞到子元素中了,這樣就完成了一次 measure 過程。接著子元素會重復(fù)父容器的 measure 過程,如此反復(fù)就完成了整個 View 樹的遍歷。同理,performLayout 和 performDraw 的傳遞流程和 performMeasure 是類似的,唯一不同的是,performDraw 的傳遞過程是在 draw 方法中通過 dispatchDraw 來實現(xiàn)的,不過這并沒有本質(zhì)區(qū)別。
接下來結(jié)合源碼來分析這三個過程。
Measure 測量過程
這里分兩種情況,View 的測量過程和 ViewGroup 的測量過程。
View 的測量過程
View 的 測量過程由其 measure 方法來完成,源碼如下:
可以看到 measure 方法是一個 final 類型的方法,這意味著子類不能重寫此方法。
在 13 行 measure 中會調(diào)用 onMeasure 方法,這個方法是測量的主要方法,繼續(xù)看 onMeasure 的實現(xiàn)
setMeasuredDimension 方法的作用是設(shè)置 View 寬和高的測量值,我們主要看 getDefaultSize 方法
是如何生成測量的尺寸。
可以看到要得到測量的尺寸需要用到 MeasureSpec,MeasureSpec 是什么鬼呢,敲黑板了,重點來了。
MeasureSpec 決定了 View 的測量過程。確切來說,MeasureSpec 在很大程度上決定了一個 View 的尺寸規(guī)格。
來看 MeasureSpec 類的實現(xiàn)
可以看出 MeasureSpec 中有兩個主要的值,SpecMode 和 SpecSize, SpecMode 是指測量模式,而 SpecSize 是指在某種測量模式下的規(guī)格大小。
MeasureSpec代表一個32位的int值,高2位代表SpecMode,低30位代表SpecSize
SpecMode 有三種模式:
UNSPECIFIED
不限制:父容器不對 View 有任何限制,要多大給多大,這種情況比較少見,一般不會用到。
EXACTLY
限制固定值:父容器已經(jīng)檢測出 View 所需要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應(yīng)于 LayoutParams 中的 match_parent 和具體的數(shù)值這兩種模式。
AT_MOST
限制上限:父容器指定了一個可用大小即 SpecSize,View 的大小不能大于這個值,具體是什么值要看不同 View 的具體實現(xiàn)。它對應(yīng)于 LayoutParams 中的 wrap_content。
MeasureSpec 中三個主要的方法來處理 SpecMode 和 SpecSize
makeMeasureSpec 打包 SpecMode 和 SpecSize
getMode 解析出 SpecMode
getSize 解析出 SpecSize
不知道童鞋們之前有沒有注意到 onMeasure 有兩個參數(shù) widthMeasureSpec 和 heightMeasureSpec,那這兩個值從哪來的呢,這兩個值都是由父視圖經(jīng)過計算后傳遞給子視圖的,說明父視圖會在一定程度上決定子視圖的大小,但是最外層的根視圖 也就是 DecorView ,它的 widthMeasureSpec 和 heightMeasureSpec 又是從哪里得到的呢?這就需要去分析 ViewRoot 中的源碼了,在 performTraversals 方法中調(diào)了 measureHierarchy 方法來創(chuàng)建 MeasureSpec 源碼如下:
里面調(diào)用了 getRootMeasureSpec 方法生成 MeasureSpec,繼續(xù)查看 getRootMeasureSpec 源碼
通過上述代碼,DecorView 的 MeasureSpec 的產(chǎn)生過程就很明確了,具體來說其遵守如下規(guī)則,根據(jù)它的 LayoutParams 中的寬和高的參數(shù)來劃分。
LayoutParams.MATCH_PARENT:限制固定值,大小就是窗口的大小 windowSize
LayoutParams.WRAP_CONTENT:限制上限,大小不定,但是不能超過窗口的大小 windowSize
固定大?。合拗乒潭ㄖ担笮?LayoutParams 中指定的大小 rootDimension
對于 DecorView 而言, rootDimension 的值為 lp.width 和 lp.height 也就是屏幕的寬和高,所以說 根視圖 DecorView 的大小默認總是會充滿全屏的。那么我們使用的 View 也就是 ViewGroup 中 View 的 MeasureSpec 產(chǎn)生過程又是怎么樣的呢,在 ViewGroup 的測量過程中會具體介紹。
先回頭看 getDefaultSize 方法:
現(xiàn)在理解起來是不是很簡單呢,如果 specMode 是 AT_MOST 或 EXACTLY 就返回 specSize,這也是系統(tǒng)默認的行為。之后會在 onMeasure 方法中調(diào)用 setMeasuredDimension 方法來設(shè)定測量出的大小,這樣 View 的 measure 過程就結(jié)束了,接下來看 ViewGroup 的 measure 過程。
ViewGroup 的測量過程
ViewGroup中定義了一個 measureChildren 方法來去測量子視圖的大小,如下所示
從上述代碼來看,除了完成自己的 measure 過程以外,還會遍歷去所有在頁面顯示的子元素,
然后逐個調(diào)用 measureChild 方法來測量相應(yīng)子視圖的大小
measureChild 的實現(xiàn)如下
measureChild 的思想就是取出子元素的 LayoutParams,然后再通過 getChildMeasureSpec 來創(chuàng)建子元素的 MeasureSpec,接著將 MeasureSpec 直接傳遞給 View 的 measure 方法來進行測量。
那么 ViewGroup 是如何創(chuàng)建來創(chuàng)建子元素的 MeasureSpec 呢,我們繼續(xù)看 getChildMeasureSpec 方法源碼:
上面的代碼理解起來很簡單,為了更清晰地理解 getChildMeasureSpec 的邏輯,這里提供一個表,表中對 getChildMeasureSpec 的工作原理進行了梳理,表中的 parentSize 是指父容器中目前可使用的大小,childSize 是子 View 的 LayoutParams 獲取的值,從 measureChild 方法中可看出
表如下:
通過上表可以看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地確定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以進一步確定出子元素測量后的大小了。
至此,View 和 ViewGroup 的測量過程就告一段落了。來個小結(jié)。
MeasureSpec 的模式和生成規(guī)則
MeasureSpec 中 specMode 有三種模式:
UNSPECIFIED
不限制:父容器不對 View 有任何限制,要多大給多大,這種情況比較少見,一般不會用到。
EXACTLY
限制固定值:父容器已經(jīng)檢測出 View 所需要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應(yīng)于 LayoutParams 中的 match_parent 和具體的數(shù)值這兩種模式。
AT_MOST
限制上限:父容器指定了一個可用大小即 SpecSize,View 的大小不能大于這個值,具體是什么值要看不同 View 的具體實現(xiàn)。它對應(yīng)于 LayoutParams 中的 wrap_content。
生成規(guī)則:
對于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定。
對于不同 ViewGroup 中的不同 View 生成規(guī)則參照上表。
MeasureSpec 測量過程:
measure 過程主要就是從頂層父 View 向子 View 遞歸調(diào)用 view.measure 方法,measure 中調(diào) onMeasure 方法的過程。
說人話呢就是,視圖大小的控制是由父視圖、布局文件、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發(fā)人員可以在 XML 文件中指定視圖的大小,然后視圖本身會對最終的大小進行拍板。
那么測量過后,怎么獲取 View 的測量結(jié)果呢
一般情況下 View 測量大小和最終大小是一樣的,我們可以使用 getMeasuredWidth 方法和 getMeasuredHeight 方法來獲取視圖測量出的寬高,但是必須在 setMeasuredDimension 之后調(diào)用,否則調(diào)用這兩個方法得到的值都會是0。為什么要說是一般情況下是一樣的呢,在下文介紹 Layout 中會具體介紹。
Layout 布局過程
測量結(jié)束后,視圖的大小就已經(jīng)測量好了,接下來就是 Layout 布局的過程。上文說過 ViewRoot 的 performTraversals 方法會在 measure 結(jié)束后,執(zhí)行 performLayout 方法,performLayout 方法則會調(diào)用 layout 方法開始布局,代碼如下
View 類中 layout 方法實現(xiàn)如下:
layout 方法接收四個參數(shù),分別代表著左、上、右、下的坐標,當然這個坐標是相對于當前視圖的父視圖而言的,然后會調(diào)用 setFrame 方法來設(shè)定 View 的四個頂點的位置,即初始化 mLeft、mRight、mTop、mBottom 這四個值,View 的四個頂點一旦確定,那么 View 在父容器中的位置也就確定了,接著會調(diào)用 onLayout 方法,這個方法的用途是父容器確定子元素的位置,和 onMeasure 方法類似
onLayout 源碼如下:
納尼,怎么是個空方法,沒錯,就是一個空方法,因為 onLayout 過程是為了確定視圖在布局中所在的位置,而這個操作應(yīng)該是由布局來完成的,即父視圖決定子視圖的顯示位置,我們繼續(xù)看 ViewGroup 中的 onLayout 方法
可以看到,ViewGroup 中的 onLayout 方法竟然是一個抽象方法,這就意味著所有 ViewGroup 的子類都必須重寫這個方法。像 LinearLayout、RelativeLayout 等布局,都是重寫了這個方法,然后在內(nèi)部按照各自的規(guī)則對子視圖進行布局的。所以呢我們?nèi)绻远x ViewGroup 那么就要重寫 onLayout 方法。
xml 中使用
顯示效果如下:
不知道童鞋們發(fā)現(xiàn)了沒,我給自定義的 ViewGroup 設(shè)置了背景色,看效果貌似占滿全屏了,可是我在 xml 中設(shè)置的 wrap_content 啊,這是什么情況,我們回頭看看 ViewGroup 中 View 的 MeasureSpec 的創(chuàng)建規(guī)則
從表中可看出因為 ViewGroup 的父布局設(shè)置的 match_parent 也就是限制固定值模式,而 ViewGroup 設(shè)置的 wrap_content,那么最后 ViewGroup 使用的是 父布局的大小,也就是窗口大小 parentSize,那么如果我們給 ViewGroup 設(shè)置固定值就會使用 我們設(shè)置的值,來改下代碼。
效果如下:
表中的其他情況,建議童鞋們自己寫下代碼,會理解的更好。
之前說過,一般情況下 View 測量大小和最終大小是一樣的,為什么呢,因為最終大小在 onLayout 中確定,我們來改下代碼:
顯示效果
沒錯,onLayout 就是這么任性,所以要獲取 View 的真實大小最好在 onLayout 之后獲取。那么如何來獲取 view 的真實大小呢,可以通過下面的代碼來獲取
打印如下:
可以看到實際高度和測試的高度是不一樣的,因為我們在 onLayout 中做了修改。
因為 View 的繪制過程和 Activity 的生命周期是不同步的,所以我們可能在 onCreate 中獲取不到值。這里提供幾種方法來獲取
1.Activity 的 onWindowFocusChanged 方法
2.view.post(runnable) 也就是我上面使用的方法
3.ViewTreeObserver 這里童鞋們搜索下就可以找到使用方法,篇幅較長就不舉例子了
Draw 繪制過程
確定了 View 的大小和位置后,那就要開始繪制了,Draw 過程就比較簡單,它的作用是將 View 繪制到屏幕上面。View 的繪制過程遵循如下幾步:
繪制背景 background.draw (canvas)
繪制自己(onDraw)
繪制 children(dispatchDraw)
繪制裝飾(onDrawScrollBars)
View 的繪制過程的傳遞是通過 dispatchDraw 實現(xiàn)的,dispatchdraw 會遍歷調(diào)用所有子元素的 draw 方法,如此 draw 事件就一層一層的傳遞下去。和 Layout 一樣 View 是不會幫我們繪制內(nèi)容部分的,因此需要每個視圖根據(jù)想要展示的內(nèi)容來自行繪制,重寫 onDraw 方法。具體可參考 TextView 或者 ImageView 的源碼。