Android中View和ViewGroup關(guān)系大揭密
1. 概念
Android中的View與我們以前理解的“視圖”不同。在Android中,View比視圖具有更廣的含義繁莹,它包含了用戶交互和顯示粘秆,更像Windows操作系統(tǒng)中的window。
ViewGroup是View的子類下隧,所以它也具有View的特性奢人,但它主要用來(lái)充當(dāng)View的容器,將其中的View視作自己的孩子淆院,對(duì)它的子View進(jìn)行管理何乎,當(dāng)然它的孩子也可以是ViewGroup類型。
ViewGroup(樹(shù)根)和它的孩子們(View和ViewGroup)以樹(shù)形結(jié)構(gòu)形成了一個(gè)層次結(jié)構(gòu)土辩,View類有接受和處理消息的功能支救,android系統(tǒng)所產(chǎn)生的消息會(huì)在這些ViewGroup和 View之間傳遞。
2. Android的窗口系統(tǒng)
Android的窗口系統(tǒng)是Client/Server模式的拷淘,我在這里只講窗口系統(tǒng)的客戶端(圖1)各墨。? ? 我們所提到的概念:View,ViewGroup启涯,DecorView欲主,ViewRoot都是存在于窗口系統(tǒng)的Client端。
Android中的Window是表示Top Level等頂級(jí)窗口的概念逝嚎。DecorView是Window的Top-Level View扁瓢,這個(gè)View可以稱之為主View,DecorView會(huì)缺省的attach到Activity的主窗口中补君。
ViewRoot建立了主View(DecorView)與窗口系統(tǒng)Server端的通訊橋梁, ViewRoot是 Handler的子類引几,即它其實(shí)是個(gè)Handler,它接受窗口系統(tǒng)服務(wù)器端的消息并將消息投遞到窗口系統(tǒng)的客戶端(圖1)挽铁,然后消息就從客戶端的主View往其下面的子View傳遞伟桅,直到消息被完全處理掉為止。
DecorView實(shí)際上是一個(gè)ViewGroup叽掘。在依存關(guān)系上來(lái)講楣铁,對(duì)單個(gè)主窗口來(lái)講,DecorView是Top-Level View更扁。View并不是關(guān)注的重點(diǎn)盖腕,重要的是我們需要知道消息分發(fā)路徑是建立在什么關(guān)系上的赫冬。View的成員變量mParent用來(lái)管理View上級(jí)關(guān)系的。而ViewGroup顧名思義就是一組View的管理溃列,于是在ViewGroup構(gòu)建了焦點(diǎn)管理和子View節(jié)點(diǎn)數(shù)組劲厌。這樣通過(guò)View的mParent和ViewGroup的mChildren構(gòu)建了Android中View直接的關(guān)系網(wǎng)。
3. View的介紹
(1) ?事件和繪制
繪制流程:
繪制按照視圖樹(shù)的順序執(zhí)行听隐。視圖繪制時(shí)會(huì)先繪制子控件补鼻。如果視圖的背景可見(jiàn),視圖會(huì)在調(diào)用onDraw函數(shù)之前繪制背景雅任。強(qiáng)制重繪风范,可以使用invalidate()。
事件的基本流程如下:
1沪么、事件分配給相應(yīng)視圖乌企,視圖處理它,并通知相關(guān)監(jiān)聽(tīng)器成玫。
2、操作過(guò)程中如果發(fā)生視圖的尺寸變化拳喻,則該視圖用調(diào)用requestLayout()方法哭当,向父控件請(qǐng)求再次布局。
3冗澈、操作過(guò)程中如果發(fā)生視圖的外觀變化钦勘,則該視圖用調(diào)用invalidate()方法,請(qǐng)求重繪亚亲。
4彻采、如果requestLayout()或invalidate()有一個(gè)被調(diào)用,框架會(huì)對(duì)視圖樹(shù)進(jìn)行相關(guān)的測(cè)量捌归、布局和繪制肛响。
注意,視圖樹(shù)是單線程操作惜索,直接調(diào)用其它視圖的方法必須要在UI線程里特笋。跨線程的操作必須使用句柄Handler巾兆。
焦點(diǎn)處理:
框架處理焦點(diǎn)的轉(zhuǎn)移猎物,來(lái)響應(yīng)用戶輸入。isFocusable()函數(shù)表示視圖是否能接受焦點(diǎn)角塑。setFocusable(boolean)函數(shù)可以改變視圖能否接受焦點(diǎn)蔫磨。觸摸屏模式(Touch Mode)的相關(guān)函數(shù)是isFocusableInTouchMode()和setFocusableInTouchMode(boolean)。
焦點(diǎn)轉(zhuǎn)移按照就近算法圃伶。按哪個(gè)方向就近可以在XML布局文件中配置堤如。
nextFocusDown
nextFocusLeft
nextFocusRight
nextFocusUp
視圖請(qǐng)求焦點(diǎn)可以使用requestFocus()蒲列。
(2) 成員介紹
protected ViewParent mParent;
mParent用于記錄它的父親,就是我們前面提到的ViewGroup煤惩。
protected OnClickListener mOnClickListener;
mOnClickListener是click事件的回調(diào)接口.
大家經(jīng)常使用的setOnClickListener(OnClickListener listener):
public void setOnClickListener(OnClickListener I) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener =I;
}
可以看出嫉嘀,mOnClickListener其實(shí)就是保存我們?cè)趹?yīng)用程序中定義的OnClickListener接口的。
public void draw(Canvas canvas)
這個(gè)函數(shù)用于渲染View和它的孩子魄揉,我們不應(yīng)該在子類對(duì)它進(jìn)行override剪侮。
protected void onDraw(Canvas canvas)
我們一般override此函數(shù)來(lái)實(shí)現(xiàn)自己的繪制操作。
IWindowSession getWindowSession() {
return mAttachInfo != null ? mAttachInfo.mSession : null;
}
函數(shù)getWindowSession()用戶得到窗口系統(tǒng)Client端和服務(wù)器端通訊的接口IWindowSession洛退。這是一個(gè)AIDL接口瓣俯,android系統(tǒng)中的跨進(jìn)程通訊就是用AIDL接口實(shí)現(xiàn)的。
public final void layout(int l, int t, int r, int b)
此函數(shù)用于確定View和其子View的尺寸和位置兵怯,它的調(diào)用發(fā)生在onMeasure之后彩匕。
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
此函數(shù)在layout調(diào)用完成后執(zhí)行,View的子類一般override此函數(shù)媒区,并在函數(shù)中對(duì)其每個(gè)孩子調(diào)用layout方法驼仪。
public View getRootView()
此函數(shù)用于得到View層次結(jié)構(gòu)的top-level View,即上文中提到的DecorView袜漩。
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
此函數(shù)用戶找出View的大小绪爸,它的參數(shù)widthMeasureSpec、heightMeasureSpec是其父親傳遞給它的宙攻,這2個(gè)參數(shù)是View找出其大小時(shí)的限制條件奠货,其實(shí)真正的精確大小確定是由onMeasure()完成的,onMeasure由measure函數(shù)調(diào)用座掘。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
此函數(shù)測(cè)量View并根據(jù)其內(nèi)容來(lái)決定View的高和寬递惋,它應(yīng)該被子類override以實(shí)現(xiàn)大小的精確測(cè)量。在onMeasure中我們必須調(diào)用View.setMeasuredDimension(int, int)來(lái)保存測(cè)量得到的大小,高和寬分別被保存在View.mMeasuredHeight和View.mMeasureWidth中溢陪。
public boolean onKeyUp(int keyCode, KeyEvent event)
此函數(shù)會(huì)在鍵盤按鍵釋放后被調(diào)用萍虽,但前提是View必須獲得焦點(diǎn)。
public boolean onTouchEvent(MotionEvent event)
此函數(shù)用于響應(yīng)觸摸屏事件形真。
public void invalidate()
此函數(shù)將調(diào)用onDraw贩挣,強(qiáng)制重繪。
public void requestLayout()
當(dāng)某些東西發(fā)生改變后没酣,當(dāng)前View層次結(jié)構(gòu)無(wú)效了王财,調(diào)用此函數(shù)對(duì)View的層次結(jié)構(gòu)進(jìn)行重新布局。
4.ViewGroup介紹
ViewGroup繼承于View裕便,它可以包含其他的View绒净,就像一個(gè)View的容器,我們可以調(diào)用其成員函數(shù)addView()將View當(dāng)作孩子放到ViewGroup中偿衰。
我們經(jīng)常使用的LinearLayout挂疆、relativeLayout等都是ViewGroup的子類改览,ViewGroup類中有一個(gè)內(nèi)部類ViewGroup.LayoutParams,我們經(jīng)常使用LayoutParams的子類來(lái)構(gòu)造布局參數(shù)缤言。
我們也可以自定義自己的布局宝当,以方便日后使用和維護(hù),這時(shí)我們就需要繼承ViewGroup類并在派生類中重寫ViewGroup的一些方法胆萧,下面是一個(gè)簡(jiǎn)單的例子:
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context) {
super(context);
initChilren(context);? //向容器中添加孩子
}
private void initChilren (Context context) {
Button aBtn = new Button(context);
this.addView(aBtn);
Button bBtn = new Button(context);
this.addView(bBtn);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
//對(duì)容器的孩子進(jìn)行布局庆揩。
………………
………………
child.measure(r - l, b - t);
child.layout(0, 50, child.getMeasuredWidth(), child .getMeasuredHeight() + 50);
………………
………………
}
}
5. 不管是View還是ViewGroup,最重要一點(diǎn)他們的繪制原理如下:此函數(shù)將調(diào)用onDraw跌穗,強(qiáng)制重繪订晌。
public void requestLayout()
當(dāng)某些東西發(fā)生改變后,當(dāng)前View層次結(jié)構(gòu)無(wú)效了蚌吸,調(diào)用此函數(shù)對(duì)View的層次結(jié)構(gòu)進(jìn)行重新布局锈拨。
從上圖,我們可以理出大致的顯示過(guò)程如下:
【1】ActivityManagerService創(chuàng)建Activity線程羹唠,激活一個(gè)activity
【2】系統(tǒng)調(diào)用Instrumentation.newActivity創(chuàng)建一個(gè)activity
【3】Activity創(chuàng)建后奕枢,attach到一個(gè)新創(chuàng)建的phonewindow中。這樣Activity獲取一個(gè)唯一的WindowManager服務(wù)的實(shí)例
【4】Activity創(chuàng)建過(guò)程中使用setcontentView設(shè)置用用戶UI佩微,這些VIEW被加入到PhoneWindow的ContentParent中缝彬。
【5】Activity線程繼續(xù)執(zhí)行,當(dāng)執(zhí)行到Activity.makeVisible是將根view DecoView加入到WindowManger中喊衫,WindowManger實(shí)全會(huì)為每個(gè)DecoView創(chuàng)建對(duì)應(yīng)的ViewRoot
【6】每個(gè)ViewRoot擁有一個(gè)Surface,每個(gè)Surface將會(huì)調(diào)用底層庫(kù)創(chuàng)建圖形繪制的內(nèi)存空間杆怕。這個(gè)底層庫(kù)就是SurfaceFlinger族购。SurfaceFlinger同時(shí)也負(fù)責(zé)將個(gè)View繪制的圖形合到一塊(按照Z(yǔ)軸)顯示到用戶屏幕。
【7】如果用戶直接在Canvas上繪制陵珍,實(shí)際上它直接操作Surface寝杖。但對(duì)每個(gè)View的變更,它是要通知到ViewRoot互纯,然后 ViewRoot獲取Canvas瑟幕。如果繪制完成,surfaceFlinger得到通知留潦,合并Surface成一個(gè)Surface到設(shè)備屏幕只盹。
從上面的圖形輸出過(guò)程分析,我們可以知道真正顯示圖形的實(shí)際上跟Activity沒(méi)有關(guān)系兔院,完全由WindowManager來(lái)決定殖卑。 WindowManager是一個(gè)系統(tǒng)服務(wù),因此可以直接調(diào)用這個(gè)服務(wù)來(lái)創(chuàng)建界面坊萝,并且更絕的是Dialog孵稽、Menu也是有WindowManager 來(lái)管理的许起。另外一個(gè)我們也可以看到,最底層都是Surface來(lái)菩鲜,因此园细,常見(jiàn)開(kāi)發(fā)游戲的人都推薦你使用SurfaceView來(lái)創(chuàng)建界面。
詳細(xì)繪制流程如下:
整個(gè)View樹(shù)的繪圖流程是在ViewRoot.Java類的performTraversals()函數(shù)展開(kāi)的接校,該函數(shù)做的執(zhí)行過(guò)程可簡(jiǎn)單概況為
根據(jù)之前設(shè)置的狀態(tài)猛频,判斷是否需要重新計(jì)算視圖大小(measure)、是否重新需要安置視圖的位置(layout)馅笙、以及是否需要重繪
(draw)伦乔,其框架過(guò)程如下:
步驟其實(shí)為host.layout()
接下來(lái)溫習(xí)一下整個(gè)View樹(shù)的結(jié)構(gòu),對(duì)每個(gè)具體View對(duì)象的操作董习,其實(shí)就是個(gè)遞歸的實(shí)現(xiàn)烈和。
流程一:? ? ? mesarue()過(guò)程
主要作用:為整個(gè)View樹(shù)計(jì)算實(shí)際的大小,即設(shè)置實(shí)際的高(對(duì)應(yīng)屬性:mMeasuredHeight)和寬(對(duì)應(yīng)屬性:
mMeasureWidth)皿淋,每個(gè)View的控件的實(shí)際寬高都是由父視圖和本身視圖決定的招刹。
具體的調(diào)用鏈如下:
ViewRoot根對(duì)象地屬性mView(其類型一般為ViewGroup類型)調(diào)用measure()方法去計(jì)算View樹(shù)的大小,回調(diào)
View/ViewGroup對(duì)象的onMeasure()方法窝趣,該方法實(shí)現(xiàn)的功能如下:
1疯暑、設(shè)置本View視圖的最終大小,該功能的實(shí)現(xiàn)通過(guò)調(diào)用setMeasuredDimension()方法去設(shè)置實(shí)際的高(對(duì)應(yīng)屬性:
mMeasuredHeight)和寬(對(duì)應(yīng)屬性:mMeasureWidth)? 哑舒;
2 妇拯、如果該View對(duì)象是個(gè)ViewGroup類型,需要重寫該onMeasure()方法洗鸵,對(duì)其子視圖進(jìn)行遍歷的measure()過(guò)程越锈。
2.1? 對(duì)每個(gè)子視圖的measure()過(guò)程,是通過(guò)調(diào)用父類ViewGroup.java類里的measureChildWithMargins()方法去
實(shí)現(xiàn)膘滨,該方法內(nèi)部只是簡(jiǎn)單地調(diào)用了View對(duì)象的measure()方法甘凭。(由于measureChildWithMargins()方法只是一個(gè)過(guò)渡
層更簡(jiǎn)單的做法是直接調(diào)用View對(duì)象的measure()方法)。
整個(gè)measure調(diào)用流程就是個(gè)樹(shù)形的遞歸過(guò)程
measure函數(shù)原型為 View.java 該函數(shù)不能被重載
流程二火邓、 layout布局過(guò)程:
主要作用 :為將整個(gè)根據(jù)子視圖的大小以及布局參數(shù)將View樹(shù)放到合適的位置上丹弱。
具體的調(diào)用鏈如下:
host.layout()開(kāi)始View樹(shù)的布局,繼而回調(diào)給View/ViewGroup類中的layout()方法铲咨。具體流程如下
1 躲胳、layout方法會(huì)設(shè)置該View視圖位于父視圖的坐標(biāo)軸,即mLeft纤勒,mTop泛鸟,mLeft,mBottom(調(diào)用setFrame()函數(shù)去實(shí)現(xiàn))
接下來(lái)回調(diào)onLayout()方法(如果該View是ViewGroup對(duì)象踊东,需要實(shí)現(xiàn)該方法北滥,對(duì)每個(gè)子視圖進(jìn)行布局) 刚操;
2、如果該View是個(gè)ViewGroup類型再芋,需要遍歷每個(gè)子視圖chiildView菊霜,調(diào)用該子視圖的layout()方法去設(shè)置它的坐標(biāo)值。
layout函數(shù)原型為 济赎,位于View.java
流程三鉴逞、 draw()繪圖過(guò)程
由ViewRoot對(duì)象的performTraversals()方法調(diào)用draw()方法發(fā)起繪制該View樹(shù),值得注意的是每次發(fā)起繪圖時(shí)司训,并不
會(huì)重新繪制每個(gè)View樹(shù)的視圖构捡,而只會(huì)重新繪制那些“需要重繪”的視圖,View類內(nèi)部變量包含了一個(gè)標(biāo)志位DRAWN壳猜,當(dāng)該
視圖需要重繪時(shí)勾徽,就會(huì)為該View添加該標(biāo)志位。
調(diào)用流程 :
mView.draw()開(kāi)始繪制统扳,draw()方法實(shí)現(xiàn)的功能如下:
1 喘帚、繪制該View的背景
2 、為顯示漸變框做一些準(zhǔn)備操作(見(jiàn)5咒钟,大多數(shù)情況下吹由,不需要改漸變框)
3、調(diào)用onDraw()方法繪制視圖本身?? (每個(gè)View都需要重載該方法朱嘴,ViewGroup不需要實(shí)現(xiàn)該方法)
4倾鲫、調(diào)用dispatchDraw ()方法繪制子視圖(如果該View類型不為ViewGroup,即不包含子視圖萍嬉,不需要重載該方法)
值得說(shuō)明的是乌昔,ViewGroup類已經(jīng)為我們重寫了dispatchDraw ()的功能實(shí)現(xiàn),應(yīng)用程序一般不需要重寫該方法帚湘,但可以重載父類
函數(shù)實(shí)現(xiàn)具體的功能玫荣。
4.1 dispatchDraw()方法內(nèi)部會(huì)遍歷每個(gè)子視圖甚淡,調(diào)用drawChild()去重新回調(diào)每個(gè)子視圖的draw()方法(注意大诸,這個(gè)
地方“需要重繪”的視圖才會(huì)調(diào)用draw()方法)。值得說(shuō)明的是贯卦,ViewGroup類已經(jīng)為我們重寫了dispatchDraw()的功能
實(shí)現(xiàn)资柔,應(yīng)用程序一般不需要重寫該方法,但可以重載父類函數(shù)實(shí)現(xiàn)具體的功能撵割。
強(qiáng)調(diào)一點(diǎn)的就是贿堰,在這三個(gè)流程中,Google已經(jīng)幫我們把draw()過(guò)程框架已經(jīng)寫好了啡彬,自定義的ViewGroup只需要實(shí)現(xiàn)
measure()過(guò)程和layout()過(guò)程即可 羹与。
這三種情況故硅,最終會(huì)直接或間接調(diào)用到三個(gè)函數(shù),分別為invalidate()纵搁,requsetLaytout()以及requestFocus() 吃衅,接著
這三個(gè)函數(shù)最終會(huì)調(diào)用到ViewRoot中的schedulTraversale()方法,該函數(shù)然后發(fā)起一個(gè)異步消息腾誉,消息處理中調(diào)用
performTraverser()方法對(duì)整個(gè)View進(jìn)行遍歷徘层。
invalidate()方法 :
說(shuō)明:請(qǐng)求重繪View樹(shù),即draw()過(guò)程利职,假如視圖發(fā)生大小沒(méi)有變化就不會(huì)調(diào)用layout()過(guò)程趣效,并且只繪制那些“需要重繪的”
視圖,即誰(shuí)(View的話猪贪,只繪制該View 跷敬;ViewGroup,則繪制整個(gè)ViewGroup)請(qǐng)求invalidate()方法哮伟,就繪制該視圖干花。
一般引起invalidate()操作的函數(shù)如下:
1、直接調(diào)用invalidate()方法楞黄,請(qǐng)求重新draw()池凄,但只會(huì)繪制調(diào)用者本身。
2鬼廓、setSelection()方法 :請(qǐng)求重新draw()肿仑,但只會(huì)繪制調(diào)用者本身。
3碎税、setVisibility()方法 : 當(dāng)View可視狀態(tài)在INVISIBLE轉(zhuǎn)換VISIBLE時(shí)尤慰,會(huì)間接調(diào)用invalidate()方法,
繼而繪制該View雷蹂。
4 伟端、setEnabled()方法 : 請(qǐng)求重新draw(),但不會(huì)重新繪制任何視圖包括該調(diào)用者本身匪煌。
requestLayout()方法:會(huì)導(dǎo)致調(diào)用measure()過(guò)程 和 layout()過(guò)程 责蝠。
說(shuō)明:只是對(duì)View樹(shù)重新布局layout過(guò)程包括measure()和layout()過(guò)程,不會(huì)調(diào)用draw()過(guò)程萎庭,但不會(huì)重新繪制
任何視圖包括該調(diào)用者本身霜医。
一般引起invalidate()操作的函數(shù)如下:
1、setVisibility()方法:
當(dāng)View的可視狀態(tài)在INVISIBLE/ VISIBLE 轉(zhuǎn)換為GONE狀態(tài)時(shí)驳规,會(huì)間接調(diào)用requestLayout() 和invalidate方法肴敛。
同時(shí),由于整個(gè)個(gè)View樹(shù)大小發(fā)生了變化,會(huì)請(qǐng)求measure()過(guò)程以及draw()過(guò)程医男,同樣地砸狞,只繪制需要“重新繪制”的視圖。
requestFocus()函數(shù)說(shuō)明:
說(shuō)明:請(qǐng)求View樹(shù)的draw()過(guò)程镀梭,但只繪制“需要重繪”的視圖