在实际的App软件中,几乎每个应用都涉及页面跳转的操作,诸如通讯录的联系人列表→联系人详细信息等。这些跳转都是借助于Android的Intent实现的。Intent是一种消息传递机制,用于Android的核心组件(Activity,BroadcastReceiver,Service)通信和数据交换。它不仅可以在同一个应用程序内部的不同组件之间进行通信,也可以在不同应用程序的组件之间传递信息。因此,由于Intent的存在,使得Android系统中相互独立的组件不再是一个个独立的孤岛,而成为可以互相通信的集合。
Intent,中文意思是“意图,意向”,可以理解为,应用程序要启动另一个组件就需要用到Intent。Intent负责对应用中一次操作的动作、涉及的数据、附加数据进行描述,Android则根据此Intent的描述,负责找到对应的组件,将Intent传递给调用的组件,并完成组件的调用。由于Intent可以方便地启动Activity和Service组件,也可以在Android系统上发布广播消息,从而将系统中互相独立的组件连接成可以互相通信的整体。本章的案例都是针对两个Activity进行通信的。根据Activity的启动方式,Intent支持显式启动和隐式启动。
形式如下:
Intent intent=new Intent(MainActivity.this,SubActivity.class); //定义一个Intent
startActivity(intent); //启动Activity
以上示例代码的作用是从MainActivity这个Activity类跳转到SubActivity中。Intent构造函数中的两个参数分别代表当前组件和目标组件,然后通过startXXX()方法启动该Intent对象,即可实现两个组件之间的跳转。由于在参数中明确指定了组件,所以这种启动方式称作显式启动。这种启动方式代码简单,易于理解。
形式如下:
Intent intent=new Intent(Intent.ACTION_VIEW,Uri.parse("content://com.android.contacts/contacts"));
startActivity(intent);
以上示例是使用了Intent的另一个构造函数,它没有明确指定需要启动的组件,而是由Android系统决定。至于Android系统应该如何选择组件作为要启动的对象呢?在程序运行时解析Intent,根据一定的规则对Intent和组件进行匹配,匹配的组件可以是程序本身的组件,也可以是Android系统内置的组件,甚至是第三方应用程序中的组件。上述示例代码的功能是打开了系统中的通讯录界面,其中Uri代表统一资源标识符,是Uniform Resource Identifier的简称。
这种启动方式虽然稍微有些复杂,但是它的好处是不必与某个具体的组件耦合,降低了Android系统中组件之间的耦合度,有利于组件分离,并允许替换应用程序中的元素,强调了Android组件的可复用性。当隐式启动和显式启动同时存在时,隐式启动会被忽略。
下面详细介绍Intent的匹配机制。
当通过显式启动方式来进行组件启动时,系统会根据指定的参数,直接启动目标组件。当隐式启动时,Android系统则需要通过某种匹配机制来寻找目标组件。这种匹配机制就是依赖于Android系统中的Intent过滤器(Intent Filters)来实现的。如图3-9所示。
图3-9 Intent隐式启动
Intent过滤器是根据Intent中的动作(action)、类别(category)和数据(data)内容,对目标组件进行匹配和筛选的机制。当Intent匹配到一个过滤器上,系统就会启动对应的组件并传递相应的Intent对象;如果匹配出多个过滤器,系统会弹出对话框,由用户进行选择。
通过上述的描述说明,Intent过滤器需要依附于Android的组件上,其格式内容如下(Android Manifest文件):
<activity android:name="ShareActivity">
<!--This activity handles "SEND" actions with text data-->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
<!--This activity also handles "SEND" and "SEND_MULTIPLE" with media data-->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="application/vnd.google.panorama360+jpg"/>
<data android:mimeType="image/*"/>
<data android:mimeType="video/*"/>
</intent-filter>
</activity>
上述代码定义了两个Intent过滤器。Intent过滤器由<intent-filter> 进行定义,嵌入到对应的组件中(例如本例中的activity)。在每个<intent-filter> 中,可以定义以下3种标签。
通过android:name属性指定组件能响应的动作,用字符串表示。表3-2列举了几种常用的动作,随后的示例我们便是以ACTION_VIEW为例的。
表3-2 常见Action常量及说明
【说明】 以上常量对应的值,通常是android.intent.action.XXX 的形式。例如,示例中的android.intent.action.SEND,其实就是ACTION_SEND。
通过一个或多个属性来指定响应的scheme,host,port,path和MIME type等值。接受的是一个Uri对象,形式如下:
content://com.android.contacts/contacts/1
其中,content是scheme部分,com.android.contacts是host部分,/contacts/1是path部分。例子中主要定义了MIME type属性,用于声明该组件所能匹配的Intent的Type属性。
通过android:name属性指定组件能响应的服务方式,用字符串表示。每个过滤器可以定义多个<category> 标签。为了能够响应隐式启动,必须定义一个CATEGORY_DEFAULT。因为startActivity()和 startActivityForResult()方法只对具有该标签的过滤器进行解析,否则隐式启动将永远无法启动对应的组件。表3-3列举了几种常用的动作,随后的示例我们便是以CATEGORY_DEFAULT为例的。
表3-3 常见Category常量及说明
【说明】 以上常量对应的值,通常是android.intent.category.XXX 的形式。例如,示例中的android.intent.category.DEFAULT,其实就是CATEGORY_DEFAULT。
【例 3-2】 在项目 ActivityIntentDemo 中的主界面里,有两个按钮,单击后,分别进入SubActivity1 和SubActivity2 中,效果如图3-10 所示。
图3-10 运行效果图
实现步骤如下:
(1)新建项目ActivityIntentDemo,并在该项目下增加两个Activity类。操作界面如图3-11所示,同时也会自动生成相关的布局文件。
图3-11 项目中增加Activity子类
因此,该项目中存在如图3-12所示的目录结构。其中,MainActivity管理activity_main.xml布局文件;SubActivity1管理activity_sub1.xml布局文件;SubActivity2管理activity_sub2.xml布局文件。
图3-12 项目结构
(2)activity_main布局文件:增加两个按钮,并为其设置id值分别为button1和button2。布局文件中增加按钮的代码如下:
<Button
android:layout_width="wrap_content"
android:layout_depth="wrap_content"
android:text="显式启动"
android:id="@+id/button1"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true" />
<Button
android:layout_width="wrap_content"
android:layout_depth="wrap_content"
android:text="隐式启动"
android:id="@+id/button2"
android:layout_below="@+id/button1"
android:layout_alignParentStart="true"
android:layout_marginTop="27dp" />
(3)Activity类:在对应的MainActivity类中完成以下代码:
public class MainActivity extends Activity {
private Button button1,button2;
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//映射activity_main.xml中的id=button1按钮
button1=(Button)findViewById(R.id.button1);
//映射activity_main.xml中的id=button2按钮
button2=(Button)findViewById(R.id.button2);
//为两个按钮增加单击事件监听
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
Intent intent1=new Intent(MainActivity.this,SubActivity1.class);
startActivity(intent1);
}
});
button2.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
Intent intent2=new Intent();
//设置Action响应方式
intent2.setAction(Intent.ACTION_VIEW);
//Data内容:scheme://host形式
intent2.setData(Uri.parse("intentdemo://cn.edu.neusoft"));
startActivity(intent2);
}
});
}
}
上述代码主要是将布局文件中的两个按钮通过 findViewById方法进行获取,然后对其增加单击事件监听,从而保证单击按钮后实现各自的Intent对象调用。显然,id为button1的按钮启动了SubActivity1界面。而button2的启动对象,需要通过Manifest文件中的过滤器进行匹配。
(4)增加过滤器:在Android Manifest文件中,对新建的两个Activity类代码进行调整。其中,SubActivity2类的过滤器定义如下:
<activity android:name=".SubActivity1" >
</activity>
<activity android:name=".SubActivity2" >
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"></category>
<data android:scheme="intentdemo" android:host="cn.edu.neusoft"></data>
</intent-filter>
</activity>
在<intent-filter>标签中,通过action、category、data三个标签对SubActivity2类进行过滤器定义:android:name="android.intent.action.VIEW"表示将通过ACTION_VIEW的方式进行动作响应,即通过Uri的内容进行解析;android:name="android.intent.category.DEFAULT"表示隐式启动可以找到该组件;<data android:scheme="intentdemo" android:host="cn.edu.neusoft">则是Uri中的内容,根据scheme://host/path的结构进行分解,则为intentdemo://cn.edu.neusoft。
(5)调整两个布局文件的内容:第一个子页面的布局文件(activity_sub1.xml)中增加TextView和Button的代码,内容如下:
<TextView
android:layout_width="wrap_content"
android:layout_depth="wrap_content"
android:text="第一个子页面"
android:id="@+id/textView"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true" />
<Button
android:layout_width="wrap_content"
android:layout_depth="wrap_content"
android:text="返回"
android:id="@+id/button"
android:layout_below="@+id/textView"
android:layout_alignParentStart="true" />
第二个子页面(activity_sub2.xml)中,增加TextView的内容即可:
<TextView
android:layout_width="wrap_content"
android:layout_depth="wrap_content"
android:text="第二个子页面"
android:id="@+id/textView2"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true" />
(6)运行项目,查看结果:以上代码完成后,启动模拟器运行,可以实现图3-10的跳转效果。
Intent在实现两个组件之间跳转的同时,经常也需要传递数据,也就是在Intent对象上附加Bundle对象的数据。在MainActivity中执行如下代码:
Bundle bundle=new Bundle();
bundle.putString("data","test");
Intent intent=new Intent(MainActivity.this,SubActivity.class);
intent.putExtras(bundle);
startActivity(intent);
在SubActivity中,获取Intent中的数据“data”,代码如下:
Bundle bundle=this.getIntent().getExtras();
String result=bundle.getString("data");
以上代码就实现了Activity之间的数据传递。
在上述的代码上,通过startActivity方法启动其他界面以后,两个Activity之间便失去了联系。但是,在一些情况下,启动的Activity(父Activity)希望能够获得被启动Activity(子Activity)的返回结果。具体的实施步骤如下:
① 父Activity通过startActivityForResult方法启动Intent对象;
② 子Activity通过setResult方法设置返回结果;
③ 父Activity通过onActivityResult方法获取子Activity返回的结果,并进行处理。
【例3-3】 修改例3-2:显示启动第一个子页面后,在第一个子页面中随机生成一个随机数,单击“返回”按钮后,将该随机数显示在第一个页面中。运行效果如图3-13 所示。
图3-13 运行效果图
实现步骤如下:
(1)修改启动方法(startActivity→startActivityForResult),将MainActivity类中的显式启动按钮监听事件修改:
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
Intent intent1=new Intent(MainActivity.this,SubActivity1.class);
//此处的第二个参数用在(3)步中的requestCode参数上
startActivityForResult(intent1,1);
}
});
(2)通过 setResult方法设置返回结果:在第一个界面对应的Activity类中增加返回按钮的事件监听:
Button btn=(Button)findViewById(R.id.button);
btn.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
//1.生成随机数
Random rand=new Random();
int r=rand.nextInt(100);
//2.准备Bundle对象传递Intent数据
Bundle bundle=new Bundle();
bundle.putInt("random",r);
Intent intent3=new Intent();
intent3.putExtras(bundle);
//3.设置返回结果intent3,并且其中包含传递的参数
setResult(RESULT_OK,intent3);
finish();//关闭当前页面
}
});
上述代码准备了一个0~99的随机数,然后将其装入Bundle对象中,在返回跳转时将该Bundle对象传递到父Activity中。setResult方法将返回到父Activity中,并传递resultCode值和Intent对象(其中封装了返回结果Bundle对象)。
(3)父Activity通过onActivityResult方法获取子Activity返回的结果,并进行处理。在MainActivity类中重载onActivityResult方法,根据返回的requestCode和resultCode确定Intent对象data的内容来自于何处。此处的requestCode值和startActivityForResult方法的requestCode参数一致;resultCode值和setResult方法中的resultCode参数内容一致;data中则可以获取对应的内容。代码如下:
@Override
protected void onActivityResult(int requestCode,int resultCode,Intent data){
if(requestCode==1)
{
if(resultCode==RESULT_OK)
{
Bundle random=data.getExtras();
Toast.makeText(MainActivity.this,random.getInt("random")+"",Toast.LENGTH_LONG).show();
}
}
}
上述代码中,使用Toast显示随机数。
(4)运行项目。
【案例延伸】 尝试在第二个页面中增加返回按钮,并准备内容(诸如开发人员的名字),在父Activity 中显示。