什麼?你說你掌握了自定義View?來來來,試着回答以下問題:java
什麼?你說這些問題太抽象?來來來,繼續回答以下問題:android
其實,說了這麼多,到底怎樣才能學好自定義View?其實只需掌握三個問題,就能夠輕鬆搞定它:bash
關於這個問題,最權威的固然是官方文檔,以下:數據結構
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.ide
可見這句話,包含三層含義:佈局
View是用戶接口組件的基本構建塊
。通俗講,在Android中,一個用戶與一個應用的交互,其實就是與這個應用中的許許多多的View的交互,這些View既能夠是簡單的View,也能夠是若干View組合而成的一個ViewGroup。由此咱們能夠明白,所謂View是基本構件塊,緣由就在於它是複合View(就是ViewGroup)的基本組成單元。這層含義,就是告訴你,View就是用來與用戶交互的,那麼很天然地,咱們要問,咱們用戶在哪裏與View交互,以及怎樣與View交互呢?post
View在屏幕上佔據一個矩形區域
。這是說,既然View是用戶與應用交互的基本構建塊,而用戶使用Android設備時,主要是經過一個觸摸屏來交互的,相應的,Andorid的設計者們,就讓一個View就在屏幕上佔據一個矩形區域,用戶在這個區域中發生的交互動做(點擊、滑動、拖動等),就是與這個View的交互。什麼?爲何不讓View佔據一個圓形區域或者五角星區域呢?固然是爲了簡單。這就解決了在哪裏與View交互的問題。很天然地,咱們又想問,View在屏幕上佔據一個矩形區域,這個區域的大小、位置怎麼肯定,它們會不會變化,誰來決定這個變化呢?若是這個變化不是由View本身來決定的,而是其餘外界因素決定的,View又要怎樣響應這種變化呢?不要急,後面都會有答案。動畫
View經過繪製本身與事件處理兩種方式與用戶交互
。這是解決了如何交互的問題。簡單講,View與用戶交互就兩個辦法,一個是改變本身的模樣,也就是經過繪製本身與用戶交互,好比,當用戶點擊本身時,就改變本身的背景顏色,以此來告訴用戶:「本View已經響應你的點擊了!」第二個方式就是事件處理,好比,當用戶點擊View時,就完成必定的任務,而後彈出一個Toast,告訴用戶該View完成了什麼任務,這樣,用戶也就知道此次交互結果如何。ui
如今咱們明白了,設計View,主要是爲了讓應用可以與用戶交互,要想完成交互,這個View就要在屏幕上佔據一個矩形區域,而後利用這塊屏幕區域與用戶交互,交互的方式就兩種,繪製本身與事件處理。spa
解決了第一個問題,咱們極可能有更多的疑問,咱們想知道:
首先,一個用戶界面,上面有許多View,既有基本View,也有複合View,把它們組織起來還讓它們很好地協做確實是一個難題,Google的解決方案是:首先,一套完整的用戶界面用一個Window來表示,Window這個概念和咱們在計算機上所說的Window很類似。Window負責管理全部的View們,怎麼管理?很簡單,借鑑複合View的思路,Window首先加載一個超級複合View,用它來包含全部的其餘View,這個超級複合View就叫作DecorView。可是這個DecorView除了包含咱們的用戶界面上那些View,還包含了做爲一個Window特有的View,叫作titlebar,這個咱們就不細說了。
這樣,在Window中的View們被組織起來了,造成一個巨大的ViewGroup,下面又有若干ViewGroup和若干View,每一個ViewGroup下面又有若干ViewGroup和若干View,很像數據結構中的樹,葉子節點就是基本View。
好了,這些View已經被組織起來了,DecorView已經可以徹底控制它們了,同時,DecorView掌握着可以分配給這些View的屏幕區域,包括區域的大小和位置。
咱們知道,屏幕的大小是有限的,一個Window的DecorView可以控制的屏幕區域更加有限,AndroidN中引入多Window機制後,DecorView能掌控的屏幕區域更加小了,由於屏幕上有多個Window將成爲常態。這些有限的區域還要被Window特有的View(titlebar)佔去一小部分,剩下的纔是留給用戶界面上的View們分的,若是你是DecorView,你確定爲難了,如何將這些有限的屏幕區域分給這些View們?分給他們後還得爲每一個View排好在屏幕上的位置,難上加難。
停一停,想想,若是是你,你怎麼解決這個問題?
首先,不一樣的View是爲了完成特定的交互任務的,好比,Button就是用來點擊的,TextView就是用來顯示字符的,等等。
DecorView知道,不一樣的View爲了完成本身的交互任務所須要的屏幕區域大小是不一樣的,因此DecorView在肯定給每一個View分配的屏幕區域大小時,是容許View參與進來,與它一塊兒商量的。可是每一個View在屏幕區域中的位置就不能讓View本身來決定了,而是由DecorView一手操辦,這個比較簡單,咱們就先來看看DecorView是怎樣決定每一個View的位置的吧。
咱們在Activity中,調用了setContentView(View),實際上就是將用戶界面的全部的View交給了DecorView中的一個FrameLayout,這個FrameLayou表明着能夠分配給用戶界面使用的屏幕區域。而用戶界面View既能夠是一個簡單的View,也能夠是一個ViewGroup,若是是一個簡單的View,好比就是一個TextView,那麼這個TextView就會佔據整個FrameLayout的屏幕區域,也就是說,此時用戶在FrameLayout的屏幕區域內的全部交互都是與這個TextView交互。可是更常見的狀況時,咱們的用戶界面是一個ViewGroup(想一想經常使用的佈局五大金剛),裏面包含着其餘的ViewGroup和View。這個時候,首先這個ViewGroup就會佔據FrameLayout所表明的屏幕區域,剩下的任務,就是這個ViewGroup給它內部的小弟們(各類ViewGroup和各類View)分配區域了。至於怎麼分,不一樣的ViewGroup有不一樣的分法,整體來看,可說是有總有分。所謂總,舉例來說,像vertical的LinearLayout,它按照 本身的小弟數量,把本身豎向裁成不一樣的區域,以下圖所示:
雖然View沒法決定本身在ViewGroup中的位置,可是開發者在使用View時,能夠向ViewGroup表達本身所用的View要放在哪裏,以vertical LinearLayout爲例,開發者書寫佈局文件時,子View在LinearLayout中的出現順序將決定它們在屏幕上的上下順序,同時還能夠藉助layout_margin ,layout_gravity等配置進一步調整子View在分給本身的矩形區域中的位置。
咱們能夠理解,layout_*之類的配置雖然在書寫上與View的屬性在一塊兒,但它們並非View的屬性,它們只是使用該View的使用者用來細化調整該View在ViewGroup中的位置的,同時,這些值在Inflate時,是由ViewGroup讀取,而後生成一個ViewGroup特定的LayoutParams對象,再把這個對象存入子View中的,這樣,ViewGroup在爲該子View安排位置時,就能夠參考這個LayoutParams中的信息了。進一步思考,咱們發現,調用inflate時,除了輸入佈局文件的id外,通常要求傳入parent ViewGroup,傳入這個參數的目的,就是爲了讀取佈局文件中的layout配置信息,若是沒有傳入,這些信息將會丟失。
不一樣的ViewGroup擁有不一樣的LayoutParams內部類,這是由於,它們容許子View調整本身的位置的方式是不同的
,具體講就是配置子View時,容許使用的layout_*是不同的,好比,RelativeLayout就容許layout_toRightOf等配置,其餘的ViewGroup沒有這些配置。
肯定View位置的過程,是被包裝在View 的layout方法中
,這樣也很容易理解,對於基本View而言,這個方法是沒有用的,因此都是空的,你能夠查看下ImageView、TextView等的源代碼,驗證下這一點。對於ViewGroup而言,它們會用該方法爲本身的子View安排位置。
要肯定View的大小,這是一個開發者
、View
與ViewGroup
三方相互商量的過程。
第一步
,開發者在書寫佈局文件時,會爲一個View寫上android:layout_width="\*\*\*" android:layout_height="\*\*\*"
兩個配置,這是開發者向ViewGroup表達的,我這個View須要的大小是多少。星號的取值有三種:
具體值
:如50dp,很簡單,很少講match_parent
:表示開發者向ViewGroup說,把你全部的屏幕區域都給這個View吧。wrap_parent
:表示開發者向ViewGroup說,只要給這個View夠他展現本身的空間就行,至於到底給多少,你直接跟View溝通吧,看它怎麼說。
第二步
,ViewGroup收到了開發者對View大小的說明,而後ViewGroup會綜合考慮本身的空間大小以及開發者的請求,而後生成兩個MeasureSpec
對象(width與height)傳給View,這兩個對象是ViewGroup向子View提出的要求,就至關於告訴子View:「我已經與你的使用者(開發者)商量過了,如今把咱們商量肯定的結果告訴你,你的寬度不能違反width MeasureSpec對象的要求,你的高度不能違反height MeasureSpec對象的要求,如今,你趕忙根據這個要求肯定下本身要多大空間,只許少,不準多哦。」
而後,這兩個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的整體態度:
EXACTLY
表示,ViewGroup對View說,你只能用100dp,緣由是多樣的,多是你的使用者說要你徹底佔據個人空間,而我只有100dp。也可能這是你的使用者的要求,他須要你佔這麼大的空間,而我剛好也有這麼多的空間,你的使用者讓你佔這麼大的空間,確定有他本身的考慮,你不能不理不顧,否則你達不到他的要求,他可能就不用你了。AT_MOST
表示,你最多隻能用100dp,這是由於你的使用者說讓你佔據wrap_content的大小,讓我跟你商量,我又不知道你到底要佔多大區域,可是我告訴你,我只有100dp,你最多也只能用這麼多哈。(這裏,能夠看出,當使用者在佈局文件中要求一個View是wrap_content
時,此時,View的大小決定權就交給View本身了,默認的View類中的實現,比較粗暴,就是將此時ViewGroup提供的空間全佔據,徹底沒有真正根據本身的內容來肯定大小,爲何這麼粗暴?由於View是一個基類,全部的組件都是它的子類,每一個子類的content都各不相同,View怎麼可能知道content的大小呢,因此,它把wrap_content狀況下,本身尺寸大小的決定權下放給了不一樣的子組件,讓它們本身根據本身的內容去決定本身的大小,一樣,咱們自定義View時,也要考慮這一點)UNSPECIFIED
表示,你本身看着辦,把你最理想的大小告訴我,我考慮考慮。
第三步
,好了,子View已經清楚地理解了ViewGroup和它的使用者對它的大小的指望和要求了。下步就要在該要求下來肯定本身的大小並告訴ViewGroup了。(廢話,不告訴ViewGroup大小,它怎麼給你安排位置(layout),沒法給你layout,你也就佔據不了一塊屏幕區域,佔不了屏幕區域,你就沒法與用戶交互,沒法與用戶交互,要你何用啊!)關於子View怎麼肯定本身的大小,不一樣的View有不一樣的態度,可是有幾點基本的規矩是要遵照的:
規矩一就是
,不要違反ViewGroup的規定,最後設置的尺寸必定要在ViewGroup要求的範圍內(不管是寬度仍是高度),可是你說,假如我就是想要更大的空間,難道就沒有辦法了嗎,我能不能遵照要求的狀況下,同時告訴ViewGroup,雖然我告訴你的我要求的尺寸是遵守你的旨意來的,但實際上我是委屈求全的,我真實想要的大小不是這樣的,你能不能再考慮一下。答案是:有。那就是以下調用:resolveSizeAndState((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想要的空間太大時設置的標記了。
規矩二
就是要在該方法中調整本身的繪製參數,這一點很好理解,畢竟ViewGroup提出了尺寸要求,要及時根據這一要求調整本身的繪製,好比,若是本身的背景圖片太大,那就算算要縮放多少才合適,而且設置一個合理的縮放值。
規矩三
就是必定要設置本身考慮後的尺寸,若是不設置就至關於沒有告訴ViewGroup本身想要的大小,這會致使ViewGroup沒法正常工做,設置的辦法就是在onMeasure方法的最後,調用setMeasuredDimension
方法。爲何調用這個方法就能夠了呢?這只是一個約定,沒有必要深究了。
關於View的繪製,很是簡單,就是一個方法onDraw。
以上,View的三個基本知識點,咱們都瞭解了,即View 的位置如何肯定,大小如何肯定以及如何繪製本身。這都是默認的View類中爲咱們準備好的。
好了,View的位置和大小怎麼肯定咱們都清楚了,如今,是時候開始自定義View了。
首先,關於View所要具有的通常功能,View類中都有了基本的實現,好比肯定位置,它有layout方法,固然,這個只適用於ViewGroup,實現本身的ViewGroup時,才須要修改該方法。肯定大小,它有onMeasure方法,若是你不滿意默認的確認大小的方法,也能夠本身定義。改變默認的繪製,就覆寫onDraw方法。下面,咱們經過一張圖,來看看,自定義View時,咱們最可能須要修改的方法是哪些:
把這些方法都搞明白了,你也就理解了View的生命週期了。
好比View被inflated出來後,系統會回調該View的onFinishInflate
方法,你的View能夠在這個方法中,作一些準備工做。
若是你的View所屬的Window可見性發生了變化,系統會回調該View的onWindowVisibilityChanged
方法,你也能夠根據須要,在該方法中完成必定的工做,好比,當Window顯示時,註冊一個監聽器,根據監聽到的廣播事件改變本身的繪製,當Window不可見時,解除註冊,由於此時改變本身的繪製已經沒有意義了,本身也要跟着Window變成不可見了。
當ViewGroup中的子View數量增長或者減小,致使ViewGroup給本身分配的屏幕區域大小發生變化時,系統會回調View的onSizeChanged
方法,該方法中,View能夠獲取本身最新的尺寸,而後根據這個尺寸相應調整本身的繪製。
當用戶在View所佔據的屏幕區域發生了觸摸交互,系統會將用戶的交互動做分解成如DOWN、MOVE、UP等一系列的MotionEvent,而且把這些事件傳遞給View的onTouchEvent
方法,View能夠在這個方法中進行與用戶的交互處理。固然這個是基本的流程,實際的流程會稍複雜些。
除了這些方法,View還實現了三個接口,以下: