初識ViewRoot和DecorView
ViewRoot的實現是 ViewRootImpl 類,是連接WindowManager和DecorView的紐帶, View的三大流程(mearsure、layout、draw)均是通過ViewRoot來完成。當Activity對象被創建完畢後,會將DecorView添加到Window中,同時創建 ViewRootImpl 對象,並將 ViewRootImpl 對象和DecorView建立連接。
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams, panelParentView);
View的三大流程:
-
measure用來測量View的寬高
-
layout來確定View在父容器中的位置
-
draw負責將View繪製在屏幕上
View的繪製流程從ViewRoot的 performTraversals 方法開始:
-
performTraversals會依次調用 performMeasure 、 performLayout 和 performDraw 三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程。
-
其中 performMeasure 中會調用 measure 方法,在 measure 方法中又會調用 onMeasure 方法,在 onMeasure 方法中則會對所有子元素進行measure過程,這樣就完成了一次measure過程;子元素會重複父容器的measure過程,如此反覆完成了整個View數的遍歷。另外兩個過程同理。
Measure完成後, getMeasuredWidth / getMeasureHeight 方法來獲取View測量後的寬/高。
Layout過程決定了View的四個頂點的座標和實際View的寬高,完成後可通 過 getTop 、 getBotton 、 getLeft 和 getRight 拿到View的四個頂點座標。
DecorView作爲頂級View,其實是一個 FrameLayout ,它包含一個豎直方向 的 LinearLayout ,這個 LinearLayout 分爲標題欄和內容欄兩個部分。在Activity通過 setContextView所設置的佈局文件其實就是被加載到內容欄之中的。這個內容欄的id 是 R.android.id.content ,通過 ViewGroup content = findViewById(R.android.id.content);
可以得到這個contentView。View層的事件都是先經 過DecorView,然後才傳遞到子View。
理解MeasureSpec
測量過程,系統將View的 LayoutParams 根據父容器所施加的規則轉換成對應的 MeasureSpec,然後根據這個MeasureSpec來測量出View的寬高。
MeasureSpec代表一個32位int值,高2位代表SpecMode(測量模式),低30位代表 SpecSize(在某個測量模式下的規格大小)。
SpecMode有三種:
-
UNSPECIFIED :父容器不對View進行任何限制,要多大給多大,一般用於系統內部
-
EXACTLY:父容器檢測到View所需要的精確大小,這時候View的最終大小就是 SpecSize所指定的值,對應LayoutParams中的 match_parent 和具體數值這兩種模式
-
AT_MOST :對應View的默認大小,不同View實現不同,View的大小不能大於父容 器的SpecSize,對應 LayoutParams 中的 wrap_content
View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定。 View的measure過程由ViewGroup傳遞而來,參考ViewGroup的 measureChildWithMargins 方 法,通過調用子元素的 getChildMeasureSpec 方法來得到子元素的MeasureSpec,再調用子元素的 measure 方法。
- 當View採用固定寬/高時(即設置固定的dp/px),不管父容器的MeasureSpec是什麼, View的MeasureSpec都是EXACTLY模式,並且大小遵循我們設置的值。
- 當View的寬/高是 match_parent 時,View的MeasureSpec都是EXACTLY模式並且其大小等於父容器的剩餘空間。
- 當View的寬/高是 wrap_content 時,View的MeasureSpec都是AT_MOST模式並且其大小不能超過父容器的剩餘空間。
- 父容器的UNSPECIFIED模式,一般用於系統內部多次Measure時,表示一種測量的狀態,一般來說我們不需要關注此模式。
View的工作流程
measure過程
分兩種情況: 1. View通過 measure 方法就完成了測量過程 2. ViewGroup除了完成自己的測量過程還會便利調用所有子View的 measure 方法,而且各 個子View還會遞歸執行這個過程。
View的measure過程
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
- setMeasuredDimension方法會設置View的寬/高的測量值
- getDefaultSize方法返回的大小就是measureSpec中的specSize,也就是View測量後的大小,絕大部分情況和View的最終大小(layout階段確定)相同。
- getSuggestedMinimumWidth方法,作爲getDefaultSize的第一個參數(建議寬度)
直接繼承View的自定義控件需要重寫 onMeasure 方法並設置 wrap_content (即specMode 是 AT_MOST 模式)時的自身大小,否則在佈局中使用 wrap_content 相當於使 用 match_parent 。對於非 wrap_content 的情形,我們沿用系統的測量值即可。
ViewGroup的measure過程
ViewGroup是一個抽象類,沒有重寫View的 onMeasure 方法,但是它提供了一 個 measureChildren 方法。這是因爲不同的ViewGroup子類有不同的佈局特性,導致他們的測量細節各不相同,比如 LinearLayout 和 RelativeLayout ,因此ViewGroup沒辦法同一實現 onMeasure 方法。
measureChildren方法的流程:
- 取出子View的 LayoutParams
- 通過 getChildMeasureSpec 方法來創建子元素的 MeasureSpec
- 將 MeasureSpec 直接傳遞給View的measure方法來進行測量
View的measure過程是三大流程中最複雜的一個,measure完成以後,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量後寬/高。在某些情況下,系統可能需要多次measure才能確定最終的測量寬/高,所以在onMeasure中拿到的寬/高很可能不是準確的。同時View的measure過程和Activity的生命週期並不是同步執行,因此無法保證在 Activity的 onCreate、onStart、onResume 時某個View就已經測量完畢。所以有以下四種方式來 獲取View的寬高:
-
Activity/View#onWindowFocusChanged。 onWindowFocusChanged這個方法的含義 是:VieW已經初始化完畢了,寬高已經準備好了,需要注意:它會被調用多次,當 Activity的窗口得到焦點和失去焦點均會被調用。
-
view.post(runnable)。 通過post將一個runnable投遞到消息隊列的尾部,當Looper調用此 runnable的時候,View也初始化好了。
-
ViewTreeObserver。 使用 ViewTreeObserver 的衆多回調可以完成這個功能,比如 OnGlobalLayoutListener 這個接口,當View樹的狀態發送改變或View樹內部的View的 可見性發生改變時, onGlobalLayout 方法會被回調。需要注意的是,伴隨着View樹狀態 的改變, onGlobalLayout 會被回調多次。
-
view.measure(int widthMeasureSpec,int heightMeasureSpec)。
(1) match_parent:
無法measure出具體的寬高,因爲不知道父容器的剩餘空間,無法測量出View的大小
(2) 具體的數值(dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); view.measure(widthMeasureSpec,heightMeasureSpec);
(3) wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); view.measure(widthMeasureSpec,heightMeasureSpec);
layout過程
layout的作用是ViewGroup用來確定子View的位置,當ViewGroup的位置被確定後,它會在 onLayout中遍歷所有的子View並調用其layout方法,在 layout 方法中, onLayout 方法又會被調用。 layout 方法確定View本身的位置,源碼流程如下:
- setFrame 確定View的四個頂點位置,即確定了View在父容器中的位置。
- 調用 onLayout 方法,確定所有子View的位置。
draw過程
View的繪製過程遵循如下幾步:
- 繪製背景 drawBackground(canvas)
- 繪製自己 onDraw
- 繪製children dispatchDraw 遍歷所有子View的 draw 方法
- 繪製裝飾 onDrawScrollBars
View的繪製過程是通過dispatchDraw來實現的,它會遍歷所有子元素的draw方法。
如果一個View不需要繪製任何內容,那麼設置setWillNotDraw爲true後,系統會進行相應的優化;ViewGroup默認爲true,如果我們的自定義ViewGroup需要通過onDraw來繪製內容的時候,需要顯示的關閉它。
自定義View
自定義View的分類
-
繼承View 通過 onDraw 方法來實現一些效果,需要自己支持 wrap_content ,並且 padding也要去進行處理。
-
繼承ViewGroup 實現自定義的佈局方式,需要合適地處理ViewGroup的測量、佈局這兩 個過程,並同時處理子View的測量和佈局過程。
-
繼承特定的View子類(如TextView、Button) 擴展某種已有的控件的功能,且不需要自 己去管理 wrap_content 和padding。
-
繼承特定的ViewGroup子類(如LinearLayout)
直接繼承View或ViewGroup的控件, 需要在onMeasure中對wrap_content做特殊處理。
直接繼承View的控件,如果不在draw方法中處理padding,那麼padding屬性就無法起作用。直接繼承ViewGroup的控件也需要在onMeasure和onLayout中考慮padding和子元素margin的影響,不然padding和子元素的margin無效。
View內部提供了post系列的方法,完全可以替代Handler的作用。
View中有線程和動畫,需要在View的onDetachedFromWindow中停止。
View帶有滑動嵌套情形時,需要處理好滑動衝突