- Android應用程序開發與典型案例
- 鄭萌 趙常松等編著
- 4703字
- 2018-12-27 18:22:15
第5章 用戶界面開發
在上一章的學習中,主要了解了Android系統的進程優先級排序、不同優先級進程之間的變化方式,Android系統的4大基本組件及其用途,Activity的生命周期中各個狀態及狀態間的變化關系、Android應用程序的調試方法和工具。在此基礎上,本章將對Android程序界面開發的學習,包括用戶界面基礎、用戶界面的控件的使用、界面布局的特點及使用方法、菜單的使用方法、界面事件的處理方法等。
5.1 用戶界面基礎
用戶界面(User Interface,UI)是系統和用戶之間進行信息交換的媒介,實現信息的內部形式與用戶可以接收形式之間的轉換。
在Android系統中,Android自帶有許多要求,預示著其用戶界面的復雜性:它是一個支持多個并發應用程序的多處理系統,接受多種形式的輸入,有著高交互性,必須具有足夠的靈活性,以支持現在和未來廣泛的設備。令人印象深刻的是豐富的用戶界面及其易用性,實現了所有給定的功能。但為了使用應用程序在不同的設備上正常的顯示以及運行,避免對系統性能造成過大的負擔,應該明白其工作原理。
Android使用XML文件描述用戶界面;資源文件獨立保存在資源文件夾中;對用戶界面描述非常靈活,允許不明確定義界面元素的位置和尺寸,僅聲明界面元素的相對位置和粗略尺寸。以下就來介紹一下Android的用戶界面框架。
Android是在Java環境中增加了一個圖形用戶界面(GUI)工具包,聯合了AWT,Swing,SWT,和J2ME(撇開Web UI的工具包)。Android框架和他們一樣,它是單線程的,事件驅動的,并建立一個嵌套的組件庫。
Android用戶界面框架(Android UI Framework),像其他的UI框架一樣,采用了MVC(Model-View-Controller)模型,提供了處理用戶輸入的控制器(Controller),顯示用戶界面和圖像的視圖(View),以及保存數據和代碼的模型(Model)。

圖5-1 Android用戶界面框架MVC模型
其中Model是應用程序的核心。雖然特定應用程序的視圖(View)和控制器(Controller)必然反映他們操縱的Model,但一個Model可能是由幾個不同的應用使用。想想看,例如,一個MP3播放器的應用程序以及一個將MP3文件轉換成WAV MP3文件的程序,對于這兩個應用程序,Model包括它的MP3文件格式和編解碼器。然而,前者的應用程序,有熟悉的停止,啟動和暫停控制等操作。后者可能不會產生任何聲音;相反,它會設置比特率的控制等。此時,他們的Model都是對所有的文件數據。
其中的控制器(Controller)能夠接收并響應程序的外部動作,如按鍵動作或觸摸屏動作等。控制器使用隊列處理外部動作,每個外部動作作為一個對應的事件被加入隊列中,然后Android用戶界面框架按照“先進先出”的規則從隊列中獲取事件,并將這個事件分配給所對應的事件處理方法。例如,當用戶按下他的手機上的鍵,Android系統生成的KeyEvent,并將其添加到事件隊列中。最后,在之前已排隊的事件被處理后,KeyEvent是從隊列中刪除的,并作為當前選擇View的dispatchKeyEvent方法的調用參數傳遞。一旦事件被分派到的焦點組件,該組件可能會采取適當的行動來改變程序的內部狀態。例如,在MP3播放器應用程序中,當用戶點擊屏幕上的播放/暫停按鈕時,觸發該按鈕的事件,處理方法可能更新Model,恢復播放一些先前所選樂曲。
視圖(View)是應用程序給用戶的反饋。它負責應用程序的部分渲染顯示,發送音頻揚聲器,產生觸覺反饋等。視圖部分應用視圖樹(View Tree)模型。視圖樹是由Android用戶界面框架中的界面元素以一種樹形結構組織在一起的,Android系統會依據視圖樹的結構從上至下繪制每一個界面元素。每個元素負責對自身的繪制,如果元素包含子元素,該元素會通知其下所有子元素進行繪制。
下面就來詳細介紹一下視圖樹。Android當中的可視化界面單元,可分為“容器”與“非容器”兩類,容器類繼承ViewGroup,非容器類則從View衍生出來,如圖5-2所示。

圖5-2 Android視圖樹(View Tree)
視圖樹由View和ViewGroup構成。其中,View是界面的最基本的可視單元,存儲了屏幕上特定矩形區域內所顯示內容的數據結構,并能夠實現所占據區域的界面繪制、焦點變化、用戶輸入和界面事件處理等功能。同時View也是一個重要的基類,所有在界面上的可見元素都是View的子類。ViewGroup是一種能夠承載含多個View的顯示單元,它承載了界面布局,同時還承載了具有原子特性的重構模塊。
如圖5-3所示,這些Layout可以套疊式地組成一棵視圖樹。其中,父節點的Layout與子節點的LayoutParams之間有控制關系,例如,若父節點是RelativeLayout,則子節點的單元中可以指定RelativeLayout.LayoutParams中的屬性,以控制子節點在父節點中的排列狀況。

圖5-3 ViewGroup樹形層次結構
在單線程用戶界面中,控制器從隊列中獲取事件和視圖在屏幕上繪制用戶界面,使用的都是同一個線程。這樣的單線程用戶界面使得處理方法具有順序性,能夠降低應用程序的復雜程度,同時也能降低開發的難度。
問:單線程用戶界面有什么缺點呢?
答:如果事件處理方法過于復雜,可能會導致用戶界面失去響應。
5.2 界面布局
界面布局(Layout)是用戶界面結構的描述,定義了界面中所有的元素、結構和相互關系。
界面布局(Layout)是為了適應多種Android設備上的屏幕而設計的解決方案:它們可以有不同的像素密度、尺寸和不同的縱橫比。典型的Android設備,如HTC G1手機,甚至允許應用程序運行時改變屏幕的方向(縱向或橫向),因此布局的基礎設施需要能夠應對這種情況。布局的目的是為開發人員提供一種方式來表示View之間的物理關系,因為它們是在屏幕上繪制。作為Android的界面布局,它使用開發需求來滿足與開發要求最接近的屏幕布局。
Android開發者使用術語“布局”,指的是兩種含意中的一種。布局的兩種定義如下。
? 一種資源,它定義了在屏幕上畫什么。布局資源存儲在應用程序的/res/layout資源目錄下的XML文件中。布局資源簡單地說就是一個用于用戶界面屏幕,或屏幕的一部分,以及內容的模板。
? 一種視圖類,它主要是組織其他控件。這些布局類(LinearLayout,RelativeLayout,TableLayout等)用于在屏幕上顯示子控件,如文本控件、按鈕或圖片。
Eclipse的Android開發插件包含了一個很方便的用于設計和預覽布局資源的布局資源設計器。這個工具包括兩個標簽視圖:布局視圖允許你預覽在不同的屏幕下及對于每一個方向控件會如何展現;XML視圖告訴你資源的XML定義。
這里有一些關于在Eclipse中使用布局資源編輯器的技巧。
? 使用概要(Outline)窗格在你的布局資源中添加和刪除控件。
? 選擇特定的控件(在預覽或概要窗口)并使用屬性窗格來調整特定控件的屬性。
? 使用XML標簽來直接編輯XML定義。
很重要的是要記住一點,Eclipse布局資源編輯器不能完全精確的模擬出布局在最終用戶設備上的呈現形式。對此,必須在適當配置的模擬器中測試,更重要的是在目標設備上測試。而且一些“復雜”控件,包括標簽或視頻查看器,也不能在Eclipse中預覽。
聲明Android程序的界面布局有兩種方法。
? 使用XML文件描述界面布局。
? 在程序運行時動態添加或修改界面布局。
用戶既可以獨立使用任何一種聲明界面布局的方式,也可以同時使用兩種方式。
使用XML文件聲明界面布局有以下3 個特點:將程序的表現層和控制層分離;在后期修改用戶界面時,無須更改程序的源代碼;用戶還能夠通過可視化工具直接看到所設計的用戶界面,有利于加快界面設計的過程,并且為界面設計與開發帶來極大的便利性。
設計程序用戶界面最方便且可維護的方式是創建XML布局資源。這個方法極大地簡化了UI設計過程,將許多用戶界面控件的布局,以及控件屬性定義移到XML中,代替了寫代碼。它適應了UI設計師(更關心布局)和開發者(了解Java和實現應用程序功能)潛在的區別。開發者依然可以在必要時動態地改變屏幕內容。復雜控件,像ListView或GridView,通常用程序動態地處理數據。
XML布局資源必須存放在項目目錄的/res/layout下。對于每一屏(與某個活動緊密關聯)都創建一個XML布局資源是一個通用的做法,但這并不是必需的。理論上來說,可以創建一個XML布局資源并在不同的活動中使用它,為屏幕提供不同的數據。如果需要的話,也可以分散布局資源并用另外一個文件包含它們。
現在把注意力轉向對組織其他控件很有用的布局控件。Android中Layout的列表,如表5-1所示。
表5-1 Layout分類表

5.2.1 線性布局(LinearLayout)
線性布局是最簡單的布局之一,它提供了控件水平或者垂直排列的模型。如圖5-4 所示,線性布局中,所有的子元素如果垂直排列,則每行僅包含一個界面元素;如果水平排列,則每列僅包含一個界面元素。

圖5-4 線性布局(LinearLayout)效果圖
同時,使用此布局時可以通過設置控件的Weight參數控制各個控件在容器中的相對大小。LinearLayout布局的屬性既可以在布局文件(XML)中設置,也可以通過成員方法進行設置。表5-2給出了LinearLayout常用的屬性及這些屬性的對應設置方法。
表5-2 LinearLayout常用屬性及對應方法

在線性布局中可使用gravity屬性來設置控件的對齊方式,gravity可取的值及說明如表5-3所示。
表5-3 gravity可取的屬性及說明

