Skip to content
<

活动 Activity ​

本章介绍 Android 四大组件之一 Activity 的基本概念和常见用法。主要包括如何正确地启动和停止活动页面、如何在两个活动之间传递各类消息、如何在意图之外给活动添加额外的信息,等等。

启停活动页面 ​

本节介绍如何正确地启动和停止活动页面,首先描述活动页面的启动方法与结束方法,用户看到的页面就是开发者塑造的活动;接着详细分析活动的完整生命周期,以及每个周期方法的发生场景和流转过程;然后描述活动的几种启动模式,以及如何在代码中通过启动标志控制活动的跳转行为。

Activity 的启动和结束 ​

在第 2 章的“跳到另一个页面”一节中,提到通过 startActivity 方法可以从当前页面跳到新页面,具体格式如“startActivity(new Intent(源页面.this, 目标页面.class));”。由于当时尚未介绍按钮控件,因此只好延迟三秒后才自动调用 startActivity 方法。现在有了按钮控件,就能利用按钮的点击事件去触发页面跳转,譬如以下代码便在重写后的点击方法 onClick 中执行页面跳转动作。

(完整代码见 chapter04\src\main\java\com\example\chapter04\ActStartActivity.java)

java
// 活动类直接实现点击监听器的接口View.OnClickListener
public class ActStartActivity extends AppCompatActivity implements View.OnClickListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_act_start);
        // setOnClickListener来自于View,故而允许直接给View对象注册点击监听器
        findViewById(R.id.btn_act_next).setOnClickListener(this);
    }
    @Override
    public void onClick(View v) { // 点击事件的处理方法
        if (v.getId() == R.id.btn_act_next) {
            // 从当前页面跳到指定的新页面
            //startActivity(new Intent(ActStartActivity.this, ActFinishActivity.class));
            startActivity(new Intent(this, ActFinishActivity.class));
        }
    }
}

以上代码中的 startActivity 方法,清楚标明了从当前页面跳到新的 ActFinishActivity 页面。之所以给新页面取名 ActFinishActivity,是为了在新页面中演示如何关闭页面。众所周知,若要从当前页面回到上一个页面,点击屏幕底部的返回键即可实现,但不是所有场景都使用返回键。比如页面左上角的箭头图标经常代表着返回动作,况且有时页面上会出现“完成”按钮,无论点击箭头还是点击完成按钮,都要求马上回到上一个页面。包含箭头图标与“完成”按钮的演示界面如图 4-1 所示。

既然点击某个图标或者点击某个按钮均可能触发返回动作,就需要 App 支持在某个事件发生时主动返回上一页。回到上一个页面其实相当于关闭当前页面,因为最开始由 A 页面跳到 B 页面,一旦关闭了 B 页面,App 应该展示哪个页面呢?当然是展示跳转之前的 A 页面了。在 Java 代码中,调用 finish 方法即可关闭当前页面,前述场景要求点击箭头图标或完成按钮都返回上一页面,则需给箭头图标和完成按钮分别注册点击监听器,然后在 onClick 方法中调用 finish 方法。下面便是添加了 finish 方法的新页面代码例子:

(完整代码见 chapter04\src\main\java\com\example\chapter04\ActFinishActivity.java)

java
public class ActFinishActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_act_finish);
        findViewById(R.id.iv_back).setOnClickListener(this);
        findViewById(R.id.btn_finish).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.iv_back || v.getId() == R.id.btn_finish) {
            // 结束当前的活动页面
            finish();
        }
    }
}

另外,所谓“打开页面”或“关闭页面”沿用了浏览网页的叫法,对于 App 而言,页面的真实名称是“活动”——Activity。打开某个页面其实是启动某个活动,所以有 startActivity 方法却无 openActivity 方法;关闭某个页面其实是结束某个活动,所以有 finish 方法却无 close 方法。

Activity 的生命周期 ​

App 引入活动的概念而非传统的页面概念,这是有原因的,但从字面意思理解,页面更像是静态的,而活动更像是动态的。犹如花开花落那般,活动也有从含苞待放到盛开再到凋零的生命过程。每次创建新的活动页面,自动生成的 Java 代码都给出了 onCreate 方法,该方法用于执行活动创建的相关操作,包括加载 XML 布局、设置文本视图的初始文字、注册按钮控件的点击监听,等等。onCreate 方法所代表的创建动作,正是一个活动最开始的行为,除了 onCreate,活动还有其他几种生命周期行为,它们对应的方法说明如下:

  • onCreate:创建活动。此时会把页面布局加载进内存,进入了初始状态。
  • onStart:开启活动。此时会把活动页面显示在屏幕上,进入了就绪状态。
  • onResume:恢复活动。此时活动页面进入活跃状态,能够与用户正常交互,例如允许响应用户的点击动作、允许用户输入文字等。
  • onPause:暂停活动。此时活动页面进入暂停状态(也就是退回就绪状态),无法与用户正常交互。
  • onStop:停止活动。此时活动页面将不在屏幕上显示。
  • onDestroy:销毁活动。此时回收活动占用的系统资源,把页面从内存中清除掉。
  • onRestart:重启活动。处于停止状态的活动,若想重新开启的话,无须经历 onCreate 的重复创建过程,而是走 onRestart 的重启过程。
  • onNewIntent:重用已有的活动实例。

