1. 程式人生 > >自定義View 篇一--------《自定義View流程分析》

自定義View 篇一--------《自定義View流程分析》

本文部分內容參考自掘金網:點選開啟連結

座標圖解:


概述

Android已經為我們提供了大量的View供我們使用,但是可能有時候這些元件不能滿足我們的需求,這時候就需要自定義控制元件了。自定義控制元件對於初學者總是感覺是一種複雜的技術。因為裡面涉及到的知識點會比較多。但是任何複雜的技術後面都是一點點簡單知識的積累。通過對自定義控制元件的學習去可以更深入的掌握android的相關知識點,所以學習android自定義控制元件是很有必要的。所以,今天寫的是怎麼去自定義一個控制元件。而不是裡面涉及到的細化知識點。一個東西我們先知道怎麼用,再去問為什麼。

自定義控制元件需要考慮的點

根據Android Developers官網的介紹,自定義控制元件你需要以下的步驟。(根據你的需要,某些步驟可以省略)

1、建立View

2、處理View的佈局

3、繪製View

4、與使用者進行互動

5、優化已定義的View

上面列出的五項就是android官方給出的自定義控制元件的步驟。每個步驟裡面又包括了很多細小的知識點。我們可以記住這五個點,並且瞭解每個點裡包含的小知識點。再加上一些自定義控制元件的練習。不斷的將這些知識熟練於心,相信我們每個人都能夠定義出優秀的自定義控制元件。接下來我們開始對上面列出的5個要點進行細化解說

自定義控制元件5個要點詳細說明

1、建立View

繼承View

對Android有一些瞭解的朋友都知道,android為我們提供的很多View都是繼承於View的。所以我們自定義的View當然也是繼承於View,當然如果你要自定義的View擁有某些android已經提供的控制元件的功能,你可以直接繼承於已經提供的控制元件。

我們在使用android提供的控制元件的時候,我們在.xml檔案中編輯了一個控制元件,在執行的時候就能夠看到和獲得這個控制元件。我們自定義的控制元件當然也要支援配置和一些自定義屬性,所以下面的構造方法就必須有了。這個構造方法允許我們在.xml檔案中建立和編輯我們自定義控制元件的例項。

上面說了那麼多其實就是下面一段程式碼。

class PieChart extends View {
    public PieChart(Context context, AttributeSet attrs) {       
        super(context, attrs);    } }

定義自定義屬性

自定義屬性,參考自定義View專欄文章: 自定義View 篇二--------《自定義屬性》

2、處理View的佈局.

在開始寫這一部分之前,穿插入個人之前總結的View的繪製原理的相關知識。

View的繪製原理:

1).測量-measure()---onMeasure();

2).指定在螢幕的位置--layout()--onLayout()

 子類只有建議權,父類才有決定權 

 一般view中不使用,並且原始碼中是空的方法;

 ViewGroup中該方法是抽象的,必須要實現,因為要指定位置孩子的位置

3).繪製控制元件到螢幕上--draw()---onDraw()

自定義View的時候一般重新onMeasure(int,int)和onDraw(canvas);

基本操作由三個函式完成:measure()、layout()、draw(),其內部又分別包含了onMeasure()、onLayout()、onDraw()三個子方法。具體操作如下:

1)、measure操作

     measure操作主要用於計算檢視的大小,即檢視的寬度和長度。在view中定義為final型別,要求子類不能修改。measure()函式中又會呼叫下面的函式:

    (1)onMeasure(),檢視大小的將在這裡最終確定,也就是說measure只是對onMeasure的一個包裝,子類可以覆寫onMeasure()方法實現自己的計算檢視大小的方式,並通過setMeasuredDimension(width, height)儲存計算結果。

2)、layout操作

     layout操作用於設定檢視在螢幕中顯示的位置。在view中定義為final型別,要求子類不能修改。layout()函式中有兩個基本操作:

     (1)setFrame(l,t,r,b),l,t,r,b即子檢視在父檢視中的具體位置,該函式用於將這些引數儲存起來;

     (2)onLayout(),在View中這個函式什麼都不會做,提供該函式主要是為viewGroup型別佈局子檢視用的;

3)、draw操作

     draw操作利用前兩部得到的引數,將檢視顯示在螢幕上,到這裡也就完成了整個的檢視繪製工作。子類也不應該修改該方法,因為其內部定義了繪圖的基本操作:

     (1)繪製背景;

     (2)如果要檢視顯示漸變框,這裡會做一些準備工作;

     (3)繪製檢視本身,即呼叫onDraw()函式。在view中onDraw()是個空函式,也就是說具體的檢視都要覆寫該函式來實現自己的顯示(比如TextView在這裡實現了繪製文字的過程)。而對於ViewGroup則不需要實現該函式,因為作為容器是“沒有內容“的,其包含了多個子view,而子View已經實現了自己的繪製方法,因此只需要告訴子view繪製自己就可以了,也就是下面的dispatchDraw()方法;

     (4)繪製子檢視,即dispatchDraw()函式。在view中這是個空函式,具體的檢視不需要實現該方法,它是專門為容器類準備的,也就是容器類必須實現該方法;

     (5)如果需要(應用程式呼叫了setVerticalFadingEdge或者setHorizontalFadingEdge),開始繪製漸變框;

     (6)繪製滾動條;

      從上面可以看出自定義View需要最少覆寫onMeasure()和onDraw()兩個方法。