提示:當需要為gravity設置多個值時,用“|”分隔即可。
以下用一個線性布局的例子來加深對線性布局的理解。
1.創建一個名為LinearLayout的Android工程
包名稱是edu.hrbeu.LinearLayout,Activity名稱為LinearLayout。為了能夠完整體驗創建線性布局的過程,我們需要刪除Eclipse自動建立的/res/layout/main.xml文件,之后我們將手動創建一個XML布局文件。
2.建立XML線性布局文件
首先,刪除Eclipse自動建立的/res/layout/main.xml文件;其次,建立用于顯示垂直排列線性布局的XML文件:右擊/res/layout文件夾,選擇“New”→“File”命令打開新文件建立向導,文件名為main_vertical.xml,保存位置為LinearLayout/res/layout,如圖5-5所示。

圖5-5 新建線性布局XML文件
3.編輯XML線性布局文件
打開XML文件編輯器,對main_vertical.xml文件的代碼做如代碼清單5-1所示的修改。
代碼清單5-1 main_vertical.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> </LinearLayout>
第2行代碼是聲明XML文件的根元素為線性布局;第4、5、6行代碼是在屬性編輯器中修改過的寬度、高度和排列方式的屬性。同樣地,用戶可以在可視化編輯器和屬性編輯器中對頁面布局進行修改,這些修改會同步地反映在XML文件中。
4.添加控件
將四個界面控件TextView、EditText、Button、Button先后拖曳到可視化編輯器中,所有控件都自動獲取控件名稱,并把該名稱顯示在控件上,如TextView01、EditText01、Button01和Button02。

圖5-6 線性布局添加控件
修改界面控件的屬性如表5-4所示。
表5-4 線性布局控件屬性

打開XML文件編輯器查看main_vertical.xml文件代碼,發現在屬性編輯器內填入的文字已經正常寫入XML文件中,如代碼清單5-2中第11、20、25行代碼。
代碼清單5-2 main_vertical.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="用戶名: " > </TextView> <EditText android:id="@+id/entry" android:layout_height="wrap_content" android:layout_width="match_parent"> </EditText> <Button android:id="@+id/ok" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="確認"> </Button> <Button android:id="@+id/cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消" > </Button> </LinearLayout>
5.修改LinearLayout.java文件
將LinearLayout.java文件中的setContentView(R.layout.main),更改為setContentView (R.layout.main_vertical)。
同理,按照以上步驟,可以得到橫向線性布局。
? 建立main_ horizontal.xml文件。
? 線性布局的Orientation屬性的值設置為horizontal。
? 將EditText的Layout width屬性的值設置為wrap_content。
? 將LinearLayout.java文件中的setContentView(R.layout.main_vertical)修改為setContentView(R.layout.main_ horizontal)。
5.2.2 框架布局(FrameLayout)
框架布局(FrameLayout)是最簡單的界面布局,它在屏幕上開辟出了一塊區域,在這塊區域中可以添加多個子控件,但是所有的子控件都被對齊到屏幕的左上角。框架布局的大小由子控件中尺寸最大的那個子控件來決定。如果子控件一樣大,同一時刻只能看到最上面的子控件。
FrameLayout繼承自ViewGroup,除了繼承自父類的屬性和方法,FrameLayout類中包含了自己特有的屬性和方法,如表5-5所示。
表5-5 FrameLayout常用屬性及對應方法

提示:
在FrameLayout中,子控件是通過棧來繪制的,所以后添加的子控件會被繪制在上層。
以下用一個FrameLayout的例子來加深對FrameLayout的理解。
(1)在Eclipse中新建一個項目FrameLayout。打開其res/values目錄下的strings.xml,在其中輸入如代碼清單5-3所示代碼。在該段代碼中聲明了應用程序總會用到的字符串資源。
代碼清單5-3 strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">FrameExample</string> <string name="big">大的</string> <string name="middle">中的</string> <string name="small">小的</string> </resources>
(2)在項目rers/values目錄下新建一個colors.xml,在其中輸入如代碼清單5-4所示代碼。該段代碼聲明了應用程序中將會用到的顏色資源。這樣將所有顏色資源統一管理有助于提高程序的可讀性及可維護性。
代碼清單5-4 colors.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="red">#FF0000</color> <color name="green">#00FF00</color> <color name="blue">#0000FF</color> <color name="white">#FFFFFF</color> </resources>
(3)打開項目res/layout目錄下的main.xml文件,將其中已有的代碼替換為如代碼清單5-5所示代碼。
代碼清單5-5 main.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout android:id="@+id/FrameLayout01" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" xmlns:android="http://schemas.android.com/apk/res/android">< <TextView android:text="@string/big" android:id="@+id/TextView01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="60px" android:textColor="@color/green" > <!-- 聲明一個TextView控件 --> </TextView> <TextView android:text="@string/middle" android:id="@+id/TextView02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="40px" android:textColor="@color/red" > <!-- 聲明一個TextView控件 --> </TextView> <TextView android:text="@string/small" android:id="@+id/TextView03" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20px" android:textColor="@color/blue" > <!-- 聲明一個TextView控件 --> </TextView> </FrameLayout>
代碼第2~7行聲明了一個框架布局,并設置其在父控件中的顯示方式及自身的背景顏色;代碼第8~16行聲明了一個TextView控件,該控件ID為TextView01,第13行定義了其顯示內容的字號為60px,第14行定義了所顯示內容的字體顏色為綠色;代碼第17~25行聲明了一個TextView控件,該控件ID為TextView02,第22行定義了其顯示內容的字號為40px,第23行定義了所顯示內容的字體顏色為紅色;代碼第26~34行聲明了一個TextView控件,該控件id為TextView03,第22行定義了其顯示內容的字號為20px,第23行定義了所顯示內容的字體顏色為藍色。
運行程序,在圖5-7 所示的運行效果圖中可以看到,程序運行時所有的子控件都自動地對齊到容器的左上角,由于子控件的TextView是按照字號從大到小排列的,所以字號小的在最上層。

圖5-7 框架布局運行效果圖
5.2.3 表格布局(TableLayout)
TableLayout類以行和列的形式管理控件,每行為一個TableRow對象,也可以為一個View對象,當為View對象時,該View對象將跨越該行的所有列。在TableRow中可以添加子控件,每添加一個子控件為一列。
TableLayout布局中并不會為每一行、每一列或每個單元格繪制邊框,每一行可以有0或多個單元格,每個單元格為一個View對象。TableLayout中可以有空的單元格,單元格也可以像HTML中那樣跨越多個列。
圖5-8是表格布局的示意圖。

圖5-8 表格布局示意圖
在表格布局中,一個列的寬度由該列中最寬的那個單元格指定,而表格的寬度是由父容器指定的。在TableLayout中,可以為列設置3種屬性。
? Shrinkable,如果一個列被標識為shrinkable,則該列的寬度可以進行收縮,以使表格能夠適應其父容器的大小。
? Stretchable,如果一個列被標識為stretchable,則該列的寬度可以進行拉伸,以填滿表格中空閑的空間。
? Collapsed,如果一個列被標識為collapsed,則該列將會被隱藏。
注意:
一個列可以同時具有Shrinkable和Stretchable屬性,在這種情況下,該列的寬度將任意拉伸或收縮以適應父容器。
TableLayout繼承自LinearLayout類,除了繼承來自父類的屬性和方法,TableLayout類中還包含表格布局所特有的屬性和方法。這些屬性和方法說明如表5-6所示。
表5-6 TableLayout類常用屬性及對應方法說明

以下我們用一個表格布局的例子來加深對表格布局的理解。
首先,建立表格布局要注意以下幾點。
(1)向界面中添加一個表格布局,無須修改布局的屬性值。其中,ID屬性為TableLayout01,Layout width和Layout height屬性都為wrap_content。
(2)向TableLayout01中添加兩個TableRow。TableRow代表一個單獨的行,每行被劃分為幾個小的單元,單元中可以添加一個界面控件。其中,ID屬性分別為TableRow01和TableRow02,Layout width和Layout height屬性都為wrap_content。
(3)通過Outline,向TableRow01中添加TextView和EditText;向TableRow02中添加兩個Button。

圖5-9 向TableRow01中添加TextView和EditText
參考表5-7設置TableRow中4個界面控件的屬性值。
表5-7 表格布局控件屬性

(4)main.xml完整代碼如代碼清單5-6所示。
代碼清單5-6 main.xml
<?xml version="1.0" encoding="utf-8"?> <TableLayout android:id="@+id/TableLayout01" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"> <TableRow android:id="@+id/TableRow01" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:id="@+id/label" android:layout_height="wrap_content" android:layout_width="160dip" android:gravity="right" android:text="用戶名:" android:padding="3dip" > </TextView> <EditText android:id="@+id/entry" android:layout_height="wrap_content" android:layout_width="160dip" android:padding="3dip" > </EditText> </TableRow> <TableRow android:id="@+id/TableRow02" android:layout_width="wrap_content" android:layout_height="wrap_content"> <Button android:id="@+id/ok" android:layout_height="wrap_content" android:padding="3dip" android:text="確認"> </Button> <Button android:id="@+id/Button02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="3dip" android:text="取消"> </Button> </TableRow> </TableLayout>
代碼中,第3行代碼使用了<TableLayout>標簽聲明表格布局;第7行和第23行代碼聲明了兩個TableRow元素;第12行設定寬度屬性android:layout_width:160dip;第13行設定屬性android:gravity,指定文字為右對齊;第15行使用屬性android:padding,聲明TextView元素與其他元素的間隔距離為3dip。
(5)表格布局運行效果如圖5-10所示。

圖5-10 表格布局運行圖
5.2.4 相對布局(RelativeLayout)
相對布局(RelativeLayout)是一種非常靈活的布局方式,能夠通過指定界面元素與其他元素的相對位置關系,確定界面中所有元素的布局位置,能夠最大限度保證在各種屏幕類型的手機上正確顯示界面布局。
在相對布局中,子控件的位置是相對兄弟控件或父容器而決定的。出于性能考慮,在設計相對布局時要按照控件之間的依賴關系排列,如View A的位置相對于View B來決定,則需要保證在布局文件中View B在View A的前面。
在進行相對布局時用到的屬性很多,首先來看屬性值只為true或false的屬性,如表5-8所示。
表5-8 相對布局中只取true或false的屬性及說明

接下來看屬性值為其他控件ID的屬性,如表5-9所示。
表5-9 相對布局中取值為其他控件ID的屬性及說明

最后要介紹的是屬性值以像素為單位的屬性及說明,如表5-10所示。
表5-10 相對布局中取值為像素的屬性及說明

