本节书摘来自异步社区《Android应用开发》一书中的第2章,第2.2节活动类,作者 【美】Chris Haseman,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.2 活动类
Android应用开发
在一个典型的Android应用中,活动是操作的骨干。从本质上说,它们的目的是控制屏幕上的显示内容。它们消除了想要显示的数据与实现数据显示的界面布局文件、类之间的鸿沟。如果熟悉流行的“模型—视图—控制器”(MVC)架构,活动就是一个屏幕的控制器。清单文件中的活动声明如下所示:
<activity android:name=".MyActivity"
anadroid:label="@string/app_name">
<!— 关于意图过滤器工作机制的更多内容参见下一节 -->
<intent-filter>
<action android:name="Android.intent.action.MAIN" />
<category android:name="Android.intent.category. LAUNCHER" />
</intent-filter>
</activity>
android:name标记告诉系统要到程序包末尾的什么位置(从清单声明中)去寻找类定义。例如,在com.haseman.peachPit. MyActivity上的样例项目中,类装载器会期望找到一个活动类的扩展类。
为了能被找到,文件必须位于src/com/haseman/peachPit目录下。对于Android所使用的语言,这是标准操作流程。
2.2.1 看着活动类发挥作用
活动如果正确运用的话是一个对象,专门控制一个单独的屏幕。下面把现实世界里的RSS新闻读者作为案例研究,来谈谈这个虚构的活动,这样可以快速解释理论上常常忽略的问题。典型做法是使用一个活动列出一个用户订阅的所有推送新闻。用户单击一个推送时,开发人员就使用第二个活动来显示那个特定新闻推送的可用文章列表。最后,当用户单击一个特定文章时,开发人员用第三个活动来显示文章的详细内容。
很容易看出活动是如何担当一个角色的(订阅列表,文章列表,文章内容)。同时,活动是一般性的,表现在文章列表应该能够显示来自任意RSS推送的文章,而文章内容活动应该能显示通过一个RSS读者所找到的任意文章的内容。
2.2.2 实现自己的活动类
大多数情况下,理解某种事物的最好方法就是使用它。记住这一点,下面把一个新的活动添加到第1章创建的项目中。这可以解释活动是如何工作的、它的生命周期,以及和它一同工作时需要了解什么。以下是需要遵循的一般步骤。
(1)在声明中为新的活动添加一个条目。
(2)创建一个新类,扩展活动类。
(3)创建一个新文件,包含这个新活动的XML布局说明,并添加一个新的字符串格式文字,让布局来显示(别担心,尽管这听起来很难,实际操作起来却简单得多)。
(4)当所有文件都准备好的时候,就需要从现有的活动中真正启动这个新活动。
1.最基本的活动
一个活动最简单的形式就是一个扩展了活动类的对象。它应该(但不必须)实现onCreate方法。创建一个新的项目时,活动默认的定义如下:
public class MyActivity extends Activity {
/* 当活动刚创建时调用 */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
在这段代码中,设备在活动启动的时候调用onCreate方法。onCreate方法告知用户界面系统setContentView方法指定这个活动的主布局文件。每个活动都可以有一个而且最多一个内容视图,因此一旦设定就不能更改。Android SDK就是这样保证为每个屏幕使用一个新的活动,因为每次想修改根内容视图的时候,都需要一个不同的活动。
2.通知Android系统有用户友好的新活动
应该装载和启动新活动的时候,Android系统需要知道到哪去找它。
(1)在Eclipse中打开AndroidManifest.xml文件。
(2)在标记内,就在前一个活动声明的结束标记之后添加下列代码:
<activity android:name=".NewActivity"/>
这一短行代码告诉系统新的活动位于应用程序包内的什么位置。以本书的范例为例,类装载器知道在com.haseman.peachPit. NewActivity能找到新的活动。
接下去,需要把一个文件放在那里让它找到。
3.创建新活动类
创建一个新活动有几种方法,在Eclipse中创建的最简单方法如下。
(1)右键单击(或单击Control键)选中的程序包名称(这里是com.haseman.peachPit)。
(2)选择菜单New->Class。
(3)在对话框中输入新的类名字。
一个名字就可以创建一个新的文件。文件会以指定的名字保存在主程序包内。在这里的范例程序中,文件位于src/com/ haseman/peachPit/NewActivity.java目录下的项目目录中。
既然已经由一个对象扩展得到一个类,下面需要把它转换一下,扩展一个活动。
(4)在代码中完成以下粗体部分所示的修改:
package com.haseman.peachPit;
import android.app.Activity;
import android.os.Bundle;
public class NewActivity extends Activity{
public void onCreate(Bundle icicle){
super.onCreate(icicle);
}
}
注意,这段代码与现有活动中的代码非常类似。接下去要使它有所不同。
(5)在res/values/strings.xml文件中,在已有的字符串下添加以下标记中粗体显示的代码行。
<resources>
<!— 这里为了简洁,省略其他字符串 -->
<string name="new_activity_text">
Welcome to the New Activity!
</string>
</resources>
这些代码告诉Android系统,需要一个新的字符串,名字是new_activity_text,可以通过Android的资源管理器来访问它。
在后面的章节中会对/values文件夹的内容有更多介绍。接下来需要为新的活动创建一个布局文件。
4.创建一个新的屏幕布局
创建一个新布局的过程如下。
(1)在res/layout/目录下创建一个名为new_activity.xml的新文件。它应该位于已有的main.xml文件(现有的活动正在使用它)旁边。这个new_activity.xml文件看起来应该与main.xml一样,除了需要添加一个指针,指向刚刚创建的字符串。
(2)插入下面代码中粗体处理的代码行,创建一个指针,指向刚刚创建的字符串。
(3)给修改后的TextView赋予一个ID,这样用户的Java代码就可以引用它(后文会介绍关于TextView的更多内容,现在需要知道的是,TextView是在屏幕上显示文本的Android视图)。代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.Android.com/apk/ res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:id="@+id/new_activity_text_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/new_activity_text"
/>
</LinearLayout>
第3章会专门介绍资源管理和用户界面设计,目前只需要记住,前缀@是告诉Android系统,想把在别处定义的一个ID、字符串或drawable类作为一个资源来引用。
既然有了一个新的布局,带有全新的字符串,就需要告诉NewActivity类,让它使用这个特殊的布局文件。
(4)把下面粗体处理的代码行加入到NewActivity类的onCreate方法中。
public void onCreate(Bundle icicle){
super.onCreate(icicle);
setContentView(R.layout.new_activity);
}
setContentView方法是告诉Android系统新活动会显示哪个XML文件。既然已经创建了一个新的类、字符串和布局文件,下面该启动这个活动,在屏幕上显示新的视图。
5.捕捉键盘操作
启动新活动的一个简单方法是用户按他(她)手机上的center键。如果手机上没有center键,也可以轻松修改下列代码,接收想按的任意键。为了检测按键事件,需要扩展初始活动类的onKeyDown方法。记住这是一个简单的范例。在用户按一个键的时候启动一个新的活动,这可能在实践中并不是很常见的例子,但它构成了一个简单的好例子。大多数新的活动是在用户选择一个列表项、单击一个按钮或者在屏幕上进行另一种操作时启动的。
新版本的onKeyDown应该如下所示:
@Override
public boolean onKeyDown(int keyCode, KeyEvent event){
if(keyCode == KeyEvent.KEYCODE_DPAD_CENTER){
//这里启动新的活动!
return true;
}
return super.onKeyDown(keyCode, event);
}
声明onKeyDown是要重载默认的键盘事件处理方法, 采取新活动专有的动作。如果传入的按键事件不是需要活动自己来处理的事件,就把它传给父类版本的方法,这一向是好的做法。
注意,当keyCode与center键匹配时,返回true。这是告诉Android系统的活动和视图,这个按键事件已经被正确处理,不必再传给别人,否则就让活动的父类来处理这个按键事件。现在看起来也许关系不大,但当Android的活动变得越来越复杂的时候,这一点就会重要得多。
6.启动活动
终于到了启动新活动的时刻。这里会简单涉及到意图。每个新活动被启动,都是作为一个新的意图被分派到系统(拥有这个意图并进行适当的操作)内的结果。为了启动第一个活动,需要指向应用环境的一个指针和代表新活动的类对象。下面先创建新的意图。
(1)把下列代码加入到onKeyDown键盘处理方法中。
Intent startIntent=new Intent(
this.getApplicationContext(),
NewActivity.class);
这是给新的意图传送一个应用环境和想要启动的活动的类对象。这告诉Android系统到底到应用程序包的什么地方去找。有许多方法可以创建意图并和它互动,但这里介绍的是启动一个新活动的最简单方法,一旦正确构建意图后,接下来就只需要告诉Android系统想要启动这个新活动。
(2)把下列代码加入到键盘处理方法中。
startActivity(startIntent);
onKeyDown处理函数如下所示:
public boolean onKeyDown(int keyCode, KeyEvent event){
if(keyCode == KeyEvent.KEYCODE_DPAD_CENTER){
Intent startIntent=new Intent(this, NewActivity.class);
startActivity(startIntent);
return true;
}
return super.onKeyDown(keyCode, event);
}
注意:
在整个过程中, 初始活动一次也未曾访问过新活动的实例。在两个活动之间可能传递的任何信息都必须经过中间的意图。后面的2.6节会介绍如何实现这一点。
7.尝试
如果Eclipse在运行中,并且一直跟随本书所述进行编码,那么现在要做的事情就很简单,启动模拟器并安装新的活动(应该记得第1章中介绍的步骤)即可。装载了新的应用之后,按center键,查看之前所有工作的成果(见图2.1)。
既然已经知道如何创建和启动新活动,下面该讨论这个过程如何作用。要了解每当一个活动显示到屏幕上或从屏幕上消失都是调用什么方法完成的,这是考虑到此后的用户界面布局和数据管理/数据保存。
获取意图
意图可以有无数种形式。
每次需要启动一个活动或服务的时候都用到它们。而且会经常用意图实现系统范围内的通信。例如,注册一个广泛发布的意图,就可以接收关于动力系统变化的通知。如果一个活动注册了一个声明中的意图(例如com.haseman.peachPit.OhOhPickMe),那么手机上任何位置的任何应用都可以通过如下调用来直接启动活动,如果这个活动是公共的话。
startActivity(new Intent("com.haseman.peachPit.OhOhPickMe"))
2.2.3 活动的生命和重要时刻
我们知道,每个活动都有一个非常短暂但辉煌的生命。活动注册时定义为要接收一个意图,当这个意图被广播给系统的时候,就开启了活动的生命周期。系统调用活动的构造函数(必要时也会启动应用),然后以下列顺序依次调用活动的各个方法:
(1)onCreate;
(2)onStart;
(3)onResume。
实现一个活动之后,任务就是扩展构成这个生命周期的方法。唯一一个需要扩展的方法是onCreate。其他方法如果已经声明,就会在生命周期内按顺序调用。
活动是顶层可见的应用,它可以拖到屏幕上,接收关键事件,它通常是一切的中心。当用户在活动上按Back键时,相关的方法以下列顺序被调用:
(1)onPause;
(2)onStop;
(3)onDestroy。
活动短暂、残酷的一生
活动的生命极其短暂。它们被不断创建和消灭,因此非常重要的一点是,在活动中不要保存与该活动所控制的屏幕之外相关的数据。
会导致活动毁灭的所有有效操作如下。
(1)用户将手机屏幕从竖向转到横向或者反之。
(2)该活动在屏幕上不再可见,或系统资源短缺。
(3)当天是周二。
(4)用户按了Back或Home键,离开了应用。
再次确保活动的数据成员只与屏幕上的因素相关。而且,不要期望活动保存任何数据。后面的章节会介绍处理这种情况的策略。
执行了这些方法之后,活动被关闭,并准备垃圾收集。
为了理解活动在生命周期内的流转过程,下面快速浏览一下生命周期内各方法的细节。记住,各方法中都必须调用父类的方法(通常是在什么都还没做的时候),否则Android系统会抛出异常。
注意:
onCreate是应用的生命周期内唯一一个必须用户来实现的方法。笔者在大部分Android开发工作中发现,最后只能实现这些方法中的一两个,这取决于活动具体完成什么工作。
1.public void onCreate(Bundle icicle)
Android系统会在活动启动的时候调用这个方法的声明。但是要记住,在应用运行期间,可能经过每个活动的好几个实例。例如,如果用户将屏幕的方向从横向变为竖向,活动就会被销毁,然后创建同一个活动的一个新实例并对其初始化。
例如,如果活动的标题是动态的,但在活动启动后就不会改变,那么这个方法会在视图层次中想到达的地方设置标题。这个方法不是用来配置数据的,当应用位于后台或另一个活动装载到它之上的时候,数据可能会改变。
而且,如果应用运行在后台而系统在资源短缺的情况下运行,应用就可能被杀死。如果这样,当应用从后台返回的时候,就会在同一活动的一个新实例上调用onCreate方法。
onCreate方法也是唯一为活动调用setContentView的机会。正如前文所见,这是告诉系统希望这个屏幕用怎样的布局。一旦可以设置用户界面上的数据,就调用setContentView。它可以包含任何设置,从设置列表的内容到设置TextView或ImageView。
2.public void onStart()
活动启动时,在onCreate方法之后立即调用onStart方法。如果应用位于后台(另一个应用装载在该应用之上,或者用户按了Home键),当应用继续后但在活动与屏幕交互之前会调用onStart方法。一般应避免重载onStart方法,除非当应用开始使用屏幕之前需要特别查看什么内容。
3.public void onResume()
当活动有访问屏幕的许可时,onResume是活动生命周期中最后调用的方法。如果当活动在后台时用户界面的元素改变了,这个方法就用来确保用户界面和手机状态同步。活动启动时,在onCreate和onStart之后调用这个方法。当活动重新返回前台时,不管它之前处于什么状态,都会调用onResume方法。
4.太好了,活动现在在运行了
完成这些设置、配置工作之后,活动现在对用户可见了。按钮可以单击, 数据可以解析和显示,列表可以滚动,一切都在进行之中。但是,在某个时间点上(也许是因为用户按了Back键),这一切必须结束,需要让事情平息下来。
5.onPause( )
onPause是应用离开屏幕时系统调用的第一个方法。如果哪个进程或循环(例如动画)只有当活动在屏幕上显示时才能运行,那么onPause方法是阻止它们的完美场所。如果在当前显示的活动之上装载了另一个活动,就会调用onPause方法。
记住,如果系统需要资源,那么在调用onPause方法之后随时可能杀死你的进程。这种情况并不常见,但需要当心它有可能发生。
onPause方法很重要,因为它可能是对于活动(甚至是整个应用栈)正要离开的唯一警告。正是应该在这个方法中把重要信息保存到磁盘、数据库或者偏好的其他地方。
活动真正离开屏幕之后,会收到在它生命周期内的下一个调用。
6.onStop( )
Android系统调用onStop方法时,表明活动已经正式离开屏幕。而且,到用户离开当前活动、与另一个活动交互时才调用onStop方法。这并不一定意味着当前活动被关闭(尽管有这个可能),只是假定用户离开当前活动而转向另一个。如果当前活动内部有正在执行的进程,只有在其活跃状态下才能运行,那就应该做一个好公民,利用这个方法把活动关闭。
7.onDestroy( )
onDestroy是在活动消失之前调用的最后一个方法。这是让活动清理其事务的最后机会,然后就会传递给大型垃圾收集程序。
对于有可能一直被活动置于后台运行的任何后台进程(例如获取/解析数据),必须在这个方法调用中关闭。
但是,调用了onDestroy并不意味着活动会被销毁。因此,如果一个线程在运行,那么甚至在调用了onDestroy方法之后,它也可以继续运行并占用系统资源。
2.2.4 加分题——数据保存方法
正如之前所述,在调用onPause方法之后,如果系统需要资源,进程随时可能被杀死。但是,用户不会知道这种行为。为了实现这一点,Android系统提供了两个选择,可以保存状态数据以备用。
1.onSaveInstanceState(Bundle outState)
本方法传递给用户一个bundle对象,可以把需要的任何数据放入其中,从而在此后的某个时刻把活动恢复到当前状态。调用类似于outState.putString或outState.putBoolean可以实现状态的恢复。存储的每个值在存入时需要一个字符串键值,取出时需要同样的键值。要重载自己的onSaveInstanceState方法。如果已经声明,系统就会调用它,否则就错过了机会。
要恢复此前已杀死的活动时,系统会再次调用onCreate方法,把用onSaveInstanceState构建的bundle对象交还给你。
只有在系统认为此后可能会需要恢复活动的情况下才会调用onSaveInstanceState。例如,当手机显然不需要日后继续这个活动时,如果用户按了Back键,就不会调用它。同样,这个方法不用来保存用户数据,只存放在这个特别的屏幕实例中对用户界面来说重要的临时信息。
2.onRetainNonConfigurationInstance( )
当用户在竖向和横向两种屏幕模式之间切换时,活动被销毁,而创建它的一个新实例(经历完全的关闭—启动周期的方法调用)。销毁和创建活动时,尤其是因为配置的变化(最常见的原因是手机旋转)而引发时,onRetainNonConfigurationInstance有机会返回可以在新的活动实例中通过调用getLastNonConfigurationInstance来回收的任何对象。
这个策略有助于让屏幕旋转变换得更快。记住活动获取计划显示到屏幕上的数据时是否花费了相当多的时间。而调用getLastNon ConfigurationInstance可以获得此前显示的数据。
要保持简单智能
现在知道,活动有可能在无意中被杀死。onSaveInstanceState让用户有机会保存原语为日后之用。这明确说明整个活动必须能够把所有重要信息分解成一系列的原语。这进一步强化了下面的概念,即活动必须很简单,不能包含对外部应用很重要的复杂数据。要避免让活动中的大规模Java垃圾收集器一直装满数据,因为它也许会在无意中被终止。
现在应该已经基本了解以下几个方面:
创建一个新活动的步骤;
如何启动活动;
活动的生命周期。
现在有了足够的基础知识,在后面章节中介绍更复杂的主题时可以跟得上。别担心,下面很快就会回到活动这个主题。