如何掌握自定義View

國內自定義View的文章汗牛充棟荒辕,但是汗销,即便是你全部看完也未必掌握這一知識(實際上犹褒,我也看了很多,但是一旦涉及自定義View弛针,依然無從下手)叠骑。為什么,一言以蔽之削茁,你是得其術宙枷,不明其道(本文不打算講自定義View的屬性和事件分發(fā),太多文章已經很詳細講解了)茧跋。

一慰丛、自定義View你真的掌握了嗎?

如果說你已經掌握了自定義View瘾杭,那么請嘗試回答下列問題

  • Goolge提出View這個概念的目的是什么诅病?
  • View這個概念和Activity、Fragment粥烁、Drawable之間是一種什么樣的關系贤笆?
  • View能夠感知Activity的生命周期事件嗎?為什么讨阻?

What芥永?如果你說這些問題太抽象?那么請繼續(xù)回答如下問題:

  • View的聲明周期是什么变勇?
  • 當View所在的Activity進入stop狀態(tài)后恤左,View去哪兒了贴唇?如果在一個后臺線程中持有一個View引用搀绣,我們此時能夠改變他的狀態(tài)嗎?為什么戳气?
  • View能夠與其他的View交叉重疊嗎链患?重疊區(qū)域發(fā)生的點擊事件交給誰去處理?可不可以重疊的兩個View都處理瓶您?
  • View控制一個Drawable的方法途徑有哪些麻捻?Drawable能不能與View通信?如果能呀袱,如何通信贸毕?
  • 假如View所在的ViewGroup中的子View減少了,View因此獲得了更大的空間夜赵,View如何及時有效地利用這些空間明棍,改變自己的繪制?
  • 假如我要在View中動態(tài)地注冊與接觸廣播接收器寇僧,應該在那里完成呢摊腋?
  • 假如我的手機帶鍵盤(自帶或外接)沸版,你的自定義View應該如何響應鍵盤事件。
  • AnimationDrawable 作為View的背景兴蒸,會自動進行動畫视粮,View在其中扮演了怎樣的角色?

假如以上問題你能準確的回答出來橙凳,那么蕾殴,恭喜你!我覺得你的自定義View應學到家了岛啸,如果有那么幾個問題你還搞不清楚区宇,或者不是很確定,那么值戳,請上終南山议谷,閉關三個月,繼續(xù)參悟自定義View的內在玄機堕虹。

為什么看了那么多文章卧晓,還是無法愉快地與自定義View玩耍呢?是那些文章寫的不好嗎赴捞?非也逼裆!是你沒有掌握學習自定義View的正確方式。你看那些作者赦政,輕輕松松整出一個漂亮的 自定義View胜宇,你依葫蘆畫瓢也整出一個,就覺得自己好像也會了恢着,年輕人桐愉,你太傲嬌了!你想過沒有這些寫文章的人怎么掌握自定義View的掰派?請把這個問題默念三遍从诲。以后讀任何文章,都問自己這樣的問題靡羡,相信不久的將來系洛,你也會稱為Android大牛的,至少也是小壯牛一頭B圆健C璩丁!趟薄,因為绽诚,你已經從學習別人的知識,進入到學習別人的方法境界了,功力自然大增憔购!

好了宫峦,說了那么多,到底怎樣才能學好自定義View呢玫鸟?其實只需要掌握三個問題导绷,就可以輕松搞定它:

  • 問題一:從Android系統(tǒng)設計者的的角度,View這個概念究竟是做什么的屎飘?
  • 問題二:Android系統(tǒng)中那個View類妥曲,它有哪些默認功能和行為,能干什么钦购,不能干什么(知彼知己檐盟,才好自定義!)
  • 問題三:我要改變這個View的行為押桃、外觀葵萎,肯定要覆寫View類中的方法,但是怎么覆寫唱凯,覆寫哪些方法能夠改變行為羡忘?

以上三個問題,從抽象到具體磕昼,我覺得適用于學習任何技術知識卷雕,只是每個問題的問法可能因具體的技術而有所調整,總體上就是從概念上票从、從默認實現上漫雕、從自己定制上去提問,比如你學習RecycleView峰鄙,也可以問以上三個問題浸间,按照這三個問題的順序一個一個搞懂了,也就完全掌握了這一知識先馆。

從Android系統(tǒng)設計者的角度发框,View這個概念究竟是做什么的躺彬?

關于這個問題最權威的當然是官方文檔煤墙,如下:

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. View is the base class for
widgets, which are used to create interactive UI components (buttons, text fields, etc.). The ViewGroup
subclass is the base class for layouts, which are invisible containers that hold other Views (or other ViewGroups) and define their layout properties.

譯:View是用戶接口組件的基本構建塊,它在屏幕上占據一塊矩形區(qū)域宪拥,負責繪制和事件處理仿野。View是小組件的基類,用于創(chuàng)建交互式UI組件(TextView她君、Button等)脚作;ViewGroup是布局類的基類,是一個容納其他視圖并定義布局屬性的不可見容器。