需要注意的是在進行相對布局時要避免出現循環依賴,例如,設置相對布局在父容器中的排列方式為WRAP_CONTENT,就不能再將相對布局的子控件設置為ALIGN_PARENT_BOTTOM。因為這樣會造成子控件和父控件相互依賴和參照的錯誤。
以下用一個相對布局的例子來加深對線性布局的理解。首先來看一下相對布局的效果圖,如圖5-11所示。

圖5-11 相對布局效果圖
為達到以上效果,按以下步驟進行操作。
(1)添加TextView控件(用戶名),相對布局會將TextView控件放置在屏幕的最上方。
(2)添加EditText控件(輸入框),并聲明該控件的位置在TextView控件的下方,相對布局會根據TextView的位置確定EditText控件的位置。
(3)添加第一個Button控件(“取消”按鈕),聲明在EditText控件的下方,且在父控件的最右邊。
(4)添加第二個Button控件(“確認”按鈕),聲明該控件在第一個Button控件的左方,且與第一個Button控件處于相同的水平位置。
相對布局在main.xml文件的完整代碼如代碼清單5-7所示。
代碼清單5-7 main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:id="@+id/RelativeLayout01" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"> <TextView android:id="@+id/label" android:layout_height="wrap_content" android:layout_width="match_parent" android:text="用戶名:"> </TextView> <EditText android:id="@+id/entry" android:layout_height="wrap_content" android:layout_width="match_parent" android:layout_below="@id/label"> </EditText> <Button android:id="@+id/cancel" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_alignParentRight="true" android:layout_marginLeft="10dip" android:layout_below="@id/entry" android:text="取消" > </Button> <Button android:id="@+id/ok" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_toLeftOf="@id/cancel" android:layout_alignTop="@id/cancel" android:text="確認">、 </Button> </RelativeLayout>
在代碼中,第3行使用了<RelativeLayout>標簽聲明一個相對布局;第15行使用位置屬性android:layout_below,確定EditText控件在ID為label的元素下方;第20行使用屬性android:layout_alignParentRight,聲明該元素在其父元素的右邊邊界對齊;第21行設定屬性android:layout_marginLeft,左移10dip;第22行聲明該元素在ID為entry的元素下方;第28行聲明使用屬性android:layout_toLeftOf,聲明該元素在ID為cancel元素的左邊;第29行使用屬性android:layout_alignTop,聲明該元素與ID為cancel的元素在相同的水平位置。
5.2.5 絕對布局(AbsoluteLayout)
絕對布局(AbsoluteLayout)能通過指定界面元素的坐標位置,來確定用戶界面的整體布局。所謂絕對布局,是指屏幕中所有控件的擺放由開發人員通過設置控件的坐標來指定,控件容器不再負責管理其子控件的位置。由于子控件的位置和布局都通過坐標來指定,因此AbsoluteLayout類中并沒有開發特有的屬性和方法。
絕對布局是一種不推薦使用的界面布局,因為通過X軸和Y軸確定界面元素位置后,Android系統不能夠根據不同屏幕對界面元素的位置進行調整,降低了界面布局對不同類型和尺寸屏幕的適應能力。每一個界面控件都必須指定坐標(X,Y),例如圖5-12 中,“確認”按鈕的坐標是(40,120),“取消”按鈕的坐標是(120,120)。坐標原點(0,0)在屏幕的左上角。

圖5-12 絕對布局效果圖
絕對布局示例在main.xml文件的完整代碼如代碼清單5-8所示。
代碼清單5-8 main.xml
<?xml version="1.0" encoding="utf-8"?> <AbsoluteLayout android:id="@+id/AbsoluteLayout01" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"> <TextView android:id="@+id/label" android:layout_x="40dip" android:layout_y="40dip" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="用戶名:"> </TextView> <EditText android:id="@+id/entry" android:layout_x="40dip" android:layout_y="60dip" android:layout_height="wrap_content" android:layout_width="150dip"> </EditText> <Button android:id="@+id/ok" android:layout_width="70dip" android:layout_height="wrap_content" android:layout_x="40dip" android:layout_y="120dip" android:text="確認"> </Button> <Button android:id="@+id/cancel" android:layout_width="70dip" android:layout_height="wrap_content" android:layout_x="120dip" android:layout_y="120dip" android:text="取消"> </Button> </AbsoluteLayout>
上述涉及的界面布局(LinearLayout,TableLayout,RelativeLayout等)像其他控件一樣也是一個控件。這意味著布局控件可以被嵌套。比如,為了組織屏幕上的控件你可以在一個LinearLayout中使用一個RelativeLayout,反過來也行。但是需注意在界面設計過程中,盡量保證屏幕相對簡單,復雜布局加載很慢并且可能引起性能問題。
同時,在設計程序布局資源時需要考慮設備的差異性。通常情況下是可能設計出在各種不同設備上看著都不錯的靈活布局的,不管是豎屏還是模屏模式。必要時可以引入可選布局資源來處理特殊情況。例如,可以根據設備的方向或設備是不是有超大屏幕(如網絡平板)來提供不同的布局供加載。
Android SDK提供了幾個可以幫助我們設計、調試和優化布局資源的工具。除了Eclipse的Android插件中內置的布局資源設計器,還可以使用Android SDK提供的Hierarchy Viewer(層次結構查看器)和layoutopt。這些工具在Android SDK的/tools目錄下可以找到。可以使用Hierarchy Viewer來查看布局運行時的詳細情況;可以使用layoutopt(布局優化)命令行工具來優化你的布局文件。優化布局非常重要,因為復雜的布局文件加載很慢。layoutopt工具簡單地掃描XML布局文件并找出不必要的控件。在Android開發者網站的layoutopt部分查看更多信息。
5.3 界面控件
Android系統的界面控件分為定制控件和系統控件。
定制控件是用戶獨立開發的控件,或通過繼承并修改系統控件后所產生的新控件。能夠為用戶提供特殊的功能或與眾不同的顯示需求方式;系統控件是Android系統提供給用戶已經封裝的界面控件,它提供應用程序開發過程中常見功能控件。同時,系統控件更有利于幫助用戶進行快速開發,能夠使Android系統中應用程序的界面保持一致性。
這里著重講解一下系統控件的使用。
常見的系統控件包括TextView、EditText、Button、ImageButton、Checkbox、RadioButton、Spinner、ListView和TabHost。
5.3.1 TextView和EditText
TextView是一種用于顯示字符串的控件;EditText則是用來輸入和編輯字符串的控件,它是一個具有編輯功能的TextView。
每個TextView期望的這樣一個組件的屬性:可以改變它的高度、寬度、字體、文字顏色、背景顏色等。TextView也有一些有用的獨特屬性,如表5-11所示。
表5-11 TextView也有一些有用的獨特屬性

下面就通過一個例子來加深對這兩個控件的理解。
首先,建立一個“TextViewDemo”的程序,包含TextView和EditText兩個控件,如圖5-13所示。上方“用戶名”部分使用的是TextView,下方的文字輸入框使用的是EditText。

圖5-13 TextView與EditView效果圖
TextViewDemo在XML文件中的代碼如代碼清單5-9所示。
代碼清單5-9 main.xml
<TextView android:id="@+id/TextView01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="TextView01" > </TextView> <EditText android:id="@+id/EditText01" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="EditText01" > </EditText>
在上述代碼中,第1行android:id屬性聲明了TextView的ID,這個ID主要用于在代碼中引用這個TextView對象;“@+id/TextView01”表示所設置的ID值;@表示后面的字符串是ID資源;加號(+)表示需要建立新資源名稱,并添加到R.java文件中;斜杠后面的字符串(TextView01)表示新資源的名稱;如果資源不是新添加的,或屬于Android框架的ID資源,則不需要使用加號(+),對于Android框架中的ID資源,還必須添加Android包的命名空間,如android:id="@android:id/empty"。
第2行的android:layout_width屬性用來設置TextView的寬度,wrap_content表示TextView的寬度只要能夠包含所顯示的字符串即可。
第3行的android:layout_height屬性用來設置TextView的高度。
第4行表示TextView所顯示的字符串,在后面將通過代碼更改TextView的顯示內容。
第7行中“fill_content”表示EditText的寬度將等于父控件的寬度。
在上述步驟之后,修改TextViewDemo.java文件中代碼為代碼清單5-10所示的代碼:
代碼清單5-10 TextViewDemo.java
TextView textView = (TextView)findViewById(R.id.TextView01); EditText editText = (EditText)findViewById(R.id.EditText01); textView.setText("用戶名:"); editText.setText("");
第1行代碼的findViewById()方法能夠通過ID引用界面上的任何控件,只要該控件在XML文件中定義過ID即可。
第3行代碼的setText()方法用來設置TextView所顯示的內容。
5.3.2 Button和ImageButton
Button是一種按鈕控件,用戶能夠在該控件上點擊,并后引發相應的事件處理方法;ImageButton用以實現能夠顯示圖像功能的控件按鈕。
下面通過一個例子來加深對這兩個控件的理解。
1.建立一個“ButtonDemo”的程序
程序包含Button和ImageButton兩個按鈕,上方是“Button按鈕”,下方是一個ImageButton控件,如圖5-14所示。

