解讀創建自定義組件 – Android移動開發技術文章_手機開發 Android移動開發教學課程

自定義組件
   Android系統為用戶創建自己的UI提供瞭功能強大的組件模型,這個模型是基於View和ViewGroup這些基本的佈局類。Android系統包含瞭預先制作好的View和ViewGroup的子類————分別是widgets(窗口部件)和layouts(佈局)————你可以使用這些已經提供的子類構建自己的UI,在剛開始接觸Android開發時,我們都是使用這些系統提供的,然而,隨著項目需要,這些基本的UI組件已經不能滿足我們的需要瞭,這個時候就需要我們遵循組件模型來創建自定義UI組件。
  widgets(窗口部件)主要包含瞭如下部分:Button、TextView、EditView、ListView、CheckBox、RadioButton、Gallery、Spinner,當然,也包括一些具有特殊用途的widgets:AutoCompleteTextView、ImageSwitcher和TextSwitcher。
  layouts(佈局)主要包含瞭:LinearLayout、FrameLayout、RelativeLayout和其他的。更多的示例,請看Common Layout Objects。
  當已經存在的組件不能滿足需求,我們就需要創建自己的View子類。如果,你僅僅是需要對存在的widget或layout進行一些小的調整,你可以簡單的創建該widget或layout的子類並且覆蓋其中的方法。
  通過創建自己的View子類可以使自己精確的控制屏幕上每個元素的顯示和其功能。這裡列出瞭一些示例,給出瞭如何控制自定義組件的思路:
  *你可以創建一個完全自定義渲染的View類型,例如:使用2D圖形渲染“音量控制”旋鈕,使其看起來像是一個模擬電量控制。
  *你可以將一組View組件結合起來形成一個新的單個的組件,可能是使其像一個ComboBox(是由popup list和一個沒有任何條目的text組合而成),一個dual-pane選擇控制器(左右各有一個面板,並且每個面板中都有一個list,你可以通過指定一個pane中的內容使另一個pane隨之改變,說瞭這麼多,其實就是大傢經常使用的天氣預報中第一個下拉框中選擇省份,隨之第二個下拉框會隨之變為該省份中包含的城市,這下懂瞭吧,呵呵!)等等示例。
  *你可以覆蓋EditeText組件在屏幕上渲染的方式(NotePad Tutorial使用這個方法創建一個lined-notepad頁面)。
  *你可以捕獲其他一些事件,例如按鍵並且通過自定義的方式處理這些事件(例如在一個遊戲中)。
  下面的內容解釋瞭如何創建自定義的Views並且在應用程序中使用這個自定義組件。對於詳盡的參考信息,請看View類。
 
基本的方法
   站在一個較高的層次上來看,要創建一個自定義的View組件,那麼你必須知道如下內容:
   1、讓自己的類繼承自存在的View類或者是View的子類。
   2、覆蓋超類的一些方法。要覆蓋的超類的方法都是以”on“開頭的,例如:onDraw()、ONMeasure()和onKeyDown()。這有點類似於Acitivity或ListActivity的on…事件,你為它們的生命周期和完成其他的功能而覆蓋這些方法(on…事件).
   3、使用你自己新建的擴展類。一旦完成,你可以在能夠使用基類的地方使用自定義的這個擴展類。
   提示:擴展類可以作為內部類在要使用這個擴展類的activities中定義。這一點不是必須的,但是卻是非常有用的,因為它控制對擴展類的使用權限。
完全自定義的組件
   完全自定義的組件可以用來創建圖形組件在任何你想要顯示的地方顯示。可能是一個看起來像老式模擬計的圖形VU計,或者是一個帶有前進的球的長文本框,球隨著字符移動,使得你可以用一臺卡拉OK機唱歌。還有,你需要做一些事情,可是無論怎麼組合現有的組件都無法達到想要的效果(這個時候就需要完全自定義的組件)。
   幸運地是,你可以很容易創建出樣式和功能都符合自己需求的組件,限制的因素僅僅是你的想象力、屏幕的尺寸和可用的電量(必須記住,你的應用程序通常都是在比桌面環境電量低得多的設備上運行)。
   創建一個完全自定義的組件:
   1、毫無疑問,最常繼承的就是View。因此,你通常是繼承這個類來實現自定義組建的;
   2、你可以提供一個構造器來從XML文件中獲取和解析屬性及其參數,並且你可以自定義屬性及參數(可能是VU表的顏色和取值范圍或者是面條的寬度等等);
   3、你可能希望創建自己的事件監聽器,屬性的accessors(獲取器)和modifiers(修改器)或者是其他更復雜的動作。
   4、如果你希望通過自定義組件顯示一些事物,那麼你幾乎總是需要覆蓋onMeasure()並且經常需要覆蓋onDraw()。以上兩者都包含默認的動作,onDraw()的默認動作是不執行任何操作,onMeasure()的默認動作是設置顯示尺寸是100X100————而這個默認尺寸可能並不是你所想要的(尺寸)。
   5、其他的on…的方法在需要的時候也必須覆蓋。
擴展onDraw()和onMeasure()
   onDraw()方法傳遞一個Canvas給你,你可以在這個Canvas上面完成自己想要的效果:2D圖形、其他標準的或自定義的組件、styled text、或者其他任何你想要的事物。
   註意:這個並不能用來完成3D圖形。如果你想使用3D圖形,那麼你必須擴展SurfaceView來取代View,並且在一個獨立的進程中完成繪制。對於細節部分可以查看GLSurfaceViewActivity示例。
   onMeasure()要涉及得更多一點,onMeasure()是自定義組件和包含它的容器間的rendering contract非常嚴格的一個部分。onMeasure()必須被覆蓋來非常高效和準確的報告它所包含部分的尺寸。來自父組件(傳遞進onMeasure方法的部分)的限制和通過測量的尺寸來調用setMeasureDimension()方法都將會使得這變得有一點點復雜。如果你從一個覆蓋的onMeasure()方法中調用這個方法(setMeasureDimension())失敗,那麼在測量的時候將會引起異常。
   站在一個較高的層次來看,實現onMeasure()就如同下面這樣:
   1、覆蓋的onMeasure()方法是隨著寬度和高度測量規范(widthMeasureSpec和heightMeasureSpec參數,都是代表尺寸的整型數)來調用的。對於這些規范可以獲取的限制種類的完整參考可以在View.onMeasure(int, int)這個文檔中找到(這個參考文檔對完整的測量操作作出瞭比較好的解釋)。
   2、你自定義組件的onMeasure()方法必須計算出一個寬度和高度值,這兩個值是用來渲染組件用來。這兩個值必須處於傳遞進去的測量規范之內,盡管它可以選擇擴展它們(寬度和高度這兩個值)(在這種情況下,父組件可以選擇如何處理:包括可以翻轉、滾動、拋出一場或者是讓onMeasure()以不同的測量規范重試)。
   3、一旦寬度和高度計算好瞭之後,必須用得到的這兩個計算值作為參數來調用setMeasureDimension(int width, int height)方法。如果這個操作失敗瞭,那麼將會引起拋出異常。
   下面是對應用程序框架將會在views上面調用的標準方法的一個總結,如下圖:

一個自定義View的例子
   在API Demos子中提供的 Custom View例子是解釋創建自定義View的很好的例子。Custom View例子中自定義的View是定義再LabelView這個類中的。
   LabelView展示瞭一個自定義組件很多不同的方面:
   *通過擴展View類來創建一個完全自定義的組件;
   *參數化的構造器,使得可以解析參數(參數定義在XML文件中)。參數中的一部分是要傳遞給View這個基類的,但是更重要的,這個例子裡還為LabelView這個組件定義瞭一些自定義屬性。
   *使用瞭label這個組件類型的標準公共方法,例如:setText()、setTextSize()、setTextColor()等等。
   *使用瞭覆蓋的onDraw()方法,將這個label畫在提供的canvas上。
   你可以在custom_view_1.xml這個文件中看到LabelView的一些示例用法。特別的是,你將會看到混合使用瞭android:namespace參數和自定義的app:namespace參數。LabelView可以識別這些app:參數,並工作良好,這些參數都定義在示例的R資源定義類的一個styleable內部類中。
<!– Demonstrates defining custom views in a layout file. –> 
 
 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
        xmlns:app="http://schemas.android.com/apk/res/com.example.android.apis" 
        android:orientation="vertical" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content"> 
 
 
    <com.example.android.apis.view.LabelView 
            android:background="@drawable/red" 
            android:layout_width="match_parent" 
            android:layout_height="wrap_content"  
            app:text="Red"/> 
 
 
    <com.example.android.apis.view.LabelView 
            android:background="@drawable/blue" 
            android:layout_width="match_parent" 
            android:layout_height="wrap_content"  
            app:text="Blue" app:textSize="20dp"/> 
 
 
    <com.example.android.apis.view.LabelView 
            android:background="@drawable/green" 
            android:layout_width="match_parent" 
            android:layout_height="wrap_content"  
            app:text="Green" app:textColor="#ffffffff" /> 
 
 
</LinearLayout> 
<!– Demonstrates defining custom views in a layout file. –>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res/com.example.android.apis"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <com.example.android.apis.view.LabelView
            android:background="@drawable/red"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:text="Red"/>

    <com.example.android.apis.view.LabelView
            android:background="@drawable/blue"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:text="Blue" app:textSize="20dp"/>

    <com.example.android.apis.view.LabelView
            android:background="@drawable/green"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:text="Green" app:textColor="#ffffffff" />

</LinearLayout>組合控制
   如果你不想創建一個完全自定義的組件,而是想將已經存在的一組組件組合在一起使用,這個時候就應該創建一個組合組件(或者叫組合控制),來滿足需求。概括地說,就是將一組組件已經邏輯放在一起,然後將這一組組件當作一個整體來操作。例如:一個Combo Box可以通過一個單行的EditText和一個帶有PopupList的按鈕組合而成。如果按下按鈕,並且從list中選擇瞭一個項,那麼你選擇的項就會填充這個EditText,但是如果用戶願意,他們也可以直接再EditText中手動輸入內容而不是從list中選擇。
   在Android系統中,實際上已經提供瞭另外兩個組件可以完成這件事:Spinner和AutoCompleteTextView,但是,以一個Combo Box作為一個例子更容易理解。
   要創建一個組合組件:
   1、通常是從某種Layout入手,因此創建一個類擴展一個Layout。在之前舉的Combo Box例子中,我們可以擴展一個水平放置的LinearLayout。要記住的是其他的layouts可以嵌套在其中,因此組合組件可以任意構造和及其復雜。註意,就好像Activity一樣,你可以通過XML文件類創建包含的組件,也可以通過編碼實現佈局。
   2、在新類的的構造器中,接收基類可以接收的任意參數,並且首先將他們傳遞給基類的構造器。這樣你就可以在自己的組件中使用其他的組件瞭;這裡就是你可以創建EditText和PopupList的地方瞭。註意:你同樣可以在XML文件中定義自己的屬性和參數,這些參數可以被你的構造器使用。
   3、如果你包含的組件可以產生事件,那麼你同樣可以為這些事件創建事件監聽器,例如:給一個被選中的列表項設置事件監聽器來更新EditText的內容。
   4、你同樣可以為accessors(獲取器)和modifiers(修改器)定義自己的屬性,例如:可以在組件中為EditText指定一個初始值,並且在需要的時候可以查詢它的值。
   5、在擴展一個Layout的情況下,你不需要覆蓋onDraw()和onMeasure()方法,因為layout已經包含瞭默認的行為,可以使其正常工作。然而,如果需要,你同樣可以覆蓋他們。
   6、你也可以覆蓋其他以on..開頭的方法,例如:onKeyDown(),當一個特定的鍵被按下的時候,可以從一個combo box的popup list中選取一個默認的值。
   總結一下:用Layout作為Custom Control的基礎有一系列的好處,包括:
   *你可以像一個Activity一樣使用XML文件來定義佈局,也可以通過編碼來實現佈局
   *onDraw()和onMeasure()方法(大多數的以on..開頭的方法)將會有瞭合適的動作,因此你不必覆蓋他們
   *最後,你可以快速的構建任何復雜的組件,並且可以將他們作為一個當個的組件復用。
組合控制的示例:
   在隨著SDK發佈的API Dmeos中,包含瞭兩個List示例————在Views/Lists中的Example4和Example6展示瞭一個擴展自LinearLayout的SpeechView來顯示Speech quotes。相應的類包含在示例代碼的List4.java和List6.java中。