這句話言簡意賅球涛,高屋建瓴劣针,一針見血,力透紙背亿扁,入木三分捺典,令人銷魂佩服!需要我們認真體會从祝,它包含三層含義:

  • View是用戶接口組件的基本構建塊襟己。通俗講,在Android中牍陌,一個用戶與一個應用的交互擎浴,其實就是與這個應用中的許許多多的View的交互,這些View既可以是簡單的View毒涧,也可以是若干View組合而成的一個復合View贮预。由此我們可以明白,所謂View是基本構件塊契讲,原因就在于它是復合View(就是ViewGroup)的基本組成單元萌狂。這層含義,就是告訴你怀泊,View就是用來與用戶交互的茫藏,那么很自然地,我們要問霹琼,我們用戶在哪里與View交互务傲,以及怎樣與View交互呢?
  • View在屏幕上占據一個矩形區(qū)域枣申。這是說售葡,既然View是用戶與應用交互的基本構建塊,而用戶使用Android設備時忠藤,主要是通過一個觸摸屏來交互的挟伙,相應的,Andorid的設計者們模孩,就讓一個View就在屏幕上占據一個矩形區(qū)域尖阔,用戶在這個區(qū)域中發(fā)生的交互動作(點擊、滑動榨咐、拖動等)介却,就是與這個View的交互。什么块茁?為什么不讓View占據一個圓形區(qū)域或者五角星區(qū)域呢齿坷?當然是為了簡單桂肌。這就解決了在哪里與View交互的問題。很自然地永淌,我們又想問崎场,View在屏幕上占據一個矩形區(qū)域,這個區(qū)域的大小遂蛀、位置怎么確定照雁,它們會不會變化,誰來決定這個變化呢答恶?如果這個變化不是由View自己來決定的饺蚊,而是其他外界因素決定的,View又要怎樣響應這種變化呢悬嗓?不要急污呼,后面都會有答案十办。
  • View通過繪制自己與事件處理兩種方式與用戶交互赃春。這是解決了如何交互的問題。簡單講路媚,View與用戶交互就兩個辦法周瞎,一個是改變自己的模樣苗缩,也就是通過繪制自己與用戶交互,比如声诸,當用戶點擊自己時酱讶,就改變自己的背景顏色,以此來告訴用戶:“本View已經響應你的點擊了彼乌!”第二個方式就是事件處理泻肯,比如,當用戶點擊View時慰照,就完成一定的任務灶挟,然后彈出一個Toast,告訴用戶該View完成了什么任務毒租,這樣稚铣,用戶也就知道這次交互結果如何。

看到沒墅垮,這就是官方文檔的魅力惕医,短短一句話,勝君讀千篇水文∝澹現在我們明白了曹锨,設計View,主要是為了讓應用能夠與用戶交互剃允,要想完成交互,這個View就要在屏幕上占據一個矩形區(qū)域,然后利用這塊屏幕區(qū)域與用戶交互斥废,交互的方式就兩種椒楣,繪制自己與事件處理。

View類牡肉,有哪些默認的功能和行為捧灰,能干什么,不能干什么统锤?

對于上面的解釋毛俏,想必大家有很多疑問,我們想知道:

  • View 是怎樣被顯示到屏幕上的饲窿?
  • View 在屏幕上的位置是怎樣決定的煌寇?
  • View 所占的矩形大小是怎樣決定的?
  • 屏幕上肯定不止一個View逾雄,View之間相互知道嗎阀溶?他們能協(xié)作嗎?
  • View 完成與用戶的交互后鸦泳,能夠自動隱藏嗎银锻,在需要交互的時候,能重新顯示在屏幕上嗎做鹰?
    .....

現在我們就一點點來講击纬,學習的同時,最好能用心體會Google工程師設計的思路钾麸,這樣學習效果最好掉弛。

首先,一個用戶界面喂走,上面有許多View殃饿,既有基本的View,也有復合的View芋肠,把他們組織起來還讓他們很好的協(xié)作確實是一個難題乎芳,Google的解決方案是:首先,一套完整的用戶界面用一個Window來表示帖池,Window這個概念和我們在計算機上所說的Window很相似奈惑。Window負責管理所有的View,怎么管理睡汹?很簡單肴甸,借鑒復合View的思路,Window首先加載一個超級復合View囚巴,用它包含住所有的其他View原在,這個超級復合View就叫做DecorView友扰。但是這個DecorView除了包含我們的用戶界面上的那些View,還包含了作為一個Window特有的View庶柿,叫做TitleBar村怪,這個我們就不細說了。

這樣浮庐,在Window中的所有View被組織起來了甚负,一個巨大的ViewGroup(以后我們不再用復合View這個說法,用ViewGroup取而代之审残,二者是一回事)梭域,下面有若干ViewGroup和若干View,每個ViewGroup下面又有若干ViewGroup和若干View搅轿,很像數據結構中的樹病涨,葉子結點就是基本View。

