Android应用程序开发与典型案例
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第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文件中