圖5-14 Button與ImageButton效果圖
ButtonDemo在XML文件中的代碼如代碼清單5-11所示。
代碼清單5-11 main.xml
<Button android:id="@+id/Button01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button01" > </Button> <ImageButton android:id="@+id/ImageButton01" android:layout_width="wrap_content" android:layout_height="wrap_content"> </ImageButton>
在上述代碼中,定義Button控件的高度、寬度和內容及ImageButton控件的高度和寬度,但是沒定義顯示的圖像,在后面的代碼中進行定義。
2.引入資源
將download.png文件復制到/res/drawable文件夾下,在/res目錄上選擇Refresh,就可以看到新添加的文件顯示在/res/drawable文件夾下,同時R.java文件內容也得到了更新,否則提示無法找到資源的錯誤。
3.更改Button和ImageButton內容
在ButtonDemo.java中引入android.widget.Button和android.widget.ImageButton,并修改其代碼如代碼清單5-12所示。
代碼清單5-12 ButtonDemo.java
Button button = (Button)findViewById(R.id.Button01); ImageButton imageButton = (ImageButton)findViewById(R.id.ImageButton01); button.setText("Button按鈕"); imageButton.setImageResource(R.drawable.download);
上述代碼中,第1行代碼用于引用在XML文件中定義的Button控件。
第2行代碼用于引用在XML文件中定義的ImageButton控件。
第3行代碼將Button的顯示內容更改為“Button按鈕”。
第4行代碼利用setImageResource()方法,將新加入的png文件R.drawable.download傳遞給ImageButton。
4.按鈕響應點擊事件:添加點擊事件的監聽器
在ButtonDemo.java中添加代碼清單5-13所示的代碼。
代碼清單5-13 ButtonDemo.java
final TextView textView = (TextView)findViewById(R.id.TextView01); button.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { textView.setText("Button按鈕"); } }); imageButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { textView.setText("ImageButton按鈕"); } });
在本段代碼中,第2行代碼中button對象通過調用setOnClickListener()方法,注冊一個點擊(Click)事件的監聽器View.OnClickListener()。
第3行代碼是點擊事件的回調方法。
第4行代碼將TextView的顯示內容更改為“Button按鈕”。
這里我們來了解一下View.OnClickListener()。
View.OnClickListener()是View定義的點擊事件的監聽器接口,并在接口中僅定義了onClick()方法。當Button從Android界面框架中接收到事件后,首先檢查這個事件是否是點擊事件,如果是點擊事件,同時Button又注冊了監聽器,則會調用該監聽器中的onClick()方法。每個View僅可以注冊一個點擊事件的監聽器,如果使用setOnClickListener()方法注冊第二個點擊事件的監聽器,之前注冊的監聽器將被自動注銷。
多個按鈕注冊到同一個點擊事件的監聽器上,代碼如代碼清單5-14所示。
代碼清單5-14多個按鈕注冊到一個點擊事件的監聽器上
Button.OnClickListener buttonListener = new Button.OnClickListener(){ @Override public void onClick(View v) { switch(v.getId()){ case R.id.Button01: textView.setText("Button按鈕"); return; case R.id.ImageButton01: textView.setText("ImageButton按鈕"); return; } }}; Button.setOnClickListener(buttonListener); ImageButton.setOnClickListener(buttonListener);
該段代碼中,第1行至第12行代碼定義了一個名為buttonListener的點擊事件監聽器;第13行代碼將該監聽器注冊到Button上;第14行代碼將該監聽器注冊到ImageButton上。
5.3.3 CheckBox和RadioButton
CheckBox是一個同時可以選擇多個選項的控件;而RadioButton則是僅可以選擇一個選項的控件;RadioGroup是RadioButton的承載體,程序運行時不可見,應用程序中可能包含一個或多個RadioGroup,一個RadioGroup包含多個RadioButton,在每個RadioGroup中,用戶僅能夠選擇其中一個RadioButton。
下面就通過一個例子來加深對這兩個控件的理解,其效果如圖5-15所示。

圖5-15 CheckBox與RadioButton效果圖
1.建立一個“CheckboxRadiobuttonDemo”程序
程序包含5 個控件,從上至下分別是TextView01、CheckBox01、 CheckBox02、RadioButton01、RadioButton02,當選擇RadioButton01時,RadioButton02則無法選擇。
CheckboxRadiobuttonDemo在XML文件中的代碼如代碼清單5-15所示。
代碼清單5-15 main.xml
<TextView android:id="@+id/TextView01“ android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hello"/> <CheckBox android:id="@+id/CheckBox01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="CheckBox01" > </CheckBox> <CheckBox android:id="@+id/CheckBox02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="CheckBox02" > </CheckBox> <RadioGroup android:id="@+id/RadioGroup01" android:layout_width="wrap_content" android:layout_height="wrap_content"> <RadioButton android:id="@+id/RadioButton01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="RadioButton01" > </RadioButton> <RadioButton android:id="@+id/RadioButton02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="RadioButton02" > </RadioButton> </RadioGroup>
上述代碼中,第15行<RadioGroup>標簽聲明了一個RadioGroup;在第18行和第23行分別聲明了兩個RadioButton,這兩個RadioButton是RadioGroup的子元素。
2.引用CheckBox和RadioButton
引用CheckBox和RadioButton的方法參考代碼清單5-16所示的代碼。
代碼清單5-16引用CheckBox和RadioButton
CheckBox checkBox1= (CheckBox)findViewById(R.id.CheckBox01); RadioButton radioButton1 =(RadioButton)findViewById(R.id.RadioButton01);
3.響應點擊事件:添加點擊事件的監聽器
CheckBox設置點擊事件監聽器的方法與Button設置點擊事件監聽器中介紹的方法相似,唯一不同在于將Button.OnClickListener換成了CheckBox.OnClickListener。
代碼清單5-17設置CheckBox點擊事件監聽器
CheckBox.OnClickListener checkboxListener = new CheckBox.OnClickListener(){ @Override public void onClick(View v) { //過程代碼 }}; checkBox1.setOnClickListener(checkboxListener); checkBox2.setOnClickListener(checkboxListener);
RadioButton設置點擊事件監聽器的方法如代碼清單5-18所示。
代碼清單5-18設置RadioButton點擊事件監聽器
RadioButton.OnClickListener radioButtonListener = new RadioButton.OnClickListener(){ @Override public void onClick(View v) { //過程代碼 }}; radioButton1.setOnClickListener(radioButtonListener); radioButton2.setOnClickListener(radioButtonListener);
通過上述的講解,可以得出這樣的結論:CheckBox是可以選擇多個選項的復選框控件,當其中選項被選中時,顯示相應的checkmark。這時,需要創建一個“OnClickListener”捕獲點擊事件,并可以添加所需的功能代碼。
RadioGroup是一個包含一些RadioButton的ViewGroup。用戶可選擇一個按鈕,通過對每一個RadioButton設置監聽OnClickListeners來獲取其選擇。這里需注意,點擊RadioButton并不觸發RadioGroup的Click事件。
5.3.4 Spinner
Spinner是一種能夠從多個選項中選擇選項的控件,類似于桌面程序的組合框(ComboBox),但沒有組合框的下拉菜單,而是使用浮動菜單為用戶提供選擇,如圖5-16所示。

圖5-16 Spinner效果圖
下面就通過一個例子來加深對Spinner的理解。
1.建立一個程序“SpinnerDemo”
程序包含3個子項,Spinner控件在XML文件中的代碼如代碼清單5-19所示。
代碼清單5-19 main.xml
<TextView android:id="@+id/TextView01" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hello"/> <Spinner android:id="@+id/Spinner01" android:layout_width="300dip" android:layout_height="wrap_content"> </Spinner>
在上述代碼中,第5行使用<Spinner>標簽聲明了一個Spinner控件;第6行代碼中指定了該控件的寬度為“300dip”。
2.修改SpinnerDemo.java文件
在SpinnerDemo.java文件中,定義一個ArrayAdapter適配器,在ArrayAdapter中添加Spinner的內容,需要在代碼中引入android.widget.ArrayAdapter和android.widget.Spinner。
代碼清單5-20 SpinnerDemo.java
Spinner spinner = (Spinner) findViewById(R.id.Spinner01); List<String> list = new ArrayList<String>(); list .add("Spinner子項1"); list .add("Spinner子項2"); list .add("Spinner子項3"); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, list ); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter);
本段代碼中,第2行代碼建立了一個字符串數組列表(ArrayList),這種數組列表可以根據需要進行增減,<String>表示數組列表中保存的是字符串類型的數據。
在代碼的第3、4、5行中,使用add()方法分別向數組列表中添加3個字符串。
第6行代碼建立了一個ArrayAdapter的數組適配器,數組適配器能夠將界面控件和底層數據綁定在一起。
第7行代碼設定了Spinner的浮動菜單的顯示方式,其中,android.R.layout.simple_spinner_dropdown_item是Android系統內置的一種浮動菜單。
第8行代碼實現綁定過程,所有ArrayList中的數據,將顯示在Spinner的浮動菜單中。
利用該段代碼,適配器綁定界面控件和底層數據,如果底層數據更改了,用戶界面也相應修改顯示內容,因此不需要應用程序再監視,從而極大地簡化了代碼的復雜性。
由上述例子可以得出結論:與上一小節中的CheckBox和RadioButton相比,Sipnner需要的工作量最大,但可以為用戶提供相對來說較好的屏幕顯示。如上所示,Spinner顯示當前選中的選項,當單擊右側的下拉列表時,彈出一個可供選擇的選項列表。為了實現該功能需滿足以下條件。
(1)創建一個可供選擇的選項列表(該列表可以是動態創建并被應用程序修改)。
(2)為Spinner的列表創建一個ArrayAdapter以實現其下拉列表的顯示。這里需注意ArrayAdapter的格式(simple_spinner_item和simple_spinner_dropdown_item)是由Android系統定義的,它們不會出現在資源XML文件中。
(3)創建onItemSelectedListener來捕捉Spinner的選擇事件。監聽onItemSelected Listener包含onItemSelected()方法和onNothingSelected()方法。
5.3.5 ListView
ListView是一種用于垂直顯示的列表控件,如果顯示內容過多,則會出現垂直滾動條。
ListView能夠通過適配器將數據和自身綁定,在有限的屏幕上提供大量內容供用戶選擇,所以是經常使用的用戶界面控件。同時,ListView支持點擊事件處理,用戶可以用少量的代碼實現復雜的選擇功能。例如,調用setAdapter()提供的數據和View子項,并通過setOnItemSelectedListener()方法監聽ListView上子項選擇事件。
若Activity由一個單一的列表控制,則Activity需繼承ListActivity類而不是之前介紹的常規的Activity類。如果主視圖僅僅只是列表,甚至不需要建立一個layout,ListActivity會為用戶構建一個全屏幕的列表。如果想自定義布局,則需要確定ListView的id為@android:id/list,以便ListActivity知道其Activity的主要清單。
下面就通過一個例子來加深對ListView的理解,如圖5-17所示。

圖5-17 ListView效果圖
1.建立一個“ListViewDemo”程序
XML文件中的代碼如代碼清單5-21所示。
代碼清單5-21 main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="@+id/selection" android:layout_width="match_parent" android:layout_height="wrap_content"/> <ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="match_parent" android:drawSelectorOnTop="false" /> </LinearLayout>
2.修改ListViewDemo.java文件
在ListViewDemo.java文件中,首先需要為ListView創建適配器,配置和連接列表,添加ListView中所顯示的內容。
代碼清單5-22 ListViewDemo.java
public class ListViewDemo extends ListActivity { TextView selection; String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,items)); selection=(TextView)findViewById(R.id.selection); } public void onListItemClick(ListView parent, View v, int position,long id) { selection.setText(items[position]); } }
繼承ListActivity后,可以通過setListAdapter()方法設置列表。這種情況下,提供了一個ArrayAdapter包裝的字符串數組。其中ArrayAdapter的第二個參數android.R.layout.simple_list_item_1控制了ListView中行的顯示,上例中android.R.layout.simple_list_item_1該值提供了標準的Android清單行:大字體、很多的填充、文本和白色。重寫onListItemClick方法以在列表上子項的選擇發生變化時及時更新其文本。
在默認情況下,ListView只對列表子項的點擊事件進行監聽。但ListView也跟蹤用戶的選擇,或多個可能的選擇列表,但它需要一些變化。
? 在Java代碼中調用ListView的setChoiceMode()方法來設置選擇模式,可供選擇的模式有:CHOICE_MODE_SINGLE和CHOICE_MODE_MULTIPLE兩種。可以通過getListView()方法在ListActivity中獲取ListView。
? 在構造ArrayAdapter時,第二個參數選擇使用以下兩種參數可以使列表上子項單選或是復選:android.R.layout.simple_list_item_single_choice和android.R.layout. simple_list_item_multiple_choice,如圖5-18所示。

圖5-18 單選、復選模式
? 通過調用getCheckedItemPositions()方法來判斷用戶選擇的子項。
5.3.6 TabHost
Tab標簽頁是界面設計時經常使用的界面控件,可以實現多個分頁之間的快速切換,每個分頁可以顯示不同的內容。
對Tab標簽頁的使用,首先要設計所有的分頁的界面布局,在分頁設計完成后,使用代碼建立Tab標簽頁,并給每個分頁添加標識和標題,最后確定每個分頁所顯示的界面布局。其中,每個分頁建立一個XML文件,用以編輯和保存分頁的界面布局,使用的方法與設計普通用戶界面一樣。
下面就通過一個例子來加深對Tab標簽頁的理解,如圖5-19所示的效果圖。

圖5-19 Tab標簽頁效果圖
1.建立一個“TabDemo”程序
程序包含兩個XML文件,分別為tab1.xml和tab2.xml,這兩個文件分別使用線性布局、相對布局和絕對布局示例中的main.xml的代碼,并將布局的ID分別定義為layout01和layout02。
其中,tab1.xml文件代碼如代碼清單5-23所示。
代碼清單5-23 tab1.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:id = "@+id/layout01" … </LinearLayout>
tab2.xml文件代碼如代碼清單5-24所示。
代碼清單5-24 tab2.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:id="@+id/layout03" … </RelativeLayout>
2.修改TabDemo.java文件
在TabDemo.java文件中輸入代碼清單5-25所示的代碼,創建Tab標簽頁,并建立子頁與界面布局直接的關聯關系。
代碼清單5-25 TabDemo.java
package com.example.TabDemo; import android.app.TabActivity; import android.os.Bundle; import android.widget.TabHost; import android.view.LayoutInflater; public class TabDemo extends TabActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TabHost tabHost = getTabHost(); LayoutInflater.from(this).inflate(R.layout.tab1, tabHost.getTabContentView(),true); LayoutInflater.from(this).inflate(R.layout.tab2, tabHost.getTabContentView(),true); tabHost.addTab(tabHost.newTabSpec("TAB1") .setIndicator("線性布局").setContent(R.id.layout01)); tabHost.addTab(tabHost.newTabSpec("TAB2") .setIndicator("相對布局").setContent(R.id.layout02)); }
該段代碼中,第8行代碼“public class TabDemo extends TabActivity”的聲明TabDemo類繼承于TabActivity,與以往繼承Activity不同,TabActivity支持內嵌多個Activity或View。
第12行代碼“TabHost tabHost = getTabHost();”通過getTabHost()方法獲得Tab標簽頁的容器,用以承載可以點擊的Tab標簽和分頁的界面布局。
第13行代碼“LayoutInflater.from(this).inflate(R.layout.tab1, tabHost. getTabContent View(),true);”通過LayoutInflater將tab1.xml文件中的布局轉換為Tab標簽頁可以使用的View對象。
第14行代碼“tabHost.addTab(tabHost.newTabSpec("TAB1").setIndicator("線性布局").setContent(R.id.layout01));”使用addTab()方法添加了第1個分頁,tabHost.newTabSpec ("TAB1")表明在第12行代碼中建立的tabHost上,添加一個標識為TAB1的Tab分頁,同時使用setIndicator()方法設定分頁顯示的標題,使用setContent()方法設定分頁所關聯的界面布局。
問:在使用Tab標簽頁時,只能像上述例子中一樣將不同分頁的界面布局保存在不同的XML文件中嗎?
答:除了像上述中將不同分頁的界面布局保存在不同的XML文件中外,也可以將所有分頁的布局保存在同一個XML文件中。兩者有不同的利弊:
? 第一種方法有利于在Eclipse開發環境中進行可視化設計,并且不同分頁的界面布局在不同的文件中更加易于管理。
? 第二種方法則可以產生較少的XML文件,同時編碼時的代碼也會更加簡潔。
5.4 菜單
菜單是應用程序中非常重要的組成部分,能夠在不占用界面空間的前提下,為應用程序提供統一的功能和設置界面,并為程序開發人員提供了易于使用的編程接口。Android系統支持3種菜單:選項菜單(Option Menu)、子菜單(Submenu)、快捷菜單(Context Menu)。
5.4.1 選項菜單
選項菜單是一種經常被使用的Android系統菜單,可以分為圖標菜單(Icon Menu)和擴展菜單(Expanded Menu)兩類,可通過“菜單鍵”(Menu key)打開。
圖標菜單能夠同時顯示文字和圖標,最多支持6個子項,但圖標菜單不支持單選框和復選框。
擴展菜單在圖標菜單子項多余6個時才出現,通過點擊圖標菜單最后的子項“More”才能打開。擴展菜單是垂直的列表型菜單,不能夠顯示圖標,但支持單選框和復選框。

圖5-20 圖標菜單