好了介时,這些View已經被組織起來了没宾,DecorView已經能夠完全控制它們了,同時沸柔,DecorView掌握著能夠分配給這些View的屏幕區(qū)域循衰,包括區(qū)域的大小和位置。我們知道褐澎,屏幕的大小是有限的会钝,一個Window的DecorView能控制的屏幕區(qū)域更加有限,AndroidN中引入多Window機制后工三,DecorView能夠掌控的屏幕區(qū)域更加小了迁酸,因為屏幕上有多個Window將成為常態(tài)。這些有限的區(qū)域還要被Window特有的View(TitleBar)占去一小部分俭正,剩下的才是留給用戶界面上的View分的奸鬓。如果你是DecorView,你肯定為難了掸读,如何將這些有限的屏幕區(qū)域分給這些View們?分給他們還得為每個View排好在屏幕上的位置串远,難上加難。

停下來儿惫,想一想澡罚,如果是你,怎么解決這個問題肾请?

首先留搔,不同的View是為了完成特定的交付任務的。比如铛铁,Button就是用來點擊的隔显,TextView就是用來顯示字符的却妨,等等。DecorView知道荣月,不同的View管呵,為了完成自己的交互任務所需要的屏幕區(qū)域大小是不同的梳毙,所以DecorView在確定給每個View分配的屏幕區(qū)域大小時哺窄,是允許View參與進來的,與它一起商量的账锹。但是每個View在屏幕區(qū)域位置就不能讓View自己來決定了萌业,而是有DecorView一手操辦,這個比較簡單奸柬,我們先看看DecorView是怎樣決定每個View的位置的吧生年。

1、確定每個View的位置

我們在Activity中廓奕,調用了setContentView(View view)抱婉,實際上就是將用戶界面所有的View交給了DecorView中的一個FrameLayout,這個FrameLayout代表著可以分配給用戶界面使用的區(qū)域桌粉。而用戶界面View既可以是一個簡單的View蒸绩,也可以是一個ViewGroup,如果是一個簡單的View铃肯,比如就是一個TextView患亿,那么這個TextView就會占據整個FrameLayout的屏幕區(qū)域,也就是說押逼,此時用戶在FrameLayout的屏幕區(qū)域內的所有交互是與這個TextView交互步藕。但是更常見的情況是,我們的用戶界面是一個ViewGroup挑格,里面包含著其他的ViewGroup和View咙冗。這個時候,首先這個ViewGroup就會占據FrameLayout所代表的屏幕區(qū)域漂彤,剩下的任務雾消,就是這個ViewGroup給它內部的小弟們分配區(qū)域了。至于怎么分显歧,不同的ViewGroup有不同的分法仪或,總體來看,可說是有總有分士骤。所謂總范删,舉例來講,就像LinearLayout的vertical拷肌,他按照自己 小弟的數量到旦,把自己豎向裁成不同的區(qū)域旨巷,如下圖所示:


LinearLayout-sample

雖然View無法決定自己在ViewGroup中的位置,但是開發(fā)者在使用View時添忘,可以向ViewGroup表達自己所用的View要放在哪里采呐,以LinearLayout vertical 為例,開發(fā)者書寫布局文件時搁骑,子View在LinearLayout中的出現順序將決定他們在屏幕上的上下順序斧吐,同時還可以借助layout_margin,layout_gravity等配置進一步調整子View分給自己的矩形區(qū)域中的位置仲器。到這里煤率,我么可以理解,layout_*之類的配置雖然在書寫上與View的屬性在一起乏冀,但他們并不是View的屬性蝶糯,他們只是使用該View的使用者來細化調整該View在ViewGroup中的位置的,同時辆沦,這些值在Inflate時昼捍,是由ViewGroup讀取,然后生成一個ViewGroup特定的LayoutParams對象肢扯,再把這個對象存入子View中的妒茬,這樣,ViewGroup在為該子View安排位置時鹃彻,就可以參考這個LayoutParams中的信息了郊闯。

進一步思考,我們發(fā)現蛛株,調用inflate時团赁,除了輸入布局文件的id外,一般要求傳入parent ViewGroup谨履,傳入這個參數的目的欢摄,就是為了讀取布局文件中的layout配置信息,如果沒有傳入笋粟,這些信息將會丟失怀挠,感興趣的同學可以自己實驗驗證下,這里就不展開了害捕。

不同的ViewGroup擁有不同的LayoutParam內部類绿淋,這是因為,他們所允許的子view微微調整自己的位置的方式是不一樣的尝盼,具體講究就是配置子View時吞滞,允許使用的layout_*是不一樣的,比如,RelativeLayout就允許layout_toRightOf等配置裁赠,其他的ViewGroup就沒有這些配置殿漠。

這些確定View的位置的過程,被包裝在View的Layout方法中佩捞,這樣我們也很容易理解绞幌,對于基本View而言,這個方法是沒有用的一忱,所有都是空的莲蜘,你可以查看下ImageView、TextView等的源代碼掀潮,驗證下這一點菇夸。對于ViewGroup而言琼富,他們會用該方法為自己的子View安排位置仪吧。

2、確定View的大小

下面就要確定View的大小了鞠眉,這是一個開發(fā)者薯鼠、View與ViewGroup三方相互商量的過程。(這里的講解可能與一般的文章不同械蹋,是我個人的理解出皇,一搬的文章都不會說三反商量,二十直接說View與ViewGroup兩方的商量)