修改一個已經存在的View類型
   在特地的環境下,這裡還有一種非常簡單的創建自定義View的方法。如果已經存在一種和你想要的組件非常相近的組件,你可以僅僅擴展這個組件並且隻覆蓋其中你想改變的部分。你可以通過創建一個完全自定義組件來完成所有這些事情,但是如果你從存在於View層級結構中的特殊的類入手,你就可以獲得許多你可能想得到的效果,這將大大減小自己的工作量。
   例如:SDK包含瞭一個NotePad 的示例應用程序。這個應用程序展示瞭Android平臺的許多特性,在這個示例中通過擴展一個EditText View來創建一個lined notepad。這不是一個特別恰當的示例,用來完成這個操作的APIs也許已經隨著系統版本的變化而被改變瞭,但是它已經足夠表明期中的原理瞭。
   如果你沒這樣做,那麼將NotePad示例導入Eclipse中(或者是通過下面提供的鏈接來查看源文件)。特別是要仔細看看定義在NoteEditor.java文件中的MyEditText。
   一些需要註意的點如下:
   1、定義:
   這個類是像下面這樣定義的:
   public static class MyEditText extends EditText
   *它是作為NoteEditor的一個內部類定義的,但是它的訪問權限是public,所以你可以在NoteEditor類的外部通過NoteEditor.MyEditText來使用這個類。
   *這個類也是靜態的,意味著它不用產生所謂的允許父類操作數據的"synthetic methods",這也意味著它是作為一個獨立的類而不是和NoteEditor強關聯的類。如果它們不需要從外部類獲取狀態,那麼這就是一種幹凈的創建內部類的方法,確保生成的類是十分小巧的,並且可以在其他類中十分方便的使用。
   *它繼承自EditText,我們在這裡選擇它來進行我們的自定義View。當我們完成瞭自定義工作後,這個新類就可以代替普通的EditText瞭。
   2、類初始化:
   一如往常,最先調用的是超類。但是,這不是默認構造器,而是包含一個參數的構造器。EditText是通過從XML layout文件中解析這些參數來創建的,因此,我們的構造器需要接收這些參數並且將他們傳遞給超類的構造器。
   3、覆蓋的方法:
   在這個例子中,這裡僅僅有一個被覆蓋的方法:onDraw()————但是,當你需要的時候,你也可以很容易覆蓋其他的方法來創建自定義的組件。
   在NotePad示例中,覆蓋onDraw()方法允許我們在EditText view的canvas(傳遞進onDraw()方法的canvas)上面畫綠色的線。在這個方法完成之前,我們要掉用super.onDraw()這個方法。超類的方法應該被調用,但是在這種情況下,我們是在完成繪制我們想要包含的部分後再去調用這個方法的。
   4、使用自定義組件:
   此時,我們已經完成瞭自定義組件,但是我們怎樣來使用這個自定義組件呢?在NotePad這個例子中,我們是直接在layout佈局文件中使用的,因此看一下res/layout目錄下的note_editor.xml文件:
<view 
class="com.android.notepad.NoteEditor$MyEditText"  
id="@+id/note" 
android:layout_width="fill_parent" 
android:layout_height="fill_parent" 
android:background="@android:drawable/empty" 
android:padding="10dip" 
android:scrollbars="vertical" 
android:fadingEdge="vertical" /> 
   <view
  class="com.android.notepad.NoteEditor$MyEditText"
  id="@+id/note"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:background="@android:drawable/empty"
  android:padding="10dip"
  android:scrollbars="vertical"
  android:fadingEdge="vertical" />  *自定義組件是作為一個一般的組件在XML文件中創建的,並且自定義組件類是通過完整的包名來指定的。註意:我們是通過NoteEditor$MyEditText這個符號來引用內部類的。這是JAVA語言中使用內部類的標準方法。
  如果你的自定義組件不是作為一個內部類定義的,這是你也可以選擇使用XML元素的名字(但是要去掉class這個屬性)來聲明自定義的View組件,例如:
<com.android.notepad.MyEditText 
id="@+id/note" 
… /> 
  <com.android.notepad.MyEditText
  id="@+id/note"
  … />  註意,此時MyEditText類是一個單獨的類文件。當這個類嵌套在NoteEditor類中,這個技術就不會工作瞭,所以一定要是單獨的文件。
  *在定義中的其他屬性和參數都是傳遞給自定義組建的構造器,然後傳遞給EditText構造器,因此,他們和你使用EditText的參數都是一樣的。註意:你也可以添加自己的參數,我們將會在下面介紹這個方面的內容。
  這就是所有的內容瞭,誠然這是比較簡單的事情,但是這一點很重要————依據自己的需要來創建自定義組件(滿足需求即可,不必創建過於復雜的自定義組件)。
  一個功能更加強大的組件可能需要覆蓋更多以on..開頭的方法並且需要引進一些自己的方法,實現自定義的屬性的動作。惟一的限制就是你的想象力和你需要這個組件組的工作。

摘自 chenlong12580的專欄

發佈留言