學(xué)習(xí)內(nèi)容
View的底層工作原理,比如View的測(cè)量流程、布局流程以及繪制流程;以及常見的View回調(diào)方法;熟悉掌握前面的知識(shí)后,自定義View的時(shí)候也會(huì)更加的得心應(yīng)手。
4.1 初識(shí)ViewRoot和DecorView
- ViewRoot對(duì)應(yīng)于ViewRootImpl類,是連接WindowManager和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的。在ActivityThread中,當(dāng)Activity對(duì)象被創(chuàng)建完畢后,會(huì)將DecorView添加到Window中,同時(shí)會(huì)創(chuàng)建ViewRootImpl對(duì)象,并將ViewRootImpl對(duì)象和DecorView建立關(guān)聯(lián)。
- View的繪制流程從ViewRoot的performTraversals開始,經(jīng)過measure、layout和draw三個(gè)過程才可以把一個(gè)View繪制出來,其中measure用來測(cè)量View的寬高,layout用來確定View在父容器中的放置位置,而draw則負(fù)責(zé)將View繪制到屏幕上。
-
performTraversals會(huì)依次調(diào)用performMeasure、performLayout和performDraw三個(gè)方法,這三個(gè)方法分別完成頂級(jí)View的measure、layout和draw這三大流程。其中performMeasure中會(huì)調(diào)用measure方法,在measure方法中又會(huì)調(diào)用onMeasure方法,在onMeasure方法中則會(huì)對(duì)所有子元素進(jìn)行measure過程,這樣就完成了一次measure過程;子元素會(huì)重復(fù)父容器的measure過程,如此反復(fù)完成了整個(gè)View數(shù)的遍歷。另外兩個(gè)過程類似,大致調(diào)用流程如下圖:
performTraversals工作流程圖.png - measure過程決定了View的寬/高,完成后可通過getMeasuredWidth/getMeasureHeight方法來獲取View測(cè)量后的寬/高。Layout過程決定了View的四個(gè)頂點(diǎn)的坐標(biāo)和實(shí)際View的寬高,完成后可通過getTop、getBotton、getLeft和getRight拿到View的四個(gè)定點(diǎn)坐標(biāo)。Draw過程決定了View的顯示,完成后View的內(nèi)容才能呈現(xiàn)到屏幕上。
- 如下圖,DecorView作為頂級(jí)View,一般情況下它內(nèi)部包含了一個(gè)豎直方向的LinearLayout,里面分為兩個(gè)部分(具體情況和Android版本和主題有關(guān)),上面是標(biāo)題欄,下面是內(nèi)容欄。在Activity通過setContextView所設(shè)置的布局文件其實(shí)就是被加載到內(nèi)容欄之中的。
//獲取內(nèi)容欄
ViewGroup content = findViewById(R.android.id.content);
//獲取我們?cè)O(shè)置的Viewcontext.getChildAt(0);
DecorView其實(shí)是一個(gè)FrameLayout,View層的事件都先經(jīng)過DecorView,然后才傳給我們的View。
4.2 理解MeasureSpec
- MeasureSpec很大程度上決定一個(gè)View的尺寸規(guī)格,測(cè)量過程中,系統(tǒng)會(huì)將View的layoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對(duì)應(yīng)的MeasureSpec,再根據(jù)這個(gè)measureSpec來測(cè)量出View的寬/高。
- MeasureSpec代表一個(gè)32位的int值,高2位為SpecMode,低30位為SpecSize,SpecMode是指測(cè)量模式,SpecSize是指在某種測(cè)量模式下的規(guī)格大小。
MpecMode有三類;
1.UNSPECIFIED 父容器不對(duì)View進(jìn)行任何限制,要多大給多大,一般用于系統(tǒng)內(nèi)部
2.EXACTLY 父容器檢測(cè)到View所需要的精確大小,這時(shí)候View的最終大小就是SpecSize所指定的值,對(duì)應(yīng)LayoutParams中的match_parent和具體數(shù)值這兩種模式。
3.AT_MOST 父容器指定了一個(gè)可用大小即SpecSize,View的大小不能大于這個(gè)值,不同View實(shí)現(xiàn)不同,對(duì)應(yīng)LayoutParams中的wrap_content。 - 當(dāng)View采用固定寬/高的時(shí)候,不管父容器的MeasureSpec的是什么,View的MeasureSpec都是精確模式兵其大小遵循Layoutparams的大小。 當(dāng)View的寬/高是match_parent時(shí),如果他的父容器的模式是精確模式,那View也是精確模式并且大小是父容器的剩余空間;如果父容器是最大模式,那么View也是最大模式并且起大小不會(huì)超過父容器的剩余空間。 當(dāng)View的寬/高是wrap_content時(shí),不管父容器的模式是精確還是最大化,View的模式總是最大化并且不能超過父容器的剩余空間。
4.3 View的工作流程
1. View的measure過程
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
- setMeasuredDimension方法會(huì)設(shè)置View的寬/高的測(cè)量值
- getDefaultSize方法返回的大小就是measureSpec中的specSize,也就是View測(cè)量后的大小,絕大部分情況和View的最終大小(layout階段確定)相同。
- getSuggestedMinimumWidth方法,作為getDefaultSize的第一個(gè)參數(shù)(建議寬度)
- 直接繼承View的自定義控件,需要重寫onMeasure方法并且設(shè)置
wrap_content時(shí)的自身大小,否則在布局中使用了wrap_content相當(dāng)于使用了match_parent。解決方法:在onMeasure時(shí),給View指定一個(gè)內(nèi)部寬/高,并在wrap_content時(shí)設(shè)置即可,其他情況沿用系統(tǒng)的測(cè)量值即可。
2. ViewGroup的measure過程
- 對(duì)于ViewGroup來說,除了完成自己的measure過程之外,還會(huì)遍歷去調(diào)用所有子元素的measure方法,個(gè)個(gè)子元素再遞歸去執(zhí)行這個(gè)過程,和View不同的是,ViewGroup是一個(gè)抽象類,沒有重寫View的onMeasure方法,提供了measureChildren方法。
- measureChildren方法,遍歷獲取子元素,子元素調(diào)用measureChild方法
- measureChild方法,取出子元素的LayoutParams,再通過getChildMeasureSpec方法來創(chuàng)建子元素的MeasureSpec,接著將MeasureSpec傳遞給View的measure方法進(jìn)行測(cè)量。
- ViewGroup沒有定義其測(cè)量的具體過程,因?yàn)椴煌腣iewGroup子類有不同的布局特征,所以其測(cè)量過程的onMeasure方法需要各個(gè)子類去具體實(shí)現(xiàn)。
- measure完成之后,通過getMeasureWidth/Height方法就可以獲取View的測(cè)量寬/高,需要注意的是,在某些極端情況下,系統(tǒng)可能要多次measure才能確定最終的測(cè)量寬/高,比較好的習(xí)慣是在onLayout方法中去獲取測(cè)量寬/高或者最終寬/高。
如何在Activity中獲取View的寬/高信息
因?yàn)閂iew的measure過程和Activity的生命周期不是同步進(jìn)行,如果View還沒有測(cè)量完畢,那么獲取到的寬/高就是0;所以在Activity的onCreate、onStart、onResume中均無法正確的獲取到View的寬/高信息。下面給出4種解決方法。
- Activity/View#onWindowFocusChanged。
onWindowFocusChanged這個(gè)方法的含義是:VieW已經(jīng)初始化完畢了,寬高已經(jīng)準(zhǔn)備好了,需要注意:它會(huì)被調(diào)用多次,當(dāng)Activity的窗口得到焦點(diǎn)和失去焦點(diǎn)均會(huì)被調(diào)用。 - view.post(runnable)。
通過post將一個(gè)runnable投遞到消息隊(duì)列的尾部,當(dāng)Looper調(diào)用此runnable的時(shí)候,View也初始化好了。 - ViewTreeObserver。
使用ViewTreeObserver的眾多回調(diào)可以完成這個(gè)功能,比如OnGlobalLayoutListener這個(gè)接口,當(dāng)View樹的狀態(tài)發(fā)送改變或View樹內(nèi)部的View的可見性發(fā)生改變時(shí),onGlobalLayout方法會(huì)被回調(diào)。需要注意的是,伴隨著View樹狀態(tài)的改變,onGlobalLayout會(huì)被回調(diào)多次。 - view.measure(int widthMeasureSpec,int heightMeasureSpec)。
(1). match_parent:
無法measure出具體的寬高,因?yàn)椴恢栏溉萜鞯氖S嗫臻g,無法測(cè)量出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的默認(rèn)實(shí)現(xiàn)中,View的測(cè)量寬/高和最終寬/高是相等的,測(cè)量寬/高形成于View的measure過程,而最終寬/高形成于View的layout過程。
3. View的draw過程
- 將View繪制到屏幕上,大概的幾個(gè)步驟:
1.繪制背景background.draw(canvas)
2.繪制自己(onDraw)
3.繪制children(dispatchDraw)
4.繪制裝飾(onDrawScrollBars)
- View的繪制過程是通過dispatchDraw來實(shí)現(xiàn)的,它會(huì)遍歷所有子元素的draw方法。
- 如果一個(gè)View不需要繪制任何內(nèi)容,那么設(shè)置setWillNotDraw為true后,系統(tǒng)會(huì)進(jìn)行相應(yīng)的優(yōu)化;ViewGroup默認(rèn)為true,如果我們的自定義ViewGroup需要通過onDraw來繪制內(nèi)容的時(shí)候,需要顯示的關(guān)閉它。
4.4 自定義View
- 直接繼承View或ViewGroup的控件, 需要在onmeasure中對(duì)wrap_content做特殊處理。
- 直接繼承View的控件,如果不在draw方法中處理padding,那么padding屬性就無法起作用。直接繼承ViewGroup的控件也需要在onMeasure和onLayout中考慮padding和子元素margin的影響,不然padding和子元素的margin無效。
- View內(nèi)部提供了post系列的方法,完全可以替代Handler的作用。
- View中有線程和動(dòng)畫,需要在View的onDetachedFromWindow中停止。
- 自定義View示例請(qǐng)看原著和隨書源碼
歡迎各位小伙伴留言交流喲