在上一篇文章中,我們介紹了<code>DecorView</code>與<code>MeasureSpec</code>, 下面的文章就開始讨論View的三大流程。
View的三大流程都是通過<code>ViewRoot</code>來完成的。<code>ViewRoot</code>對應于<code>ViewRootImpl</code>類,它是連接配接<code>WindowManager</code>與<code>DecorView</code>的紐帶。在ActivityThread中,當<code>Activity</code>對象被建立完畢之後,會将<code>DecorView</code>添加到<code>Window</code>中,同時建立<code>ViewRootImpl</code>對象,并将<code>ViewRootImpl</code>對象和<code>DecorView</code>建立關聯。
View的繪制流程是從<code>ViewRootImpl#performTraversals()</code>開始的,它經過measure(測量),layout(布局),draw(繪制)三個過程才能最終将一個view顯示出來。
measure過程測量View的寬高尺寸。Measure完成以後,就可以通過<code>getMeasuredWidth()</code>和<code>getMeasuredHeight()</code>來擷取View的寬高了。
layout過程确定View在父容器中的擺放位置。layout()完成之後,就可以通過<code>getTop()</code>,<code>getLeft()</code>, <code>getRight()</code>, <code>getBottom()</code>來拿到View的左上角,右下角的坐标資料了。
draw過程負責将View繪制在螢幕上。draw()方法完成之後,View才能顯示在螢幕上。
<code>ViewRootImpl#performTraversals()</code>依次調用<code>performMeasure</code>, <code>performLayout</code>,<code>performDraw</code>三個方法。這三個方法依次完成View的 measure,layout,draw過程。
借用網上的一張圖,可以清晰的表達這個過程。

<code>performMeasure</code>調用 <code>View#measure</code>方法,而<code>View#measure</code> 則又調用 <code>onMeasure</code>方法,而對于View中的<code>onMeasure</code>方法,直接儲存了測量得到的尺寸,而類似<code>FrameLayout</code>,<code>RelativeLayout</code>等各種容器ViewGroup,則在自己覆寫重寫的的<code>onMeasure</code>方法中,對所有的子元素進行measure過程,此時measure流程就從父容器傳遞到子View中了,這就完成了一次measure過程,最終這個遞歸的過程完成了整個View樹的周遊。
<code>performLayout</code>,<code>performDraw</code>的傳遞流程也是類似的。唯一不同的是<code>performDraw</code>的傳遞過程是通過<code>draw</code>方法中通過<code>dispatchDraw</code>來實作的,但是道理都是相同的。
traversals的意思就是周遊。
其實到了這個時候,我們大概的流程就已經知道了,那麼剩下的具體就看看View和ViewGroup當中的具體的測量過程了。這兩個還要分情況讨論,因為View是屬于自己一人吃飽,全家不餓,把自己測量好就行了,可是ViewGroup不僅要測量自己,還要周遊去調用所有子元素的measure過程,進而形成一個遞歸過程。
而measure的大流程則如下:
測量的時候父控件的<code>onMeasure</code>方法會周遊他所有的子控件,挨個調用子控件的measure方法,measure方法會調用onMeasure,然後會調用setMeasureDimension方法儲存測量的大小,一次周遊下來,第一個子控件以及這個子控件中的所有孫控件都會完成測量工作;然後開始測量第二個子控件…;最後父控件所有的子控件都完成測量以後會調用setMeasureDimension方法儲存自己的測量大小。值得注意的是,這個過程有可能重複執行多次,因為有的時候,一輪測量下來,父控件發現某一個子控件的尺寸不符合要求,就會重新測量一遍。
借用某個大神部落格上的一張圖
View的測量過程由<code>measure()</code>來完成,<code>measure()</code>方法是<code>final</code>的,子類無法重寫覆寫。它會調用<code>onMeasure</code>方法。
根據<code>measure</code>的源碼,View其實也是比較喜歡偷懶的。意思是執行了<code>measure</code>方法并不一定将測量過程完整走一遍(就是調用<code>onMeasure</code>方法)。具體來說,如果View發現不是強制測量,且本次傳入的<code>MeasureSpec</code>與上次傳入的相同,那麼View就沒必要重新測量一遍。如果真的需要測量,View也先檢視之前是否緩存過相應的計算結果,如果有緩存,直接儲存計算結果,就不用再調用<code>onMeasure</code>了。這樣也是最大限度的提高效率,避免重複工作。
<code>onMeasure</code>方法代碼如下:
首先需要注意,該方法注釋的一句話,
This method is invoked by {@link #measure(int, int)} and should be overridden by subclasses to provide accurate and efficient measurement of their contents.
這就意味着,我們在進行自定義View時,是應該重寫覆寫這個方法的。
先看一下<code>onMeasure</code>方法中調用的<code>getDefaultSize</code>方法。
<code>UNSPECIFIED</code>模式一般用于系統内部的測量過程,在這種情況下,View的大小為<code>getDefaultSize</code>的第一個參數size,即寬度為<code>getSuggestedMinimumWidth()</code>傳回的值,而<code>getSuggestedMinimumWidth()</code>可以用一句僞代碼來表示。
對于測量過程而言,width,height過程都是一樣的流程。是以為了行文簡單,是以我們就簡單的隻說width。
再回過頭來看 <code>getDefaultSize</code>方法的源碼。可以看到,不管我們的View的<code>LayoutParams</code>設定的是 <code>match_parent</code>或者 <code>wrap_content</code>,它的最終尺寸都是 相同的。再結合上一篇文章末尾我們根據<code>getChildMeasureSpec</code>方法而整理出來的那張表。
當View為<code>wrap_content</code>時,它的size就是parentSize-padding,這和<code>match_parent</code>時,size是一樣的。(雖然mode可能不一樣)。
結論就是: 對于View來講,使用<code>wrap_content</code>和使用<code>match_parent</code>效果是一樣的,它倆的預設size是相同的。
是以各個子View,(當然也包括我們<code>extends View</code>,自定義的View),在測量階段,針對<code>wrap_content</code>都要做相應的處理,否則使用 <code>wrap_content</code>就和使用 <code>match_parent</code>效果都是一樣的, 那就是預設填充滿父View剩下的空間。
而我們在自定義View時,針對<code>wrap_content</code>情況一般處理方式是,在<code>onMeasure</code>中,增加對<code>MeasureSpec.AT_MOST</code>的判斷語句,結合具體業務場景或情況,設定一個預設值,或計算出一個值。
<code>wrap_content</code>的意思就是 包裹内容,但是仔細思考一下,内容又是什麼呢,具體到不同的子View場景,肯定有不同的意義,是以從這個角度來思考,作為繼承結構上最頂級,同時也是最抽象的View而言,<code>wrap_cotent</code>和<code>match_parent</code>預設尺寸一樣,也就有道理了。
其實普通View的onMeasure邏輯大同小異,基本都是測量自身内容和背景,然後根據父View傳遞過來的MeasureSpec進行最終的大小判定,例如TextView會根據文字的長度,大小,行高,行寬,顯示方式,背景圖檔,以及父View傳遞過來的模式和大小最終确定自身的大小.
在測量結束時,調用了<code>setMeasuredDimension</code>來存儲測量得到的寬高值,該方法源碼當中,注釋是這樣的。
This method must be called by {@link #onMeasure(int, int)} to store the measured width and measured height. Failing to do so will trigger an exception at measurement time.
如果我們自定義View時,重寫了<code>onMeasure</code>方法,那麼就需要調用<code>setMeasuredDimension</code>方法來儲存結果,否則就會抛出異常。
ViewGroup比View複雜,因為它要周遊去調用所有子元素的measure過程,各個子元素再遞歸去執行這個過程。正所謂上司都要能者多勞之,上司要幹的工作也比較多。
ViewGroup是一個抽象類,它沒有重寫View的<code>onMeasure</code>方法,這是合理的,因為各個不同的子容器(比如<code>LinearLayout</code>, <code>FrameLayout</code>,<code>RelativeLayout</code>等)它們有它們特定的具體布局方式(比如如何擺放子View),是以ViewGroup沒辦法具體統一,<code>onMeasure</code>的實作邏輯,都是在各個具體容器類中實作的。
但是ViewGroup當中,提供了三個測量子控件的方法。
望名知意,根據方法的命名我們就能知道每個方法的作用。<code>measureChild</code>,或<code>measureChildWithMargin</code>,它們的作用就是取出child的<code>layoutParams</code>,然後再通過<code>getChildMeasureSpec</code>來建立child的<code>MeasureSpec</code>,然後再将<code>MeasureSpec</code>傳遞給child進行measure。這樣就完成了一輪遞歸。
是以我們在上一篇部落格中,着重介紹了<code>getChildMeasureSpec</code>方法,指出這個方法是很重要的。
<code>measureChildWithMargin</code>和<code>measureChild</code>的差別就是父控件是否支援margin屬性。
因為各個不同的容器(比如<code>LinearLayout</code>,<code>FrameLayout</code>,<code>RelativeLayout</code>等),都有各自的measure方式,是以我們就挑選<code>LinearLayout</code>來看一下它的measure流程。
我們就選擇豎直方向的measure過程來分析。
源碼比較長,我們隻看主流程。
系統會周遊子元素,并對每個子元素執行<code>measureChildBeforeLayout</code>方法,該方法内部會調用子元素的measure方法,這樣各個子元素就依次進入measure過程。系統通過<code>mTotalLength</code>這個變量動态存儲LinearLayout在豎直方向上的高度,并且伴随着每測量一個子元素,<code>mTotalLength</code>則會逐漸增加。增加的部分包括了子元素的高度以及子元素在豎直方向上的margin等。
而當所有子元素都測量完畢之後,LinearLayout則會測量自己的大小。
針對豎直方向的LinearLayout而言,它在水準方向上的測量過程遵循View的測量過程,而豎直方向則和View有所不同,具體來說,如果它的高度參數是具體數值或<code>match_parent</code>,則測量過程和View一緻,即高度為SpecSize;而如果高度參數采用的是<code>wrap_content</code>,那麼它的高度就是所有子元素所占用的高度總和,但是仍然不能超過它父容器的剩餘空間。并且最終高度還要考慮豎直方向的padding。
具體可以參考以下源碼:
<code>resolveSizeAndState</code>方法和<code>getDefaultSize</code>方法類似,其内部實作邏輯是一樣的,不過差別在于,<code>resolveSizeAndState</code>方法除了傳回尺寸資訊,還會傳回測量的status标志位資訊。
View的測量過程是三大流程中比較複雜的,隻有測量完畢之後,我們才有可能得到正确的寬高值。
而View的measure過程和Activity的生命周期方法是不同步的。是以我們不能簡單的通過<code>onStart</code>,<code>onResume</code>方法來擷取View的尺寸值。
參考内容:
任玉剛 《Anroid開發藝術探索》 Android自定義View(三、深入解析控件測量onMeasure) Android View 測量流程(Measure)完全解析 源碼解析Android中View的measure量算過程
作者:
www.yaoxiaowen.com
github:
https://github.com/yaowen369
歡迎對于本人的部落格内容批評指點,如果問題,可評論或郵件([email protected])聯系