第一步哗戈,開發(fā)者在書寫布局文件時郊艘,會為一個View寫上android:layout_width="..." android:layout_height="..."兩個配置,這是開發(fā)者向ViewGroup表達的唯咬,我這個View需要的大小是多少纱注。...的取值有三種:

  • 具體值,如50dp胆胰,很簡單狞贱,不多講
  • match_parent ,表示開發(fā)者向ViewGroup說蜀涨,把你所有的屏幕區(qū)域都給這個View吧瞎嬉。
  • wrap_parent,表示開發(fā)者向ViewGroup說厚柳,只要給這個View夠他展示自己的空間就行氧枣,至于到底給多少,你直接跟View溝通吧别垮,看它怎么說便监。

第二步:ViewGroup收到了開發(fā)者對View大小的說明,然后ViewGroup會綜合考慮自己的空間大小以及開發(fā)者的請求宰闰,然后生成兩個MeasureSpace對象(width與height)傳給View,這兩個對象是ViewGroup向子View提出的要求茬贵,就像相當于告訴子View:“我已經與你的使用者(開發(fā)者)商量過了簿透,現在把我們商量確定的結果告訴你,你的寬度不能違反width MeasureSpec對象的要求解藻,你的高度不能違反height MeasureSpec對象的要求老充,現在,你趕緊根據這個要求確定下自己要多大空間螟左,只許少啡浊,不許多哦〗罕常”

然后巷嚣,這兩個對象將會傳到子View的protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中。子View能怎么辦呢钳吟?它肯定是要先看看ViewGroup的要求是什么吧廷粒,于是,它從傳入的兩個對象中解譯出如下信息:

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize =  MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize =  MeasureSpec.getSize(heightMeasureSpec);

Mode與Size一起红且,準確表達出了ViewGroup的要求坝茎。下面我們舉例說明,假設Size是100dp暇番,
Mode的取值有三種嗤放,它們代表了ViewGroup的總體態(tài)度:

  • 1、EXACTLY 表示壁酬,ViewGroup對View說次酌,你只能用100dp,原因是多樣的舆乔,可能是你的使用者說要你完全占據我的空間岳服,而我只有100dp。也可能這是你的使用者的要求蜕煌,他需要你占這么大的空間派阱,而我恰好也有這么多的空間,你的使用者讓你占這么大的空間斜纪,肯定有他自己的考慮贫母,你不能不理不顧,不然你達不到他的要求盒刚,他可能就不用你了腺劣。
  • 2、AT_MOST表示因块,你最多只能用100dp橘原,這是因為你的使用者說讓你占據wrap_content的大小,讓我跟你商量,我又不知道你到底要占多大區(qū)域趾断,但是我告訴你拒名,我只有100dp,你最多也只能用這么多哈芋酌。(這里增显,可以看出,當使用者在布局文件中要求一個View是wrap_content時脐帝,此時同云,View的大小決定權就交給View自己了,默認的View類中的實現堵腹,比較粗暴炸站,就是將此時ViewGroup提供的空間全占據,完全沒有真正根據自己的內容來確定大小疚顷,為什么這么粗暴旱易?因為View是一個基類,所有的組件都是它的子類荡含,每個子類的content都各不相同咒唆,View怎么可能知道content的大小呢,所以释液,它把wrap_content情況下,自己尺寸大小的決定權下放給了不同的子組件装处,讓它們自己根據自己的內容去決定自己的大小误债,同樣,我們自定義View時妄迁,也要考慮這一點)
  • 3寝蹈、UNSPECIFIED表示,你自己看著辦登淘,把你最理想的大小告訴我箫老,我考慮考慮。

第三步:好了黔州,子View已經清楚第理解了ViewGroup和它的使用者對它的大小的期望和要求了耍鬓。下步就要在該要求下來確定自己的大小并告訴ViewGroup了。(廢話流妻,不告訴ViewGroup大小牲蜀,它怎么給你安排位置(layout),無法給你layout绅这,你也就占據不了一塊屏幕區(qū)域涣达,占不了屏幕區(qū)域,你就無法與用戶交互,無法與用戶交互度苔,要你何用按衣ā!)

關于子View怎么確定自己的大小寇窑,不同的View有不同的態(tài)度奕删,但是有幾點基本的規(guī)矩是要遵守的:
規(guī)矩一就是,不要違反ViewGroup的規(guī)定疗认,最后設置的尺寸一定要在ViewGroup要求的范圍內(不論是寬度還是高度)完残,但是你說,假如我就是想要更大的空間横漏,難道就沒有辦法了嗎谨设,我能不能遵守要求的情況下,同時告訴ViewGroup缎浇,雖然我告訴你的我要求的尺寸是遵照你的旨意來的扎拣,但實際上我是委屈求全的,我真實想要的大小不是這樣的,你能不能再考慮一下素跺。答案是:有二蓝。那就是如下調用:

    esolveSizeAndState((int)(wantedWidth), widthMeasureSpec, 0),    
    resolveSizeAndState((int) (wantedHeight), heightMeasureSpec, 0);

View可以把自己想要的寬和高進行一個resolveSizeAndState處理,就可以達到上述目的指厌。即如果想要的大小沒超過要求刊愚,一切都Ok,如果超過了踩验,在該方法內部鸥诽,就會把尺寸調整成符合ViewGroup要求的,但是也會在尺寸中設置一個標記箕憾,告訴ViewGroup牡借,這個大小是子View委屈求全的結果。至于ViewGroup會不會理會這一標記袭异,要看不同的ViewGroup了钠龙。如果你實現自己的ViewGroup,最好還是關注下這個標記御铃,畢竟作為大哥的你碴里,最主要的職責就是把自己的小弟(子View)安排好,讓它們都滿意嘛畅买。(這一點并闲,我沒有看到任何一篇講解自定義View的文章提到過!)
什么谷羞?好奇的你想看看究竟是怎樣設置標記的帝火?來來來溜徙,滿足你:

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想要的空間太大時設置的標記了。

規(guī)矩二就是要在該方法中調整自己的繪制參數犀填,這一點很好理解蠢壹,畢竟ViewGroup提出了尺寸要求,要及時根據這一要求調整自己的繪制九巡,比如图贸,如果自己的背景圖片太大,那就算算要縮放多少才合適冕广,并且設置一個合理的縮放值疏日。
規(guī)矩三就是一定要設置自己考慮后的尺寸,如果不設置就相當于沒有告訴ViewGroup自己想要的大小撒汉,這會導致ViewGroup無法正常工作沟优,設置的辦法就是在onMeasure方法的最后,調用
setMeasuredDimension方法睬辐。為什么調用這個方法就可以了呢挠阁?這只是一個約定,沒有必要深究了溯饵。

關于View的繪制侵俗,非常簡單,就是一個方法onDraw丰刊,后面的自定義View實戰(zhàn)部分會細說隘谣,這里先略過了。

以上藻三,View的三個基本知識點洪橘,我們都了解了,即View 的位置如何確定棵帽,大小如何確定以及如何繪制自己。這都是默認的View類中為我們準備好的渣玲。

四逗概、我要改變這個View的行為,外觀忘衍,肯定是覆寫View類中的方法逾苫,但是怎么覆寫,覆寫哪些方法能夠改變哪些行為枚钓?

好了铅搓,View的位置和大小怎么確定我們都清楚了,現在搀捷,是時候開始自定義View了星掰。
首先多望,關于View所要具備的一般功能,View類中都有了基本的實現氢烘,比如確定位置怀偷,它有l(wèi)ayout方法送讲,當然胚嘲,這個只適用于ViewGroup,實現自己的ViewGroup時翘簇,才需要修改該方法蜀踏。確定大小维蒙,它有onMeasure方法,如果你不滿意默認的確認大小的方法果覆,也可以自己定義颅痊。改變默認的繪制,就覆寫onDraw方法随静。下面八千,我們通過一張圖,來看看燎猛,自定義View時恋捆,我們最可能需要修改的方法是哪些:

image.png

把這些方法都搞明白了,你也就理解了View的生命周期了重绷。

比如View被inflated出來后沸停,系統(tǒng)會回調該View的onFinishInflate方法,你的View可以在這個方法中昭卓,做一些準備工作愤钾。

如果你的View所屬的Window可見性發(fā)生了變化,系統(tǒng)會回調該View的onWindowVisibilityChanged方法候醒,你也可以根據需要能颁,在該方法中完成一定的工作,比如倒淫,當Window顯示時伙菊,注冊一個監(jiān)聽器,根據監(jiān)聽到的廣播事件改變自己的繪制敌土,當Window不可見時镜硕,解除注冊,因為此時改變自己的繪制已經沒有意義了返干,自己也要跟著Window變成不可見了兴枯。

當ViewGroup中的子View數量增加或者減少,導致ViewGroup給自己分配的屏幕區(qū)域大小發(fā)生變化時矩欠,系統(tǒng)會回調View的onSizeChanged方法财剖,該方法中悠夯,View可以獲取自己最新的尺寸,然后根據這個尺寸相應調整自己的繪制峰伙。

當用戶在View所占據的屏幕區(qū)域發(fā)生了觸摸交互疗疟,系統(tǒng)會將用戶的交互動作分解成如DOWN、MOVE瞳氓、UP等一系列的MotionEvent策彤,并且把這些事件傳遞給View的onTouchEvent方法,View可以在這個方法中進行與用戶的交互處理匣摘。當然這個是基本的流程店诗,實際的流程會稍復雜些,你可以閱讀我的另一篇文章音榜,是專門講解事件分發(fā)的庞瘸,文章非常經典,你讀了一定不后悔赠叼。

除了這些方法擦囊,View還實現了三個接口,如下


View繼承類關系圖.jpg

三個接口是:
Drawable.Callback
KeyEvent.Callback
AccessibilityEventSource

每個接口都有自己的作用嘴办。

KeyEvent回調接口瞬场,是用來處理鍵盤事件的,這與onTouchEvent用來處理觸摸事件是相對的涧郊。

Drawable回調接口是用來讓View中的Drawable能夠與View通信的贯被,尤其是AnimationDrawable,更是必須依賴該回調才能實現動畫效果妆艘,關于這一點彤灶,我深入地研究了FrameWork的源碼,對AnimationDrawable如何實現動畫批旺,有了深入徹底的掌握幌陕,我也在考慮要不要就此寫一篇文章,看大家需要吧汽煮,如果本文贊數過百苞轿,我就寫,絕不食言逗物。

