自訂群組件
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的專欄