ViewGroup中的擴充套件操作:

     首先Viewgroup是一個抽象類。

1)、對子檢視的measure過程

     (1)measureChildren(),內部使用一個for迴圈對子檢視進行遍歷,分別呼叫子檢視的measure()方法;

     (2)measureChild(),為指定的子檢視measure,會被 measureChildren呼叫;

     (3)measureChildWithMargins(),為指定子檢視考慮了margin和padding的measure;

      以上三個方法是ViewGroup提供的3個對子view進行測量的參考方法,設計者需要在實際中首先覆寫onMeasure(),之後再對子view進行遍歷measure,這時候就可以使用以上三個方法,當然也可以自定義方法進行遍歷。

2)、對子檢視的layout過程

     在ViewGroup中onLayout()被定義為abstract型別,也就是具體的容器必須實現此方法來安排子檢視的佈局位置,實現中主要考慮的是檢視的大小及檢視間的相對位置關係,如gravity、layout_gravity。

3、對子檢視的draw過程

   (1)dispatchDraw(),該方法用於對子檢視進行遍歷然後分別讓子檢視分別draw,方法內部會首先處理佈局動畫(也就是說佈局動畫是在這裡處理的),如果有佈局動畫則會為每個子檢視產生一個繪製時間,之後再有一個for迴圈對子檢視進行遍歷,來呼叫子檢視的draw方法(實際為下邊的drawChild());

    (2)drawChild(),該方法用於具體呼叫子檢視的draw方法,內部首先會處理檢視動畫(也就是說檢視動畫是在這裡處理的),之後呼叫子檢視的draw()。

    從上面分析可以看出自定義viewGroup的時候需要最少覆寫onMeasure()和onLayout()方法,其中onMeasure方法中可以直接呼叫measureChildren等已有的方法,而onLayout方法就需要設計者進行完整的定義;一般不需要覆寫以dispatchDraw()和drawChild()這兩個方法,因為上面兩個方法已經完成了基本的事情。但是可以通過覆寫在該基礎之上做一些特殊的效果,比如

其他

      從以上分析可以看出View樹的繪製是一個遞迴的過程,從ViewGroup一直向下遍歷,直到所有的子view都完成繪製,那這一切的源頭在什麼地方(是誰最發起measure、layout和draw的)?當然就是在View樹的源頭了——ViewRoot!,ViewRoot中包含了視窗的總容器DecorView,ViewRoot中的performTraversal()方法會依次呼叫decorView的measure、layout、draw方法,從而完成view樹的繪製。

     invalidate()方法

     invalidate()方法會導致View樹的重新繪製,而且view中的狀態標誌mPrivateFlags中有一個關於當前檢視是否需要重繪的標誌位DRAWN,也就是說只有標誌位DRAWN置位的檢視才需要進行重繪。當檢視呼叫invalidate()方法時,首先會將當前檢視的DRAWN標誌置位,之後有一個迴圈呼叫parent.invalidateChildinParent(),這樣會導致從當前檢視依次向上遍歷直到根檢視ViewRoot,這個過程會將需要重繪的檢視標記DRAWN置位,之後ViewRoot呼叫performTraversals()方法,完成檢視的繪製過程。

測量

一個View是在展示時總是有它的寬和高,我畫的View是一個大象大小還是一個螞蟻大小,因此必須先確定下來。測量View就是為了能夠讓自定義的控制元件能夠根據各種不同的情況以合適的寬高去展示。提到測量就必須要提到onMeasure方法了。onMeasure方法是一個view確定它的寬高的地方。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    }

onMeasure方法裡有兩個重要的引數, widthMeasureSpec, heightMeasureSpec。在這裡你只需要記住它們包含了兩個資訊:mode和size 
我們可以通過以下程式碼拿到mode和size

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

那麼獲取到的mode和size又代表了什麼呢? 
mode代表了我們當前控制元件的父控制元件告訴我們控制元件,你應該按怎樣的方式來佈局。
mode有三個可選值:EXACTLY, AT_MOST, UNSPECIFIED。它們的含義是:

EXACTLY:父控制元件告訴我們子控制元件了一個確定的大小,你就按這個大小來佈局。比如我們指定了確定的dp值和macth_parent的情況。 
AT_MOST:當前控制元件不能超過一個固定的最大值,一般是wrap_content的情況。 
UNSPECIFIED:當前控制元件沒有限制,要多大就有多大,這種情況很少出現。

size其實就是父佈局傳遞過來的一個大小,父佈局希望當前佈局的大小。