第三個回調接口,我沒有細致研究瑟俭,不便多說翎卓。

寫到這里你應該發(fā)現,我們的第三個問題摆寄,自定義View失暴,應該覆寫哪些方法坯门,能夠實現哪些功能也已經解決了。


休息一刻逗扒,如侵權古戴,刪除

五、光說不練假把式矩肩,實戰(zhàn)自定義View

說了這么多现恼,不自定一個View,怎么對的起你辛苦讀到這里呢黍檩。好叉袍,我們現在就來自定義一個鐘表,而且可以自己走的刽酱。如下圖所示:

Demo圖

這個時鐘可是能夠走動的哈喳逛。下面我們就開始吧。首先棵里,準備三張圖片資源润文,如下:

clock_dial.png
clock_hand_hour.png
clock_hand_minute.png

聰明如你,一看就應該知道這是做什么用的了殿怜。準備圖片時典蝌,使用了一個小技巧,就是時針和分針稳捆,你所看到的圖像只是圖片的一半赠法,在圖像的下方,還有同樣大小的空白乔夯,這個是做什么用的呢砖织?主要是為了繪制圖片時的方便,待會兒就可以明白了末荐。

材料齊全侧纯,開工!

public class AnalogClock extends View {   

      private Time mCalendar;    //用來記錄當前時間

      //用來存放三張圖片資源
      private Drawable mHourHand;  
      private Drawable mMinuteHand; 
      private Drawable mDial;   

    //用來記錄表盤圖片的寬和高甲脏,
    //以便幫助我們在onMeasure中確定View的大
    //小眶熬,畢竟,我們的View中最大的一個Drawable就是它了块请。
       private int mDialWidth; 
       private int mDialHeight;   


//用來記錄View是否被加入到了Window中娜氏,我們在View attached到
//Window時注冊監(jiān)聽器,監(jiān)聽時間的變更墩新,并根據時間的變更贸弥,改變自己
//的繪制,在View從Window中剝離時海渊,解除注冊绵疲,因為我們不需要再監(jiān)聽
//時間變更了哲鸳,沒人能看得到我們的View了。
       private boolean mAttached;    

//看名字
        private float mMinutes;    
        private float mHour;    

//用來跟蹤我們的View 的尺寸的變化盔憨,
//當發(fā)生尺寸變化時徙菠,我們在繪制自己
//時要進行適當的縮放。
        private boolean mChanged;
...
}

下面郁岩,我們來確定自定義View 的構造方法婿奔,查看View類,我們知道驯用,View類有四個構造方法脸秽,我們相應地,也寫四個構造方法蝴乔,并且初始化相關變量:

/第一個構造方法
public AnalogClock(Context context) {   
     this(context, null);
}
//第二個構造方法
public AnalogClock(Context context, AttributeSet attrs) {  
      this(context, attrs, 0);
}
//第三個構造方法
public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) { 
      this(context, attrs, defStyleAttr, 0);
}
//第四個構造方法
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();
}

請注意记餐,以上為自定義View設置的構造方法是適用性最廣的一種寫法,這樣寫薇正,可以確保我們的自定義View能夠被最大多數的開發(fā)者使用片酝,是一種最佳實踐。

接下來挖腰,確定我們的自定義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想要的尺寸當然就是與表盤一樣大的尺寸猴仑,這樣可以保證我們的View有最佳的展示审轮,可是如果ViewGroup給的尺寸比較小,我們就根據表盤圖片的尺寸辽俗,進行適當的按比例縮放疾渣。注意,這里我們沒有直接使用ViewGroup給我們的較小的尺寸崖飘,而是對我們的表盤圖片的寬高進行相同比例的縮放后榴捡,設置的尺寸,這樣的好處是朱浴,可以防止表盤圖片繪制時的拉伸或者擠壓變形吊圾。

確定了大小,是不是就可以繪制了翰蠢,先不著急项乒,我們先要處理兩件事,一件就是讓我們的自定義View能夠感知自己尺寸的變化梁沧,這樣每次繪制時板丽,可以先判斷下尺寸是否發(fā)生了變化,如果有變化,就及時調整我們的繪制策略埃碱。代碼如下:

protected void onSizeChanged(int w, int h, int oldw, int oldh) {    
       super.onSizeChanged(w, h, oldw, oldh);   
       mChanged = true;
}

我們會在onDraw使用mChanged變量的。

第二件事就是讓我們的View能夠監(jiān)聽時間變化酥泞,并及時更新該View中的mCalendar變量砚殿,然后根據它來更新自身的繪制。為此芝囤,我們先寫一個更新時間的方法似炎,代碼如下:

private void onTimeChanged() {    
        mCalendar.setToNow();  

        int hour = mCalendar.hour;   
        int minute = mCalendar.minute;  
        int second = mCalendar.second;   
        /*這里我們?yōu)槭裁床恢苯影裮inute設置給mMinutes,而是要加上
            second /60.0f呢悯姊,這個值不是應該一直為0嗎羡藐?
            這里又涉及到Calendar的 一個知識點,
            也就是它可以是Linient模式悯许,
            此模式下仆嗦,second和minute是可能超過60和24的,具體這里就不展開了先壕,
            如果不是很清楚瘩扼,建議看看Google的官方文檔中講Calendar的部分*/
         mMinutes = minute + second / 60.0f;    
         mHour = hour + mMinutes / 60.0f;   
         mChanged = true;
}

