
國(guó)內(nèi)自定義View的文章汗牛充棟,但是,即使你全部看完它們也未必能掌握這一知識(shí)點(diǎn)(實(shí)際上,我就幾乎看完了所有的國(guó)內(nèi)文章)。為什么?一言以蔽之,你是得其術(shù)不明其道。(本文不打算講自定義屬性和事件處理,因?yàn)樘嗟奈恼轮v這些了)
一、自定義View,你真的掌握了嗎?
什么?你說(shuō)你掌握了自定義View?來(lái)來(lái)來(lái),回答老衲如下問(wèn)題:
- Google提出View這個(gè)概念的目的是什么?
- View這個(gè)概念與Activtiy、Fragment以及Drawable之間是一種什么樣的關(guān)系?
- View能夠感知Activity的生命周期事件嗎?為什么?
什么?你說(shuō)這些問(wèn)題太抽象?來(lái)來(lái)來(lái),繼續(xù)回答如下問(wèn)題:
- View的生命周期是什么?
- 當(dāng)View所在的Activity進(jìn)入stop狀態(tài)后,View去哪了?如果我在一個(gè)后臺(tái)線程中持有一個(gè)View的引用,我此時(shí)能夠改變它的狀態(tài)嗎?為什么?
- View能夠與其他的View交叉重疊嗎?重疊區(qū)域發(fā)生的點(diǎn)擊事件交給誰(shuí)去處理呢?可不可以重疊的兩個(gè)View都處理?
- View控制一個(gè)Drawable的方法途徑有哪些?Drawable能不能與View通信?如果能如何通信?
- 假如View所在的ViewGroup中的子View減少了,View因此獲得了更大的空間,View如何及時(shí)有效地利用這些空間,改變自己的繪制?
- 假如我要在View中動(dòng)態(tài)地注冊(cè)與解除廣播接收器,應(yīng)該在哪里完成呢?
- 假如我的手機(jī)帶鍵盤(自帶或者外接),你的自定義View應(yīng)該如何響應(yīng)鍵盤事件。
- AnimationDrawable作為View的背景,會(huì)自動(dòng)進(jìn)行動(dòng)畫,View在其中扮演了怎樣的角色?
假如以上問(wèn)題你都能準(zhǔn)確地回答出來(lái),那么,恭喜你!我覺得你的自定義View已經(jīng)學(xué)到家了,如果有那么幾個(gè)問(wèn)題你還搞不清楚,或者不是很確定,那么,請(qǐng)上終南山,閉關(guān)三個(gè)月,繼續(xù)參悟自定義View的內(nèi)在玄機(jī)。
為什么看了那么多文章,還是無(wú)法愉快地與自定義View玩耍?是那些文章不好嗎?非也,是你沒有掌握學(xué)習(xí)自定義View的正確姿勢(shì)(即使你會(huì)很多姿勢(shì),也木有用,嘎嘎)。你看那些作者,輕輕松松整出一個(gè)漂亮的自定義View,你依葫蘆畫瓢也整出一個(gè),就覺得自己好像也會(huì)了,年輕人,你太傲嬌了!你想過(guò)沒有,寫這些文章的人是怎么掌握自定義View的?請(qǐng)把這個(gè)問(wèn)題在心中默念三遍。以后讀任何技術(shù)文章,都問(wèn)自己這樣的問(wèn)題,相信不久的將來(lái),你也會(huì)成為Android大牛的,至少也是小壯牛一頭!因?yàn)?,你已?jīng)從學(xué)習(xí)別人的知識(shí),進(jìn)入到學(xué)習(xí)別人的方法的境界了,功力怎能不大增!
好了,說(shuō)了這么多,到底怎樣才能學(xué)好自定義View?其實(shí)只需掌握三個(gè)問(wèn)題,就可以輕松搞定它:
- 問(wèn)題一:從Android系統(tǒng)設(shè)計(jì)者的角度,View這個(gè)概念究竟是做什么的?
- 問(wèn)題二:Android系統(tǒng)中那個(gè)View類,它有哪些默認(rèn)功能和行為,能干什么,不能干什么?(知己知彼,才好自定義?。?/li>
- 問(wèn)題三:我要改變這個(gè)View的行為,外觀,肯定是覆寫View類中的方法,但是怎么覆寫,覆寫哪些方法能夠改變哪些行為?
以上三個(gè)問(wèn)題,從抽象到具體,我覺得適用于學(xué)習(xí)任何技術(shù)知識(shí),只是每個(gè)問(wèn)題的問(wèn)法可能因具體技術(shù)而有所調(diào)整,總體上就是從概念上,從默認(rèn)實(shí)現(xiàn)上,從自己定制上去提問(wèn),比如你學(xué)習(xí)RecyclerView,也可以問(wèn)以上三個(gè)問(wèn)題,按照這三個(gè)問(wèn)題的順序一個(gè)一個(gè)搞懂了,也就完全掌握了這一知識(shí)點(diǎn)。
下面,我們就一個(gè)問(wèn)題一個(gè)問(wèn)題地來(lái)解答。
二、從Android系統(tǒng)設(shè)計(jì)者的角度,View這個(gè)概念究竟是做什么的?
關(guān)于這個(gè)問(wèn)題,最權(quán)威的當(dāng)然是官方文檔,如下:
This class represents the basic building block for user interface components. A View occupies a rectangular area on the screen and is responsible for drawing and event handling.
這句話言簡(jiǎn)意賅,高屋建瓴,一針見血,力透紙背,入木三分,令人銷魂佩服!需要我們認(rèn)真體會(huì),它包含三層含義:
- View是用戶接口組件的基本構(gòu)建塊。通俗講,在Android中,一個(gè)用戶與一個(gè)應(yīng)用的交互,其實(shí)就是與這個(gè)應(yīng)用中的許許多多的View的交互,這些View既可以是簡(jiǎn)單的View,也可以是若干View組合而成的一個(gè)復(fù)合View。由此我們可以明白,所謂View是基本構(gòu)件塊,原因就在于它是復(fù)合View(就是ViewGroup)的基本組成單元。這層含義,就是告訴你,View就是用來(lái)與用戶交互的,那么很自然地,我們要問(wèn),我們用戶在哪里與View交互,以及怎樣與View交互呢?
- View在屏幕上占據(jù)一個(gè)矩形區(qū)域。這是說(shuō),既然View是用戶與應(yīng)用交互的基本構(gòu)建塊,而用戶使用Android設(shè)備時(shí),主要是通過(guò)一個(gè)觸摸屏來(lái)交互的,相應(yīng)的,Andorid的設(shè)計(jì)者們,就讓一個(gè)View就在屏幕上占據(jù)一個(gè)矩形區(qū)域,用戶在這個(gè)區(qū)域中發(fā)生的交互動(dòng)作(點(diǎn)擊、滑動(dòng)、拖動(dòng)等),就是與這個(gè)View的交互。什么?為什么不讓View占據(jù)一個(gè)圓形區(qū)域或者五角星區(qū)域呢?當(dāng)然是為了簡(jiǎn)單。這就解決了在哪里與View交互的問(wèn)題。很自然地,我們又想問(wèn),View在屏幕上占據(jù)一個(gè)矩形區(qū)域,這個(gè)區(qū)域的大小、位置怎么確定,它們會(huì)不會(huì)變化,誰(shuí)來(lái)決定這個(gè)變化呢?如果這個(gè)變化不是由View自己來(lái)決定的,而是其他外界因素決定的,View又要怎樣響應(yīng)這種變化呢?不要急,后面都會(huì)有答案。
- View通過(guò)繪制自己與事件處理兩種方式與用戶交互。這是解決了如何交互的問(wèn)題。簡(jiǎn)單講,View與用戶交互就兩個(gè)辦法,一個(gè)是改變自己的模樣,也就是通過(guò)繪制自己與用戶交互,比如,當(dāng)用戶點(diǎn)擊自己時(shí),就改變自己的背景顏色,以此來(lái)告訴用戶:“本View已經(jīng)響應(yīng)你的點(diǎn)擊了!”第二個(gè)方式就是事件處理,比如,當(dāng)用戶點(diǎn)擊View時(shí),就完成一定的任務(wù),然后彈出一個(gè)Toast,告訴用戶該View完成了什么任務(wù),這樣,用戶也就知道這次交互結(jié)果如何。
看到?jīng)],這就是官方文檔的魅力,短短一句話,勝君讀千篇水文?,F(xiàn)在我們明白了,設(shè)計(jì)View,主要是為了讓應(yīng)用能夠與用戶交互,要想完成交互,這個(gè)View就要在屏幕上占據(jù)一個(gè)矩形區(qū)域,然后利用這塊屏幕區(qū)域與用戶交互,交互的方式就兩種,繪制自己與事件處理。
三、Android系統(tǒng)中那個(gè)View類,它有哪些默認(rèn)功能和行為,能干什么,不能干什么?
解決了第一個(gè)問(wèn)題,我們很可能有更多的疑問(wèn),我們想知道:
View是怎樣被顯示到屏幕上的?
View在屏幕上的位置是怎樣決定的?
View所占據(jù)的矩形大小是怎樣決定的?
屏幕上肯定不止一個(gè)View,View之間互相知道對(duì)方嗎?它們之間能協(xié)作嗎?
View完成與用戶的交互后,能夠自動(dòng)隱藏,在需要交互的時(shí)候重新顯示在屏幕上嗎?
......
現(xiàn)在我們就一點(diǎn)點(diǎn)來(lái)講,學(xué)習(xí)的同時(shí),最好能夠用心體會(huì)Google工程師設(shè)計(jì)時(shí)的思路。
這樣學(xué)習(xí)效果最好。
首先,一個(gè)用戶界面,上面有許多View,既有基本View,也有復(fù)合View,把它們組織起來(lái)還讓它們很好地協(xié)作確實(shí)是一個(gè)難題,Google的解決方案是:首先,一套完整的用戶界面用一個(gè)Window來(lái)表示,Window這個(gè)概念和我們?cè)谟?jì)算機(jī)上所說(shuō)的Window很相似。Window負(fù)責(zé)管理所有的View們,怎么管理?很簡(jiǎn)單,借鑒復(fù)合View的思路,Window首先加載一個(gè)超級(jí)復(fù)合View,用它來(lái)包含住所有的其他View,這個(gè)超級(jí)復(fù)合View就叫做DecorView。但是這個(gè)DecorView除了包含我們的用戶界面上那些View,還包含了作為一個(gè)Window特有的View,叫做titlebar,這個(gè)我們就不細(xì)說(shuō)了。
這樣,在Window中的View們被組織起來(lái)了,一個(gè)巨大的ViewGroup(以后,我們不再用復(fù)合View這個(gè)說(shuō)法,而代之以ViewGroup,二者是一回事),下面有若干ViewGroup和若干View,每個(gè)ViewGroup下面又有若干ViewGroup和若干View,很像數(shù)據(jù)結(jié)構(gòu)中的樹,葉子節(jié)點(diǎn)就是基本View。
好了,這些View已經(jīng)被組織起來(lái)了,DecorView已經(jīng)能夠完全控制它們了,同時(shí),DecorView掌握著能夠分配給這些View的屏幕區(qū)域,包括區(qū)域的大小和位置。我們知道,屏幕的大小是有限的,一個(gè)Window的DecorView能夠控制的屏幕區(qū)域更加有限,AndroidN中引入多Window機(jī)制后,DecorView能掌控的屏幕區(qū)域更加小了,因?yàn)槠聊簧嫌卸鄠€(gè)Window將成為常態(tài)。這些有限的區(qū)域還要被Window特有的View(titlebar)占去一小部分,剩下的才是留給用戶界面上的View們分的,如果你是DecorView,你肯定為難了,如何將這些有限的屏幕區(qū)域分給這些View們?分給他們后還得為每個(gè)View排好在屏幕上的位置,難上加難。
停一停,想一想,如果是你,你怎么解決這個(gè)問(wèn)題?
首先,不同的View是為了完成特定的交互任務(wù)的,比如,Button就是用來(lái)點(diǎn)擊的,TextView就是用來(lái)顯示字符的,等等。DecorView知道,不同的View為了完成自己的交互任務(wù)所需要的屏幕區(qū)域大小是不同的,所以DecorView在確定給每個(gè)View分配的屏幕區(qū)域大小時(shí),是允許View參與進(jìn)來(lái),與它一起商量的。但是每個(gè)View在屏幕區(qū)域中的位置就不能讓View自己來(lái)決定了,而是由DecorView一手操辦,這個(gè)比較簡(jiǎn)單,我們就先來(lái)看看DecorView是怎樣決定每個(gè)View的位置的吧。
1、確定每個(gè)View的位置
我們?cè)贏ctivity中,調(diào)用了setContentView(View),實(shí)際上就是將用戶界面的所有的View交給了DecorView中的一個(gè)FrameLayout,這個(gè)FrameLayou代表著可以分配給用戶界面使用的屏幕區(qū)域。而用戶界面View既可以是一個(gè)簡(jiǎn)單的View,也可以是一個(gè)ViewGroup,如果是一個(gè)簡(jiǎn)單的View,比如就是一個(gè)TextView,那么這個(gè)TextView就會(huì)占據(jù)整個(gè)FrameLayout的屏幕區(qū)域,也就是說(shuō),此時(shí)用戶在FrameLayout的屏幕區(qū)域內(nèi)的所有交互都是與這個(gè)TextView交互。但是更常見的情況時(shí),我們的用戶界面是一個(gè)ViewGroup(想想常用的布局五大金剛),里面包含著其他的ViewGroup和View。這個(gè)時(shí)候,首先這個(gè)ViewGroup就會(huì)占據(jù)FrameLayout所代表的屏幕區(qū)域,剩下的任務(wù),就是這個(gè)ViewGroup給它內(nèi)部的小弟們(各種ViewGroup和各種View)分配區(qū)域了。至于怎么分,不同的ViewGroup有不同的分法,總體來(lái)看,可說(shuō)是有總有分。所謂總,舉例來(lái)講,像vertical的LinearLayout,它按照
自己的小弟數(shù)量,把自己豎向裁成不同的區(qū)域,如下圖所示:

雖然View無(wú)法決定自己在ViewGroup中的位置,但是開發(fā)者在使用View時(shí),可以向ViewGroup表達(dá)自己所用的View要放在哪里,以vertical LinearLayout為例,開發(fā)者書寫布局文件時(shí),子View在LinearLayout中的出現(xiàn)順序?qū)Q定它們?cè)谄聊簧系纳舷马樞?,同時(shí)還可以借助layout_margin ,layout_gravity等配置進(jìn)一步調(diào)整子View在分給自己的矩形區(qū)域中的位置。到這里,我們可以理解,layout_*之類的配置雖然在書寫上與View的屬性在一起,但它們并不是View的屬性,它們只是使用該View的使用者用來(lái)細(xì)化調(diào)整該View在ViewGroup中的位置的,同時(shí),這些值在Inflate時(shí),是由ViewGroup讀取,然后生成一個(gè)ViewGroup特定的LayoutParams對(duì)象,再把這個(gè)對(duì)象存入子View中的,這樣,ViewGroup在為該子View安排位置時(shí),就可以參考這個(gè)LayoutParams中的信息了。進(jìn)一步思考,我們發(fā)現(xiàn),調(diào)用inflate時(shí),除了輸入布局文件的id外,一般要求傳入parent ViewGroup,傳入這個(gè)參數(shù)的目的,就是為了讀取布局文件中的layout配置信息,如果沒有傳入,這些信息將會(huì)丟失,感興趣的同學(xué)可以自己試驗(yàn)驗(yàn)證下,這里就不展開了。
不同的ViewGroup擁有不同的LayoutParams內(nèi)部類,這是因?yàn)?,它們所允許的子View微微調(diào)整自己的位置的方式是不一樣的,具體講就是配置子View時(shí),允許使用的layout_*是不一樣的,比如,RelativeLayout就允許layout_toRightOf等配置,其他的ViewGroup沒有這些配置。
這些確定View的位置的過(guò)程,被包裝在View 的layout方法中,這樣我們也很容易理解,對(duì)于基本View而言,這個(gè)方法是沒有用的,所以都是空的,你可以查看下ImageView、TextView等的源代碼,驗(yàn)證下這一點(diǎn)。對(duì)于ViewGroup而言,它們會(huì)用該方法為自己的子View安排位置。
2、確定View大小
下面,是要確定View的大小了,這是一個(gè)開發(fā)者、View與ViewGroup三方相互商量的過(guò)程。(這里的講解可能與一般的文章不同,是我個(gè)人的理解,一般的文章都不會(huì)說(shuō)是三方商量,而是直接說(shuō)View與ViewGroup兩方商量)
第一步,開發(fā)者在書寫布局文件時(shí),會(huì)為一個(gè)View寫上android:layout_width="***"android:layout_height="***"兩個(gè)配置,這是開發(fā)者向ViewGroup表達(dá)的,我這個(gè)View需要的大小是多少。星號(hào)的取值有三種:
- 具體值,如50dp,很簡(jiǎn)單,不多講
- match_parent ,表示開發(fā)者向ViewGroup說(shuō),把你所有的屏幕區(qū)域都給這個(gè)View吧。
- wrap_parent,表示開發(fā)者向ViewGroup說(shuō),只要給這個(gè)View夠他展示自己的空間就行,至于到底給多少,你直接跟View溝通吧,看它怎么說(shuō)。
第二步,ViewGroup收到了開發(fā)者對(duì)View大小的說(shuō)明,然后ViewGroup會(huì)綜合考慮自己的空間大小以及開發(fā)者的請(qǐng)求,然后生成兩個(gè)MeasureSpec對(duì)象(width與height)傳給View,這兩個(gè)對(duì)象是ViewGroup向子View提出的要求,就相當(dāng)于告訴子View:“我已經(jīng)與你的使用者(開發(fā)者)商量過(guò)了,現(xiàn)在把我們商量確定的結(jié)果告訴你,你的寬度不能違反width MeasureSpec對(duì)象的要求,你的高度不能違反height MeasureSpec對(duì)象的要求,現(xiàn)在,你趕緊根據(jù)這個(gè)要求確定下自己要多大空間,只許少,不許多哦。”
然后,這兩個(gè)對(duì)象將會(huì)傳到子View的protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中。子View能怎么辦呢?它肯定是要先看看ViewGroup的要求是什么吧,于是,它從傳入的兩個(gè)對(duì)象中解譯出如下信息:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Mode與Size一起,準(zhǔn)確表達(dá)出了ViewGroup的要求。下面我們舉例說(shuō)明,假設(shè)Size是100dp,
Mode的取值有三種,它們代表了ViewGroup的總體態(tài)度:
- EXACTLY 表示,ViewGroup對(duì)View說(shuō),你只能用100dp,原因是多樣的,可能是你的使用者說(shuō)要你完全占據(jù)我的空間,而我只有100dp。也可能這是你的使用者的要求,他需要你占這么大的空間,而我恰好也有這么多的空間,你的使用者讓你占這么大的空間,肯定有他自己的考慮,你不能不理不顧,不然你達(dá)不到他的要求,他可能就不用你了。
- AT_MOST表示,你最多只能用100dp,這是因?yàn)槟愕氖褂谜哒f(shuō)讓你占據(jù)wrap_content的大小,讓我跟你商量,我又不知道你到底要占多大區(qū)域,但是我告訴你,我只有100dp,你最多也只能用這么多哈。(這里,可以看出,當(dāng)使用者在布局文件中要求一個(gè)View是wrap_content時(shí),此時(shí),View的大小決定權(quán)就交給View自己了,默認(rèn)的View類中的實(shí)現(xiàn),比較粗暴,就是將此時(shí)ViewGroup提供的空間全占據(jù),完全沒有真正根據(jù)自己的內(nèi)容來(lái)確定大小,為什么這么粗暴?因?yàn)閂iew是一個(gè)基類,所有的組件都是它的子類,每個(gè)子類的content都各不相同,View怎么可能知道content的大小呢,所以,它把wrap_content情況下,自己尺寸大小的決定權(quán)下放給了不同的子組件,讓它們自己根據(jù)自己的內(nèi)容去決定自己的大小,同樣,我們自定義View時(shí),也要考慮這一點(diǎn))
- UNSPECIFIED表示,你自己看著辦,把你最理想的大小告訴我,我考慮考慮。
第三步,好了,子View已經(jīng)清楚地理解了ViewGroup和它的使用者對(duì)它的大小的期望和要求了。下步就要在該要求下來(lái)確定自己的大小并告訴ViewGroup了。(廢話,不告訴ViewGroup大小,它怎么給你安排位置(layout),無(wú)法給你layout,你也就占據(jù)不了一塊屏幕區(qū)域,占不了屏幕區(qū)域,你就無(wú)法與用戶交互,無(wú)法與用戶交互,要你何用?。。?/p>
關(guān)于子View怎么確定自己的大小,不同的View有不同的態(tài)度,但是有幾點(diǎn)基本的規(guī)矩是要遵守的:
規(guī)矩一就是,不要違反ViewGroup的規(guī)定,最后設(shè)置的尺寸一定要在ViewGroup要求的范圍內(nèi)(不論是寬度還是高度),但是你說(shuō),假如我就是想要更大的空間,難道就沒有辦法了嗎,我能不能遵守要求的情況下,同時(shí)告訴ViewGroup,雖然我告訴你的我要求的尺寸是遵照你的旨意來(lái)的,但實(shí)際上我是委屈求全的,我真實(shí)想要的大小不是這樣的,你能不能再考慮一下。答案是:有。那就是如下調(diào)用:
resolveSizeAndState((int)(wantedWidth), widthMeasureSpec, 0),
resolveSizeAndState((int) (wantedHeight), heightMeasureSpec, 0);```
View可以把自己想要的寬和高進(jìn)行一個(gè)resolveSizeAndState處理,就可以達(dá)到上述目的。即如果想要的大小沒超過(guò)要求,一切都Ok,如果超過(guò)了,在該方法內(nèi)部,就會(huì)把尺寸調(diào)整成符合ViewGroup要求的,但是也會(huì)在尺寸中設(shè)置一個(gè)標(biāo)記,告訴ViewGroup,這個(gè)大小是子View委屈求全的結(jié)果。至于ViewGroup會(huì)不會(huì)理會(huì)這一標(biāo)記,要看不同的ViewGroup了。如果你實(shí)現(xiàn)自己的ViewGroup,最好還是關(guān)注下這個(gè)標(biāo)記,畢竟作為大哥的你,最主要的職責(zé)就是把自己的小弟(子View)安排好,讓它們都滿意嘛。(這一點(diǎn),我沒有看到任何一篇講解自定義View的文章提到過(guò)?。?什么?好奇的你想看看究竟是怎樣設(shè)置標(biāo)記的?來(lái)來(lái)來(lái),滿足你:
```java
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
上面的代碼中的MEASURED_STATE_TOO_SMALL就是在子View想要的空間太大時(shí)設(shè)置的標(biāo)記了。
規(guī)矩二就是要在該方法中調(diào)整自己的繪制參數(shù),這一點(diǎn)很好理解,畢竟ViewGroup提出了尺寸要求,要及時(shí)根據(jù)這一要求調(diào)整自己的繪制,比如,如果自己的背景圖片太大,那就算算要縮放多少才合適,并且設(shè)置一個(gè)合理的縮放值。
規(guī)矩三就是一定要設(shè)置自己考慮后的尺寸,如果不設(shè)置就相當(dāng)于沒有告訴ViewGroup自己想要的大小,這會(huì)導(dǎo)致ViewGroup無(wú)法正常工作,設(shè)置的辦法就是在onMeasure方法的最后,調(diào)用
setMeasuredDimension方法。為什么調(diào)用這個(gè)方法就可以了呢?這只是一個(gè)約定,沒有必要深究了。
關(guān)于View的繪制,非常簡(jiǎn)單,就是一個(gè)方法onDraw,后面的自定義View實(shí)戰(zhàn)部分會(huì)細(xì)說(shuō),這里先略過(guò)了。
以上,View的三個(gè)基本知識(shí)點(diǎn),我們都了解了,即View 的位置如何確定,大小如何確定以及如何繪制自己。這都是默認(rèn)的View類中為我們準(zhǔn)備好的。
四、我要改變這個(gè)View的行為,外觀,肯定是覆寫View類中的方法,但是怎么覆寫,覆寫哪些方法能夠改變哪些行為?
好了,View的位置和大小怎么確定我們都清楚了,現(xiàn)在,是時(shí)候開始自定義View了。
首先,關(guān)于View所要具備的一般功能,View類中都有了基本的實(shí)現(xiàn),比如確定位置,它有l(wèi)ayout方法,當(dāng)然,這個(gè)只適用于ViewGroup,實(shí)現(xiàn)自己的ViewGroup時(shí),才需要修改該方法。確定大小,它有onMeasure方法,如果你不滿意默認(rèn)的確認(rèn)大小的方法,也可以自己定義。改變默認(rèn)的繪制,就覆寫onDraw方法。下面,我們通過(guò)一張圖,來(lái)看看,自定義View時(shí),我們最可能需要修改的方法是哪些:

把這些方法都搞明白了,你也就理解了View的生命周期了。
比如View被inflated出來(lái)后,系統(tǒng)會(huì)回調(diào)該View的onFinishInflate方法,你的View可以在這個(gè)方法中,做一些準(zhǔn)備工作。
如果你的View所屬的Window可見性發(fā)生了變化,系統(tǒng)會(huì)回調(diào)該View的onWindowVisibilityChanged方法,你也可以根據(jù)需要,在該方法中完成一定的工作,比如,當(dāng)Window顯示時(shí),注冊(cè)一個(gè)監(jiān)聽器,根據(jù)監(jiān)聽到的廣播事件改變自己的繪制,當(dāng)Window不可見時(shí),解除注冊(cè),因?yàn)榇藭r(shí)改變自己的繪制已經(jīng)沒有意義了,自己也要跟著Window變成不可見了。
當(dāng)ViewGroup中的子View數(shù)量增加或者減少,導(dǎo)致ViewGroup給自己分配的屏幕區(qū)域大小發(fā)生變化時(shí),系統(tǒng)會(huì)回調(diào)View的onSizeChanged方法,該方法中,View可以獲取自己最新的尺寸,然后根據(jù)這個(gè)尺寸相應(yīng)調(diào)整自己的繪制。
當(dāng)用戶在View所占據(jù)的屏幕區(qū)域發(fā)生了觸摸交互,系統(tǒng)會(huì)將用戶的交互動(dòng)作分解成如DOWN、MOVE、UP等一系列的MotionEvent,并且把這些事件傳遞給View的onTouchEvent方法,View可以在這個(gè)方法中進(jìn)行與用戶的交互處理。當(dāng)然這個(gè)是基本的流程,實(shí)際的流程會(huì)稍復(fù)雜些,你可以閱讀我的另一篇文章,是專門講解事件分發(fā)的,文章非常經(jīng)典,你讀了一定不后悔。
除了這些方法,View還實(shí)現(xiàn)了三個(gè)接口,如下:

三個(gè)接口是:
Drawable.Callback
KeyEvent.Callback
AccessibilityEventSource
每個(gè)接口都有自己的作用。
KeyEvent回調(diào)接口,是用來(lái)處理鍵盤事件的,這與onTouchEvent用來(lái)處理觸摸事件是相對(duì)的。
Drawable回調(diào)接口是用來(lái)讓View中的Drawable能夠與View通信的,尤其是AnimationDrawable,更是必須依賴該回調(diào)才能實(shí)現(xiàn)動(dòng)畫效果,關(guān)于這一點(diǎn),我深入地研究了FrameWork的源碼,對(duì)AnimationDrawable如何實(shí)現(xiàn)動(dòng)畫,有了深入徹底的掌握,我也在考慮要不要就此寫一篇文章,看大家需要吧,如果本文贊數(shù)過(guò)百,我就寫,絕不食言。
第三個(gè)回調(diào)接口,我沒有細(xì)致研究,不便多說(shuō)。
寫到這里你應(yīng)該發(fā)現(xiàn),我們的第三個(gè)問(wèn)題,自定義View,應(yīng)該覆寫哪些方法,能夠?qū)崿F(xiàn)哪些功能也已經(jīng)解決了。
五、光說(shuō)不練假把式,實(shí)戰(zhàn)自定義View
說(shuō)了這么多,不自定一個(gè)View,怎么對(duì)的起你辛苦讀到這里呢。好,我們現(xiàn)在就來(lái)自定義一個(gè)鐘表,而且可以自己走的。如下圖所示:

這個(gè)時(shí)鐘可是能夠走動(dòng)的哈。下面我們就開始吧。首先,準(zhǔn)備三張圖片資源,如下:



聰明如你,一看就應(yīng)該知道這是做什么用的了。準(zhǔn)備圖片時(shí),使用了一個(gè)小技巧,就是時(shí)針和分針,你所看到的圖像只是圖片的一半,在圖像的下方,還有同樣大小的空白,這個(gè)是做什么用的呢?主要是為了繪制圖片時(shí)的方便,待會(huì)兒就可以明白了。
材料齊全,開工!
public class AnalogClock extends View {
private Time mCalendar; //用來(lái)記錄當(dāng)前時(shí)間
//用來(lái)存放三張圖片資源
private Drawable mHourHand;
private Drawable mMinuteHand;
private Drawable mDial;
//用來(lái)記錄表盤圖片的寬和高,
//以便幫助我們?cè)趏nMeasure中確定View的大
//小,畢竟,我們的View中最大的一個(gè)Drawable就是它了。
private int mDialWidth;
private int mDialHeight;
//用來(lái)記錄View是否被加入到了Window中,我們?cè)赩iew attached到
//Window時(shí)注冊(cè)監(jiān)聽器,監(jiān)聽時(shí)間的變更,并根據(jù)時(shí)間的變更,改變自己
//的繪制,在View從Window中剝離時(shí),解除注冊(cè),因?yàn)槲覀儾恍枰俦O(jiān)聽
//時(shí)間變更了,沒人能看得到我們的View了。
private boolean mAttached;
//看名字
private float mMinutes;
private float mHour;
//用來(lái)跟蹤我們的View 的尺寸的變化,
//當(dāng)發(fā)生尺寸變化時(shí),我們?cè)诶L制自己
//時(shí)要進(jìn)行適當(dāng)?shù)目s放。
private boolean mChanged;
...
}
下面,我們來(lái)確定自定義View 的構(gòu)造方法,查看View類,我們知道,View類有四個(gè)構(gòu)造方法,我們相應(yīng)地,也寫四個(gè)構(gòu)造方法,并且初始化相關(guān)變量:
//第一個(gè)構(gòu)造方法
public AnalogClock(Context context) {
this(context, null);
}
//第二個(gè)構(gòu)造方法
public AnalogClock(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
//第三個(gè)構(gòu)造方法
public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
//第四個(gè)構(gòu)造方法
public AnalogClock(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final Resources r = context.getResources();
if (mDial == null) {
mDial = context.getDrawable(R.drawable.clock_dial);
}
if (mHourHand == null) {
mHourHand = context.getDrawable(R.drawable.clock_hand_hour);
}
if (mMinuteHand == null) {
mMinuteHand =
context.getDrawable(R.drawable.clock_hand_minute);
}
mCalendar = new Time();
mDialWidth = mDial.getIntrinsicWidth();
mDialHeight = mDial.getIntrinsicHeight();}
請(qǐng)注意,以上為自定義View設(shè)置的構(gòu)造方法是適用性最廣的一種寫法,這樣寫,可以確保我們的自定義View能夠被最大多數(shù)的開發(fā)者使用,是一種最佳實(shí)踐。
接下來(lái),確定我們的自定義View 的大小,也就是改寫onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
float hScale = 1.0f;
float vScale = 1.0f;
if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < mDialWidth) {
hScale = (float) widthSize / (float) mDialWidth;
}
if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < mDialHeight) {
vScale = (float )heightSize / (float) mDialHeight;
}
float scale = Math.min(hScale, vScale);
setMeasuredDimension(
resolveSizeAndState((int) (mDialWidth * scale), widthMeasureSpec, 0),
resolveSizeAndState((int) (mDialHeight * scale), heightMeasureSpec, 0)
);
}
在該方法中,我們的View想要的尺寸當(dāng)然就是與表盤一樣大的尺寸,這樣可以保證我們的View有最佳的展示,可是如果ViewGroup給的尺寸比較小,我們就根據(jù)表盤圖片的尺寸,進(jìn)行適當(dāng)?shù)陌幢壤s放。注意,這里我們沒有直接使用ViewGroup給我們的較小的尺寸,而是對(duì)我們的表盤圖片的寬高進(jìn)行相同比例的縮放后,設(shè)置的尺寸,這樣的好處是,可以防止表盤圖片繪制時(shí)的拉伸或者擠壓變形。
確定了大小,是不是就可以繪制了,先不著急,我們先要處理兩件事,一件就是讓我們的自定義View能夠感知自己尺寸的變化,這樣每次繪制時(shí),可以先判斷下尺寸是否發(fā)生了變化,如果有變化,就及時(shí)調(diào)整我們的繪制策略。代碼如下:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mChanged = true;
}
我們會(huì)在onDraw使用mChanged變量的。
第二件事就是讓我們的View能夠監(jiān)聽時(shí)間變化,并及時(shí)更新該View中的mCalendar變量,然后根據(jù)它來(lái)更新自身的繪制。為此,我們先寫一個(gè)更新時(shí)間的方法,代碼如下:
private void onTimeChanged() {
mCalendar.setToNow();
int hour = mCalendar.hour;
int minute = mCalendar.minute;
int second = mCalendar.second;
/*這里我們?yōu)槭裁床恢苯影裮inute設(shè)置給mMinutes,而是要加上
second /60.0f呢,這個(gè)值不是應(yīng)該一直為0嗎?
這里又涉及到Calendar的 一個(gè)知識(shí)點(diǎn),
也就是它可以是Linient模式,
此模式下,second和minute是可能超過(guò)60和24的,具體這里就不展開了,
如果不是很清楚,建議看看Google的官方文檔中講Calendar的部分*/
mMinutes = minute + second / 60.0f;
mHour = hour + mMinutes / 60.0f;
mChanged = true;
}
然后我們還要實(shí)現(xiàn)一個(gè)廣播接收器,接收系統(tǒng)發(fā)出的時(shí)間變化廣播,然后更新該View的mCalendar,如下:
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//這個(gè)if判斷主要是用來(lái)在時(shí)區(qū)發(fā)生變化時(shí),更新mCalendar的時(shí)區(qū)的,這
//樣,我們的自定義View在全球都可以使用了。
if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {
String tz = intent.getStringExtra("time-zone");
mCalendar = new Time(TimeZone.getTimeZone(tz).getID());
}
//進(jìn)行時(shí)間的更新
onTimeChanged();
//invalidate當(dāng)然是用來(lái)引發(fā)重繪了。
invalidate();
}
};
現(xiàn)在,我們要給我們的View動(dòng)態(tài)地注冊(cè)廣播接收器,沒錯(cuò),我們就是要在
onAttachedToWindow和onDetachedFromWindow中完成這一功能。代碼如下:
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!mAttached) {
mAttached = true;
IntentFilter filter = new IntentFilter();
//這里確定我們要監(jiān)聽的三種系統(tǒng)廣播
filter.addAction(Intent.ACTION_TIME_TICK);
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
getContext().registerReceiver(mIntentReceiver, filter);
}
mCalendar = new Time();
onTimeChanged();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAttached) {
getContext().unregisterReceiver(mIntentReceiver);
mAttached = false;
}
}
萬(wàn)事具備,只欠東風(fēng),開始繪制我們的View吧。代碼如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//View尺寸變化后,我們用changed變量記錄下來(lái),
//同時(shí),恢復(fù)mChanged為false,以便繼續(xù)監(jiān)聽View的尺寸變化。
boolean changed = mChanged;
if (changed) {
mChanged = false;
}
/* 請(qǐng)注意,這里的availableWidth和availableHeight,
每次繪制時(shí)是可能變化的,
我們可以從mChanged變量的值判斷它是否發(fā)生了變化,
如果變化了,說(shuō)明View的尺寸發(fā)生了變化,
那么就需要重新為時(shí)針、分針設(shè)置Bounds,
因?yàn)槲覀冃枰獣r(shí)針,分針始終在View的中心。*/
int availableWidth = super.getRight() - super.getLeft();
int availableHeight = super.getBottom() - super.getTop();
/* 這里的x和y就是View的中心點(diǎn)的坐標(biāo),
注意這個(gè)坐標(biāo)是以View的左上角為0點(diǎn),向右x,向下y的坐標(biāo)系來(lái)計(jì)算的。
這個(gè)坐標(biāo)系主要是用來(lái)為View中的每一個(gè)Drawable確定位置。
就像View的坐標(biāo)是用parent的左上角為0點(diǎn)的坐標(biāo)系計(jì)算得來(lái)的一樣。
簡(jiǎn)單來(lái)講,就是ViewGroup用自己左上角為0點(diǎn)的坐標(biāo)系為
各個(gè)子View安排位置,
View同樣用自己左上角為0點(diǎn)的坐標(biāo)系
為它里面的Drawable安排位置。
注意不要搞混了。*/
int x = availableWidth / 2;
int y = availableHeight / 2;
final Drawable dial = mDial;
int w = dial.getIntrinsicWidth();
int h = dial.getIntrinsicHeight();
boolean scaled = false;
/*如果可用的寬高小于表盤圖片的寬高,
就要進(jìn)行縮放,不過(guò)這里,我們是通過(guò)坐標(biāo)系的縮放來(lái)實(shí)現(xiàn)的。
而且,這個(gè)縮放效果影響是全局的,
也就是下面繪制的表盤、時(shí)針、分針都會(huì)受到縮放的影響。*/
if (availableWidth < w || availableHeight < h) {
scaled = true;
float scale = Math.min((float) availableWidth / (float) w,
(float) availableHeight / (float) h);
canvas.save();
canvas.scale(scale, scale, x, y);
}
/*如果尺寸發(fā)生變化,我們要重新為表盤設(shè)置Bounds。
這里的Bounds就相當(dāng)于是為Drawable在View中確定位置,
只是確定的方式更直接,直接在View中框出一個(gè)與Drawable大小
相同的矩形,
Drawable就在這個(gè)矩形里繪制自己。
這里框出的矩形,是以(x,y)為中心的,寬高等于表盤圖片的寬高的一個(gè)矩形,
不用擔(dān)心表盤圖片太大繪制不完整,
因?yàn)槲覀円呀?jīng)提前進(jìn)行了縮放了。*/
if (changed) {
dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
}
dial.draw(canvas);
canvas.save();
/*根據(jù)小時(shí)數(shù),以點(diǎn)(x,y)為中心旋轉(zhuǎn)坐標(biāo)系。
如果你對(duì)來(lái)回旋轉(zhuǎn)的坐標(biāo)系感到頭暈,摸不著頭腦,
建議你看一下**徐宜生**《安卓群英傳》中講解2D繪圖部分中的Canvas一節(jié)。*/
canvas.rotate(mHour / 12.0f * 360.0f, x, y);
final Drawable hourHand = mHourHand;
//同樣,根據(jù)變化重新設(shè)置時(shí)針的Bounds
if (changed) {
w = hourHand.getIntrinsicWidth();
h = hourHand.getIntrinsicHeight();
/* 仔細(xì)體會(huì)這里設(shè)置的Bounds,我們所畫出的矩形,
同樣是以(x,y)為中心的
矩形,時(shí)針圖片放入該矩形后,時(shí)針的根部剛好在點(diǎn)(x,y)處,
因?yàn)槲覀冎白鰰r(shí)針圖片時(shí),
已經(jīng)讓圖片中的時(shí)針根部在圖片的中心位置了,
雖然,看起來(lái)浪費(fèi)了一部分圖片空間(就是時(shí)針下半部分是空白的),
但卻換來(lái)了建模的簡(jiǎn)單性,還是很值的。*/
hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
}
hourHand.draw(canvas);
canvas.restore();
canvas.save();
//根據(jù)分針旋轉(zhuǎn)坐標(biāo)系
canvas.rotate(mMinutes / 60.0f * 360.0f, x, y);
final Drawable minuteHand = mMinuteHand;
if (changed) {
w = minuteHand.getIntrinsicWidth();
h = minuteHand.getIntrinsicHeight();
minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
}
minuteHand.draw(canvas);
canvas.restore();
//最后,我們把縮放的坐標(biāo)系復(fù)原。
if (scaled) {
canvas.restore();
}
}
大功告成,現(xiàn)在我們的時(shí)鐘終于完成了,任何開發(fā)者都可以使用我們的View,獲得一個(gè)不斷走動(dòng)的模擬時(shí)鐘。該View的完整代碼已經(jīng)上傳到Github,猛戳https://github.com/like4hub/CustomViewForClock。(注:該時(shí)鐘的實(shí)現(xiàn),主要參考了AOSP中模擬時(shí)鐘)