學習內(nèi)容
View的底層工作原理,比如View的測量流程中符、布局流程以及繪制流程掺栅;以及常見的View回調(diào)方法烙肺;熟悉掌握前面的知識后,自定義View的時候也會更加的得心應手氧卧。
4.1 初識ViewRoot和DecorView
- ViewRoot對應于ViewRootImpl類桃笙,是連接WindowManager和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的假抄。在ActivityThread中怎栽,當Activity對象被創(chuàng)建完畢后,會將DecorView添加到Window中宿饱,同時會創(chuàng)建ViewRootImpl對象熏瞄,并將ViewRootImpl對象和DecorView建立關(guān)聯(lián)。
- View的繪制流程從ViewRoot的performTraversals開始谬以,經(jīng)過measure强饮、layout和draw三個過程才可以把一個View繪制出來,其中measure用來測量View的寬高为黎,layout用來確定View在父容器中的放置位置邮丰,而draw則負責將View繪制到屏幕上行您。
-
performTraversals會依次調(diào)用performMeasure、performLayout和performDraw三個方法剪廉,這三個方法分別完成頂級View的measure娃循、layout和draw這三大流程。其中performMeasure中會調(diào)用measure方法斗蒋,在measure方法中又會調(diào)用onMeasure方法捌斧,在onMeasure方法中則會對所有子元素進行measure過程,這樣就完成了一次measure過程泉沾;子元素會重復父容器的measure過程捞蚂,如此反復完成了整個View數(shù)的遍歷。另外兩個過程類似跷究,大致調(diào)用流程如下圖:
- measure過程決定了View的寬/高姓迅,完成后可通過getMeasuredWidth/getMeasureHeight方法來獲取View測量后的寬/高。Layout過程決定了View的四個頂點的坐標和實際View的寬高俊马,完成后可通過getTop丁存、getBotton、getLeft和getRight拿到View的四個定點坐標潭袱。Draw過程決定了View的顯示柱嫌,完成后View的內(nèi)容才能呈現(xiàn)到屏幕上。
- 如下圖屯换,DecorView作為頂級View编丘,一般情況下它內(nèi)部包含了一個豎直方向的LinearLayout,里面分為兩個部分(具體情況和Android版本和主題有關(guān))彤悔,上面是標題欄嘉抓,下面是內(nèi)容欄。在Activity通過setContextView所設(shè)置的布局文件其實就是被加載到內(nèi)容欄之中的晕窑。
//獲取內(nèi)容欄
ViewGroup content = findViewById(R.android.id.content);
//獲取我們設(shè)置的Viewcontext.getChildAt(0);
DecorView其實是一個FrameLayout抑片,View層的事件都先經(jīng)過DecorView,然后才傳給我們的View杨赤。
4.2 理解MeasureSpec
- MeasureSpec很大程度上決定一個View的尺寸規(guī)格敞斋,測量過程中,系統(tǒng)會將View的layoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對應的MeasureSpec疾牲,再根據(jù)這個measureSpec來測量出View的寬/高植捎。
- MeasureSpec代表一個32位的int值,高2位為SpecMode阳柔,低30位為SpecSize焰枢,SpecMode是指測量模式,SpecSize是指在某種測量模式下的規(guī)格大小。
MpecMode有三類济锄;
1.UNSPECIFIED 父容器不對View進行任何限制暑椰,要多大給多大,一般用于系統(tǒng)內(nèi)部
2.EXACTLY 父容器檢測到View所需要的精確大小荐绝,這時候View的最終大小就是SpecSize所指定的值一汽,對應LayoutParams中的match_parent和具體數(shù)值這兩種模式。
3.AT_MOST 父容器指定了一個可用大小即SpecSize很泊,View的大小不能大于這個值角虫,不同View實現(xiàn)不同沾谓,對應LayoutParams中的wrap_content委造。 - 當View采用固定寬/高的時候,不管父容器的MeasureSpec的是什么均驶,View的MeasureSpec都是精確模式兵其大小遵循Layoutparams的大小昏兆。 當View的寬/高是match_parent時,如果他的父容器的模式是精確模式妇穴,那View也是精確模式并且大小是父容器的剩余空間爬虱;如果父容器是最大模式,那么View也是最大模式并且起大小不會超過父容器的剩余空間腾它。 當View的寬/高是wrap_content時跑筝,不管父容器的模式是精確還是最大化,View的模式總是最大化并且不能超過父容器的剩余空間瞒滴。
4.3 View的工作流程
1. View的measure過程
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
- setMeasuredDimension方法會設(shè)置View的寬/高的測量值
- getDefaultSize方法返回的大小就是measureSpec中的specSize曲梗,也就是View測量后的大小,絕大部分情況和View的最終大小(layout階段確定)相同妓忍。
- getSuggestedMinimumWidth方法虏两,作為getDefaultSize的第一個參數(shù)(建議寬度)
- 直接繼承View的自定義控件,需要重寫onMeasure方法并且設(shè)置
wrap_content時的自身大小世剖,否則在布局中使用了wrap_content相當于使用了match_parent定罢。解決方法:在onMeasure時,給View指定一個內(nèi)部寬/高旁瘫,并在wrap_content時設(shè)置即可祖凫,其他情況沿用系統(tǒng)的測量值即可。
2. ViewGroup的measure過程
- 對于ViewGroup來說酬凳,除了完成自己的measure過程之外惠况,還會遍歷去調(diào)用所有子元素的measure方法,個個子元素再遞歸去執(zhí)行這個過程粱年,和View不同的是售滤,ViewGroup是一個抽象類,沒有重寫View的onMeasure方法,提供了measureChildren方法完箩。
- measureChildren方法赐俗,遍歷獲取子元素,子元素調(diào)用measureChild方法
- measureChild方法弊知,取出子元素的LayoutParams阻逮,再通過getChildMeasureSpec方法來創(chuàng)建子元素的MeasureSpec,接著將MeasureSpec傳遞給View的measure方法進行測量秩彤。
- ViewGroup沒有定義其測量的具體過程叔扼,因為不同的ViewGroup子類有不同的布局特征,所以其測量過程的onMeasure方法需要各個子類去具體實現(xiàn)漫雷。
- measure完成之后瓜富,通過getMeasureWidth/Height方法就可以獲取View的測量寬/高,需要注意的是降盹,在某些極端情況下与柑,系統(tǒng)可能要多次measure才能確定最終的測量寬/高,比較好的習慣是在onLayout方法中去獲取測量寬/高或者最終寬/高蓄坏。
如何在Activity中獲取View的寬/高信息
因為View的measure過程和Activity的生命周期不是同步進行价捧,如果View還沒有測量完畢,那么獲取到的寬/高就是0涡戳;所以在Activity的onCreate结蟋、onStart、onResume中均無法正確的獲取到View的寬/高信息渔彰。下面給出4種解決方法嵌屎。
- Activity/View#onWindowFocusChanged。
onWindowFocusChanged這個方法的含義是:VieW已經(jīng)初始化完畢了胳岂,寬高已經(jīng)準備好了编整,需要注意:它會被調(diào)用多次,當Activity的窗口得到焦點和失去焦點均會被調(diào)用乳丰。 - view.post(runnable)掌测。
通過post將一個runnable投遞到消息隊列的尾部,當Looper調(diào)用此runnable的時候产园,View也初始化好了。 - ViewTreeObserver。
使用ViewTreeObserver的眾多回調(diào)可以完成這個功能事富,比如OnGlobalLayoutListener這個接口,當View樹的狀態(tài)發(fā)送改變或View樹內(nèi)部的View的可見性發(fā)生改變時,onGlobalLayout方法會被回調(diào)。需要注意的是戚绕,伴隨著View樹狀態(tài)的改變列肢,onGlobalLayout會被回調(diào)多次跨晴。 - view.measure(int widthMeasureSpec,int heightMeasureSpec)。
(1). match_parent:
無法measure出具體的寬高,因為不知道父容器的剩余空間焚鹊,無法測量出View的大小
(2). 具體的數(shù)值(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);
2. View的layout過程
- 在View的默認實現(xiàn)中璧针,View的測量寬/高和最終寬/高是相等的申屹,測量寬/高形成于View的measure過程,而最終寬/高形成于View的layout過程。
3. View的draw過程
- 將View繪制到屏幕上,大概的幾個步驟:
1.繪制背景background.draw(canvas)
2.繪制自己(onDraw)
3.繪制children(dispatchDraw)
4.繪制裝飾(onDrawScrollBars)
- View的繪制過程是通過dispatchDraw來實現(xiàn)的,它會遍歷所有子元素的draw方法。
- 如果一個View不需要繪制任何內(nèi)容,那么設(shè)置setWillNotDraw為true后,系統(tǒng)會進行相應的優(yōu)化;ViewGroup默認為true,如果我們的自定義ViewGroup需要通過onDraw來繪制內(nèi)容的時候脾歇,需要顯示的關(guān)閉它。
4.4 自定義View
- 直接繼承View或ViewGroup的控件激况, 需要在onmeasure中對wrap_content做特殊處理作彤。
- 直接繼承View的控件膘魄,如果不在draw方法中處理padding,那么padding屬性就無法起作用竭讳。直接繼承ViewGroup的控件也需要在onMeasure和onLayout中考慮padding和子元素margin的影響创葡,不然padding和子元素的margin無效。
- View內(nèi)部提供了post系列的方法绢慢,完全可以替代Handler的作用灿渴。
- View中有線程和動畫,需要在View的onDetachedFromWindow中停止胰舆。
- 自定義View示例請看原著和隨書源碼
歡迎各位小伙伴留言交流喲