Android自定義View:View(二)

zeroXuan發表於2019-05-05

什麼?你說你掌握了自定義View?來來來,試著回答如下問題:

  • Google提出View這個概念的目的是什麼?
  • View這個概念與Activtiy、Fragment以及Drawable之間是一種什麼樣的關係?
  • View能夠感知Activity的生命週期事件嗎?為什麼?

什麼?你說這些問題太抽象?來來來,繼續回答如下問題:

  • View的生命週期是什麼?
  • 當View所在的Activity進入stop狀態後,View去哪了?如果我在一個後臺執行緒中持有一個View的引用,我此時能夠改變它的狀態嗎?為什麼?
  • View能夠與其他的View交叉重疊嗎?重疊區域發生的點選事件交給誰去處理呢?可不可以重疊的兩個View都處理?
  • View控制一個Drawable的方法途徑有哪些?Drawable能不能與View通訊?如果能如何通訊?
  • 假如View所在的ViewGroup中的子View減少了,View因此獲得了更大的空間,View如何及時有效地利用這些空間,改變自己的繪製?
  • 假如我要在View中動態地註冊與解除廣播接收器,應該在哪裡完成呢?
  • 假如我的手機帶鍵盤(自帶或者外接),你的自定義View應該如何響應鍵盤事件。
  • AnimationDrawable作為View的背景,會自動進行動畫,View在其中扮演了怎樣的角色?

其實,說了這麼多,到底怎樣才能學好自定義View?其實只需掌握三個問題,就可以輕鬆搞定它:

  • 問題一:從Android系統設計者的角度,View這個概念究竟是做什麼的?
  • 問題二:Android系統中那個View類,它有哪些預設功能和行為,能幹什麼,不能幹什麼?(知己知彼,才好自定義!)
  • 問題三:我要改變這個View的行為,外觀,肯定是覆寫View類中的方法,但是怎麼覆寫,覆寫哪些方法能夠改變哪些行為?

從Android系統設計者的角度,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是使用者介面元件的基本構建塊。通俗講,在Android中,一個使用者與一個應用的互動,其實就是與這個應用中的許許多多的View的互動,這些View既可以是簡單的View,也可以是若干View組合而成的一個ViewGroup。由此我們可以明白,所謂View是基本構件塊,原因就在於它是複合View(就是ViewGroup)的基本組成單元。這層含義,就是告訴你,View就是用來與使用者互動的,那麼很自然地,我們要問,我們使用者在哪裡與View互動,以及怎樣與View互動呢?

  • View在螢幕上佔據一個矩形區域。這是說,既然View是使用者與應用互動的基本構建塊,而使用者使用Android裝置時,主要是通過一個觸控式螢幕來互動的,相應的,Andorid的設計者們,就讓一個View就在螢幕上佔據一個矩形區域,使用者在這個區域中發生的互動動作(點選、滑動、拖動等),就是與這個View的互動。什麼?為什麼不讓View佔據一個圓形區域或者五角星區域呢?當然是為了簡單。這就解決了在哪裡與View互動的問題。很自然地,我們又想問,View在螢幕上佔據一個矩形區域,這個區域的大小、位置怎麼確定,它們會不會變化,誰來決定這個變化呢?如果這個變化不是由View自己來決定的,而是其他外界因素決定的,View又要怎樣響應這種變化呢?不要急,後面都會有答案。

  • View通過繪製自己與事件處理兩種方式與使用者互動。這是解決了如何互動的問題。簡單講,View與使用者互動就兩個辦法,一個是改變自己的模樣,也就是通過繪製自己與使用者互動,比如,當使用者點選自己時,就改變自己的背景顏色,以此來告訴使用者:“本View已經響應你的點選了!”第二個方式就是事件處理,比如,當使用者點選View時,就完成一定的任務,然後彈出一個Toast,告訴使用者該View完成了什麼任務,這樣,使用者也就知道這次互動結果如何。

現在我們明白了,設計View,主要是為了讓應用能夠與使用者互動,要想完成互動,這個View就要在螢幕上佔據一個矩形區域,然後利用這塊螢幕區域與使用者互動,互動的方式就兩種,繪製自己與事件處理。

Android系統中的View類,它有哪些預設功能和行為?能幹什麼,不能幹什麼?

解決了第一個問題,我們很可能有更多的疑問,我們想知道:

  • View是怎樣被顯示到螢幕上的?
  • View在螢幕上的位置是怎樣決定的?
  • View所佔據的矩形大小是怎樣決定的?
  • 螢幕上肯定不止一個View,View之間互相知道對方嗎?它們之間能協作嗎?
  • View完成與使用者的互動後,能夠自動隱藏,在需要互動的時候重新顯示在螢幕上嗎?

首先,一個使用者介面,上面有許多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的位置的吧。

確定每個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,它按照 自己的小弟數量,把自己豎向裁成不同的區域,如下圖所示:

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的大小,這是一個開發者ViewViewGroup三方相互商量的過程。

  • 第一步,開發者在書寫佈局檔案時,會為一個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物件將會傳到子Viewprotected 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的繪製

關於View的繪製,非常簡單,就是一個方法onDraw。

以上,View的三個基本知識點,我們都瞭解了,即View 的位置如何確定,大小如何確定以及如何繪製自己。這都是預設的View類中為我們準備好的。

我要改變這個View的外觀和行為,肯定是覆寫View類中的方法,但是怎麼覆寫,覆寫哪些方法能夠改變哪些行為?

好了,View的位置和大小怎麼確定我們都清楚了,現在,是時候開始自定義View了。

首先,關於View所要具備的一般功能,View類中都有了基本的實現,比如確定位置,它有layout方法,當然,這個只適用於ViewGroup,實現自己的ViewGroup時,才需要修改該方法。確定大小,它有onMeasure方法,如果你不滿意預設的確認大小的方法,也可以自己定義。改變預設的繪製,就覆寫onDraw方法。下面,我們通過一張圖,來看看,自定義View時,我們最可能需要修改的方法是哪些:

custom_view_override_mothed

把這些方法都搞明白了,你也就理解了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還實現了三個介面,如下:

  • Drawable.Callback:是用來讓View中的Drawable能夠與View通訊的,尤其是AnimationDrawable,更是必須依賴該回撥才能實現動畫效果。
  • KeyEvent.Callback:是用來處理鍵盤事件的,這與onTouchEvent用來處理觸控事件是相對的。
  • AccessibilityEventSource

目錄結構

參考文章

相關文章