圖5-21 擴展菜單
1.重寫onCreateOptionMenu()方法
在Android應用程序中使用選項菜單,需重載Activity的onCreateOptionMenu()方法。初次使用選項菜單時,會調用onCreateOptionMenu()方法,用來初始化菜單子項的相關內容,因此這里需要設置菜單子項自身的子項ID和組ID、菜單子項顯示的文字和圖片等。代碼如代碼清單5-26所示。
代碼清單5-26重載onCreateOptionMenu()方法
final static int MENU_DOWNLOAD = Menu.FIRST; final static int MENU_UPLOAD = Menu.FIRST+1; @Override public boolean onCreateOptionsMenu(Menu menu){ menu.add(0,MENU_DOWNLOAD,0,"下載設置"); menu.add(0,MENU_UPLOAD,1,"上傳設置"); return true; }
第1行和第2行代碼將菜單子項ID定義成靜態常量,并使用靜態常量Menu.FIRST(整數類型,值為1)定義第一個菜單子項,以后的菜單子項僅需在Menu.FIRST增加相應的數值即可。
第4行代碼Menu對象作為一個參數被傳遞到方法內部,因此在onCreateOptionsMenu()方法中,用戶可以使用Menu對象的add()方法添加菜單子項。其中add()方法的語法如下。
MenuItem android.view.Menu.add(int groupId, int itemId, int order, CharSequence title)
第1個參數groupId是組ID,用以批量的對菜單子項進行處理和排序;第2個參數itemId是子項ID,是每一個菜單子項的唯一標識,通過子項ID使應用程序能夠定位到用戶所選擇的菜單子項;第3個參數order是定義菜單子項在選項菜單中的排列順序;第4個參數title是菜單子項所顯示的標題。
第7行代碼是onCreateOptionsMenu()方法返回值,方法的返回值類型為布爾型:返回true將顯示方法中設置的菜單,否則不能夠顯示菜單。
做完以上步驟后,使用setIcon()方法和setShortcut()方法,添加菜單子項的圖標和快捷鍵,如代碼清單5-27所示。
代碼清單5-27添加菜單子項的圖標和快捷鍵
menu.add(0,MENU_DOWNLOAD,0,"下載設置") .setIcon(R.drawable.download); .setShortcut(','d');
代碼中,利用MENU_DOWNLOAD菜單設置圖標和快捷鍵的代碼;第2行代碼中使用了新的圖像資源,用戶將需要使用的圖像文件復制到/res/drawable目錄下;setShortcut()方法第一個參數是為數字鍵盤設定的快捷鍵,第二個參數是為全鍵盤設定的快捷鍵,且不區分字母的大小寫。
2.重寫onPrepareOptionsMenu()方法
重載onPrepareOptionsMenu()方法,能夠動態的添加、刪除菜單子項,或修改菜單的標題、圖標和可見性等內容。onPrepareOptionsMenu()方法的返回值的含義與onCreateOptions Menu()方法相同:返回true則顯示菜單,返回false則不顯示菜單。
代碼清單5-28所示的代碼是在用戶每次打開選項菜單時,在菜單子項中顯示用戶打開該子項的次數。
代碼清單5-28菜單子項中顯示用戶打開該子項的次數
static int MenuUploadCounter = 0; @Override public boolean onPrepareOptionsMenu(Menu menu){ MenuItem uploadItem = menu.findItem(MENU_UPLOAD); uploadItem.setTitle("上傳設置:" +String.valueOf(MenuUploadCounter)); return true; }
第1行代碼設置一個菜單子項的計數器,用來統計用戶打開“上傳設置”子項的次數;第4行代碼是通過將菜單子項的ID傳遞給menu.findItem()方法,獲取到菜單子項的對象;第5行代碼是通過MenuItem的setTitle()方法修改菜單標題。
問:onCreateOptionMenu()方法和onPrepareOptionsMenu()方法有什么區別?
答:onCreateOptionMenu()方法在Menu顯示之前只調用一次;而onPrepareOptionsMenu()方法在每次顯示Menu之前都會調用,一般用它執行Menu的更新操作。
3.onOptionsItemSelected ()方法
onOptionsItemSelected ()方法能夠處理菜單選擇事件,且該方法在每次單擊菜單子項時都會被調用。
下面的代碼說明了如何通過菜單子項的ID執行不同的操作。
代碼清單5-29 onOptionsItemSelected()
@Override public boolean onOptionsItemSelected(MenuItem item){ switch(item.getItemId()){ case MENU_DOWNLOAD: MenuDownlaodCounter++; return true; case MENU_UPLOAD: MenuUploadCounter++; return true; } return false; }
onOptionsItemSelected ()的返回值表示是否對菜單的選擇事件進行處理,如果已經處理過則返回true,否則返回false;第3行的MenuItem.getItemId()方法可以獲取到被選擇菜單子項的ID。
程序運行后,通過單擊“菜單鍵”可以調出程序設計的兩個菜單子項,如圖5-22所示。

圖5-22 運行效果圖
5.4.2 子菜單
子菜單是能夠顯示更加詳細信息的菜單子項,如圖5-23所示。其中,菜單子項使用了浮動窗體的顯示形式,能夠更好地適應小屏幕的顯示方式。

圖5-23 菜單子項
Android系統的子菜單使用非常靈活,可以在選項菜單或快捷菜單中使用子菜單,有利于將相同或相似的菜單子項組織在一起,便于顯示和分類。但是,子菜單不支持嵌套。
子菜單的添加使用addSubMenu()方法實現,代碼如代碼清單5-30所示。
代碼清單5-30 onOptionsItemSelected()
SubMenu uploadMenu = (SubMenu) menu.addSubMenu (0,MENU_UPLOAD,1,"上傳設置").setIcon(R.drawable.upload); uploadMenu.setHeaderIcon(R.drawable.upload); uploadMenu.setHeaderTitle("上傳參數設置"); uploadMenu.add(0,SUB_MENU_UPLOAD_A,0,"上傳參數A"); uploadMenu.add(0,SUB_MENU_UPLOAD_B,0,"上傳參數B");
第1行代碼在onCreateOptionsMenu()方法傳遞的menu對象上調用addSubMenu()方法,在選項菜單中添加一個菜單子項,用戶單擊后可以打開子菜單;addSubMenu()方法與選項菜單中使用過的add()方法支持相同的參數,同樣可以指定菜單子項的ID、組ID和標題等參數,并且能夠通過setIcon()方法顯示菜單的圖標。
第2行代碼使用setHeaderIcon ()方法,定義子菜單的圖標。
第3行定義子菜單的標題,若不規定子菜單的標題,子菜單將顯示父菜單子項標題,即第1行代碼中“上傳設置”。
第4行和第5行在子菜單中添加了兩個菜單子項,菜單子項的更新方法和選擇事件處理方法,仍然使用onPrepareOptionsMenu()方法和onOptionsItemSelected ()方法。
以上一小節的代碼為基礎,將“上傳設置”改為子菜單,并在子菜單中添加“上傳參數A”和“上傳參數B”兩個菜單子項。運行結果如圖5-24所示。

圖5-24 運行效果圖
5.4.3 上下文菜單(Context Menu)
快捷菜單同樣采用了浮動窗體的顯示方式,與子菜單的實現方式相同,但兩種菜單的啟動方式卻截然不同。
? 啟動方式:快捷菜單類似于普通桌面程序中的“右鍵菜單”,當用戶點擊界面元素超過2秒后,將啟動注冊到該界面元素的快捷菜單。
? 使用方法:與使用選項菜單的方法非常相似,需要重載onCreateContextMenu()方法和onContextItemSelected()方法。
1.onCreateContextMenu()方法
onCreateContextMenu()方法主要用來添加快捷菜單所顯示的標題、圖標和菜單子項等內容,選項菜單中的onCreateOptionsMenu()方法僅在選項菜單第一次啟動時被調用一次,而快捷菜單的onCreateContextMenu()方法每次啟動時都會被調用一次。
代碼清單5-31 onCreateContextMenu ()
final static int CONTEXT_MENU_1 = Menu.FIRST; final static int CONTEXT_MENU_2 = Menu.FIRST+1; final static int CONTEXT_MENU_3 = Menu.FIRST+2; @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo){ menu.setHeaderTitle("快捷菜單標題"); menu.add(0, CONTEXT_MENU_1, 0,"菜單子項1"); menu.add(0, CONTEXT_MENU_2, 1,"菜單子項2"); menu.add(0, CONTEXT_MENU_3, 2,"菜單子項3"); }
ContextMenu類支持add()方法(代碼第7行)和addSubMenu()方法,可以在快捷菜單中添加菜單子項和子菜單。
第5行代碼的onCreateContextMenu()方法中的參數:第1個參數menu是需要顯示的快捷菜單;第2個參數v是用戶選擇的界面元素;第3個參數menuInfo是所選擇界面元素的額外信息。
2.onContextItemSelected ()方法
菜單選擇事件的處理需要重載onContextItemSelected()方法,該方法在用戶選擇快捷菜單中的菜單子項后被調用,與onOptionsItemSelected ()方法的使用方法基本相同。
代碼清單5-32 onContextItemSelected()
@Override public oolean onContextItemSelected(MenuItem item){ switch(item.getItemId()){ case CONTEXT_MENU_1: LabelView.setText("菜單子項1"); return true; case CONTEXT_MENU_2: LabelView.setText("菜單子項2"); return true; case CONTEXT_MENU_3: LabelView.setText("菜單子項3"); return true; } return false; 3.registerForContextMenu()方法 }
使用registerForContextMenu()方法,將快捷菜單注冊到界面控件上(代碼清單5-33中第7行)。這樣,用戶在長時間點擊該界面控件時,便會啟動快捷菜單。
為了能夠在界面上直接顯示用戶所選擇快捷菜單的菜單子項,在代碼中引用了界面元素TextView(代碼清單5-33中第6行),通過更改TextView的顯示內容(代碼清單5-32中第5、8和11行),顯示用戶所選擇的菜單子項。
代碼清單5-33 registerForContextMenu ()
TextView LabelView = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); LabelView = (TextView)findViewById(R.id.label); registerForContextMenu(LabelView); }
4.main.xml
代碼清單5-34 main.xml
<TextView android:id="@+id/label" android:layout_width="match_parent" android:layout_height="match_parent" android:text="@string/hello" />
上述代碼為/src/layout/main.xml文件的部分內容,第1行聲明了TextView的ID為label,在代碼清單5-33的第6行中,通過R.id.label將ID傳遞給findViewById()方法,這樣用戶便能夠引用該界面元素,并能夠修改該界面元素的顯示內容。
需要注意的一點,代碼清單5-34的第2行,將android:layout_width設置為match_parent,這樣TextView將填充滿父節點的所有剩余屏幕空間,用戶點擊屏幕TextView下方任何位置都可以啟動快捷菜單。如果將android:layout_width設置為wrap_content,則用戶必須準確單擊TextView才能啟動快捷菜單。
圖5-25為快捷菜單的運行效果圖。

圖5-25 運行效果圖
問:菜單可不可以像界面布局一樣在XML文件中進行定義?
答:菜單可以像界面布局一樣在XML文件中進行定義。使用XML文件定義界面菜單,將代碼與界面設計分類,有助于簡化代碼的復雜程度,并且更有利于界面的可視化。
下面將快捷菜單的示例程序MyContextMenu改用XML實現,新程序的工程名稱為MyXLMContoxtMenu。
首先需要創建保存菜單內容的XML文件:在/src目錄下建立子目錄menu,并在menu下建立context_menu.xml文件,代碼如代碼清單5-35所示。
代碼清單5-35 context_menu.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/contextMenu1" android:title="菜單子項1"/> <item android:id="@+id/contextMenu2" android:title="菜單子項2"/> <item android:id="@+id/contextMenu3" android:title="菜單子項3"/> </menu>
在描述菜單的XML文件中,必須以<menu>標簽(代碼第1行)作為根節點,<item>標簽(代碼第2行)用來描述菜單中的子項,<item>標簽可以通過嵌套實現子菜單的功能。
XML菜單的顯示結果如圖5-26所示。

