第3章 Android游戏开发中的交互式通信
在 Android 平台下,应用程序可以方便地调用其他应用程序的功能来实现自己的功能,这是Android 的特性之一。本章将向读者介绍 Android 程序内部或程序之间进行交互式通信的方式,在介绍这些通信方式之前,首先让读者了解Android应用程序的构成框架、了解各个组件的特性以及使用方式。
3.1 Android应用程序的基本组件
一个Android应用程序可以由几个不同的组件构成,本节就对Android应用程序的结构做一个完整的解析。同时,本节还将对体现各个组件之间关系的AndroidManifest.xml配置文件进行简单介绍。
Android应用程序的基本组件包括Activity、Service、BroadcastReceiver和ContentProvider等,不同组件具有不同的特性以及各自的生命周期,下面就对每个组件做一个简单的介绍。
3.1.1 Activity组件
Activity是最常见的一种Android组件,每个Activity都相当于一个屏幕,其为用户提供了进行交互的可视界面。应用程序可以根据需要包含一个或多个Activity,这些Activity一般都继承自android.app包下的Activity类,并且这些Activity之间的运行是相互独立的。
Activity的生命周期主要包含3个阶段。
● 运行态(running state)
此时 Activity 显示在屏幕前台,并且具有焦点,可以和用户的操作动作进行交互,如向用户提供信息、捕获用户单击按钮的事件并作处理。
● 暂停态(paused state)
此时 Activity 失去了焦点,并被其他的运行态的 Activity 取代在屏幕前台显示。如果掩盖暂停态 Activity 的运行态,Activity 并不能铺满整个屏幕窗口或者具有透明效果,则该暂停态的Activity对用户仍然可见,但是不可以与其进行交互。
暂停态的 Activity 仍然保留其状态和成员等其他信息,当系统的内存非常匮乏时,暂停态的Activity会被结束掉以获得更多的资源。
● 停止态(stopped state)
停止态的 Activity 不仅没有焦点,而且是完全不可见的,虽然其也保留状态和成员等信息,停止态的Activity会在系统需要的时候被结束。
当 Activity 在不同的状态之间切换时,可以通过重写相应的回调方法来编写状态改变时应该执行的动作,这些状态和相应的回调方法如图3-1所示。
图3-1 Activity的生命周期
Activity显示的内容可以有两种声明方式,第一种是通过XML配置文件来声明,第二种则是将屏幕设置为某一个继承自View类的对象。下面分别对这两种方式做一个简单介绍。
● 通过XML配置文件声明
Activity的配置文件位于res目录下的layout目录中,一个配置文件就相当于一个View容器,其中可以添加Android平台下的一些内置的View,如TextView、ImageView等,也可以添加继承自View类的子类对象,还可以继续添加View容器。
布局文件还指定了View对象在View容器中的排布方式,如线性布局(LinearLayout)、表格布局(TableLayout)等。以下给出了一个简单的通过XML配置文件实现Activity显示的代码。
代码位置:见随书光盘中源代码/第3章/Sample_3_1/src/wyf/wpf目录下的Sample_3_1.java。
1 package wyf.wpf; //声明包语句
2 import android.app.Activity; //引入相关类
3 import android.os.Bundle; //引入相关类
4 public class Sample_3_1 extends Activity {
5 @Override
6 public void onCreate(Bundle savedInstanceState) { //重写onCreate方法
7 super.onCreate(savedInstanceState); //调用父类onCreate方法
8 setContentView(R.layout.main); //设置所要显示的XML配置文件
9 }
10 }
代码第8行调用setContentView方法将屏幕设定为R.layout.main,其代表位于res\layout目录下的main.xml文件,由于篇幅有限,该文件代码不予列出,读者可以自行查阅随书光盘。
● 通过View的子类对象声明
通过XML配置文件将不同的View整合到一起非常方便,但是留给开发人员的自主性不够,尤其是当进行游戏编程时,往往Android系统中已经存在的View无法满足要求,这种情况下一般会通过继承和扩展View来开发自己想要的用户界面。下面的例子给出了通过View的子类对象来决定Activity显示内容的示例,首先给出的是本例中Activity的代码。
代码位置:见随书光盘中源代码/第3章/Sample_3_2/src/wyf/wpf目录下的Sample_3_2.java。
1 package wyf.wpf; //声明包语句
2 import android.app.Activity; //引入相关类
3 import android.os.Bundle; //引入相关类
4 public class Sample_3_2 extends Activity {
5 @Override
6 public void onCreate(Bundle savedInstanceState) {
7 super.onCreate(savedInstanceState); //调用父类onCreate方法
8 MyContentView mcv = new MyContentView(this); //创建View对象
9 setContentView(mcv); //设置当前屏幕
10 }
11 }
上述代码中使用MyContentView 作为Activity的显示内容,其代码如下。
代码位置:见随书光盘中源代码/第3章/Sample_3_2/src/wyf/wpf目录下的MyView.java。
1 package wyf.wpf; //声明包
2 import android.content.Context;import android.graphics.Canvas; //引入相关类
3 import android.graphics.Color;import android.graphics.Paint; //引入相关类
4 import android.view.View; //引入相关类
5 //继承自View的子类
6 public class MyContentView extends View{
7 public MyContentView(Context context) { //构造器
8 super(context);
9 }
10 @Override
11 protected void onDraw(Canvas canvas) { //重写View类绘制时的回调方法
12 Paint paint = new Paint(); //创建画笔
13 paint.setTextSize(18); //设置字体大小
14 paint.setAntiAlias(true); //设置抗锯齿
15 paint.setColor(Color.RED); //设置字体颜色
16 canvas.drawText("这是通过继承和扩展View类来显示的。", 0, 50, paint);//绘制字体到屏幕
17 }
18 }
尽管一个Android应用程序中可以包含多个Activity,但一般都选择一个作为程序启动后第一个显示在屏幕上的Activity,其他的Activity可以通过当前Activity中的startActivity方法来启动,这个在之后的小节中将会详细讲解。
3.1.2 Service组件
与Activity不同的是,Service没有提供与用户进行交互的表示层。Service是运行在后台的一种Android组件,当应用程序需要进行某种不需要前台显示的计算或数据处理时,就可以启动一个Service来完成,每个Service都继承自android.app包下的Service类。
Service一般由Activity或其他Context对象来启动,当启动Service之后,该Service将会在后台运行,即使启动这个Service的Activity或其他组件的生命周期已经结束,Service仍然会继续运行,直到自己的生命周期结束为止。每个Service都应该在AndroidManifest.xml中进行声明。Service的启动方式有两种,对应的生命周期也各不相同。
● 通过startService方法启动。当系统调用startService方法时,如果该Service还未启动,则依次调用其onCreate方法和onStart方法来启动。当其他Context对象调用stopService方法、Service调用自身的stopSelf或stopService方法时才会停止Service的执行。
● 通过bindService方法启动。当系统调用bindService方法时,如果该Service未启动,则会调用其onCreate方法完成初始化工作,然后会将该Service和Context对象(如Activity)进行绑定,当被绑定的Context对象被销毁时,与之绑在一起的Service也会停止运行。
通过不同的方式启动Service,其生命周期也不尽相同,两种启动Service的方式以及相应阶段会涉及的回调方法如图3-2和图3-3所示。
图3-2 通过startService方法启动Service
图3-3 通过bindService方法启动服务
需要注意的是,尽管存在两种方式启动Service,但是无论Service是通过什么方式启动的,都可以将其与Context对象进行绑定。在Android平台下启动服务需要涉及Intent等方面的知识,本书将在稍后的小节中做详细的介绍。
3.1.3 Broadcast Receiver组件
Broadcast Receiver同 Service一样,并不提供与用户交互的表示层,其是一种负责接收广播消息并对消息做出反应的组件。在Android的系统中就存在许多这样的广播,比如电池电量过低或信号过低时,系统就会发出广播进行通知。
应用程序如果需要响应某一个广播消息,应该注册对应的BroadcastReceiver对象,该对象继承自BroadcastReceiver类,该类位于android.content包下。这样一来,当系统或另外的应用程序发出特定广播时,该应用程序就可以接收并做出回应,如启动Activity等。
1.BroadcastReceiver发布广播的方式
发布一个广播比较容易,在需要的地方创建一个Intent对象,将信息的内容和用于过滤的信息封装起来,通过调用 Context.sendBroadcast 方法、Context.sendOrderedBroadcast 方法或者Context.sendStickyBroadcast方法将该Intent对象广播出去,3种发布广播方式的区别如下。
● 通常,使用 sendBroadcast 或 sendStickyBroadcast 发送出去的 Intent,所有满足条件的 BroadcastReceiver都会执行其onReceive方法。但若有多个满足条件的BroadcastReceiver,其执行onReceive方法的顺序是没有保证的。而通过sendOrderedBroadcast方法发送出去的Intent,会根据BroadcastReceiver注册时IntentFilter设置的优先级的顺序来执行onReceive方法,相同优先级的BroadcastReceiver执行onReceive方法的顺序是没有保证的。
● sendStickyBroadcast 主要的不同是, Intent 在发送后会一直存在,并且在以后调用registerReceiver注册相匹配的Receiver时会把这个Intent对象直接返回给新注册的Receiver。
2.BroadcastReceiver接收广播的方式
发布的广播实体是Intent,那么接收广播的时候就需要通过IntentFilter对象来进行过滤。Broadcast Receiver 的生命周期比较简单,只有一个回调方法——onReceive,该方法在应用程序接收到发给自己广播时候调用,所以Broadcast Receiver的使用方法也相对简单,只需要对onReceive方法进行合理重写,在适当的地方注册该Broadcast Receiver即可。
注册BroadcastReceiver对象的方式有以下两种。
● 在 AndroidManifest.xml 文件中声明。注册信息包含在<receiver></receiver>标签中,并在<intent-filter>标签内设定过滤规则。
● 在代码中创建并设置IntentFilter对象。该IntentFilter对象包含了对广播的过滤规则,然后在需要的地方调用Context.registerReceiver方法和Context.unregisterReceiver方法进行注册和取消注册,如果采用这种方式注册,当Context对象被销毁时,该BroadcastReceiver也就不复存在了。
提示
发布广播和使用BroadcastReceiver对象注册监听广播的内容涉及了后面的Intent对象的知识,将在稍后的小节进行详细探讨。
3.1.4 Content Provider组件
Content Provider和其他的应用程序组件有很大的不同,Content Provider主要用于不同的应用程序之间进行数据共享。在Android平台下,每个应用程序都有独立的内存空间,如果某一个应用程序需要使用其他应用程序的数据,就必须采用ContentProvider对象。
每个ContentProvider都继承自android.content包下的ContentProvider类,其功能就是提供自己的数据给外部应用程序使用,提供的数据可以存储为Android文件、SQLite数据库文件(将会在后面的章节进行介绍)或其他合法的格式。
ContentProvider提供数据及访问数据的接口,真正访问数据的是ContentResolver对象,该对象可以与ContentProvider对象进行通信,以达到共享数据的目的。
提示
ContentProvider组件所涉及的知识超过了本章涵盖的范围,将会在随后的章节进行详细介绍。
3.1.5 AndroidManifest.xml文件简介
前面的小节介绍了Android平台下应用程序的基本组件,这些组件要想被应用程序使用,需要在AndroidManifest.xml配置文件中进行声明。但AndroidManifest.xml文件的作用远不止这些,每个Android应用程序都必须包含一个AndroidManifest.xml配置文件,而且文件名称不可改变。
除了Broadcast Receiver组件既可以在AndroidManifest.xml文件中声明,也可以在代码中直接创建之外,其他的应用程序组件必须在AndroidManifest.xml文件中进行声明,否则系统将无法使用该组件。AndroidManifest.xml文件的主要内容包括以下几点。
● 声明应用程序的Java包名,该包名将作为该应用程序的惟一标识符。
● 描述应用程序所包含的组件,如Activity等。除了描述实现某种组件的类的名称外,还需要声明该组件对于Intent对象的过滤规则,即告知系统在何种状态下该组件可以被启动。
● 指出应用程序组件运行在哪个进程,默认情况下所有的组件都运行在主进程中,如果需要让其运行在别的线程,需要在AndroidManifest.xml中设置。
● 声明应用程序必须具有的用来访问受保护的API或与其他应用程序交互的权限。
● 声明其他应用程序必须具有的用来访问自己组件的权限。
● 列出该应用程序中的Instrumentation对象,Instrumentation的用途是对应用程序的运行进行监控,其只在应用程序的开发过程中起作用,在程序发布前会被移除。
● 声明应用程序所要求的最低Android API版本。
● 声明应用程序需要链接到的默认Android类库之外的库。
以下代码给出了一个普通项目的AndroidManifest.xml文件的结构。由于本书篇幅有限,故只将AndroidManifest.xml文件的代码列出。
代码位置:见随书光盘中源代码/第3章/Sample_3_3目录下的AndroidManifest.xml。
1 <?xml version="1.0" encoding="utf-8"?>
2 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3 package="wyf.wpf"
4 android:versionCode="1"
5 android:versionName="1.0"> <!-- 标记,记录应用程序的包及版本等信息 -->
6 <application android:icon="@drawable/icon" android:label="@string/app_name">
7 <activity android:name=".Sample_3_3"
8 android:label="@string/app_name"> <!-- 声明Activity组件 -->
9 <intent-filter>
10 <action android:name="android.intent.action.MAIN" />
11 <category android:name="android.intent.category.LAUNCHER" />
12 </intent-filter> <!-- 为Activity设置IntentFilter -->
13 </activity>
14 <service android:name=".MyService"
15 android:process=":remote" >
16 </service> <!-- 声明Service组件 -->
17 </application>
18 <uses-sdk android:minSdkVersion="7" /> <!-- 指定应用程序运行的最低SDK版本 -->
19 <uses-permission android:name="android.permission.INTERNET" /><!-- 指定应用程序的权限 -->
20 </manifest>
从上边的代码可以看出 AndroidManifest.xml 文件的基本结构,所有的信息写在根标记<manifest>中,<manifest>标记所包含的属性及说明如表3-1所示。
表3-1 AndroidManifest.xml文件主要标记及其说明
在根标记<manifest>中必须包含<application>标记,所有的组件如Activity等均在<application>标记中声明。除了<application>标记外,<manifest>标记中还可以包含<uses-permission>等标记。<manifest>标记中所包含的子标记及其属性和说明如表3-2所示。
表3-2 <manifest>的子标记及其属性说明
<application>组件中包含了应用程序包含的各种组件的标记,如<activity>、<service>等,这些组件的标记中很多属性值和<application>标记中的属性名称相同,如果进行了设置将会覆盖掉<application>中的同名属性值,如果未设置则取<application>中的同名属性值。<application>标记包含的子标记及其属性说明如表3-3所示。
表3-3 <application>标记所包含的子标记及属性
在<activity>、<service>和<receiver>等标记中,还可以包含<intent-filter>标记,该标记指明了组件的intent过滤规则,<intent-filter>标记的属性及其说明如表3-4所示。
表3-4 <intent-filter>标记的属性及说明
<intent-filter>标记中的子标记有<action>、<category>和<data>,其中<action>标记是必须包含的,并且可以为多个。这些子标记的属性值如表3-5所示。
表3-5 <intent-filter>标记包含的子标记及其属性说明
以上为AndroidManifest.xml配置文件中常见的一些标记及其属性值说明,其中<intent-filter>标记中涉及的一些知识将会在随后的小节详细介绍。
3.2 应用程序的内部通信
在Android应用程序中,内部通信简单来讲是指主线程和自己开发的子线程之间的通信。在前面的小节中已经提到,在Android应用程序运行时,默认情况下会为第一个启动的组件创建一个进程,之后启动的组件都运行在这个进程中。
当为应用程序创建了一个进程后,一个主线程将会被创建。这个主线程主要负责维护组件对象和应用程序创建的所有窗口,如果应用程序中创建了自己的线程,这些线程将无法对主线程控制的内容进行修改,此时就需要使用 Handler 来同主线程进行交互,本节就来简单介绍 Handler类及其基本的用法。
3.2.1 消息的处理者——Handler类简介
Handler类主要用于应用程序的主线程同用户自己创建的线程进行通信,应用程序在主线程中维护一个消息队列。Handler机制使得线程间的通信通过Message和Runnable对象来传递和处理。
每个Handler对象都与一个线程及其消息队列相关联。当创建一个Handler对象时,其便与创建这个Handler对象的线程的消息队列绑定,之后该Handler对象将会向消息队列传递message或runnable并处理执行队列中的元素。
Handler最主要的用法就是安排Message和Runnable对象使其在未来的某个时刻被处理或运行。
1.传递消息对象
使用Handler传递消息时将消息内容封装到一个Message对象中,Message类中包含了消息的描述和任何形式的可以被Handler发送的数据对象。一个Message对象中主要的字段如表3-6所示。
表3-6 Message对象中主要字段及说明
提示
getData和setData返回和接收的都是Bundle对象,该对象是以String为键、以任意可封装的类型为值的map。
Handler 发出消息时,既可以指定消息到达后立即被处理,也可以指定其经过特定的时间间隔后被处理,还可以指定一个绝对时间让消息在那之前被处理。不同的发送消息的方法如表3-7所示。
表3-7 不同的发送消息的方法及说明
无论消息以何种方式发出,接受并处理消息的方法都是 handleMessage 方法,该方法接受的参数为一个Message对象,在其中可以根据程序需要对不同的消息进行不同的处理。
提示
虽然Message类提供了公有的构造方法,但是最好的获得一个Message对象的途径是调用 Message 类的静态方法 obtain 或者是 Handler 类的一系列方法,如obtainMessage,这样创建的Message对象是可复用的。
2.传递Runnable对象
传递Runnable对象与传递Message对象类似,Runnable对象为实现了Runnable接口的对象, Handler 在传递 Runnable 对象时也可以设置其经过指定的时间间隔或在指定的绝对时间之前被处理。由于篇幅有限,本书在此不再赘述,有兴趣的读者可以自行查阅相关书籍。
3.2.2 使用Handler进行内部通信
前面的小节对 Handler 类进行了一个简单的介绍,本小节就来举一个简单的例子说明Handler的具体用法。在本例中,用户通过单击屏幕上的按钮,启动一个线程与主线程通过Handler进行通信以更新显示数据。运行效果如图3-4所示。
图3-4 Sample_3_4运行效果图
本应用程序中只包含 Activity 一个组件,接收用户单击按钮事件和发送 Handler 等功能都在Activity中实现。整个程序分为如下3个步骤来完成。
(1)Activity的主要代码。程序运行后的屏幕中包含一个Button和一个TextView对象,单击按钮后会在新启动的线程中向主线程发 Handler 消息,主线程收到消息后会根据消息的内容修改TextView的显示内容。Activity的主要代码如下。
代码位置:见随书光盘中源代码/第3章/Sample_3_4/src/wyf/wpf目录下的Sample_3_4.java。
1 package wyf.wpf; //声明包语句
2 import android.app.Activity; //引入相关类
3 import android.os.Bundle; //引入相关类
4 import android.os.Handler; //引入相关类
5 import android.os.Message; //引入相关类
6 import android.view.View; //引入相关类
7 import android.widget.Button; //引入相关类
8 import android.widget.TextView; //引入相关类
9 //继承自Activity的子类
10 public class Sample_3_4 extends Activity {
11 public static final int UPDATE_DATA = 0; //常量,代表更新数据
12 public static final int UPDATE_COMPLETED = 1; //常量,代表更新数据
13 TextView tv; //TextView对象的引用
14 Button btnStart; //Button对象的引用
15 …… //此处省略创建Handler对象的相关代码,将在后面的步骤中补全
16 @Override
17 public void onCreate(Bundle savedInstanceState) {
18 super.onCreate(savedInstanceState);
19 setContentView(R.layout.main); //设置当前屏幕为R.layout.main布局文件
20 tv = (TextView)findViewById(R.id.tv); //获得屏幕中TextView对象引用
21 btnStart = (Button)findViewById(R.id.btnStart);//获得屏幕中Button对象引用
22 …… //此处省略为按钮添加单击事件监听器的相关代码,将在后面的步骤中补全
23 }
24 }
● 代码第11~12行定义了用来表示消息类别的静态常量,这样代码的可读性和可维护性都好,建议读者在开发的过程中也采用这种写法。
● 代码第17~23行为重写Activity的onCreate方法,该方法在创建Activity组件时被调用。
● 代码第20~21行通过调用findViewById方法获取布局文件中Button和TextView对象的引用,以便在后续的代码中修改其属性值。
(2)采用匿名内部类的方式声明并创建一个继承自Handler类的子类对象,并在该内部类中重写handleMessage方法,下述创建Handler对象的代码来自于步骤(1)代码中的第15行省略的部分。
1 Handler myHandler = new Handler(){
2 @Override
3 public void handleMessage(Message msg) { //重写处理消息方法
4 switch(msg.what){ //判断消息类别
5 case UPDATE_DATA: //消息为更新的数据
6 tv.setText("正在更新来自线程的数据:"+msg.arg1+"%...");//更新TextView显示的内容
7 break;
8 case UPDATE_COMPLETED: //消息为更新完毕
9 tv.setText("已完成来自线程的更新数据!"); //改变TextView显示的内容
10 break;
11 }
12 }
13 };
说明
上述代码创建了一个与主线程绑定的 Handler 对象,其中重写了HandleMessage (Message msg)方法用来接收和处理消息,主要是采用判断Message对象的What字段来确定消息类别。
(3)为按钮btnStart添加单击事件监听器。在步骤(1)中代码的第22行省略的部分添加如下代码,为按钮添加单击事件监听器。
1 btnStart.setOnClickListener(new View.OnClickListener() {//为Button添加单击事件监听器
2 @Override
3 public void onClick(View v) {
4 new Thread(){ //启动一个新线程
5 public void run(){
6 for(int i=0;i<100;i++){
7 try{ //睡眠一段时间
8 Thread.sleep(150);
9 }
10 catch(Exception e){
11 e.printStackTrace();
12 }
13 Message m = myHandler.obtainMessage(); //创建Message对象
14 m.what = UPDATE_DATA; //为what字段赋值
15 m.arg1=i+1; //为arg1字段赋值
16 myHandler.sendMessage(m); //发出Message对象
17 }
18 myHandler.sendEmptyMessage(UPDATE_COMPLETED);//发更新完毕消息
19 }
20 }.start();
21 }
22 });
● 代码第3~21行重写了 onClick(View view)方法,该方法主要的工作是在单击事件发生后启动一个线程,该线程的主要工作是每隔一段时间向主线程发送 Handler 消息,主线程收到消息后会根据其内容修改TextView的值。
● 代码第16 行和第18 行都是发送Handler 消息的方法,当所发送的消息内容只需要what属性就可以时,可以采用sendEmptyMessage方法。
3.3 应用程序组件之间的通信
在 3.1小节介绍了Android应用程序的基本组件,这些基本组件除了Content Provider之外,几乎全部都是依靠Intent对象来激活和通信的。本节就来介绍Intent类的相关知识,并通过示例来说明Intent的一般用法。
3.3.1 Intent类简介
Intent类的对象是组件间通信的载体,组件之间进行通信就是一个个Intent对象在不断地传递。Intent对象主要作用于运行在相同或不同应用程序的Activity、Service和Broadcast Receiver组件之间,对于这3种组件,其作用机制也不相同。
● 对于Activity组件,Intent主要通过调用Context.startActivity、Context.startActivityForResult等方法实现传递,其结果是启动一个新的Activity或者使当前的Activity开始新的任务。
● 对于Service组件,Intent主要通过调用Context.startService和Context.bindService方法实现传递,其作用结果是初始化并启动一个服务或者绑定服务到Context对象。
● 对于Broadcast Receiver组件,Intent主要通过 sendBroadcast等一系列发送广播的方法实现传递,其作用结果是将Intent组件以广播的形式发出以便合适的组件接收。
一个Intent对象就是一组信息,其包含接收Intent组件所关心的信息(如Action和Data)和Android系统关心的信息(如Category等),一般来讲,一个Intent对象包含如下内容。
1.Component Name部分
组件名称指明了未来要处理Intent的组件,组件名称封装在一个ComponentName对象中,该对象用于惟一标识一个应用程序组件,如Activity、Service、Content Provider等。ComponentName类包含两个 String 成员,分别代表组件的全称类名和包名,包名必须和 AndroidManifest.xml 中<application>标记中的对应信息一致。
对于Intent对象来说,组件名称不是必须的,如果添加了组件名称则该Intent为“显式Intent”,这样Intent在传递的时候会直接根据ComponentName对象的信息去寻找目标组件。如果不设置组件名称,则为“隐式Intent”,Android会根据Intent中的其他信息来确定响应该Intent的组件是哪个。
2.Action部分
Action为一个字符串对象,其描述了该Intent会触发的动作。Android系统中已经预先定义好了一些表征 Action 的常量,如 ACTION_CALL、ACTION_MAIN 等,同时,开发人员也可以自己定义Intent的动作描述,一般来讲,自己定义的Action字符串应该以应用程序的包名为前缀,如可以定义一个Action为“wyf.wpf.StartService”。
因为Action很大程度上决定了一个Intent的内容(主要是Data和Extras部分),所以定义自己的Action时应该做到见名知义,同时,如果应用程序比较复杂,应该为其定义一个整体的Action协议,使所有的Action集中管理。
3.Data部分
Data描述Intent的动作所操作到的数据的URI及类型,不同的Action对应不同的操作数据,比如Action为ACTION_VIEW的Intent的Data应该是“http:”格式的URI。当为组件进行Intent的匹配检查时,正确设置Data的URI资源和数据类型很重要。
4.Category部分
Category为字符串对象,其包含了可以处理Intent的组件的类别信息,Intent中可以包含任意个Category。同Action一样,Android系统预先定义了一些Category常量,但是不可以自行定义Category。
调用方法addCategory用来为Intent添加一个Category,方法removeCategory用来移除一个Category,getCategories方法返回已定义的Category。
5.Extras部分
Extras是一组键值对,其包含需要传递给目标组件并由其处理的一些额外信息。
6.Flags部分
一些有关系统如何启动组件的标志位,所有的标志位都已在Android系统中预先定义。
3.3.2 应用程序组件——IntentFilter类简介
当Intent在组件之间进行传递时,组件如果需要告知Android系统自己能够响应和处理哪些Intent,就需要使用IntentFilter对象。顾名思义,IntentFilter对象负责过滤掉组件无法响应和处理的Intent,只将自己关心的Intent接收进来进行处理。
IntentFilter实行“白名单”管理,即只列出组件能接收的Intent,IntentFilter只会过滤隐式Intent,显式的 Intent 会被直接传递到目标组件,一个隐式的 Intent 只有通过了组件的某一个 IntentFilter的过滤,才可以被组件接收并处理。
诸如 Activity、Service、Broadcast Receiver 这些组件可以有一个或多个 IntentFilter,每个IntentFilter相互独立,只需要通过一个即可。每个IntentFilter都是android.content包下的IntentFilter类的对象,除了用于过滤广播的IntentFilter可以在代码中动态创建外,其他组件的IntentFilter必须在AndroidManifest.xml文件中进行声明。
IntentFilter中具有与Intent对应的用于过滤Action、Data和Category的字段,一个Intent对象要想被一个组件处理,必须通过这3个环节的检查。
1.检查Action
尽管一个Intent只可以设置一种Action,但一个IntentFilter却可以持有一个或多个Action用于过滤,到达的Intent对象只需要匹配其中一个Action即可。但是IntentFilter的Action部分不可以为空,如果Action部分为空则会阻塞掉所有的Intent。相反,如果Intent的Action字段未设置,其将通过所有的IntentFilter的Action检查。
2.检查Data
同Action一样,IntentFilter中的Data部分也是可以为一个或多个,而且可以没有。每个Data包含的内容为URI和数据类型,进行Data检查时主要也是对这两点进行比较,比较规则如下。
● 如果Intent对象没有设置Data,只有IntentFilter也未作设置时才可通过检查。
● 如果Intent对象只设置了URI而没有指定数据类型,只有当其匹配IntentFilter的URI,并且IntentFilter也没有设置数据类型时该Intent对象才可以通过检查。
● 如果Intent对象只指定了数据类型而没有设置URI,只有当其匹配IntentFilter的数据类型并且也没有设置URI时该Intent对象才可以通过检查。
● 如果Intent对象既包含URI又包含数据类型,只有当其数据类型匹配IntentFilter中的数据类型并且通过了URI检查时该Intent对象才可以通过检查。
3.检查Category
IntentFilter中可以设置多个Category,检查Category时,只有当Intent对象中所有的Category都匹配IntentFilter中的Category时该Intent对象才可以通过检查,并且IntentFilter中的Category可以比Intent中的Category多,但是必须都包含Intent对象中所有的Category。如果一个Intent没有设置Category,则其将通过所有IntentFilter的Category检查。
IntentFilter 既可以在 AndroidManifest.xml 中声明,也可以在代码中动态创建。如果是在Android- Manifest.xml 中声明 IntentFilter,需要使用<intent-filter>标记。该标记包含<action>、<data>、<category>子标记,每个子标记中包含的属性以及对应的方法如表3-8所示。
表3-8 <intent-filter>子标记及其属性说明
如果一个到来的Intent对象通过了不止一个组件(如Activity、Service等)的IntentFilter的检查,那么系统将会弹出提示,让用户选择激活哪个组件。
3.3.3 示例1:与Android系统组件通信
前面的小节简单介绍了Intent类和IntentFilter类的相关知识,本小节将通过一个小例子来说明其用法,本例使用Intent和IntentFilter与Android系统预定义组件拨号程序进行通信。首先,通过Intent调用系统的拨号程序,程序的主要代码如下。
代码位置:见随书光盘中源代码/第3章/Sample_3_5/src/wyf/wpf目录下的Sample_3_5.java。
1 package wyf.wpf; //声明包
2 import android.app.Activity; //引入相关类
3 import android.content.Intent; //引入相关类
4 import android.os.Bundle; //引入相关类
5 import android.view.View; //引入相关类
6 import android.widget.Button; //引入相关类
7 //继承自Activity的子类
8 public class Sample_3_5 extends Activity {
9 Button btnDial;
10 @Override
11 public void onCreate(Bundle savedInstanceState) { //重写onCreate方法
12 super.onCreate(savedInstanceState);
13 setContentView(R.layout.main); //设置屏幕显示内容
14 btnDial = (Button)this.findViewById(R.id.btDial); //获得屏幕上按钮对象引用
15 btnDial.setOnClickListener(new Button.OnClickListener(){//为按钮添加单击事件的监听器
16 @Override
17 public void onClick(View v) { //重写onClick方法
18 Intent myIntent = new Intent(Intent.ACTION_DIAL);//创建Intent对象
19 Sample_3_5.this.startActivity(myIntent);//启动Android内置的拨号程序
20 }
21 });
22 }
23 }
● 代码第14行获取屏幕上按钮对象的引用。
● 代码第15~21行为按钮添加单击事件的监听器,其中第19~22行为重写的onClick方法,在其中创建了一个Intent并设定其Action为Android的拨号程序。
以上代码运行后单击按钮前后的屏幕如图3-5和图3-6所示。
图3-5 程序运行效果图
图3-6 单击按钮后调用系统拨号程序
下面继续完善这个程序,在AndroidManifest.xml中的<activity>标记中添加如下代码。
代码位置:见随书光盘中源代码/第3章/Sample_3_5目录下的AndroidManifest.xml。
1 <activity android:name=".Sample_3_5" android:label="@string/app_name">
2 ……<!-- 省略不必要的代码,读者可以自行查阅随书光盘 -->
3 <intent-filter> <!-- 声明一个IntentFilter -->
4 <action android:name="android.intent.action.CALL_BUTTON" /><!-- 设定Action -->
5 <category android:name="android.intent.category.DEFAULT" /><!-- 设定Category -->
6 </intent-filter>
7 </activity>
这段代码的作用是为Activity添加一个IntetnFilter,即该Activity将会对系统按下拨号键进行响应。当在待机状态下按下拨号键时,系统就会提示是启动Android系统自带的拨号程序,还是启动自己的应用程序,如图3-7所示。
图3-7 按下拨号键显示的对话框
3.3.4 示例2:应用程序组件间通信示例Activity部分的开发
前面的例子主要的功能是通过Intent来启动系统的拨号程序,以及在AndroidManifest.xml中设置应用程序的IntentFilter来使应用程序可以响应手机拨号键按下的行为。下面将会是一个应用程序内部组件之间通过 Intent和Broadcast Receiver组件通信的例子。
本例中在Activity组件中通过单击按钮启动一个Service,Service将会启动一个线程,该线程的工作是定时产生一个随机数,并将其封装到Intent对象中传递给Activity,Activity接收到Intent后提取其中的信息将其显示到TextView控件中。在服务运行的时候,可以单击Activity上的停止按钮来停止服务。
首先来开发应用程序的Activity部分。Activity的开发通过以下3个步骤来完成。
(1)开发Activity的主要代码。在Activity中定义引用指向屏幕上的Button等控件,并为Button添加单击事件的监听器,Activity的主要代码如下。
代码位置:见随书光盘中源代码/第3章/Sample_3_6/src/wyf/wpf目录下的Sample_3_6.java。
1 package wyf.wpf; //声明包语句
2 import android.app.Activity; //引入相关类
3 import android.content.BroadcastReceiver; //引入相关类
4 import android.content.Context; //引入相关类
5 import android.content.Intent; //引入相关类
6 import android.content.IntentFilter; //引入相关类
7 import android.os.Bundle; //引入相关类
8 import android.view.View; //引入相关类
9 import android.view.View.OnClickListener; //引入相关类
10 import android.widget.Button; //引入相关类
11 import android.widget.TextView; //引入相关类
12 //继承自Activity的子类
13 public class Sample_3_6 extends Activity {
14 public static final int CMD_STOP_SERVICE = 0;
15 Button btnStart; //开始服务Button对象应用
16 Button btnStop; //停止服务Button对象应用
17 TextView tv; //TextView对象应用
18 DataReceiver dataReceiver; //BroadcastReceiver对象
19 @Override
20 public void onCreate(Bundle savedInstanceState) { //重写onCreate方法
21 super.onCreate(savedInstanceState);
22 setContentView(R.layout.main); //设置显示的屏幕
23 btnStart = (Button)findViewById(R.id.btnStart);
24 btnStop = (Button)findViewById(R.id.btnStop);
25 tv = (TextView)findViewById(R.id.tv);
26 btnStart.setOnClickListener(new OnClickListener(){//为按钮添加单击事件监听
27 @Override
28 public void onClick(View v) { //重写onClick方法
29 IntentmyIntent=newIntent(Sample_3_6.this,wyf.wpf.MyService.class);
30 Sample_3_6.this.startService(myIntent);//发送Intent启动Service
31 }
32 });
33 btnStop.setOnClickListener(new OnClickListener() { //为按钮添加单击事件监听
34 @Override
35 public void onClick(View v) { //重写onClick方法
36 Intent myIntent = new Intent(); //创建Intent对象
37 myIntent.setAction("wyf.wpf.MyService");
38 myIntent.putExtra("cmd", CMD_STOP_SERVICE);
39 sendBroadcast(myIntent); //发送广播
40 }
41 });
42 }
43 ……//此处省略重写的Activity的onStart和onStop方法,将在后面的步骤中补全
44 ……//此处省略声明BroadcastReceiver子类的代码,将在后面的步骤中补全
45 }
● 上述代码声明了Activity中按钮等控件的引用,同时重写了onCreate方法为控件对象的引用赋值并为按钮的单击事件添加监听器。
● 代码的第29行和第36~38行分别创建了用于启动和停止Service的Intent对象,并通过startService方法和sendBroadcast方法发出去。
(2)服务启动后也会向Activity发 Intent,所以Activity也必须注册一个 Broadcast Receiver组件用于接收 Intent,注册之前需要编写实现了 BroadcastReceiver 的子类,下面代码即是来自于步骤(1)中代码第44行省略的部分。
1 private class DataReceiver extends BroadcastReceiver{//继承自BroadcastReceiver的子类
2 @Override
3 public void onReceive(Context context, Intent intent) {//重写onReceive方法
4 double data = intent.getDoubleExtra("data", 0);
5 tv.setText("Service的数据为:"+data); //将收到的数据显示在屏幕上
6 }
7 }
说明
实现Broadcast组件最重要的是重写onReceive方法,上述代码的第4~5行从接收到的Intent对象中提取出Extra部分的信息并将其作为TextView的显示内容。
(3)编写好Broadcast Receiver组件类的代码后就应该在合适的地方对其进行注册和取消注册了。注册和取消Broadcast Receiver组件的代码写在Activity的 onStart和onStop方法中,下面代码即是步骤(1)中代码第43行省略的部分。
1 @Override
2 protected void onStart() { //重写onStart方法
3 dataReceiver = new DataReceiver();
4 IntentFilter filter = new IntentFilter(); //创建IntentFilter对象
5 filter.addAction("wyf.wpf.Sample_3_6");
6 registerReceiver(dataReceiver, filter); //注册Broadcast Receiver
7 super.onStart();
8 }
9 @Override
10 protected void onStop() { //重写onStop方法
11 unregisterReceiver(dataReceiver); //取消注册Broadcast Receiver
12 super.onStop();
13 }
说明
上述代码第5行定义了IntentFilter的Action为自定义的字符串,该字符串必须和发送Intent时定义的Action字符串一致。
3.3.5 示例3:应用程序组件间通信示例Service部分的开发
到这里Activity部分的功能已经开发完毕,下面开发Service部分的代码,Service部分的开发主要分为以下3个步骤。
(1)在Service的onCreate方法中进行一些初始化的工作,在onStartCommand和onDestroy方法中注册和取消注册Broadcast Receiver组件。Service类中的主要代码如下。
代码位置:见随书光盘中源代码/第3章/Sample_3_1/src/wyf/wpf目录下的MyService.java。
1 package wyf.wpf; //声明包语句
2 import android.app.Service; //引入相关类
3 import android.content.BroadcastReceiver; //引入相关类
4 import android.content.Context; //引入相关类
5 import android.content.Intent; //引入相关类
6 import android.content.IntentFilter; //引入相关类
7 import android.os.IBinder; //引入相关类
8 //继承自Service的子类
9 public class MyService extends Service{
10 CommandReceiver cmdReceiver; //继承自BroadcastReceiver的子类
11 boolean flag; //线程执行的标志位
12 @Override
13 public void onCreate() { //重写onCreate方法
14 flag = true; //线程执行的标志位置为true
15 cmdReceiver = new CommandReceiver(); //创建BroadcastReceiver子类的对象
16 super.onCreate();
17 }
18 @Override
19 public IBinder onBind(Intent intent) { //重写onBind方法
20 return null;
21 }
22 @Override
23 public int onStartCommand(Intent intent, int flags, int startId){//重写onStartCommand方法
24 IntentFilter filter = new IntentFilter(); //创建IntentFilter对象
25 filter.addAction("wyf.wpf.MyService");
26 registerReceiver(cmdReceiver, filter); //注册Broadcast Receiver
27 doJob(); //调用方法启动线程
28 return super.onStartCommand(intent, flags, startId);
29 }
30 @Override
31 public void onDestroy() { //重写onDestroy方法
32 this.unregisterReceiver(cmdReceiver); //取消注册CommandReceiver
33 super.onDestroy();
34 }
35 ……//此处省略定义doJob方法的代码,将在后面的步骤中补全
36 ……//此处省略继承自BroadcastReceiver类的CommandReceiver类的代码,将在后面的步骤中补全
37 }
说明
代码第25行IntentFilter定义的Action为自定义的字符串,其必须和Activity端发送Intent时设定的Action字符串一致。
(2)在步骤(1)中的代码第27行调用了doJob方法,该方法将启动一个新的线程定时发送Intent,步骤(1)中代码第35行省略的doJob方法代码如下。
1 public void doJob(){
2 new Thread(){ //新建一个线程
3 public void run(){ //重写线程的run方法
4 while(flag){
5 try{ //睡眠一段时间
6 Thread.sleep(1000);
7 }
8 catch(Exception e){
9 e.printStackTrace(); //捕获并打印异常
10 }
11 Intent intent = new Intent(); //创建Intent对象
12 intent.setAction("wyf.wpf.Sample_3_6");
13 intent.putExtra("data", Math.random());//以data为键以随机数为值
14 sendBroadcast(intent); //发送广播
15 }
16 }
17 }.start();
18 }
● 在新启动的线程中,每隔1s就向Activity发一个Intent,并将其Action设置为"wyf.wpf. Sample_3_6",与Activity注册的IntentFilter中所包含的Action字符串对应。
● 代码第 13 行产生一个随机数并将其赋值给 Intent 的 Extra 部分,代码第 14 行调用sendBroadcast方法将广播发出。
(3)为了让Activity可以启动和停止Service,必须为Service注册Broadcast Receiver组件,需要开发继承自BroadcastReceiver的子类,步骤(1)中代码第36行省略的代码如下。
1 private class CommandReceiver extends BroadcastReceiver{//继承自BroadcastReceiver的子类
2 @Override
3 public void onReceive(Context context, Intent intent) {//重写onReceive方法
4 int cmd = intent.getIntExtra("cmd", -1); //获取Extra信息
5 if(cmd == Sample_3_6.CMD_STOP_SERVICE){ //如果发来的消息是停止服务
6 flag = false; //停止线程
7 stopSelf(); //停止服务
8 }
9 }
10 }
说明
代码第4行从Intent对象中的Extra部分提取出以“cmd”为键的值,并判断是否为停止服务的消息,如果是则设线程执行标志位为false并调用stopSelf方法停止服务。
程序运行后未启动服务时程序界面如图3-8所示,服务正在运行时程序界面如图3-9所示。
图3-8 未启动服务时程序界面
图3-9 服务运行时程序界面
需要注意的是,开发完Activity和Service部分的代码,还需要在AndroidManifest.xml文件中对组件进行声明,对于Service,如果希望其在Activity退出后还能继续运行,需要在<service>标记中添加如下内容。
代码位置:见随书光盘中源代码/第3章/Sample_3_6目录下的AndroidManifest.xml。
1 ……//省略掉不必要的代码,读者可以自行查阅随书光盘
2 <service android:name=".MyService" android:process=":remote" ><!-- 让Service运行在远端进程中 -->
3 ……//省略掉不必要的代码,读者可以自行查阅随书光盘
这样就可以保证当管理并运行Activity等组件的主线程停止后,Service仍然可以继续在后台运行。
3.4 小结
本章主要介绍了Android应用程序交互式通信的原理,首先介绍了Android应用程序的基本组件及程序中各线程和各组件之间是如何进行通信的,利用这些交互式通信机制,就可以开发出功能各异的Android应用程序。