上述的生命周期方法,涉及复杂的 App 运行状态,更直观的活动状态切换过程如图 4-2 所示。

如果一个 Activity 已经启动过,并且存在当前应用的 Activity 任务栈中,启动模式为 singleTask,singleInstance 或 singleTop(此时已在任务栈顶端),那么再次启动或者回到这个 Activity 的时候,不会创建新的实例,也就是不会执行 onCreate 方法,而是执行 onNewIntent 方法。

Activity 的启动模式 ​

上一小节提到,从第一个活动跳到第二个活动,接着结束第二个活动就能返回第一个活动,可是为什么不直接返回桌面呢?这要从 Android 的内核设计说起了,系统给每个正在运行的 App 都分配了活动栈,栈里面容纳着已经创建且尚未销毁的活动信息。鉴于栈是一种先进后出、后进先出的数据结构,故而后面入栈的活动总是先出栈,假设 3 个活动的入栈顺序为:活动 A-> 活动 B-> 活动 C,则它们的出栈顺序将变为:活动 C-> 活动 B-> 活动 A,可见活动 C结束之后会返回活动 B,而不是返回活动 A 或者别的地方。

假定某个 App 分配到的活动栈大小为 3,该 App 先后打开两个活动,此时活动栈的变动情况如图 4-7 所示。

然后按下返回键,依次结束已打开的两个活动,此时活动栈的变动情况如图 4-8 所示。

结合图 4-7 与图 4-8 的入栈与出栈流程,即可验证结束活动之时的返回逻辑了。

不过前述的出入栈情况仅是默认的标准模式,实际上 Android 允许在创建活动时指定该活动的启动模式,通过启动模式控件该活动的出入栈行为。App 提供了两种办法用于设置活动页面的启动模式,其一是修改 AndroidManifest.xml,在指定的 activity 节点添加属性 android:launchMode,表示本活动以哪个启动模式运行。其二是在代码中调用 Intent 对象的 setFlags 方法,表明后续打开的活动页面采用该启动标志。下面分别予以详细说明。

在配置文件中指定启动模式

打开 AndroidManifest.xml,给 activity 节点添加属性 android:launchMode,属性值填入 standard 表示采取标准模式,当然不添加属性的话默认就是标准模式。具体的 activity 节点配置内容示例如下:

xml
<activity android:name=".JumpFirstActivity" android:launchMode="standard" />

其中 launchMode 属性的几种取值说明见表 4-1。