然后我們還要實現一個廣播接收器,接收系統(tǒng)發(fā)出的時間變化廣播垃僚,然后更新該View的mCalendar集绰,如下:

private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {   
       @Override  
        public void onReceive(Context context, Intent intent) {    
            //這個if判斷主要是用來在時區(qū)發(fā)生變化時,更新mCalendar的時區(qū)的谆棺,這
            //樣栽燕,我們的自定義View在全球都可以使用了。
            if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {            
                  String tz = intent.getStringExtra("time-zone");         
                   mCalendar = new Time(TimeZone.getTimeZone(tz).getID());    
            }     
          //進行時間的更新  
             onTimeChanged();     
          //invalidate當然是用來引發(fā)重繪了改淑。
           invalidate();   
         }
};

現在碍岔,我們要給我們的View動態(tài)地注冊廣播接收器,沒錯溅固,我們就是要在
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;   
           }
}

萬事具備,只欠東風侍郭,開始繪制我們的View吧询吴。代碼如下:

@Override
protected void onDraw(Canvas canvas) {   
         super.onDraw(canvas);  

      //View尺寸變化后,我們用changed變量記錄下來亮元,
    //同時猛计,恢復mChanged為false,以便繼續(xù)監(jiān)聽View的尺寸變化爆捞。
          boolean changed = mChanged;   
          if (changed) {      
                mChanged = false;   
           }   
        /* 請注意奉瘤,這里的availableWidth和availableHeight,
           每次繪制時是可能變化的,
           我們可以從mChanged變量的值判斷它是否發(fā)生了變化盗温,
           如果變化了藕赞,說明View的尺寸發(fā)生了變化,
           那么就需要重新為時針卖局、分針設置Bounds斧蜕,
           因為我們需要時針,分針始終在View的中心砚偶。*/
           int availableWidth = super.getRight() - super.getLeft();   
           int availableHeight = super.getBottom() - super.getTop();  


        /* 這里的x和y就是View的中心點的坐標批销,
          注意這個坐標是以View的左上角為0點,向右x染坯,向下y的坐標系來計算的均芽。
          這個坐標系主要是用來為View中的每一個Drawable確定位置。
          就像View的坐標是用parent的左上角為0點的坐標系計算得來的一樣单鹿。
          簡單來講掀宋,就是ViewGroup用自己左上角為0點的坐標系為
          各個子View安排位置,
          View同樣用自己左上角為0點的坐標系
          為它里面的Drawable安排位置羞反。
          注意不要搞混了布朦。*/

           int x = availableWidth / 2;    
           int y = availableHeight / 2;   

           final Drawable dial = mDial;  
           int w = dial.getIntrinsicWidth();   
           int h = dial.getIntrinsicHeight();   
            boolean scaled = false;   

        /*如果可用的寬高小于表盤圖片的寬高,
           就要進行縮放昼窗,不過這里是趴,我們是通過坐標系的縮放來實現的。
          而且澄惊,這個縮放效果影響是全局的唆途,
          也就是下面繪制的表盤、時針掸驱、分針都會受到縮放的影響肛搬。*/
           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ā)生變化,我們要重新為表盤設置Bounds毕贼。
           這里的Bounds就相當于是為Drawable在View中確定位置温赔,
           只是確定的方式更直接,直接在View中框出一個與Drawable大小
           相同的矩形鬼癣,
           Drawable就在這個矩形里繪制自己陶贼。
           這里框出的矩形,是以(x,y)為中心的待秃,寬高等于表盤圖片的寬高的一個矩形拜秧,
           不用擔心表盤圖片太大繪制不完整,
            因為我們已經提前進行了縮放了章郁。*/
          if (changed) {       
                 dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 
           }    
          dial.draw(canvas);    

          canvas.save();   
          /*根據小時數枉氮,以點(x,y)為中心旋轉坐標系。
            如果你對來回旋轉的坐標系感到頭暈,摸不著頭腦聊替,
            建議你看一下**徐宜生**《安卓群英傳》中講解2D繪圖部分中的Canvas一節(jié)楼肪。*/

           canvas.rotate(mHour / 12.0f * 360.0f, x, y);  
           final Drawable hourHand = mHourHand;   

          //同樣,根據變化重新設置時針的Bounds
           if (changed) {     
                   w = hourHand.getIntrinsicWidth();    
                   h = hourHand.getIntrinsicHeight();      

            /* 仔細體會這里設置的Bounds佃牛,我們所畫出的矩形淹辞,
                同樣是以(x,y)為中心的
                矩形,時針圖片放入該矩形后俘侠,時針的根部剛好在點(x,y)處,
                因為我們之前做時針圖片時蔬将,
                已經讓圖片中的時針根部在圖片的中心位置了爷速,
                雖然,看起來浪費了一部分圖片空間(就是時針下半部分是空白的)霞怀,
                但卻換來了建模的簡單性惫东,還是很值的。*/
                  hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));  
             }    
              hourHand.draw(canvas);  
              canvas.restore();  

              canvas.save();    
            //根據分針旋轉坐標系
              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();    
            //最后毙石,我們把縮放的坐標系復原廉沮。
              if (scaled) {      
                   canvas.restore();   
              }

}