圖5-26 XML菜單的顯示結果
在XML文件中定義菜單后,在onCreateContextMenu()方法中調用inflater.inflate()方法,將XML資源文件傳遞給菜單對象,代碼如代碼清單5-36所示。
代碼清單5-36 onCreateContextMenu()
@Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo){ MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.context_menu, menu); }
第4行代碼中的getMenuInflater()為當前的Activity返回MenuInflater;第5行代碼將XML資源文件R.menu.context_menu,傳遞給menu這個快捷菜單對象。
5.5 界面事件
在Android系統中,存在多種界面事件,如點擊事件、觸摸事件、焦點事件和菜單事件等,在這些界面事件發生時,Android界面框架調用界面控件的事件處理方法對事件進行處理。
Android系統界面事件的傳遞和處理遵循以下規則。
? 如果界面控件設置了事件監聽器,則事件將先傳遞給事件監聽器。
? 如果界面控件沒有設置事件監聽器,界面事件則會直接傳遞給界面控件的其他事件處理方法。
? 即使界面控件設置了事件監聽器,界面事件也可以再次傳遞給其他事件處理方法。
? 是否繼續傳遞事件給其他處理方法是由事件監聽器處理方法的返回值決定的。
? 如果監聽器處理方法的返回值為true,表示該事件已經完成處理過程,不需要其他處理方法參與處理過程,這樣事件就不會再繼續進行傳遞。
? 如果監聽器處理方法的返回值為false,則表示該事件沒有完成處理過程,或需要其他處理方法捕獲到該事件,事件會被傳遞給其他的事件處理方法。
在MVC模型中,控制器根據界面事件(UI Event)類型不同,將事件傳遞給界面控件不同的事件處理方法。
? 按鍵事件(KeyEvent)將傳遞給onKey()方法進行處理。
? 觸摸事件(TouchEvent)將傳遞給onTouch()方法進行處理。
5.5.1 按鍵事件
下面以EditText控件中的按鍵事件為例,說明Android系統界面事件傳遞和處理過程。
假設EditText控件已經設置了按鍵事件監聽器,當用戶按下鍵盤上的某個按鍵時,控制器將產生KeyEvent按鍵事件。Android系統會首先判斷EditText控件是否設置了按鍵事件監聽器,因為EditText控件已經設置按鍵事件監聽器OnKeyListener,所以按鍵事件先傳遞到監聽器的事件處理方法onKey()中,事件能夠繼續傳遞給EditText控件的其他事件處理方法,完全根據onKey()方法的返回值來確定:如果onKey()方法返回false,事件將繼續傳遞,這樣EditText控件就可以捕獲到該事件,將按鍵的內容顯示在EditText控件中;如果onKey()方法返回true,將阻止按鍵事件的繼續傳遞,這樣EditText控件就不能夠捕獲到按鍵事件,也就不能夠將按鍵內容顯示在EditText控件中。
Android界面框架支持對按鍵事件的監聽,并能夠將按鍵事件的詳細信息傳遞給處理方法。為了處理控件的按鍵事件,先需要設置按鍵事件的監聽器,并重載onKey()方法,示例代碼如代碼清單5-37所示。
代碼清單5-37設置按鍵事件的監聽器,并重載onKey()方法
entryText.setOnKeyListener(new OnKeyListener(){ @Override public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { //過程代碼…… return true/false; }
第1行代碼是設置控件的按鍵事件監聽器。
第3行代碼的onKey ()方法中的參數:第1個參數View表示產生按鍵事件的界面控件;第2個參數keyCode表示按鍵代碼;第3個參數KeyEvent則包含了事件的詳細信息,如按鍵的重復次數、硬件編碼和按鍵標志等。
第5行代碼是onKey()方法的返回值:返回true,阻止事件傳遞;返回false,允許繼續傳遞按鍵事件。
KeyEventDemo是一個說明如何處理按鍵事件的示例。
KeyEventDemo用戶界面如圖5-27所示。

圖5-27 KeyEventDemo用戶界面
從圖5-27中可以看出,最上方的EditText控件是輸入字符的區域,中間的CheckBox控件用來控制onKey()方法的返回值,最下方的TextView控件用來顯示按鍵事件的詳細信息,包括按鍵動作、按鍵代碼、按鍵字符、UNICODE編碼、重復次數、功能鍵狀態、硬件編碼和按鍵標志。
界面的XML文件的代碼如代碼清單5-38所示
代碼清單5-38界面XML文件
<EditText android:id="@+id/entry" android:layout_width="match_parent" android:layout_height="wrap_content"> </EditText> <CheckBox android:id="@+id/block" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="返回true,阻止將按鍵事件傳遞給界面元素" > </CheckBox> <TextView android:id="@+id/label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="按鍵事件信息" > </TextView>
在EditText中,當任何一個鍵按下或抬起時,都會引發按鍵事件。為了能夠使EditText處理按鍵事件,需要使用setOnKeyListener ()方法在代碼中設置按鍵事件監聽器,并在onKey()方法中添加按鍵事件的處理過程,代碼如代碼清單5-39所示。
代碼清單5-39 setOnKeyListener()
entryText.setOnKeyListener(new OnKeyListener(){ @Override public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { int metaState = keyEvent.getMetaState(); int unicodeChar = keyEvent.getUnicodeChar(); String msg = ""; msg +="按鍵動作:" + String.valueOf(keyEvent.getAction())+"\n"; msg +="按鍵代碼:" + String.valueOf(keyCode)+"\n"; msg +="按鍵字符:" + (char)unicodeChar+"\n"; msg +="UNICODE:" + String.valueOf(unicodeChar)+"\n"; msg +="重復次數:" + String.valueOf(keyEvent.getRepeatCount())+"\n"; msg +="功能鍵狀態:" + String.valueOf(metaState)+"\n"; msg +="硬件編碼:" + String.valueOf(keyEvent.getScanCode())+"\n"; msg +="按鍵標志:" + String.valueOf(keyEvent.getFlags())+"\n"; labelView.setText(msg); if (checkBox.isChecked()) return true; else return false; }
在上述代碼中,第4行代碼用來獲取功能鍵狀態。功能鍵包括左Alt鍵、右Alt鍵和Shift鍵,當這3個功能鍵被按下時,功能鍵代碼metaState值分別為18、34和65;但沒有功能鍵被按下時,功能鍵代碼metaState值分別為0。
第5行代碼獲取了按鍵的Unicode值,而在第9行中,將Unicode轉換為了字符,顯示在TextView中。
第7行代碼獲取了按鍵動作,0表示按下按鍵,1表示抬起按鍵。第7行代碼獲取按鍵的重復次數,但當按鍵被長時間按下時,則會產生這個屬性值。
第13行代碼獲取了按鍵的硬件編碼,各硬件設備的按鍵硬件編碼都不相同,因此該值一般用于調試。
第14行獲取了按鍵事件的標志符。
5.5.2 觸摸事件
Android界面框架支持對觸摸事件的監聽,并能夠將觸摸事件的詳細信息傳遞給處理方法,不過需要設置觸摸事件的監聽器,并重載onTouch ()方法。
設置觸摸事件的監聽器,并重載onTouch ()方法的代碼如代碼清單5-40所示。
代碼清單5-40設置觸摸事件的監聽器,并重載onTouch ()方法
touchView.setOnTouchListener(new View.OnTouchListener(){ @Override public boolean onTouch(View v, MotionEvent event) { //過程代碼… return true/false; }
在上述代碼中,第1行代碼是設置控件的觸摸事件監聽器;在第3行的onTouch()方法中,第1個參數View表示產生觸摸事件的界面控件;第2個參數MontionEvent表示觸摸事件的詳細信息,如產生時間、坐標和觸點壓力等;第5行是onTouch()方法的返回值。
TouchEventDemo是一個說明如何處理觸摸事件的示例。
TouchEventDemo用戶界面如圖5-28所示。

圖5-28 TouchEventDemo用戶界面
由圖5-28可以看出,上半部分的淺藍色區域是可以接受觸摸事件的區域,用戶可以在Android模擬器中使用鼠標點擊屏幕用以模擬觸摸手機屏幕;下方黑色區域是顯示區域,用來顯示觸摸事件類型、相對坐標、絕對坐標、觸點壓力、觸點尺寸和歷史數據量等信息。
在用戶界面中使用了線性布局,并加入了3個TextView控件:第1個TextView(ID為touch_area)用來標識觸摸事件的測試區域;第2個TextView(ID為history_label)用來顯示觸摸事件的歷史數據量;第3個TextView(ID為event_label)用來顯示觸摸事件的詳細信息,包括類型、相對坐標、絕對坐標、觸點壓力和觸點尺寸。
XML文件的代碼如代碼清單5-41所示。
代碼清單5-41 XML文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/touch_area" android:layout_width="match_parent" android:layout_height="300dip" android:background="#80A0FF " android:textColor="#FFFFFF" android:text="觸摸事件測試區域"> </TextView> <TextView android:id="@+id/history_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="歷史數據量:" > </TextView> <TextView android:id="@+id/event_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="觸摸事件:" > </TextView> </LinearLayout>
在上述代碼中,第9行代碼定義了TextView的背景顏色,#80A0FF是顏色代碼;第10行代碼定義了TextView的字體顏色。
在代碼中為了能夠引用XML文件中聲明的界面元素,使用了代碼清單5-42所示的代碼。
代碼清單5-42在代碼中引用XML文件中聲明的界面元素
TextView labelView = null; labelView = (TextView)findViewById(R.id.event_label); TextView touchView = (TextView)findViewById(R.id.touch_area); final TextView historyView = (TextView)findViewById(R.id.history_label);
當手指接觸到觸摸屏、在觸摸屏上移動或離開觸摸屏時,分別會引發ACTION_DOWN、ACTION_UP和ACTION_MOVE觸摸事件,而無論是哪種觸摸事件,都會調用onTouch()方法進行處理。事件類型包含在onTouch()方法的MotionEvent參數中,可以通過getAction()方法獲取到觸摸事件的類型,然后根據觸摸事件的不同類型進行不同的處理。為了能夠使屏幕最上方的TextView處理觸摸事件,需要使用setOnTouchListener()方法在代碼中設置觸摸事件監聽器,并在onTouch()方法添加觸摸事件的處理過程。代碼如代碼清單5-43所示。
代碼清單5-43 onTouch()
touchView.setOnTouchListener(new View.OnTouchListener(){ @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); switch (action) { case (MotionEvent.ACTION_DOWN): Display("ACTION_DOWN",event); break; case (MotionEvent.ACTION_UP): int historySize = ProcessHistory(event); historyView.setText("歷史數據量:"+historySize); Display("ACTION_UP",event); break; case (MotionEvent.ACTION_MOVE): Display("ACTION_MOVE",event); break; } return true; } });
第7行代碼的Display()是一個自定義方法,主要用來顯示觸摸事件的詳細信息,方法的代碼和含義將在后面進行介紹;第10行代碼的ProcessHistory()也是一個自定義方法,用來處理觸摸事件的歷史數據;第11行代碼是使用TextView顯示歷史數據的數量。
MotionEvent參數中不僅有觸摸事件的類型信息,還有觸點的坐標信息,獲取方式是使用getX()和getY()方法,這兩個方法獲取到的是觸點相對于父界面元素的坐標信息。如果需要獲取絕對坐標信息,則可使用getRawX()和getRawY()方法。
觸點壓力是一個介于0和1之間的浮點數,用來表示用戶對觸摸屏施加壓力的大小,接近0 表示壓力較小,接近1 表示壓力較大,獲取觸摸事件觸點壓力的方式是調用getPressure()方法。
觸點尺寸指用戶接觸觸摸屏的接觸點大小,也是一個介于0和1之間的浮點數,接近0表示尺寸較小,接近1表示尺寸較大,可以使用getSize()方法獲取。
Display()將MotionEvent參數中的事件信息提取出來,并顯示在用戶界面上。代碼如代碼清單5-44所示。
代碼清單5-44 Display()
private void Display(String eventType, MotionEvent event){ int x = (int)event.getX(); int y = (int)event.getY(); float pressure = event.getPressure(); float size = event.getSize(); int RawX = (int)event.getRawX(); int RawY = (int)event.getRawY(); String msg = ""; msg += "事件類型:" + eventType + "\n"; msg += "相對坐標:"+String.valueOf(x)+","+String.valueOf(y)+"\n"; msg += "絕對坐標:"+String.valueOf(RawX)+","+String.valueOf(RawY)+"\n"; msg += "觸點壓力:"+String.valueOf(pressure)+", "; msg += "觸點尺寸:"+String.valueOf(size)+"\n"; labelView.setText(msg); }
一般情況下,如果用戶將手指放在觸摸屏上,但不移動,然后抬起手指,應先后產生ACTION_DOWN和ACTION_UP兩個觸摸事件。但如果用戶在屏幕上移動手指,然后再抬起手指,則會產生這樣的事件序列:ACTION_DOWN → ACTION_MOVE →ACTION_MOVE → ACTION_MOVE → …→ ACTION_UP。
在手機上運行的應用程序,效率是非常重要的。如果Android界面框架不能產生足夠多的觸摸事件,則應用程序就不能夠很精確地描繪觸摸屏上的觸摸軌跡。如果Android界面框架產生了過多的觸摸事件,雖然能夠滿足精度的要求,但也降低了應用程序效率。
針對以上問題Android界面框架使用了“打包”的解決方法。在觸點移動速度較快時會產生大量的數據,每經過一定的時間間隔便會產生一個ACTION_MOVE事件,在這個事件中,除了有當前觸點的相關信息外,還包含這段時間間隔內觸點軌跡的歷史數據信息,這樣既能夠保持精度,又不至于產生過多的觸摸事件。
通常情況下,在ACTION_MOVE的事件處理方法中,都先處理歷史數據,然后再處理當前數據,代碼如代碼清單5-45所示。
代碼清單5-45 ProcessHistory(MotionEvent event)
private int ProcessHistory(MotionEvent event) { int historySize = event.getHistorySize(); for (int i = 0; i < historySize; i++) { long time = event.getHistoricalEventTime(i); float pressure = event.getHistoricalPressure(i); float x = event.getHistoricalX(i); float y = event.getHistoricalY(i); float size = event.getHistoricalSize(i); // 處理過程… } return historySize; }
在上述代碼中,第3行代碼獲取了歷史數據的數量;然后在第4行至12行中循環處理這些歷史數據;第5行代碼獲取了歷史事件的發生時間;第6行代碼獲取歷史事件的觸點壓力;第7行和第8行代碼獲取歷史事件的相對坐標;第9行獲取歷史事件的觸點尺寸;在第14行返回歷史數據的數量,主要是用于界面顯示。
問:Android模擬器支持觸點壓力和觸點尺寸的模擬嗎?
答:Android模擬器并不支持觸點壓力和觸點尺寸的模擬,所有觸點壓力恒為1.0,觸點尺寸恒為0.0。同時,Android模擬器上也無法產生歷史數據,因此歷史數據量一直顯示為0。
5.6 自定義樣式和主題
Android也可以像HTML/CSS中的style一樣,使用自定義的style樣式。Android一般通過value文件夾下面新建一個styles.xml文件來設置自定義樣式。這里開發者可以設置高度、填充字體顏色、字體大小、背景顏色等描述一個View或者一個窗口的顯示屬性。這就像Web開發中的CSS樣式表,使我們的樣式獨立于內容進行設計開發。
主題和樣式都是通過在xml文件中預定義一系列屬性值,通過這些屬性值來形成統一的顯示風格。不同的是,樣式只能應用于某種類型的View;而主題剛好相反,不能應用于特定的View,而只能作用于一個或多個Activity,或是整個應用。
下面通過代碼學習一下如何自定義樣式與主題,并在程序中應用。
首先是自定義樣式和主題。在項目的res/values/目錄下添加styles.xml。如代碼清單5-46所示。
代碼清單5-46 styles.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- 定義my_style_1,沒有指定parent,用系統默認的 --> <style name="my_style_1"> <!-- 定義與指定View相關的若干屬性 --> <item name="android:hint">load from style 1</item> </style> <!-- 定義my_style_2,用自定義的my_style_1作為parent --> <style name="my_style_2" parent="@style/my_style_1"> <!-- 定義與指定View相關的若干屬性 --> <item name="android:textSize">30sp</item> <item name="android:textColor">#FFFF0000</item> <item name="android:hint">load from style 2</item> </style> <!-- 定義my_style_3,用android的EditText作為parent --> <style name="my_style_3" parent="@android:style/Widget.EditText"> <!-- 定義與指定View相關的若干屬性 --> <item name="android:hint">"load from style 3"</item> <item name="android:textStyle">bold|italic</item> <item name="android:typeface">monospace</item> <item name="android:background">@drawable/mybackground</item> </style> <!-- 定義MyTheme,用android的Theme作為parent --> <style name="MyTheme" parent="@android:style/Theme"> <item name="android:textSize">20sp</item> <item name="android:textColor">#FF0000FF</item> <item name="android:hint">"load from style 3"</item> <item name="android:textStyle">bold|italic</item> <item name="android:typeface">monospace</item> <item name="android:background">@drawable/gallery_selected_pressed</item> <item name="myStyle">@style/my_style_3</item> </style> </resources>
由上述代碼可以看出,主題和樣式都可以通過在<style>下添加多個<item>來完成其定義。下面來介紹一下<style>的屬性,如表5-12所示。
表5-12 <style>的屬性

在<item>中定義需要改變的屬性值,例如,可以定義顏色、高度等。Android中能使用的屬性可以在<sdk>/docs/reference/android/R.styleable.html中查到;也可以用自己定義的屬性值。
下面講解如何在程序中使用樣式:一方面可以在layout的布局文件中指定自定義的樣式;另一方面可以在java代碼中指定。具體代碼如代碼清單5-47、5-48所示。
代碼清單5-47在布局文件中指定自定義樣式
<EditText android:id="@+id/EditText03" style="@style/my_style_3" android:layout_width="fill_parent" android:layout_height="wrap_content"> </EditText>
代碼清單5-48在java代碼中指定自定義樣式
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); LinearLayout ll = (LinearLayout)findViewById(R.id.main); EditText et = new EditText(this, null, R.attr.myStyle);//指定樣式 ll.addView(et); }
如何在程序中使用自定義主題呢?這與使用自定義樣式相似,都可以通過兩個方法指定,不同的是主題除了可以在java代碼中指定以外,還可以在AndroidManifest中被指定。具體代碼如代碼清單5-49、5-50、5-51所示。
代碼清單5-49在AndroidManifest.xml中指定自定義主題——應用于整個程序
<application android:theme="@style/MyTheme">
代碼清單5-50在AndroidManifest.xml中指定自定義主題——應用于Activity
<activity android:theme="@style/MyTheme">
代碼清單5-51在java代碼中指定自定義主題
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTheme(R.style.MyTheme);//設置主題 setContentView(R.layout.main); }
注意:
setTheme必須在setContentView(),addContentView()或inflate()等實例化View的函數之前調用。
5.7 9Patch
9Patch是一個對png圖片做處理的工具。經9Patch處理過的圖片以*.9.png結尾,和普通圖片相比,四周多了一個邊框,如圖5-29所示。

圖5-29 9Patch處理過的圖片*.9.png
“*.9.png”是一種被Android os所支持的特殊圖片格式,是經過9Patch特殊處理過的圖片。用它可以實現部分拉伸而不出現拉伸以后圖片失真等不良現象。為將普通的PNG圖片編輯轉化為9Patch圖片,可以使用Android SDK/tools目錄下提供的draw9patch.bat編輯器。draw9patch.bat編輯器界面如圖5-30所示。

圖5-30 draw9Patch.bat編輯器界面
在draw9patch.bat編輯器中,可以對導入的png圖片進行以下操作來達到想要的圖片目標。
? Zoom:縮放左邊編輯區域的大小。
? Patch scale:縮放右邊預覽區域的大小。
? Show lock:當鼠標在圖片區域時,顯示不可編輯區域。
? Show patches:在編輯區域顯示圖片拉伸的區域。
? Show content:在預覽區域顯示圖片的內容區域。
? Show bad patches:在拉伸區域周圍用顯示可能會對拉伸后的圖片產生變形的區域;根據圖片的顏色值來區分是否為bad patch。
5.8 本章小結
本章主要對Android程序界面開發的學習,包括用戶界面基礎,界面布局特點及使用方法,用戶界面控件的使用,選項菜單、子菜單和快捷菜單的使用方法,界面事件的處理方法等。
關鍵知識點測評
1.以下有關Android用戶界面的說法,不正確的一個是( )。
A.Android用戶界面框架按照“先進后出”的規則從隊列中獲取事件
B.Android系統的資源文件獨立保存在資源文件夾中
C.Android系統允許不明確定義界面元素的位置和尺寸
D.Android系統使用XML文件描述用戶界面
2.以下有關Android組件的敘述,正確的一個是( )。
A.在程序運行時動態添加或修改界面布局使得在后期修改用戶界面時,無須更改程序的源代碼
B.絕對布局能夠最大程度保證在各種屏幕類型的手機上正確顯示界面布局
C.框架布局中,如果有多個子元素,后放置的子元素將遮擋先放置的子元素
D.在程序運行時動態添加或修改界面布局可以使得程序的表現層和控制層分離
3.以下有關Android系統控件的描述,不正確的是( )。
A.使用ListView時,如果顯示內容過多,則會出現垂直滾動條
B.Spinner使用浮動菜單為用戶提供選擇
C.RadioGroup是RadioButton的承載體,程序運行時不可見,在每個RadioGroup中,用戶僅能夠選擇其中一個RadioButton
D.使用Tab標簽頁只能將不同分頁的界面布局保存在不同的XML文件中
- Kali Linux Web Penetration Testing Cookbook
- Python自動化運維快速入門
- Java程序員面試算法寶典
- Visual FoxPro程序設計
- The Complete Coding Interview Guide in Java
- Angular開發入門與實戰
- Swift 4從零到精通iOS開發
- Mastering Akka
- Python機器學習算法與應用
- IDA Pro權威指南(第2版)
- Oracle實用教程
- Android Studio Cookbook
- Learning Android Application Testing
- Anaconda數據科學實戰
- 第五空間戰略:大國間的網絡博弈