应用程序基础知识:activity和intent
——Android开发秘籍
2.1 Android应用程序预览
Android应用程序可以包含五花八门的功能,比如编辑文本、播放音乐、设定闹钟还有开启通讯录等。这些功能可以划分为以下四类Android组件(见表2-1),每个组件都归属于一个Java基础类。
表2-1 Android应用程序所包含的四种组件
功 能 |
Java基础类 |
范 例 |
关注用户操作 |
Activity |
编辑文本,玩游戏 |
后台进程 |
Service |
播放音乐,更新天气图标 |
接收消息 |
BroadcastReceiver |
根据特定事件触发警报 |
存取数据 |
ContentProvider |
开启手机通讯录 |
每个Android应用都由一个或多个组件组成。当要用到某个组件的时候,Android操作系统就会将它们实例化。在拥有特定权限的情况下,其他应用程序同样也可以使用它们。
Android操作系统集成了很多功能(某些功能甚至并非和某个应用程序直接相关,如呼入电话),每个组件都具有以下生命周期,包括创建(create)、获得焦点(focus)、失去焦点(defocus)和销毁(destroy)。我们可以通过改写程序默认的行为,使交互对用户更加友好,比如保存变量或者恢复用户界面元素。
除了ContentProvider组件,每个组件都需要一个叫做Intent的异步消息来激活。Intent可包含一组(Bundle)描述该组件的辅助数据。这也提供了一种在组件之间传递消息的方法。
本章最后将会使用最常见的组件Activity来演示前面提到的概念。由于activity总是和具体的用户交互相关,所以每个activity在创建的时候会自动创建一个新窗口。当然还会提到一些关于UI的概要介绍。至于Service和BroadcastReceiver这两个组件我们将会在第3章讲解,而ContentProvider则会在第9章阐述。
2.1.1 秘诀1:创建工程并新建activity
创建Android工程或者组件最简单的方法莫过于使用Eclipse提供的集成开发环境(IDE),该方法能够确保正确安装辅助文件。创建Android工程的具体过程如下所示。
(1) 在Eclipse中,选择File→New→Android Project。然后就会显示Android工程的创建画面。
(2) 填写工程名称(Project name),此处输入SimpleActivityExample。
(3) 在Build Target选项框中选择编译目标,这些选项与开发电脑的SDK版本有关。
(4) 填写程序名称(Application name),此处为Example of Basic Activity。
(5) 填写应用程序包名称(Package name),此处为com.cookbook.simple_activity。
(6) 根据同样的步骤创建主activity,注意一定要勾选Create Activity,并填写activity名称,此处为SimpleActivity。
所有的activity都继承于抽象类Activity或者其子类,并通过onCreate()方法创建。activity通常在初始化的时候都会重载该方法,比如设置UI、创建监听按钮、初始化参数或者开启线程等。
如果在创建工程时没有创建主activity,或者需要添加其他activity,可以通过以下步骤来创建activity。
(1) 首先创建一个新类扩展Activity类。(在Eclipse中,右键单击project,选择New→Class,然后指定android.app.Activity作为父类。)
(2) 重载onCreate()功能。(在Eclipse中,右键单击class文件,选择Source→Override/ Implement Methods...,然后勾选onCreate()方法。)
(3) 作为最常被重载的方法之一,activity也必须激活父类方法,否则运行时可能会抛出异常。如清单2-1所示首先调用super.onCreate()方法,确保正确初始化activity。
清单2-1 src/com/cookbook/simple_activity/SimpleActivity.java
(4) 如果使用UI,则要在res/layout/目录下的一个XML文件中指定页面布局。此处为main.xml,如清单2-2所示。
清单2-2 res/layout/main.xml
(5) 通过setContentView()方法设置activity的布局,并将XML布局文件作为resource ID传递给它。此处为R.layout.main,见清单2-1。
(6) 在AndroidManifest XML文件中声明activity的属性,详细内容见清单2-5。
注意字符串类型的资源要在res/values/文件夹下的strings.xml文件中定义,如清单2-3所示。所有字符串都集中于此处定义,可以方便修改或重用。
清单2-3 res/values/strings.xml
<![endif]>
现在我们进一步探讨该工程的目录结构和自动生成内容。
2.1.2 工程目录结构及自动生成内容
图2-1为Eclipse Package Explorer显示的一个工程结构示例。
除Android 2.0库文件以外,该工程的目录结构中的文件既有用户创建的也有系统自动生成的。
用户创建的文件如下所示。
q <![endif]>src/是开发者自己编写的或者导入的Java包。每个包可以包含多个不同的.java类文件。
q <![endif]>res/layout/用来存放说明每个界面布局的XML文件。
q <![endif]>res/values/用来存放被其他文件所引用的XML格式的资源文件。
q <![endif]>res/drawable-hdpi/、res/drawable-mdpi/和res/drawable-ldpi/是程序所使用图片的资源目录,分别存放高、中、低不同dpi分辨率的图片。
q <![endif]>assets/存放程序使用的nonmedia文件。
q <![endif]>AndroidManifest.xml向Android操作系统说明该工程。
自动生成的文件如下所示。
q <![endif]>gen/存放系统自动生成代码,包括生成的R.java类。
q <![endif]>default.properties存放工程环境信息。尽管该文件由系统自动生成的,但开发人员也可以根据需要修改。
应用程序的资源包括描述布局的XML文件,描述字符串值、UI元素标签的XML文件,以及其他如图片、声音等辅助文件。编译时,对资源的引用都会添加到自动生成的包装类R.java中。该文件由AndroidAsset打包工具(aapt)自动生成。清单2-4为秘诀1使用的该文件。
清单2-4 gen/com/cookbook/simple_activity/R.java
此处的每个资源都被映射成一个唯一的整型值。通过这种方式,R.java类提供了一种在Java代码中引用外部资源的方法。例如想要在Java文件中引用main.xml布局文件,就需要使用整型值R.layout.main。如果是在XML文件中引用main.xml文件,就需要使用"@layout/main"字符串。
在Java或者XML文件中引用资源请参见表2-2。请注意,假若要定义一个ID为home_button的按钮,需要在引用字符串前添加“+”号,即:@+id/home_button。第4章再详细地探讨资源引用,此处内容对本章秘诀的学习已经足够。
表2-2 在Java和XML文件中引用不同的资源
资 源 |
在Java中引用 |
在XML中引用 |
res/layout/main.xml |
R.layout.main |
@layout/main |
res/drawable-hdpi/icon.png |
R.drawable.icon |
@drawable/icon |
@+id/home_button |
R.id.home_button |
@id/home_button |
<string name="hello"> |
R.string.hello |
@string/hello |
2.1.3 A ndroid包和Manifest清单文件
Android工程,有时也称为Android包,是Java包的集合。不同的Android包可以包含相同名称的Java包,但在安装到Android设备上时,各个Android包的名字必须是唯一的。
为了操作系统能够正确访问这些Android包,每个应用程序必须在名为AndroidManifest 的XML文件中注册声明它所使用的组件。此外该XML文件还包含运行该应用程序所需的权限及操作。清单2-5为秘诀1所用文件。
清单2-5 AndroidManifest.xml
Android包所有XML文件第一行都必须指定编码,该行代码为标准代码。manifest元素定义Android包的名称和版本号。versionCode可以根据你的程序情况定义,为确定版本高低关系的一个整数。versionName采用人可读懂的格式表示,可以声明主次修订版本号。
application元素定义用户从Android设备菜单可以看到的应用程序图标和名称。名称是一个字符串,为了确保在用户设备中将其显示在应用图标下方,应该尽量使其简短。一般来说,名称最多两个单词,每个单词最好在十个字符之内,中间不能含有空格。
activity元素定义程序启动时触发的主activity,以及该activity激活后标题栏中显示的名称。在这儿需要指定Java包名,本例为com.cookbook.simple_activity,相应activity名称为Simple- Activity。由于Java包名称一般和Android包名称一致,所以常常会使用缩写SimpleActivity。不过最好记住Android包和Java包还是有区别的。
intent-filter元素向系统说明该组件功能。鉴于此作用,它可以包含多个action,category或者data元素。该点在不同的秘诀中都有所体现。
uses-sdk元素定义运行此程序所需的API级别。一般来说,API级别定义如下:
由于Android系统向前兼容,maxSdkVersion所定义的最高API支持版本会令人极度沮丧,因为它不支持Android 2.0.1 及之后的版本。targetSdkVersion可要可不要,该项用于允许同一SDK版本的设备禁用加快操作速度的升级兼容性设置。但minSdkVersion必须定义,以确保应用程序在不支持该应用所需的功能的平台上运行时不会崩溃,定义时尽可能选择较低的API级别。
AndroidManifest存放运行该应用程序所需的权限。我们会在随后的章节中进一步详细阐述,但以上部分基本可以涵盖本章秘诀。
2.1.4 重命名应用程序中的部分文件
有时候我们需要重命名Android工程的部分文件,或许是从本书中手动复制一个文件放在工程中,或许是在开发过程修改了程序名称,需要在文件系统的目录树反映出来。Android提供了工具帮我们自动完成此项工作,并且可以同步更新交叉引用。例如在Eclipse IDE中,使用下列不同的方式来重命名应用程序的部分文件。
q <![endif]>重命名Android工程,步骤如下:
(1) 右键单击该工程选择Refactor→Move移到文件系统中的一个新目录;
(2) 右键单击该工程选择Refactor→Rename重命名工程。
q <![endif]>重命名Android包,步骤如下:
(1) 右键单击该包选择Refactor→Rename重命名该包;
(2) 更新AndroidManifest.xml包名称。
q <![endif]>重命名Android类(如Activity、Service、BroadcastReceiver、ContentProvider等主要组件),步骤如下:
(1) 右键单击该Java文件选择Refactor→Rename重命名该类文件;
(2) 更新AndroidManifest.xml确保android:name使用新组件名。
注意重命名XML文件等其他类型文件的时候,通常都要手动修改Java代码中的相应的引用。
2.2 Activity的生命周期
程序中的每个activity都有自己的生命周期。通过调用onCreate()方法,activity能且仅能被创建一次。当onDestroy()方法执行时,该activity随即关闭。正如图2-2所阐述的那样,不同事件可以导致activity不同的运行状态。秘诀2将为我们一一呈现这些功能。
图2-2 activity的生命周期,来源:http://developer.android.com/
2.2.1 秘诀2:使用其他的生命周期方法
下面的秘诀提供了一种查看活动中activity生命周期的简单方法。为便于演示,每个被重载的方法都有明确说明,我们通过加入Toast命令,使得该方法在启动的时候,在屏幕上显示。(关于Toast微件的更多内容请参见第3章)。在Android设备上运行以下代码(如清单2-6所示),并尝试各种情况,特别是注意以下几种操作:
q <![endif]>颠倒屏幕方向,将结束并重新运行activity;
q <![endif]>按下Home按钮将暂停activity,但并不结束;
q <![endif]>按下程序图标可能会开启新的activity实例,即使先前的activity没有关闭;
q <![endif]>屏幕处于休眠态时会暂停activity,屏幕重新唤醒时会继续该activity(类似于呼入电话)。
清单2-6 src/com/cookbook/activity_lifecycle/ActivityLifecycle.java
我们可以看到,用户的很多常见操作都可能会导致activity暂停运行、结束甚至启动数个应用程序版本。在继续下一部分内容之前,有必要给大家介绍两种方法来控制这种操作行为。
2.2.2 秘诀3:强制执行单任务模式
如果应用程序跳转走后再次启动的话,可能会在设备上产生多个activity实例。最终为释放内存,多余的activity实例会被系统杀死,但与此同时,也很可能会导致异常。为避免上述情况发生,程序员可以在AndroidManifest中控制每个activity的这种行为。
为确保设备上只有一个activity实例在运行,需要在activity元素中包含MAIN和LAUNCHER两个intent过滤器,如下:
该行代码确保在任务中的任何时刻,每个activity都只有唯一一个运行实例。此外,该实例的所有子activity都作为自身任务启动。为进一步限制应用程序中的所有activity都只能运行一个实例,不妨使用以下代码:
这样使得所有activity作为同一个任务,共享信息非常方便。
此外,有时我们希望无论用户通过什么方式进入activity都能够保存任务的状态。例如,如果用户离开了应用程序,不久后又重新启动了该应用程序,默认情况下系统会重设任务到初始化状态。为确保用户总是能返回到关闭之前的状态,需要在任务的根activity的activity元素的属性中作如下定义:
2.2.3 秘诀4:强制屏幕方向
每个带有加速度计的Android设备都可以判定方向。当设备由纵向模式切换到横向模式时,默认动作是相应地旋转应用程序视图。然而秘诀2,倒置屏幕会导致已经结束的activity重新启动。如果是这种情况,那么就会丢掉当前的程序状态,从而破坏用户体验。
解决屏幕倒置问题的一种方案是在发生改变之前保存用户的状态,改变方向后重新启动activity时读取用户先前状态。还有一种更简单的办法,就是强制设定屏幕的方向,禁止旋转切换视图。AndroidManifest中列出的每个activity都可以定义屏幕方向。比如为了指定某个activity始终以纵向模式运行,在activity元素中可以添加如下代码:
同样,如果想设定为横向模式,可以使用如下代码:
然而,在硬键盘滑出时,先前的情况还是会导致activity的关闭和重新启动。所以我们可以采用第三种办法,即告知Android系统处理应用程序方向和键盘滑出事件。可以在activity元素的属性中添加如下代码:
该方法可以单独使用,也可以和screenOrientation属性结合在一起使用,视应用程序要求而定。
2.2.4 秘诀5:保存和恢复activity信息
每当一个activity即将被杀死时,都会调用onSaveInstanceState()方法。重载该方法可以保存相关状态信息。当重新创建该activity时,则会调用onRestoreInstanceState()方法。重载该方法可以获取先前保存的状态信息。这样当应用程序经历生命周期变化时,就可以为用户带来无缝体验。值得注意的是,大部分UI控件状态都不需要我们亲自处理,系统会自动帮我们完成此项工作。
onPause()方法略有不同。如果另一个组件在activity中启动,就会调用onPause()方法暂停此activity活动。稍后系统如要回收内存等资源时,该activity仍然处于暂停状态,Android系统就会调用onSaveInstanceState()方法保存状态信息,然后将其杀死。
清单2-7为存取包含一个string数组和一个float数组的实例状态信息的示例。
清单2-7 SaveInstanceState()和onRestoreInstanceState()示例
请注意,onCreate()方法也包含Bundle savedInstanceState。当activity关闭之后重新初始化,之前onSaveInstanceState()方法中保存的bundle状态信息会传递给onCreate()方法。总之,所有保存的状态信息都会传递给onRestoreInstanceState()方法,所以自然会利用它来恢复之前状态。
2.3 多个activity
即使最简单的应用程序也会拥有多个功能,所以经常需要使用多个activity。比如,一个游戏可能包含两个activity,一个用来显示高分排行榜,另一个则用来显示游戏画面。一个记事本程序可能包含三个activity:查看笔记列表、阅读某条笔记、编辑某条笔记或加新笔记。
当程序启动时,就会执行AndroidManifest XML文件中定义的主activity。通过事件触发,可以跳转到另外一个activity。当第二个activity被激活时,先前的主activity就处于暂停状态。当第二个activity运行结束后,主activity 就会再次回到前台恢复运行。
若想激活程序中的某个组件,可以使用intent直接来指定该组件。但如果想通过intent过滤器指定,则可以使用隐式intent,再由系统决定最合适的组件,不管它是其他应用程序组件还是本机操作系统自带组件,都可以为其所用。要注意的是,其他应用程序中的隐式intent不需要在当前程序中的AndroidManifest文件注册声明。
Android主张尽可能利用隐式intent为用户提供强大的功能模块框架。当新开发的组件能满足隐式intent过滤器的需求,就可以用它来替代Android 的内部intent。譬如,在Android设备上加载手机通讯录。当用户选择一个联系人时,Android系统会自动通过适当的intent过滤器查找联系人来发现所有可用的activity,并让用户自己选择所使用的activity。
2.3.1 秘诀6:使用按钮和文本框
我们使用触发事件充分演示多个activity的切换。为此,我们在示例中引入了按钮按下事件。下面将在某个页面布局中添加一个按钮,并指定按钮被按下时的动作,步骤如下。
(1) 在XML页面布局文件中声明一个button控件:
(2) 通过布局文件中的button ID声明button控件对象:
(3) 添加点击按钮事件的OnClickListener监听器:
(4) 重载监听器的onClick方法执行你想要的动作:
我们可以通过改变屏幕上显示的文字向用户反馈交互结果。定义文本框并用编程手段来实现改动,步骤如下所示。
(1) 在XML布局文件中通过ID声明一个textview控件。同时也可以初始化,设定为某个值。(此处将其初始化为strings.xml文件中名为“hello”的字符串值。)
(2) 在布局文件中声明一个TextView控件,指向TextView ID:
(3) 如果要修改文本内容,可以使用setText方法:
这两个UI技巧将会在本章后面的几个秘诀中用到。第4章将会系统讲解Android的UI控件。
2.3.2 秘诀7:通过事件启动另外一个activity
在本秘诀中,MenuScreen是主activity,如清单2-8所示,这里将启动PlayGame activity。其触发事件为Button微件的单击事件。
当用户单击按钮时会运行startGame()方法,该方法启动PlayGame activity。而当用户在PlayGame activity中点击按钮时,则会调用finish()方法,将控制权移交给调用它的activity。启动activity的步骤如下:
(1) 声明一个intent,指向即将被启动的activity;
(2) 调用该intent的startActivity方法;
(3) 在AndroidManifest中声明其他的activity。
清单2-8 src/com/cookbook/launch_activity/MenuScreen.java
在匿名内部类中提供当前上下文环境 注意,在通过点击按钮启动activity时还需要作额外的考虑,如清单2-8所示。intent需要上下文环境。但this引用在onClick方法中不能正确解析。在匿名内部类中提供当前上下文环境的方法如下: q <![endif]>使用Context.this代替this; q <![endif]>使用getApplicationContext()来代替this; q <![endif]>显式地使用类名MenuScreen.this。 调用一个在适当的上下文环境级别中声明的方法,如清单2-8所使用的startGame()。 以上方法都可以互相转换,我们可以根据需要灵活运用。 |
清单2-9所示的PlayGame activity只有一个按钮,带有onClick监听器,点击该按钮调用finish()方法会将控制权返回给主activity。当然也可以根据需要为PlayGame activity添加更多的功能模块,每个分支模块的代码都可以调用finish()方法结束该activity的运行。
清单2-9 src/com/cookbook/launch_activity/PlayGame.java
如清单2-10所示,该按钮必须添加到main.xml布局文件中,按钮的ID为play_game,必须与清单2-8中所声明的内容相匹配。此处使用和设备无关的像素(dip)定义按钮大小,我们将会在第4章深入讨论。
清单2-10 res/layout/main.xml
正如清单2-11所示,PlayGame activity引用了其自己的ID为end_game的按钮,该按钮的布局资源R.layout.game对应的是XML布局文件game.xml。
清单2-11 res/layout/game.xml
虽然在各种情况下,文本都可以显式地写在文件中,但为每个字符串定义变量是一种好的编程习惯。在本秘诀中,有play_game和end_game两个字符串值,它们被定义在一个字符串XML资源文件中,见清单2-12所示。
清单2-12 res/values/strings.xml
最后需要在AndroidManifest XML文件中为PlayGame这个新类声明其默认的action,详见清单2-13。
清单2-13 AndroidManifest.xml
2.3.3 秘诀8:将语音转换成文本并启动activity显示结果
本秘诀将演示在启动activity时如何处理其返回值的问题。同时演示了如何利用Google的RecognizerIntent将语音转换输出成文本并在屏幕上显示的功能。此例的触发事件同样是按钮单击事件,它将启动RecognizerIntent activity,辨识麦克风输入的声音并将其转换成文本格式。运行结束后将文本传回给调用它的activity。
当返回时,首先会用返回的数据调用onActivityResult()方法,然后调用onResume()方法使activity正常运行。因为返回的activity可能存在问题,导致不能正确传递值,所以必须核查resultCode确保是RESULT_OK才可以继续解析返回的数据。
请注意,启动回传数据的activity通常都会调用同一个onActivityResult()方法。所以需要使用请求码(request code)判断哪个activity要回传数据。当activity启动完成后,就会把控制权重新交回给调用它的activity,并用同样的请求码调用onActivityResult()方法。
启动带有返回值的activity的步骤如下。
(1) 通过intent调用startActivityForResult(),定义要启动的activity并标记requestCode。
(2) 重载onActivityResult()方法,检查返回结果的状态以检查期望的requestCode,并解析返回数据。
使用RecognizerIntent的步骤如下。
(1) 声明一个intent,设置其动作为ACTION_RECOGNIZE_SPEECH。
(2) 向intent传递附加内容,至少EXTRA_LANGUAGE_MODEL是必需的。它可以被设置成LANGUAGE_MODEL_FREE_FORM或者LANGUAGE_MODEL_WEB_SEARCH。
(3) 返回的数据包中包含可能和原文匹配一个字符串列表。通过data.getStringArray- ListExtra可以获取这些数据,它将映射为ArrayList类型资源供稍后使用。
使用TextView将返回的文本显示到屏幕上。主activity的内容参见清单2-14。
此外还有main.xml和strings.xml这两个辅助文件,用于定义按钮和存放结果的TextView。具体内容可以参考秘诀7中清单2-10以及清单2-12的内容。当前只需要在AndroidManifest中声明主activity,这点和秘诀1中的步骤一致。RecognizerIntent activity是Android系统原生的activity,所以在使用前就不需要显式声明。
清单2-14 src/com/cookbook/launch_for_result/RecognizerIntentExample.java
2.3.4 秘诀9:实现选择列表
应用程序中常常需要提供给用户一个选择列表以供用户点击选择,利用Activity的子类ListActivity就可以实现该功能,并根据用户的选择触发事件。
创建选择列表的具体步骤如下。
(1) 首先创建一个类扩展ListActivity类,而非Activity类:
(2) 创建一个字符串数组,为每个选项指定标签:
(3) 通过ArrayAdapter调用setListAdapter()方法,并指明该选择列表和布局方式:
(4) 运行OnItemClickListener监听确定用户选择了哪个选项,并针对其作出反馈:
上述技巧在下一个秘诀中也会用到。
2.3.5 秘诀10:使用隐式intent创建activity
隐式intent不会确切指定需要使用哪个组件。相反,它们通过过滤器确定所需要的功能,再由Android系统选择最匹配该功能的组件。intent过滤器可以是动作、数据或者分类(category)。
动作是最常用的intent过滤器,而其中又数ACTION_VIEW最为常用。它需要声明一个统一资源标识符(URI),用来向用户显示数据。对于给定的URI选择最佳的处理方式。例如在下面的范例中,隐式intent在case 0、1、2中虽然句法格式相同,但产生的结果却大大不同。
使用隐式intent启动activity的步骤如下:
(1) 为intent声明恰当的intent过滤器(ACTION_VIEW、ACTION_WEB_SEARCH等);
(2) 向intent添加运行某个activity所需要的附加信息;
(3) 将intent传递给startActivity()。
清单2-15将演示如何处理多个intent。
清单2-15 src/com/cookbook/implicit_intents/ListActivityExample.java
2.3.6 秘诀11:在activity间传递基本数据类型
我们有时需要向被调用的activity传递数据,而有时被调用的activity反过来也需要向调用它的activity回传数据。比如,游戏最后得分就需要回传给高分排行榜界面。activity之间传递信息的方式有如下几种:
q <![endif]>在发起调用的activity中声明相关变量(如public int finalScore),在被调用的activity中就可以为这些变量赋值(如CallingActivity.finalScore=score);
q <![endif]>通过Bundle包附加数据(本例演示);
q <![endif]>利用Preference属性存储数据,需要时再读取(将会在第5章中阐述);
q <![endif]>利用SQLite数据库存储数据,需要时再读取(将会在第9章中阐述)。
Bundle包是字符串值到各种parcelable类型的映射。它在向intent附加属性值时创建。下例将演示如何从主activity向它所启动的activity传递数据,并且在后者修改数据之后回传结果。
在StartScreen activity中声明了两个变量(本例中为一个整形变量和一个字符串类型变量)。当创建intent调用PlayGame类时,通过putExtra方法将这两个变量传给intent。当结果从被调用的activity返回后,可以使用getExtras方法读取变量值。调用程序如清单2-16所示。
清单2-16 src/com/cookbook/passing_data_activities/StartScreen.java
传递给PlayGame activity的变量值可以使用getIntExtra和getStringExtra方法读取。当该activity结束后调用intent回传时,我们就可以使用putExtra方法返回数据到发出调用的activity。具体调用代码见清单2-17所示。
清单2-17 src/com/cookbook/passing_data_activities/PlayGame.java