大功告成,現在我們的時鐘終于完成了徐矩,任何開發(fā)者都可以使用我們的View滞时,獲得一個不斷走動的模擬時鐘。該View的完整代碼已經上傳到Github滤灯,猛戳https://github.com/like4hub/CustomViewForClock坪稽。(注:該時鐘的實現,主要參考了AOSP中模擬時鐘)

關于本文前面提出的問題鳞骤,簡單回答一下:

Q1:google提出view概念的目的是給android app提供用戶交互的機制窒百。
Q2、Q3豫尽、Q7:android framework采用的是層次架構:從上到下是:Activity篙梢、Fragment
View
Drawable
上層知道下層,下層卻不知道上層美旧。上層可以直接使用支配下層渤滞,下層卻無法支配使用上層,下層與上層的通信主要靠回調陈症。所以View處于Activity蔼水、Fragment與Drawable中間,意味著View不能夠感知Activity的生命周期录肯,但是View可以完全控制Drawable趴腋,控制的手段定義在Drawable中,凡是Drawable提供的方法,都是View控制Drawable的手段优炬,最典型的颁井,在本文中也使用了的就是setBounds方法。正如View無法感知Activity的聲明周期一樣蠢护,Drawable同樣無法感知View的生命周期雅宾。但是View實現了Drawable.Callback接口,Drawable可以通過這個接口與View通信葵硕。本文中有說明
Q4:View的生命周期請見本文View-Method-For-Override一圖眉抬,這張圖來自google官方文檔,如果看不懂懈凹,可以查看文檔獲得相關說明蜀变,如果還是看不懂,歡迎留言討論介评。

Q5:Activity進入stop狀態(tài)后库北,它的窗口會被最新呈現的窗口擋住,窗口中的view也因此無法被我們看見们陆,如果此時在后臺線程中更新一個view是可以的寒瓦,前提是要提交到UI線程中,但通常意義不大坪仇,因為此時用戶無法看到view的改變杂腰,而且,當這個Activity從stop狀態(tài)中進入resume時烟很,一般都會重新更新view颈墅,以便繼續(xù)與用戶交互,所以雾袱,在stop狀態(tài)下對view的更新沒有什么意義恤筛。
Q6:View直接是可以重疊,重疊區(qū)域的點擊事件由誰處理取決于它們的parent 在dispatch這個點擊事件時芹橡,先dispatch給誰毒坛。能不能都處理呢?一般情況下是不可以的林说,但是在最新的CoordinateLayout中煎殷,可以通過behavior實現這一需求。具體內容太多腿箩,請自行搜索豪直。

Q8:View利用這些空間的方法很簡單啊,就是在onSizeChanged方法中在新的寬高下繪制自己 珠移。新的寬高由其parent ViewGroup在其他子View被移除后弓乙,重新layout時確定末融。本文的案例中就利用了這個方法。

參考文章
步步為營

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末暇韧,一起剝皮案震驚了整個濱河市勾习,隨后出現的幾起案子,更是在濱河造成了極大的恐慌懈玻,老刑警劉巖巧婶,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異涂乌,居然都是意外死亡艺栈,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進店門湾盒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來眼滤,“玉大人,你說我怎么就攤上這事历涝。” “怎么了漾唉?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵荧库,是天一觀的道長。 經常有香客問我赵刑,道長分衫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任般此,我火速辦了婚禮蚪战,結果婚禮上,老公的妹妹穿的比我還像新娘铐懊。我一直安慰自己邀桑,他們只是感情好,可當我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布科乎。 她就那樣靜靜地躺著壁畸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪茅茂。 梳的紋絲不亂的頭發(fā)上捏萍,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天,我揣著相機與錄音空闲,去河邊找鬼令杈。 笑死,一個胖子當著我的面吹牛碴倾,可吹牛的內容都是我干的逗噩。 我是一名探鬼主播掉丽,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼给赞!你這毒婦竟也來了机打?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤片迅,失蹤者是張志新(化名)和其女友劉穎残邀,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體柑蛇,經...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡芥挣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了耻台。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片空免。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖盆耽,靈堂內的尸體忽然破棺而出蹋砚,到底是詐尸還是另有隱情,我是刑警寧澤摄杂,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布坝咐,位于F島的核電站,受9級特大地震影響析恢,放射性物質發(fā)生泄漏墨坚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一映挂、第九天 我趴在偏房一處隱蔽的房頂上張望泽篮。 院中可真熱鬧,春花似錦柑船、人聲如沸帽撑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽油狂。三九已至,卻和暖如春寸癌,著一層夾襖步出監(jiān)牢的瞬間专筷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工蒸苇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留磷蛹,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓溪烤,卻偏偏與公主長得像味咳,于是被迫代替她去往敵國和親庇勃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內容