下面是一個重寫onMeasure的固定虛擬碼寫法:

if mode is EXACTLY{
     父佈局已經告訴了我們當前佈局應該是多大的寬高, 
    所以我們直接返回從measureSpec中獲取到的size }else{     計算出希望的desiredSize    
    if mode is AT_MOST          返回desireSize和specSize當中的最小值    
    else:          返回計算出的desireSize }

上面的程式碼雖然基本都是固定的,但是需要寫的步驟還是有點多,如果你不想自己寫,你也可以用android為我們提供的工具方法:resolveSizeAndState,該方法需要傳入兩個引數:我們測量的大小和父佈局希望的大小,它會返回根據各種情況返回正確的大小。這樣我們就可以不需要實現上面的模版,只需要計算出想要的大小然後呼叫resolveSizeAndState。之後在做自定義View的時候我會展示用這個方法來確定view的大小。

計算出height和width之後在onMeasure中別忘記呼叫setMeasuredDimension()方法。否則會出現執行時異常。

計算一些自定義控制元件需要的值 onSizeChange()

onSizeChange() 方法在view第一次被指定了大小值、或者view的大小發生改變時會被呼叫。所以一般用來計算一些位置和與view的size有關的值。

3、繪製View(Draw)

一旦自定義控制元件被建立並且測量程式碼寫好之後,接下來你就可以實現onDraw()來繪製View了,onDraw方法包含了一個Canvas叫做畫布的引數,onDraw()簡單來說就兩點: 
Canvas決定要去畫什麼 
Paint決定怎麼畫

比如,Canvas提供了畫線方法,Paint就來決定線的顏色。Canvas提供了畫矩形,Paint又可以決定讓矩形是空心還是實心。

在onDraw方法中開始繪製之前,你應該讓畫筆Paint物件的資訊初始化完畢。是因為View的這重新繪製是比較頻繁的,這就可能多次呼叫onDraw,所以初始化的程式碼不應該放在onDraw方法裡。

Canvas和Paint提供的很多方法在本文中就不一一列舉了。大家可以自己去檢視api,之後的文章中我們也會用到,現在你只需要理解定義的大體步驟,然後再慢慢鍛鍊加深理解。

4、與使用者進行互動

也許某些情況你的自定義控制元件不僅僅只是展示一個漂亮的內容,還需要支援使用者點選,拖動等等操作,這時候我們的自定義控制元件就需要做使用者互動這一步驟了。

在android系統中最常見的事件就是觸控事件了,它會呼叫view的onTouchEvent(android.view.MotionEvent).重寫這個方法去處理我們的事件邏輯

  @Override
   public boolean onTouchEvent(MotionEvent event) {    
       return super.onTouchEvent(event);   }

對與onTouchEvent方法相信大家都有一定了解,如果不瞭解的話,你就先記住這是處理Touch的地方。

現在的觸控有了更多的手勢,比如輕點,快速滑動等等,所以在支援特殊使用者互動的時候你需要用到android提供的GestureDetector.你只需要實現GestureDetector中相對應的介面,並且處理相應的回撥方法。

除了手勢之外,如果有移動之類的情況我們還需要讓滑動的動畫顯示得比較平滑。動畫應該是平滑的開始和結束,而不是突然消失突然開始。在這種情況下,我們需要用到屬性動畫 property animation framework

由於與使用者進行互動中涉及到的知識舉例子會比較多,所以我在之後的自定義控制元件文章中再講解。

5、優化你的自定義View

在上面的步驟結束之後,其實一個完善的自定義控制元件已經出來了。接下來你要做的只是確保自定義控制元件執行得流暢,官方的說法是:為了避免你的控制元件看得來遲緩,確保動畫始終保持每秒60幀.

下面是官網給出的優化建議:

1、避免不必要的程式碼 
2、在onDraw()方法中不應該有會導致垃圾回收的程式碼。 
3、儘可能少讓onDraw()方法呼叫,大多數onDraw()方法呼叫都是手動呼叫了invalidate()的結果,所以如果不是必須,不要呼叫invalidate()方法。

總結

到這裡基本上自定義控制元件的大致步驟和可能涉及到的知識點都說完了。看一張圖。


圖片基本描述了自定義控制元件的大致流程,右邊是相對應的流程所涉及到的一些知識點。可以看到自定義控制元件包括了很多android知識。網上還有一張自定義View的圖片比較清晰,如下:


本篇文章只對自定義控制元件的步驟進行了大體的介紹,如果你對自定義的流程還不清楚,請你記住上面所說的步驟,至少也要有個大致的印象。在之後的文章中,我在按照這個步驟去講解一些自定義控制元件,用這裡列出的步驟去一步步實現我們想要的自定義控制元件。

有點幫助的話可以關注哈,有問題一起交流。也可以動手微信掃描下方二維碼檢視更多安卓文章:

開啟微信搜尋公眾號  Android程式設計師開發指南  或者手機掃描下方二維碼 在公眾號閱讀更多Android文章。

微信公眾號圖片: