View的工作原理
初識ViewRoot和DecorView
1、ViewRoot對應于ViewRootImpl類,它是連接WindowManager和DecorView的紐帶,View的三大流程(measure、layout、draw)均是通過ViewRoot來完成的。在ActivityThread中,當Activity對象被創(chuàng)建完畢后,會將DecorView添加到Window中,同時會創(chuàng)建ViewRootImpl對象,并將ViewRootImpl對象和DecorView建立連接。
2、View的繪制流程是從ViewRoot的performTraversals方法開始的,它經過measure、layout、draw三個過程才能最終將一個View繪制出來,其中measure用來測量View的寬和高,layout用來確定View在父容器的放置位置,而draw則負責將View繪制在屏幕上。
3、performTraversals會依次調用performMeasure、performLayout、performDraw三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程,其中performMeasure會調用measure方法,在measure方法中又會調用onMeasure方法,在onMeasure方法中對所有的子元素進行measure過程,這個時候measure流程就會從父容器傳遞到子元素中了,這樣就完成了一次measure過程。接著子元素就會重復父容器的measure過程,如此反復就完成了整個View樹的遍歷。
4、measure過程中決定了View的寬/高,Measure完成以后,可以通過getMeasureWidth和getMeasureHeight方法來獲取到View測量后的寬/高,在幾乎所有的情況下它都等同于View最終的寬高。layout決定了View的四個頂點的坐標和View的實際的寬高,通過getWidth和getHeight方法可以獲得最終的寬高。draw過程決定了View的顯示。
5、DecorView其實是一個FrameLayout,其中包含了一個豎直方向的LinearLayout,上面是標題欄,下面是內容欄(id為android.R.id.content)。View層的事件都先經過DecorView,然后才傳給我們的View。
理解MeasureSpec
1、MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的內存分配,為了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一個int值,一組SpecMode和SpecSize可以打包為一個MeasureSpec,而一個MeasureSpec可以通過解包的形式來得出其原始的SpecMode和SpecSize。
SpecMode有三類,每一類都表示特殊的含義:
- UNSPECIFIED 父容器不對View有任何的限制,要多大給多大,這種情況下一般用于系統內部,表示一種測量的狀態(tài)。
- EXACTLY 父容器已經檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值,它對應于LayoutParams中的match_parent和具體的數值這兩種模式
- AT_MOST 父容器指定了一個可用大小即SpecSize,View的大小不能大于這個值,具體是什么值要看不同View的具體實現。它對應于LayoutParams中的wrap_content
2、MeasureSpec和LayoutParams的對應關系
在View測量的時候系統會將LayoutParams在父容器的約束下轉換成對應的MeasureSpec,然后再根據這個MeasureSpec來確定View測量后的寬高。
MeasureSpec不是唯一由LayoutParams決定的,LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進一步確定View的寬高。對于DecorView,它的MeasureSpec由窗口的尺寸和其自身的LayoutParams來決定;對于普通View,它的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定
3、當view采用固定寬高時,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精確模式,并且大小是LayoutParams中的大小。
當view的寬高是match_parent時,如果父容器的模式是精確模式,那么view也是精確模式,并且大小是父容器的剩余空間;如果父容器是最大模式,那么view也是最大模式,并且大小是不會超過父容器的剩余空間。
當view的寬高是wrap_content時,不管父容器的模式是精確模式還是最大模式,view的模式總是最大模式,并且大小不超過父容器的剩余空間。
View的工作流程
1、View的measure過程和Activity的生命周期方法不是同步執(zhí)行的,因此無法保證Activity執(zhí)行了onCreate、onStart、onResume時某個View已經測量完畢了。如果View還沒有測量完畢,那么獲得的寬和高都是0。下面是四種解決該問題的方法:
- Activity/View#onWindowsChanged方法
onWindowFocusChanged方法表示View已經初始化完畢了,寬高已經準備好了,這個時候去獲取是沒問題的。這個方法會被調用多次,當Activity繼續(xù)執(zhí)行或者暫停執(zhí)行的時候,這個方法都會被調用。
- View.post(runnable)
通過post將一個Runnable投遞到消息隊列的尾部,然后等待Looper調用此runnable的時候,View也已經初始化好了。
- ViewTreeObsever
使用ViewTreeObserver的眾多回調方法可以完成這個功能,比如使用onGlobalLayoutListener接口,當View樹的狀態(tài)發(fā)生改變或者View樹內部的View的可見性發(fā)生改變時,onGlobalLayout方法將被回調。伴隨著View樹的變化,這個方法也會被多次調用。
- view.measure(int widthMeasureSpec, int heightMeasureSpec)
通過手動對View進行measure來得到View的寬高,這個要根據View的LayoutParams來處理:
match_parent:無法measure出具體的寬高
wrap_content:如下measure,設置最大值
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
精確值:例如100px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
2、在View的默認實現中,View的測量寬高和最終寬高時相等的,只不過測量寬高形成于measure過程,而最終寬高形成于layout過程。
3、draw過程大概有下面幾步:
- 繪制背景:background.draw(canvas);
- 繪制自己:onDraw();
- 繪制children:dispatchDraw;
- 繪制裝飾:onDrawScrollBars
自定義View
自定義View分為以下4類:
- 繼承view重寫onDraw方法
- 繼承ViewGroup派生特殊的Layout
- 繼承特定的View(比如TextView)
- 繼承特殊的ViewGroup(比如LinearLayout)
自定義View須知:
- 讓View支持wrap_content
- 如果有必要,讓你的View支持padding
- 盡量不要在View中使用Handler,沒必要
- View中如果有線程或者動畫,需要及時停止,參考View#onDetachedFromWindow
- View帶有滑動嵌套情形時,需要處理好滑動沖突