本节书摘来自异步社区《Android应用开发》一书中的第2章,第2.3节意图类,作者 【美】Chris Haseman,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.3 意图类
Android应用开发
一个意图是一个类。在Android平台上,意图构成了主要的通信协议,用来在应用构件之间传输信息。在一个设计良好的Android应用中,构件(活动、内容提供方或服务)永远不该直接访问其他任何构件的实例。同样,意图是这些构件之间的通信方式。
本书原本可以用大半篇幅来介绍意图类的创建、使用和细节。但为了简洁并让一切尽快地运转起来,本章只介绍几个基本概念。可以到本书的其他章节寻找意图的相关内容,它们可能是整个Android平台中最常用的类。
有以下两种主要方法可以告诉Android系统,愿意接收由系统、其他应用甚至用户自己的应用所发出的意图。
在AndroidManifest.xml文件中注册一个。
在系统中注册一个IntentFilter运行时对象。
这两种情况都需要告诉Android系统你监听什么事件。
同样有一大堆方法可以发出意图。可以把它们广播给系统,或者可以让它们面向一个特别的活动或服务。但是,要启动一个服务或活动,它必须在清单文件中注册(此前演示如何启动一个新活动的时候,介绍过一个这样的例子)。
下面看看如何在实践中使用意图。
2.3.1 清单注册
为什么不在运行时刻注册?如果一个意图声明为清单文件的一部分,系统会启动构件以便让它接收。在运行时刻注册假定了已经在运行中。由于这一点,无论何时想唤醒应用、让它就某个事件采取行动,都要在清单文件中声明。如果只有在应用运行时才能接收它,就在那个特别的构件启动之后注册一个IntentFilter(在XML中声明时是intent-filter,但在Java代码中是IntentFilter)。
下面回到初始的应用,再看看清单文件中活动的相应数据项。
<activity Android:name=".MyActivity"
android:label="@string/app_name">
<intent-filter>
<action Android:name="Android.intent.action.MAIN" />
<category Android:name="Android.intent.category. LAUNCHER"/>
</intent-filter>
</activity>
Android.intent.action.MAIN声明是告诉系统,这个活动是应用的主活动,不需要参数来启动它。在清单文件中只把一个活动列为MAIN,这是个好想法。由Eclipse运行应用的时候,adb(Android调试桥)也正是据此知道要启动哪个活动。
Android.intent.category.LAUNCHER类别是告诉系统,在手机的主应用面板上单击图标时应该启动封闭的活动。而且也是告诉Android系统,想让图标出现在应用的启动栏控件内。这是一个intent-filter的例子,由Android的项目创建工具来创建。下面添加一个用户自己的意图。
2.3.2 添加一个意图
如果跳过了2.2节中关于活动类的内容,现在正好可以回去浏览一下代码。在2.2节中介绍了如何声明和启动一个简单的新活动。但没有讲的是,在清单文件中声明一个,可以让系统访问这个活动。下面就这么做。
(1)在NewActivity的声明中添加一个intent-filter(意图过滤器)。
<activity Android:name=".NewActivity">
<intent-filter>
<action android:name="com.haseman.PURPLE_PONY_POWER"/>
<category android:name="Android.intent.category. DEFAULT"/>
</intent-filter>
</activity>
在这段代码中注册了包含com.haseman.URPLE_PONY_POWER动作的意图,把intent-filter的类别设置为默认值。
下面就用这个看起来非常荒诞的动作串来演示一个论点(以免被人认为不够严肃)——就是说,这个动作串唯一需要的条件是它对于这个特别构件的独一无二性。
在2.2节中介绍了如何使用下列代码来启动新的活动。
Intent startIntent=new Intent(this, NewActivity.class);
startActivity(startIntent);
这个方法是有效的,但有一个大的缺陷——它不能在应用的语境之外启动。这使得“活动—意图”模型所提供的最强大的特性之一失去了用处。这个特性是,手机上的任何应用如果有合适的意图,都可以使用应用内部的构件。
既然已经把加入到示例项目的清单文件中,就可以在任意位置用以下代码启动这个特殊的活动。
Intent actionStartIntent= new Intent("com.haseman.PURPLE_PONY_ POWER");
startActivity(actionStartIntent);
可以注意到,在这段代码和上面的列表之间有一个非常重要的不同。在本例中创建这个意图的时候,不需要传递进来一个Context对象(即整体而言与系统进行通信所需要的信息包)。这使得任何应用在了解了所需的意图之后,可以启动NewActivity类。
(2)在onKeyDown事件处理方法中添加以下粗体处理的代码,以不同的方式启动同一个活动。新的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;
}
if(keyCode == KeyEvent.KEYCODE_DPAD_DOWN){
Intent actionStartIntent=
new Intent("com.haseman.PURPLE_PONY_POWER");
startActivity(actionStartIntent);
}
return super.onKeyDown(keyCode, event);
}
现在,在示例应用中按向下键的时候,会看到同一个活动使用在清单文件中声明的这个新的intent-filter来启动。
如果把这个意图的动作串拼写错了或者忘记在intent-filter中添加默认的类别,系统可能会产生一个Android.content.Activity NotFoundException异常。
如果创建了一个意图,而系统无法把它连接到手机的清单文件中所列举的任何活动,则startActivity方法随时会抛出这个异常。
注册intent-filter不只是活动的权限。任何Android应用构件都可以通过注册使得当系统广播一个意图动作时会启动该构件。
多个意图,一个活动
一个活动可以注册为接收任意多的事件。典型地,发送一个意图等同于告诉这个活动“做这件事”。而“这件事”可能是从编辑一个文件到显示一个可能的文件或动作列表的任何事情。 正如此前所述,限制活动的作用范围很重要,因此只注册一个意图通常是个好想法。但是,因为一个活动可以注册不止一个意图,所以更好的思路是在onCreate方法内调用getIntent方法,检查活动为什么被启动,以便采取正确的行动(调用getAction)。
2.3.3 在运行时刻监听意图
另一个方法是在运行时刻监听意图,它只接收与应用有关的事件或者Android系统自身广播的事件。用户开启飞行模式时,活动愿意显示一个特别的屏幕或采取一个自定义的动作。为了实现这一点,需要创建一个临时的IntentFilter和一个内部的BroadcastReceiver对象实例。
1.创建一个Receiver对象
现在给MyActivity类添加运行时刻的BroadcastReceiver对象。可以猜到,一个BroadcastReceiver是一个只有一个onReceive方法的对象。将MyActivity类修改为如下所示:
public class MyActivity extends Activity {
private BroadcastReceiver simpleReceiver=new Broadcast Receiver(){
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(
Intent.ACTION_AIRPLANE_MODE_CHANGED)){
Toast.makeText(context,
R.string.airplane_change,
Toast.LENGTH_LONG).show();
}
}
};
// 活动的其他部分代码
}
这段代码创建了一个可以在活动内部局部访问的receiver对象。系统调用onReceive方法时,需要检查意图的动作是什么。这是个好想法,因为BroadcastReceiver可以注册任意多的不同意图。
接收想要找的事件时,要使用Android的Toast API在屏幕上显示一条简单的消息(这种情况下,显示的是变量名为airplane_change的字符串的内容)。在实践中,这个时机可能适合在屏幕上显示信息,表明应用需要网络连接才能正确运行。
2.告诉Android你想听到什么
既然已经创建了一个BroadcastReceiver对象,就可以在系统中注册它。下面先看实现的代码,然后看发生了什么。
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
registerReceiver(simpleReceiver, intentFilter);
}
什么是toast
Android系统中,一个toast就是一条短消息,在屏幕底部弹出的一个小消息框内显示给用户。这参考了toast的原意——祝酒词,即在干杯之前所做的简短发言。对toast来说,永远是越短小越简单越好。
再次强调,正是在SDK中初始的MyActivity类中创建了项目。
(1)在onCreate方法中创建一个意图过滤器,向其中添加动作Intent.ACTION_AIRPLANE_MODE_CHANGED。
(2)可以向这个意图过滤器中添加任意多的动作。调用receiver时,需要弄清楚到底是哪个意图调用getAction(),触发了这个BroadcastReceiver对象的onReceive方法。
(3)按住开关按钮,测试这段代码,会弹出一个包含几个选项的对话框。
(4)开启飞行模式。如果目前为止一切操作都正确,就会看到屏幕底部弹出一小条消息,内容是警示信息。
这就是应用监听系统状态信息的首要方式。采用这个工具,从电池状态到无线广播状态,一切尽在掌握。查阅Android SDK文档可以找到更多相关内容,例如以什么权限可以监控哪些活动。
3.结束监听
对于创建的每个运行时刻的注册,同样需要注销。如果用户只有在活动可见的时候才愿意接收事件,那么最好用onPause方法来关闭接收。如果只要活动在运行,即使它不可见也愿意继续监听,那么最好在onDestroy方法中注销。不管在哪个方法中结束监听,只要调用unregisterReceiver(由超类实现的一个方法),把此前创建的BroadcastReceiver对象传递进去,如下所示:
@Override
public void onDestroy(){
super.onDestroy();
unregisterReceiver(imageReceiver);
}
4.创建自包含的Broadcastreceiver对象
Broadcastreceiver对象不一定存在于活动内部。如果想了解一个系统事件,可以注册一个receiver,但在事件发生时可能不需要启动整个应用。
BroadcastReceiver可以自己注册在标记下。
在实践中,使用它们来接收可能不需要显示给用户的系统信息。如果一个活动是不需要的,那么启动它之后结果却把它关闭是非常消耗资源的,不如用一个receiver获取广播的意图,而只在必要时启动活动。
5.处理相互冲突的活动
你可能会想,“如果同一个意图注册了不止一个活动会怎么样?”这是个很有趣的问题,Android系统只通过询问用户就可以解决它。
如果两个活动在各自的清单文件中监听同一个意图,而一个应用试图用该意图启动一个活动,那么系统会弹出一个菜单,列举一个可能的应用列表,让用户从中选择(见图2.2)。
正是这种注册相似意图的功能使无缝交互成为可能,因为注册了这个意图的每个应用都在Share菜单上拥有一项。单击这个列表中的一项会启动相应的已注册活动,并把可以访问到这个图片的位置信息作为额外数据来传递。那么,什么是额外数据?这个问题问得好。
2.3.4 移动自己的数据
意图主要的特性之一是能够打包一块发送数据。一个活动永远不应该直接操作另一个活动的存储空间。但是,活动之间必须有办法交流信息。这种交流是在意图的额外数据这一数据包的帮助下完成的。这个数据包可以包含任意多的“字符串—原语”对。描述这个概念的最佳方法可能是用代码和一个例子。
前文介绍过如何使用一个基于动作的广播意图来启动一个新的活动。在之前介绍过的onKeyDown监听方法中添加以下粗体处理的代码。
public boolean onKeyDown(int keyCode, KeyEvent event){
//...跳过前面的键值代码..
if(keyCode == KeyEvent.KEYCODE_DPAD_DOWN){
Intent actionStartIntent = new
Intent("com.haseman.PURPLE_PONY_POWER");
actionStartIntent.putExtra("newBodyText",
"You Pressed the Down Key!");
startActivity(actionStartIntent);
}
这是在用意图启动一个活动之前向其中添加一个字符串负载。无论谁接收这个意图,都能把这个字符串取出(假定它们知道这个串的存在),把它用在合适的地方。既然已经知道怎样添加额外数据,下面看一个例子,在NewActivity类的onCreate方法中获取和使用这个串。
public void onCreate(Bundle icicle){
super.onCreate(icicle);
setContentView(R.layout.new_activity);
Intent currentIntent = getIntent();
if(currentIntent.hasExtra("newBodyText")){
String newText = currentIntent.getExtras().
getString("newBodyText");
TextView bodyView = (TextView)findViewById(
R.id.new_activity_text_view);
bodyView.setText(newText);
}
粗体处理的代码段是在获取这个意图,它通过调用getIntent方法来启动NewActivity。接下来要检查这个意图是否真的包含newBodyText这个额外数据。记住,这个意图可能并未包含额外数据。如果这里忘了检查,很快就会发现应用淹没在NullPointerExceptions异常之中。如果额外数据存在,就把它取出,将字符串设置为屏幕中的新文本。最后两行代码获取一个指针,指向屏幕的文本视图,并把显示的文本变为额外数据的内容。现在不必考虑这个特殊操作的实现机制,后文会更详细地介绍这个主题。
现在学会了如何注册、创建和使用一个意图的基本功能。我们知道,它们可以在清单文件中注册或在运行时刻注册。手机上的任何应用都可以发送意图,而同一个意图可以注册任意多的应用构件。
本小节的目标是开始关注和了解Android系统的意图。在后面的章节中,会在多种不同的语境下使用意图。