05.显式意图、隐式意图
一个应用程序不可能只有一个活动,创建活动的方法我们之前已经学会了,那么如何从一个活动跳转到其他活动呢?
这就需要用到意图(Intent)。
Intent(意图)是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent虽然不是四大组件,但却是连接四大组件的桥梁。
Intent一般可被用于启动活动、启动服务、以及发送广播。
由于服务、广播等概念我们暂时还未涉及,那么我们就先学习如何使用Intent启动活动。
Intent的用法大致可以分为两种,显式Intent和隐式Intent。
1、显式Intent
在ActivityTest项目中再创建一个活动。右击com.sdbi.activitytest包 > New > Activity > Empty Actity,会弹出一个创建活动的对话框,我们将活动命名为SecondActivity,并勾选Generate Layout File,给布局文件命名为activity_second,但不要勾选Launcher Activity选项。
点击Finish完成创建,Android Studio会为我们自动创建SecondActivity.java和activity_second.xml两个文件。
我们将activity_second.xml中的布局样式修改为LinearLayout,代码替换为:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/btn2" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="按钮2" /> </LinearLayout>
我们还是定义了一个按钮,按钮上显示“按钮2”。然后查看SecondActivity中的代码:
public class SecondActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); // 重要,设置布局 } }
查看AndroidManifest.xml中是否帮我们自动完成了SecondActivity的注册。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.sdbi.activitytest"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ActivityTest" tools:targetApi="31"> <activity android:name=".SecondActivity" android:exported="false" android:label="这是第二个活动" /> <activity android:name=".FirstActivity" android:exported="true" android:label="这是第一个活动"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
由于SecondActivity不是主活动,因此不需要配置<intent-filter>标签里的内容,注册活动的代码也是简单了许多。
现在第二个活动已经创建完成,剩下的问题就是如何去启动这第二个活动了,这里我们需要用到Intent(意图)。
我们先来看一下显式Intent如何使用。
Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?> cls)。这个构造函数接收两个参数:
第一个参数Context要求提供一个启动活动的上下文(它描述的是一个应用程序环境的信息,我们可以将其理解为“场景”);
第二个参数Class则是指定想要启动的目标活动,就是我们要启动的第二个活动。
通过这个构造函数就可以构建出Intent的“意图”。
怎么使用这个Intent呢?Activity类中提供了一个startActivity()方法,这个方法是专门用于启动活动的,它接收一个Intent参数,这里我们将刚刚构建好的Intent传入startActivity()方法就可以了。
修改FirstActivity中按钮1的点击事件,代码如下所示:
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(FirstActivity.this, SecondActivity.class); // Intent intent = new Intent(); // intent.setClass(FirstActivity.this, SecondActivity.class); startActivity(intent); } });
先构造一个Intent对象(两个参数的构造方法),传入FirstActivity.this作为上下文,传入SecondActivity.class作为目标活动,这样我们的“意图”就非常明显了,即在FirstActivity这个活动的基础上打开SecondActivity这个活动。然后通过startActivity()方法来执行这个Intent。
另外,我们也可以先构造一个空的Intent对象(空参数的构造方法),然后调用它的setClass()方法,将FirstActivity.this作为上下文,SecondActivity.class作为目标活动传入,实际开发过程中,根据自己的需要选择Intent的设置方法。
重新运行程序,在FirstActivity的界面点击一下按钮1,我们就可以启动SecondActivity这个活动了。
如果你想要回到上一个活动怎么办呢?很简单,按下Back键就可以销毁当前活动,从而回到上一个活动了。
2、隐式Intent
相比于显式Intent,隐式Intent则不那么明显,它没有明确指出我们想要启动哪一个活动,而是指定了一系列更为抽象的action(动作)和category(类别)等信息,然后交由系统去分析这个Intent,并帮我们找出合适的活动去启动。
如何才能找到合适的活动呢?
通过在<activity>标签下配置<intent-filter>的内容,可以指定当前活动能够响应的action和category,打开AndroidManifest.xml,添加如下代码:
<activity android:name=".SecondActivity" android:exported="false" android:label="这是第二个活动"> <intent-filter> <action android:name="com.sdbi.activitytest.ACTION_START" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
在<action>标签中我们指明了当前活动可以响应com.sdbi.activitytest.ACTION_START这个action,而<category>标签则包含了一些附加信息,更精确地指明了当前的活动能够响应的Intent中还可能带有的category,如果自己定义的某个Activity要通过隐式启动,必须加上android.intent.category.DEFAULT,否则不起作用。
只有<action>和<category>中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能响应该Intent。
修改FirstActivity中按钮的点击事件,代码如下所示:
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent("com.sdbi.activitytest.ACTION_START"); startActivity(intent); } });
可以看到,我们使用了Intent的另一个构造函数(一个字符串参数的),直接将action的字符串传了进去,表明我们想要启动能够响应com.sdbi.activitytest.ACTION_START这个action的活动。
那前面不是说要<action>和<category>同时匹配上才能响应的吗?怎么没看到哪里有指定category呢?
这是因为android.intent.category.DEFAULT是一种默认的category,在调用startActivity()方法的时候会自动将这个category添加到Intent中。
重新运行程序,在FirstActivity的界面点击一下按钮,你同样成功启动SecondActivity了。
不同的是,这次你是使用了隐式Intent的方式来启动的,说明我们在<activity>标签下配置的action和category的内容已经生效了!
每个Intent中只能指定一个action,但却能指定多个category。
目前我们的Intent中只有一个默认的category,那么现在再来增加一个吧。
修改FirstActivity中按钮的点击事件,代码如下所示:
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent("com.sdbi.activitytest.ACTION_START"); intent.addCategory("com.sdbi.activitytest.MY_CATEGORY"); startActivity(intent); } });
可以调用Intent中的addCategory()方法来添加一个category,这里我们指定了一个自定义的category,值为com.sdbi.activitytest.MY_CATEGORY。
现在重新运行程序,在FirstActivity的界面点击一下按钮,你会发现,程序崩溃了。
我们来分析一下在LogCat中输出的错误日志,你会看到如图所示的错误信息。
错误信息中提醒我们,没有任何一个活动可以响应我们的Intent,为什么呢?
这是因为我们刚刚在Intent中新增了一个category,而SecondActivity的<intent-filter>标签中并没有声明可以响应这个category,所以就出现了没有任何活动可以响应该Intent的情况。
现在我们在<intent-filter>中再添加一个category的声明,如下所示:
<activity android:name=".SecondActivity" android:exported="false" android:label="这是第二个活动"> <intent-filter> <action android:name="com.sdbi.activitytest.ACTION_START" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="com.sdbi.activitytest.MY_CATEGORY" /> </intent-filter> </activity>
再次重新运行程序,你就会发现一切都正常了。
注意:之前主活动的<category android:name="android.intent.category.LAUNCHER" />的意义:
LAUNCHER:桌面启动器,Android的桌面应用程序。如果一个App没有MAIN和LAUNCHER,则该APP不能被运行。
3、隐式Intent的其他用法
上面我们掌握了通过隐式Intent来启动活动的方法,但实际上隐式Intent还有更多的内容需要我们去了解,下面我们就来展开介绍一下。
使用隐式Intent,我们不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能。
1.打开网页
如果我们的应用程序里需要打开一个网页,我们没有必要自己去实现一个浏览器,而是只需要调用系统的浏览器来打开这个网页就行了。
修改FirstActivity中按钮点击事件的代码,如下所示:
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("http://www.sdbi.edu.cn")); startActivity(intent); } });
这里我们首先指定了Intent的action是Intent.ACTION_VIEW,这是一个Android系统内置的动作,其常量值为android.intent.action.VIEW。
然后通过静态方法Uri.parse(),将一个网址字符串(一定要写明协议名称http://)解析成一个Uri对象(通用资源标志符,Universal Resource Identifier, 简称“URI”),再调用Intent的setData()方法将这个Uri对象传递进去。
setData()方法接收一个Uri对象,用于指定当前Intent正在操作的数据,而这些数据通常都是以字符串的形式传入到Uri.parse()方法中解析产生的。
与此对应,我们还可以在<intent-filter>标签中再配置一个<data>标签,用于更精确地指定当前活动能够响应什么类型的数据。<data>标签中主要可以配置以下内容。
- android:scheme 用于指定数据的协议部分,如上例中的http部分。
- android:host 用于指定数据的主机名部分,如上例中的www.sdbi.edu.cn部分。
- android:port 用于指定数据的端口部分,一般紧随在主机名之后。
- android:path 用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
- android:mimeType 用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
只有<data>标签中指定的内容和Intent中携带的Data完全一致时,当前活动才能够响应该Intent。
不过一般在<data>标签中都不会指定过多的内容,如上面浏览器示例中,其实只需要指定android:scheme为http,就可以响应所有的http协议的Intent了。
为了让你能够更加直观地理解,我们来自己建立一个活动,让它也能响应打开网页的Intent。
右击com.sdbi.activitytest包 > New > Activity > Empty Actity,新建活动ThirdActivity,并勾选Generate Layout File,给布局文件命名为activity_third,但不要勾选Launcher Activity选项。点击Finish完成创建,然后编辑activity_third.xml布局文件,代码如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/btn3" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="按钮3" /> </LinearLayout>
ThirdActivity代码保持不变。最后在AndroidManifest.xml中为ThirdActivity进行注册。
<activity android:name=".ThirdActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="http" /> </intent-filter> </activity>
我们在ThirdActivity的<activity>中修改android:exported="true",
<intent-filter>中配置了当前活动能够响应的action是Intent.ACTION_VIEW的常量值,
而category除了默认的android.intent.category.DEFAULT值(Java常量Intent.CATEGORY_DEFAULT),
还必须指定android.intent.category.BROWSABLE(Java常量Intent.CATEGORY_BROWSABLE),
指定了此category后,系统会考虑将此目标Activity列入可选列表,供用户选择以打开链接。
另外在<data>标签中我们通过android:scheme指定了数据的协议必须是http协议,这样ThirdActivity应该就和浏览器一样,能够响应一个打开网页的Intent了。
让我们运行一下程序试试吧,在FirstActivity的界面点击一下按钮1,结果如图所示。
可以看到,系统自动弹出了一个列表,显示了目前能够响应这个Intent的所有程序。
点击“Chrome”还会像之前一样打开浏览器,并显示主页,而如果点击了ActivityTest,则会启动ThirdActivity,如图所示。
需要注意的是,虽然我们声明了ThirdActivity是可以响应打开网页的Intent的,但实际上这个活动并没有加载并显示网页的功能,所以在真正的项目中尽量不要去做这种有可能误导用户的行为,不然会让用户对我们的应用产生负面的印象。
2.拨打电话
除了http协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。
下面的代码展示了如何在我们的程序中调用系统拨号界面。
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(Intent.ACTION_DIAL); intent.setData(Uri.parse("tel:10086")); startActivity(intent); } });
首先指定了Intent的action是Intent.ACTION_DIAL(其常量值为"android.intent.action.DIAL"),这又是一个Android系统的内置动作。
然后在data部分指定了协议是tel,号码是10086。
如果要直接拨打电话,而不是跳转到系统的拨号软件界面。
那我们应该将Intent的action设置为是Intent.ACTION_CALL(其常量值为"android.intent.action.CALL"),data部分不变。
但是由于是直接拨打电话,所以要在清单文件AndroidManifest.xml中增加系统权限的获取,否则会报错。
要解决这个问题,我们需要:
① 在清单文件AndroidManifest.xml中手动增加系统权限的获取。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.sdbi.activitytest"> <uses-permission android:name="android.permission.CALL_PHONE" /> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ActivityTest" tools:targetApi="31"> ...... </application> </manifest>
② 将APP的打电话权限设置为允许。
3.启动摄像头
如果我们的应用程序需要使用摄像头拍照,我们也可以通过隐式意图启动摄像头。
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); startActivity(intent); } });
这里我们指定Intent的action是MediaStore.ACTION_IMAGE_CAPTURE,这也是一个Android系统内置的动作:启动摄像头,其常量值为"android.media.action.IMAGE_CAPTURE"。
启动相机功能:MediaStore.ACTION_IMAGE_CAPTURE(android.media.action.IMAGE_CAPTURE)
启动摄像功能:MediaStore.ACTION_VIDEO_CAPTURE(android.media.action.VIDEO_CAPTURE)
【另外】
Android7.0之前会出现错误,是因为Android6.0(API 23)添加了运行时权限,即在运行时请求应用是否启用该权限,如果用户拒绝,则要做异常处理。我们可以通过以下两个方法解决:
方法一:在启动摄像头前,判断是否有该权限。
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (ContextCompat.checkSelfPermission(FirstActivity.this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(FirstActivity.this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); startActivity(intent); } } });
方法二:通过try…catch捕获SecurityException
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); startActivity(intent); } catch (SecurityException e) { e.printStackTrace(); } } });
我们还要在清单文件中增加摄像头权限的获取:
<uses-permission android:name="android.permission.CAMERA" />
关于隐式Intent的用法,我们就先介绍这么些,还有很多系统的动作我们可以调用,随着以后的使用,大家可以慢慢总结学习。