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的用法,我们就先介绍这么些,还有很多系统的动作我们可以调用,随着以后的使用,大家可以慢慢总结学习。

posted @ 2022-08-29 17:17  熊猫Panda先生  阅读(510)  评论(0编辑  收藏  举报