Intent 类的启动标志说明
Intent.FLAG_ACTIVITY_NEW_TASK开辟一个新的任务栈,该值类似于 launchMode="standard";不同之处在于,如果原来不存在活动栈,则 FLAG_ACTIVITY_NEW_TASK 会创建一个新栈
Intent.FLAG_ACTIVITY_SINGLE_TOP当栈顶为待跳转的活动实例之时,则重用栈顶的实例。该值等同于 launchMode="singleTop"
Intent.FLAG_ACTIVITY_CLEAR_TOP当栈中存在待跳转的活动实例时,则重新创建一个新实例,并清除原实例上方的所有实例。该值与 launchMode="singleTask" 类似,但 singleTask 采取 onNewIntent 方法启用原任务,而 FLAG_ACTIVITY_CLEAR_TOP 采取先调用 onDestroy 再调用 onCreate 来创建新任务
Intent.FLAG_ACTIVITY_NO_HISTORY该标志与 launchMode="standard" 情况类似,但栈中不保存新启动的活动实例。这样下次无论以何种方式再启动该实例,也要走 standard 模式的完整流程
Intent.FLAG_ACTIVITY_CLEAR_TASK该标志非常暴力,跳转到新页面时,栈中的原有实例都被清空。注意该标志需要结合 FLAG_ACTIVITY_NEW_TASK 使用,即 setFlags 方法的参数为`Intent.FLAG_ACTIVITY_CLEAR_TASK

接下来举两个例子阐述启动模式的实际应用:在两个活动之间交替跳转、登录成功后不再返回登录页面,分别介绍如下。

  1. 在两个活动之间交替跳转

假设活动 A 有个按钮,点击该按钮会跳到活动 B;同时活动 B 也有个按钮,点击按钮会跳到活动 A;从首页打开活动 A 之后,就点击按钮在活动 A 与活动 B 之间轮流跳转。此时活动页面的跳转流程为:首页->活动 A->活动 B->活动 A->活动 B->活动 A->活动 B->……多次跳转之后想回到首页,正常的话返回流程是这样的:……->活动 B->活动 A->活动 B-> 活动 A->活动 B->活动 A->首页,注意每个箭头都代表按依次返回键,可见要按下许多次返回键才能返回首页。其实在活动 A 和活动 B 之间本不应该重复返回,因为回来回去总是这两个页面有什么意义呢?照理说每个活动返回一次足矣,同一个地方返回两次已经是多余的了,再返回应当回到首页才是。也就是说,不管过去的时候怎么跳转,回来的时候应该按照这个流程:……->活动 B->活动 A->首页,或者按照这个流程:……->活动 A->活动 B->首页,总之已经返回了的页面,决不再返回第二次。

对于不允许重复返回的情况,可以设置启动标志 FLAG_ACTIVITY_CLEAR_TOP,即使活动栈里面存在待跳转的活动实例,也会崇拜创建该活动的实例,并清除原实例上方的所有实例,保证栈中最多只有该活动的唯一实例,从而避免了无谓的重复返回。于是活动 A 内部的跳转代码就改成了下面这般:

(完整代码见 chapter04\src\main\java\com\example\chapter04\JumpFirstActivity.java)

java
// 创建一个意图对象,准备跳到指定的活动页面
Intent intent = new Intent(this, JumpSecondActivity.class);
// 栈中存在待跳转的活动实例时,则重新创建该活动的实例,并清除原实例上方的所有实例
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

当然活动 B 内部的跳转代码也要设置同样的启动标志:

(完整代码见 chapter04\src\main\java\com\example\chapter04\JumpSecondActivity.java)

java
// 创建一个意图对象,准备跳到指定的活动页面
Intent intent = new Intent(this, JumpFirstActivity.class);
// 栈中存在待跳转的活动实例时,则重新创建该活动的实例,并清除原实例上方的所有实例
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

这下两个活动的跳转代码都设置了 FLAG_ACTIVITY_CLEAR_TOP,运行测试 App 发现多次跳转之后,每个活动仅会返回一次而已。

  1. 登录成功后不再返回登陆页面

很多 App 第一次打开都要求用户登录,登录成功再进入 App 首页,如果这时按下返回键,发现并没有回到上一个登录页面,而是直接退出 App 了,这又是什么缘故呢?原来用户登录成功后,App 便记下用户的登录信息,接下来默认该用户是登录状态,自然不必重新输入用户名和密码了。既然默认用户已经登录,哪里还需要回到登录页面?不光登录页面,登录之前的其他页面包括获取验证码、找回密码等页面都不应进去,每次登录成功之后,整个 App 就焕然一新仿佛忘记了有登录页面这回事。

对于回不去的登录页面情况,可以设置启动标志 FLAG_ACTIVITY_CLEAR_TASK,该标志会清空当前活动栈里的所有实例。不过全部清空之后,意味着当前栈没法用了,必须另外找个活动栈才行,也就是同时设置启动标志 FLAG_ACTIVITY_NEW_TASK,该标志用于开辟新任务的活动栈。于是离开登录页面的跳转代码变成下面这样:

(完整代码见 chapter04\src\main\java\com\example\chapter04\LoginInputActivity.java)

java
// 创建一个意图对象,准备跳到指定的活动页面
Intent intent = new Intent(this, LoginSuccessActivity.class);
// 设置启动标志:跳转到新页面时,栈中的原有实例都被清空,同时开辟新任务的活动栈
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

运行测试 App,登录成功进入首页之后,点击返回键果然没回到登录页面。

默认启动模式 standard ​

该模式可以被设定,不在 manifest 设定时,Activity 的默认模式就是 standard。在该模式下,启动的 Activity 会依照启动顺序被依次压入 Task 栈中:

栈顶复用模式 singleTop ​

在该模式下,如果栈顶 Activity 为我们要新建的 Activity(目标 Activity),那么就不会重复创建新的 Activity。

应用场景

适合开启渠道多、多应用开启调用的 Activity,通过这种设置可以避免已经创建过的 Activity 被重复创建,多数通过动态设置使用。

栈内复用模式 singleTask ​

与 singleTop 模式相似,只不过 singleTop 模式只是针对栈顶的元素,而 singleTask 模式下,如果 task 栈内存在目标 Activity 实例,则将 task 内的对应 Activity 实例之上的所有 Activity 弹出栈,并将对应 Activity 置于栈顶,获得焦点。

应用场景

程序主界面:我们肯定不希望主界面被创建多次,而且在主界面退出的时候退出整个 App 是最好的效果。

耗费系统资源的 Activity:对于那些及其耗费系统资源的 Activity,我们可以考虑将其设为 singleTask 模式,减少资源耗费。

全局唯一模式 singleInstance ​

在该模式下,我们会为目标 Activity 创建一个新的 Task 栈,将目标 Activity 放入新的 Task,并让目标 Activity 获得焦点。新的 Task 有且只有这一个 Activity 实例。如果已经创建过目标 Activity 实例,则不会创建新的 Task,而是将以前创建过的 Activity 唤醒。

看一个示例,Activity3 设置为 singleInstance,Activity1 和 Activity2 默认(standard),下图程序流程中,黄色的代表 Background 的 Task,蓝色的代表 Foreground 的 Task。返回时会先把 Foreground 的 Task 中的 Activity 弹出,直到 Task 销毁,然后才将 Background 的 Task 唤到前台,所以最后将 Activity3 销毁之后,会直接退出应用。

动态设置启动模式 ​

在上述所有情况,都是我们在 Manifest 中通过 launchMode 属性设置的,这个被称为静态设置,动态设置是通过 Java 代码设置的。

通过 Intent 动态设置 Activity 启动模式

java
intent.setFlags();

如果同时有动态和静态设置,那么动态的优先级更高。接下来我们来看一下如何动态的设置 Activity 启动模式。

FLAG_ACTIVITY_NEW_TASK ​

对应 Flag

java
Intent.FLAG_ACTIVITY_NEW_TASK

此 Flag 跟 singleInstance 很相似,在给目标 Activity 设立此 Flag 后,会根据目标 Activity 的 affinity 进行匹配,如果已经存在与其 affinity 相同的 task,则将目标 Activity 压入此 Task。反之没有的话,则新建一个 task,新建的 task 的 affinity 值与目标 Activity 相同,然后将目标 Activity 压入此栈。

但它与 singleInstance 有不同的点,两点需要注意的地方:

  • 新的 Task 没有说只能存放一个目标 Activity,只是说决定是否新建一个 Task,而 singleInstance 模式下新的 Task 只能防止一个目标 Activity。
  • 在同一应用下,如果 Activity 都是默认的 affinity,那么此 Flag 无效,而 singleInstance 默认情况也会创建新的 Task。
FLAG_ACTIVITY_SINGLE_TOP ​

该模式比较简单,对应 Flag 如下:

java
Intent.FLAG_ACTIVITY_SINGLE_TOP

此 Flag 与静态设置中的 singleTop 效果相同

FLAG_ACTIVITY_CLEAR_TOP ​

这个模式对应的 Flag 如下:

java
Intent.FLAG_ACTIVITY_CLEAR_TOP

当设置此 Flag 时,目标 Activity 会检查 Task 中是否存在此实例,如果没有则添加压入栈。如果有,就将位于 Task 中的对应 Activity 其上的所有 Activity 弹出栈,此时有以下两种情况:

  • 如果同时设置 FLAG_ACTIVITY_SINGLE_TOP,则直接使用站内的对应 Activity。
java
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
  • 没有设置,则将栈内的对应 Activity 销毁重新创建。

按位或运算符

运算规则:0|0=0 0|1=1 1|0=0 1|1=1

总结:参加运算的两个对象只要有一个为 1,其值为 1。

例如:3|5 即 0000 0011 | 0000 0101 = 0000 0111,因此,3|5 的值得 7

在活动之间传递消息 ​

本节介绍如何在两个活动之间传递各类消息,首先描述 Intent 的用途和组成部分,以及显式 Intent 和隐式 Intent 的区别;接着阐述结合 Intent 和 Bundle 向下一个活动页面发送数据,再在下一个页面中解析收到的请求数据;然后叙述从下一个活动页面返回应答数据给上一个页面,并由上一个页面解析返回的应答数据。

显式 Intent 和隐式 Intent ​

上一小节的 Java 代码,通过 Intent 对象设置活动的启动日志,这个 Intent 究竟是什么呢?Intent 的中文名是意图,意思是我想让你干什么,简单地说,就是传递消息。Intent 是各个组件之间信息沟通的桥梁,既能在 Activity 之间沟通,又能在 Activity 与 Service 之间沟通,也能在 Activity 与 Broadcast 之间沟通。总而言之,Intent 用于 Android 各组件之间的通信,它主要完成下列 3 部分工作:

  1. 表明本次通信请求从哪里来、到哪里去、要怎么走。
  2. 发起方携带本次通信需要的数据内容,接收方从收到的意图中解析数据。
  3. 发起方若想判断接收方的处理结果,意图就要负责让接收方传回应答的数据内容。

为了做好以上工作,就要给意图配上必须的装备,Intent 的组成部分见表 4-3。

元素名称设置方法说明与用途
ComponentsetComponent组件,它指定意图的来源与目标
ActionsetAction动作,它指定意图的动作行为
DatasetData即 Uri,它指定动作要操纵的数据路径
CategoryaddCategory类别,它指定意图的操作类别
TypesetType数据类型,它指定消息的数据类型
ExtrasputExtras扩展信息,它指定装载的包裹信息
FlagssetFlags标志位,它指定活动的启动标志

指定意图对象的目标有两种表达方式,一种是显式 Intent,另一种是隐式 Intent。

  1. 显式 Intent,直接指定来源活动与目标活动,属于精确匹配

在构建一个意图对象时,需要指定两个参数,第一个参数表示跳转的来源页面,即“来源 Activity.this”;第二个参数表示待跳转的页面,即“目标 Activity.class”。具体的意图构建方式有如下 3 种:

  • 在 Intent 的构造函数中指定,示例代码如下:
java
Intent intent = new Intent(this, ActNextActivity.class); // 创建一个目标确定的意图
  • 调用意图对象的 setClass 方法指定,示例代码如下:
java
Intent intent = new Intent(); // 创建一个新意图
intent.setClass(this, ActNextActivity.class); // 设置意图要跳转的目标活动
  • 调用意图对象的 setComponent 方法指定,示例代码如下:
java
Intent intent = new Intent(); // 创建一个新意图
// 创建包含目标活动在内的组件名称对象
ComponentName component = new ComponentName(this, ActNextActivity.class);
intent.setComponent(component); // 设置意图携带的组件信息
  1. 隐式 Intent,没有明确指定要跳转的目标活动,只给出一个动作字符串让系统自动匹配,属于模糊匹配

通常 App 不希望向外部暴露活动名称,只给出一个事先定义好的标记串,这样大家约定俗成、按图索骥就好,隐式 Intent 边骑到了标记过滤作用。这个动作名称标记串,可以是自己定义的动作,也可以是已有的系统动作。常见系统动作的取值说明见表 4-4。

Intent 类的系统动作常量名系统动作的常量值说明
ACTION_MAINandroid.intent.action.MAINApp 启动时的入口
ACTION_VIEWandroid.intent.action.VIEW向用户显示数据
ACTION_SENDandroid.intent.action.SEND分享内容
ACTION_CALLandroid.intent.action.CALL直接拨号
ACTION_DIALandroid.intent.action.DIAL准备拨号
ACTION_SENDTOandroid.intent.action.SENDTO发送短信
ACTION_ANSWERandroid.intent.action.ANSWER接听电话

动作名称既可以通过 setAction 方法指定,也可以通过构造函数Intent(String action)直接生成意图对象。当然,由于动作是模糊匹配,因此有时需要更详细的路径,比如仅知道某人住在天通苑小区,并不能直接找到他家,还得说明他住在天通苑的哪一期、哪栋楼、哪一层、哪一个单元。Uri 和 Category 便是这样的路径与门类信息,Uri 数据可通过构造函数Intent(String action, Uri uri)在生成对象时一起指定,也可通过 setData 方法指定(setData 这个名字有歧义,实际相当于 setUri);Category 可通过 addCategory 方法指定,之所以用 add 而不用 set 方法,是因为一个意图允许设置多个 Category,方便一起过滤。

下面是一个调用系统拨号程序的代码例子,其中就用到了 Uri:

(完整代码见 chapter04\src\main\java\com\example\chapter04\ActionUriActivity.java)

java
String phoneNo = "12345";
Intent intent = new Intent(); // 创建一个新意图
intent.setAction(Intent.ACTION_DIAL); // 设置意图动作为准备拨号
Urine uri = Uri.parse("tel:" + phoneNo); // 声明一个拨号的 Uri
intent.setData(uri); // 设置意图前往的路径
startActivity(intent); // 启动意图通往的活动页面

隐式 Intent 还用到了过滤器的概念,把不符合匹配条件的过滤掉,剩下符合条件的按照优先顺序调用。譬如创建一个 App 模块,AndroidManifest.xml 里的 intent-filter 就是配置文件中的过滤器。像最常见的首页活动 MainActivity,它的 activity 节点下面便设置了 action 和 category 的过滤条件。其中 android.intent.action.MAIN 表示 App 的入口动作,而 android.intent.category.LAUNCHER 表示在桌面上显示 App 图标,配置样例如下:

xml
<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

向下一个 Activity 发送数据 ​

上一小节提到,Intent 对象的 setData 方法只指定到达目标的路径,并非本次通信所携带的参数信息,真正的参数信息存放在 Extras 中。Intent 重载了很多种 putExtras 方法传递各种类型的参数,包括整型、双精度型、字符串等基本数据类型,甚至 Serializable 这样的序列化结构。只是调用 putExtra 方法显然不好管理,像送快递一样大小包裹随便扔,不但找起来不方便,丢了也难以知道。所以 Android 引入了 Bundle 概念,可以把 Bundle 理解为超市的寄包柜或快递收件柜,大小包裹由 Bundle 统一存取,方便又安全。

Bundle 内部用于存放消息的数据结构是 Map 映射,既可添加或删除元素,还可判断元素是否存在。开发者若要把 Bundle 数据全部打包好,只需调用一次意图对象的 putExtras 方法;若要把 Bundle 数据全部取出来,也只需调用一次意图对象的 getExtras 方法。Bundle 对象操作各类型数据的读写方法说明见表 4-5。

数据类型读方法写方法
整型数getIntputInt
浮点数getFloatputFloat
双精度数getDoubleputDouble
布尔值getBooleanputBoolean
字符串getStringputString
字符串数组getStringArrayputStringArray
字符串列表getStringArrayListputStringArrayList
可序列化结构getSerializableputSerializable

接下来举个在活动之间传递数据的例子,首先在上一个活动使用包裹封装好数据,把包裹塞给意图对象,再调用 startActivity 方法跳到意图指定的目标活动。完整的活动跳转代码示例如下:

(完整代码见 chapter04\src\main\java\com\example\chapter04\ActSendActivity.java)

java
Intent intent = new Intent(this, ActReceiveActivity.class);
// 创建一个新包裹
Bundle bundle = new Bundle();
bundle.putString("request_time", DateUtil.getNowTime());
bundle.putString("request_content", tv_send.getText().toString());
intent.putExtras(bundle);
startActivity(intent);

然后再下一个活动中获取意图携带的快递包裹,从包裹取出各参数信息,并将传来的数据显示到文本视图。下面便是目标活动获取并展示包裹数据的代码例子:

(完整代码见 chapter04\src\main\java\com\example\chapter04\ActReceiveActivity.java)

java
TextView tv_receive = findViewById(R.id.tv_receive);
// 从上一个页面传来的意图中获取快递包裹
Bundle bundle = getIntent().getExtras();
String request_time = bundle.getString("request_time");
String request_content = bundle.getString("request_content");
String desc = String.format("收到请求消息:\n请求时间为%s\n请求内容为%s", request_time, request_content);
tv_receive.setText(desc);

代码编写完毕,运行测试 App,打开上一个页面如图 4-9 所示。单机页面上的发送按钮跳到下一个页面如图 4-10 所示,根据展示文本可知正确获得了传来的数据。

向上一个 Activity 返回数据 ​

数据传递经常是相互的,上一个页面不但把请求数据发送到下一个页面,有时候还要处理下一个页面的应答数据,所谓应答发生在下一个页面返回到上一个页面之际。如果只把请求数据发送到下一个页面,上一个页面调用 startActivity 方法即可;如果还要处理下一个页面的应答数据,此时就得分多步处理,详细步骤说明如下:

步骤一,上一个页面注册回调处理下一个页面的 response。

java
private static final String mRequest = "你睡了吗?来我家睡吧";
private ActivityResultLauncher<Intent> register;
private TextView tv_response;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_act_request);
    TextView tv_request = findViewById(R.id.tv_request);
    tv_request.setText("待发送的消息为:" + mRequest);

    tv_response = findViewById(R.id.tv_response);

    findViewById(R.id.btn_request).setOnClickListener(this);

    register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {});
}

步骤二,填充按钮的跳转动作逻辑,打包数据,使用 register.launch(intent) 进行跳转。

java
public void onClick(View v) {
    Intent intent = new Intent(this, ActResponseActivity.class);
    // 创建一个新包裹
    Bundle bundle = new Bundle();
    bundle.putString("request_time", DateUtil.getNowTime());
    bundle.putString("request_content", mRequest);
    intent.putExtras(bundle);
    register.launch(intent);
}

步骤三,下一个页面接收并解析请求数据,进行相应处理。

java
// 从上一个页面传来的意图中获取快递包裹
Bundle bundle = getIntent().getExtras();
String request_time = bundle.getString("request_time");
String request_content = bundle.getString("request_content");
String desc = String.format("收到请求消息:\n请求时间为%s\n请求内容为%s", request_time, request_content);
// 把请求消息的详情显示在文本视图上
tv_request.setText(desc);

步骤四,填充返回按钮的跳转动作逻辑,打包数据,返回上一个页面。

java
String mResponse = "我吃过了,还是你来我家吃";
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putString("response_time", DateUtil.getNowTime());
bundle.putString("response_content", mResponse);
intent.putExtras(bundle);
// 携带意图返回上一个页面。RESULT_OK表示处理成功
setResult(Activity.RESULT_OK, intent);
// 结束当前的活动页面
finish();

步骤五,填充第一步的 callback。

java
register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
    if (result != null) {
        Intent intent = result.getData();
        if (intent != null && result.getResultCode() == Activity.RESULT_OK) {
            Bundle bundle = intent.getExtras();
            String response_time = bundle.getString("response_time");
            String response_content = bundle.getString("response_content");
            String desc = String.format("收到返回消息:\n应答时间为%s\n应答内容为%s", response_time, response_content);
            // 把返回消息的详情显示在文本视图上
            tv_response.setText(desc);
        }
    }
});
旧方式 Deprecated

步骤一,上一个页面打包好请求数据,调用 startActivityForResult 方法执行跳转动作,表示需要处理下一个页面的应答数据,该方法的第二个参数表示请求代码,它用于标识每个跳转的唯一性。跳转代码示例如下:

(完整代码见 chapter04\src\main\java\com\example\chapter04\ActRequestActivity.java)

java
String mRequest = "你吃饭了吗?来我家吃吧";
Intent intent = new Intent(this, ActResponseActivity.class);
// 创建一个新包裹
Bundle bundle = new Bundle();
bundle.putString("request_time", DateUtil.getNowTime());
bundle.putString("request_content", mRequest);
intent.putExtras(bundle);
startActivityForResult(intent, 0);

步骤二,下一个页面接收并解析请求数据,进行相应处理。接受代码示例如下:

(完整代码见chapter04\src\main\java\com\example\chapter04\ActResponseActivity.java)

java
// 从上一个页面传来的意图中获取快递包裹
Bundle bundle = getIntent().getExtras();
String request_time = bundle.getString("request_time");
String request_content = bundle.getString("request_content");
String desc = String.format("收到请求消息:\n请求时间为%s\n请求内容为%s", request_time, request_content);
// 把请求消息的详情显示在文本视图上
tv_request.setText(desc);

步骤三,下一个页面在返回上一个页面时,打包应答数据并调用 setResult 方法返回数据包裹。setResult 方法的第一个参数标识应答代码(成功还是失败),第二个参数为携带包裹的意图对象。返回代码示例如下:

(完整代码见 chapter04\src\main\java\com\example\chapter04\ActResponseActivity.java)

java
String mResponse = "我吃过了,还是你来我家吃";
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putString("response_time", DateUtil.getNowTime());
bundle.putString("response_content", mResponse);
intent.putExtras(bundle);
// 携带意图返回上一个页面。RESULT_OK表示处理成功
setResult(Activity.RESULT_OK, intent);
// 结束当前的活动页面
finish();

步骤四,上一个页面重写方法 onActivityResult,该方法的输入参数包含请求代码和结果代码,其中请求代码用于判断这次返回对应哪个跳转,接过代码用于判断下一个页面是否处理成功。如果下一个页面处理成功,再对返回数据解包操作,处理返回数据的代码示例如下:

(完整代码见 chapter04\src\main\java\com\example\chapter04\ActRequestActivity.java)

java
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent intent) {
    super.onActivityResult(requestCode, resultCode, intent);
    if (intent != null && requestCode == 0 && resultCode == Activity.RESULT_OK) {
        Bundle bundle = intent.getExtras();
        String response_time = bundle.getString("response_time");
        String response_content = bundle.getString("response_content");
        String desc = String.format("收到返回消息:\n应答时间为%s\n应答内容为%s", response_time, response_content);
        // 把返回消息的详情显示在文本视图上
        tv_response.setText(desc);
    }
}

结合上述的活动消息交互步骤,运行测试 App 打开第一个活动页面如图 4-11 所示。

点击传送按钮跳转到第二个活动页面如图 4-12 所示,可见第二个页面收到了请求数据。然后点击第二个页面的返回按钮,回到第一个页面如图 4-13 所示,可见第一个页面成功收到了第二个页面的应答数据。

为活动补充附加信息 ​

本节介绍如何在意图之外给活动添加额外的信息,首先可以把字符串参数放到字符串资源文件中,待 App 运行之时再从资源文件读取字符串值;接着还能在 AndroidManifest.xml 中给指定活动配置专门的元数据,App 运行时即可获取对应活动的元数据信息;然后利用元数据的 resource 属性配置更复杂的 XML 定义,从而为 App 注册在长按桌面之时弹出的快捷菜单。

利用资源文件配置字符串 ​

利用 Bundle 固然能在页面跳转的时候传送数据,但这仅限于在代码中传递参数,如果要求临时修改某个参数的数值,就得去改 Java 代码。然而直接修改 Java 代码有两个弊端:

  1. 代码文件那么多,每个文件又有许多行代码,一下子还真不容找到修改的地方。
  2. 每次改动代码都得重新编译,让 Android Studio 编译的功夫也稍微费点时间。

有鉴于此,对于可能手工变动的参数,通常把参数名称与参数值的对应关系写入配置文件,由程序在运行时读取配置文件,这样只需修改配置文件就能改变对应数据了。res\values 目录下面的 strings.xml 就用来配置字符串形式的参数,打开该文件,发现里面已经存在名为 app_name 的字符串参数,它配置的是当前模块的应用名称。现在可于 app_name 下方补充一行参数配置,参数名称叫做“weather_str”,参数值则为“晴天”,具体的配置内容如下所示:

xml
<string name="weather_str">晴天</string>

接着打开活动页面的 Java 代码,调用 getString 方法即可根据“R.string.参数名称”获得指定参数的字符串值。获取代码示例如下:

(完整代码见 chapter04\src\main\java\com\example\chapter04\ReadStringActivity.java)

java
// 从strings.xml获取名叫weather_str的字符串值
String value = getString(R.string.weather_str);
tv_resource.setText("来自字符串资源:今天的天气是" + value);

上面的 getString 方法来自于 Context 类,由于页面所在的活动类 AppCompatActivity 追根溯源来自 Context 这个抽象类,因此凡是活动页面代码都能直接调用 getString 方法。

然后在 onCreate 方法中调用 showStringResource 方法,运行测试 App,打开读取页面如图 4-14 所示,可见从资源文件成功读到了字符串。

利用元数据传递配置信息 ​

尽管资源文件能够配置字符串参数,然而有时候为安全起见,某个参数要给某个活动专用,并不希望其他活动也能获取该参数,此时就不方便到处使用 getString 了。好在 Activity 提供了元数据(Metadata)的概念,元数据是一种描述其他数据的数据,它相当于描述固定活动的参数信息。打开 AndroidManifest.xml,在测试活动的 activity 节点内部添加 meta-data 标签,通过属性 name 指定 元数据的名称,通过属性 value 指定元数据的值。仍以天气为例,添加 meta-data 标签之后的 activity 节点如下所示:

xml
<activity
    android:name=".MetaDataActivity"
    android:exported="true"
    android:launchMode="standard">

    <meta-data
        android:name="weather"
        android:value="晴天" />
</activity>

元数据的 value 属性既可直接填字符串,也可引用 strings.xml 已定义的字符串资源,引用格式形如“@string/字符串的资源名称”。下面便是采用引用方式的 activity 节点配置:

xml
<activity
    android:name=".MetaDataActivity"
    android:exported="true"
    android:launchMode="standard">

    <meta-data
        android:name="weather"
        android:value="@string/weather_str" />
</activity>

配置好了 activity 节点的 meta-data 标签,再回到 Java 代码获取元数据信息,获取步骤分为下列 3 步:

  • 调用 getPackageManager 方法获得当前应用的包管理器。
  • 调用包管理器的 getActivityInfo 方法获得当前活动的信息对象。
  • 活动信息对象的 metadata 是 Bundle 包裹类型,调用包裹对象的 getString 即可获得指定名称的参数值。

把上述 3 个步骤串起来,得到以下的元数据获取代码:

(完整代码见 chapter04\src\main\java\com\example\chapter04\MetaDataActivity.java)

java
// 获取应用包管理器
PackageManager pm = getPackageManager();
try {
    // 从应用包管理器中获取当前的活动信息
    ActivityInfo info = pm.getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
    // 获取活动附加的元数据信息
    Bundle bundle = info.metaData;
    String weather = bundle.getString("weather");
    tv_meta.setText("来自元数据信息:今天的天气是" + weather);
} catch (PackageManager.NameNotFoundException e) {
    e.printStackTrace();
}

重新运行 App 观察到的界面如图 4-15 所示,可见成功获得 AndroidManifest.xml 配置的元数据。

给应用页面注册快捷方式 ​

元数据不单单能传递简单的字符串参数,还能传送更复杂的资源数据,从 Android 7.1 开始新增的快捷方式便用到了这点,譬如在手机桌面上长按支付宝图标,会弹出如图 4-16 所示的快捷菜单。

点击菜单项“Scan”,直接打开支付宝的扫码页面;点击菜单项“Pay/Collect”,直接打开支付宝的付款页面;点击菜单项“Collect”,直接打开支付宝的收款页面。如此不必打开支付宝首页,即可迅速跳转到常用的 App 页面,这便是所谓的快捷方式。

那么 Android 7.1 又是如何实现快捷方式的呢?那得再琢磨琢磨元数据了。原来元数据的 meta-data 标签除了前面说到的 name 属性和 value 属性,还拥有 resource 属性,该属性可指定一个 XML 文件,表示元数据想要的复杂信息保存于 XML 数据之中。借助元数据以及指定的 XML 配置,方可完成快捷方式功能,具体的实现过程说明如下:

首先打开 res/values 目录下的 strings.xml,在 resources 节点内部添加下述的 3 组(每组两个,共 6 个字)字符串配置,每组都代表一个菜单项,每组都代表一个菜单项,每组又分为长名称和短名称,平时优先展示长名称,当长名称放不下时才展示短名称。这 3 组 6 个字符串的配置定义示例如下:

xml
<string name="first_short">first</string>
<string name="first_long">启停活动</string>
<string name="second_short">second</string>
<string name="second_long">来回跳转</string>
<string name="third_short">third</string>
<string name="third_long">登录返回</string>

接着在 res 目录下创建名为 xml 的文件夹,并在该文件夹创建 shortcuts.xml,这个 XML 文件用来保存 3 组菜单项的快捷方式定义,文件内容如下所示:

(完整代码见chapter04\src\main\res\xml\shortcuts.xml)

xml
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">

    <shortcut
        android:enabled="true"
        android:icon="@mipmap/ic_launcher"
        android:shortcutId="first"
        android:shortcutLongLabel="@string/first_long"
        android:shortcutShortLabel="@string/first_short">

        <intent
            android:action="android.intent.action.VIEW"
            android:targetClass="com.dongnaoedu.chapter04.ActStartActivity"
            android:targetPackage="com.dongnaoedu.chapter04" />
        <categories android:name="android.shortcut.conversation" />
    </shortcut>


    <shortcut
        android:enabled="true"
        android:icon="@mipmap/ic_launcher"
        android:shortcutId="second"
        android:shortcutLongLabel="@string/second_long"
        android:shortcutShortLabel="@string/second_short">

        <intent
            android:action="android.intent.action.VIEW"
            android:targetClass="com.dongnaoedu.chapter04.JumpFirstActivity"
            android:targetPackage="com.dongnaoedu.chapter04" />
        <categories android:name="android.shortcut.conversation" />
    </shortcut>

    <shortcut
        android:enabled="true"
        android:icon="@mipmap/ic_launcher"
        android:shortcutId="third"
        android:shortcutLongLabel="@string/third_long"
        android:shortcutShortLabel="@string/third_short">

        <intent
            android:action="android.intent.action.VIEW"
            android:targetClass="com.dongnaoedu.chapter04.LoginInputActivity"
            android:targetPackage="com.dongnaoedu.chapter04" />
        <categories android:name="android.shortcut.conversation" />
    </shortcut>
</shortcuts>

由上述的 XML 例子中看到,每个 shortcut 节点都代表了一个菜单项,该节点的各属性说明如下:

  • shortcutId:快捷方式的编号。
  • enabled:是否启用快捷方式。true 表示启用,false 表示禁用。
  • icon:快捷菜单左侧的图标。
  • shortcutShortLabel:快捷菜单的短标签。
  • shortcutLongLabel:快捷菜单的长标签。优先展开长标签的文本,长标签放不下时才展示短标签的文本。

以上的节点属性仅仅指明了每项菜单的基本规格,点击菜单项之后的跳转动作还要由 shortcut 内部的 intent 节点定义,该节点主要有 targetPackage 与 targetClass 两个属性需要修改,其中 targetPackage 属性固定为当前 App 的包名,而 targetClass 属性描述了菜单项对应的活动类完整路径。

然后打开 AndroidManifest.xml,找到 MainActivity 所在的 activity 节点,在该节点内部补充如下的元数据配置,其中 name 属性为 android.app.shortcuts,而 resource 属性为 @xml/shorcuts:

xml
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" 
/>

这行元数据的作用,是告诉 App 首页有个快捷方式菜单,其资源内容参见位于 xml 目录下的 shortcuts.xml。完整的 activity 节点配置示例如下:

xml
<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <meta-data
        android:name="android.app.shortcuts"
        android:resource="@xml/shortcuts" />
</activity>

然后把测试应用安装到手机上,回到桌面长按应用图标,此时图标下方弹出如图 4-17 所示的快捷菜单列表。

点击其中一个菜单项,果然跳到了配置的活动页面,证明元数据成功实现了类似支付宝的快捷方式。

小结 ​

本章主要介绍了活动组件——Activity 的常见用法,包括:正确启停活动页面(Activity 的启动和结束、Activity 的生命周期、Activity 的启动模式)、在活动之间传递消息(显式 Intent 和隐式 Intent、向下一个 Activity 发送数据、向上一个 Activity 返回数据)、为活动补充附加信息(利用资源文件配置字符串、利用元数据传递配置信息、给应用页面注册快捷方式)。

通过本章的学习,我们应该能掌握以下 3 种开发技能:

  1. 理解活动的生命周期过程,并学会正确启动和结束活动。
  2. 理解意图的组成结构,并利用意图在活动之间传递消息。
  3. 理解元数据的概念,并通过元数据配置参数信息和注册快捷菜单。