使用Intent在Activity之间穿梭

0 阅读8分钟

前言

一般来说,一个应用会有多个 Activity。当我们点击应用图标时,系统只会启动我们在清单文件中声明的主 Activity,那怎么能从一个 Activity 跳转到另一个 Activity,并实现它们之间的交互呢?

答案是使用 Intent

什么是 Intent?

Intent (意图)是 Android 程序中各个组件之间进行交互的一种方式,更准确来说是一种消息传递对象。它不仅可以声明当前组件想要执行某个操作的“意图”,还可以在不同组件之间传递数据。

Intent 一般可用于启动 Activity、启动 Service 以及发送广播等场景,这里我们只关注 Activity

Intent主要分为两种:显式Intent隐式Intent,我们先来看看显式 Intent 的用法。

使用显式 Intent

显式Intent直接、明确地指定了要启动的目标组件。

前置工作

为了有跳转的目标,我们再创建一个 Activity,命名为 SecondActivity。可以勾选Generate a Layout File 以创建对应的布局文件,布局文件命名为 second_layout,但不要勾选 Launcher Activity 选项,因为它不是应用程序的入口。

image.png

创建好后,我们修改自动生成的 second_layout.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/button2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 2" />
</LinearLayout>

我们依旧是在界面上放置了一个按钮,标识符为 button2

实现跳转

要实现 Activity 的跳转,首先我们需要创建一个 Intent 对象,可以使用 Intent(Context packageContext, Class<?> cls) 构造函数来创建。

第一个参数需要提供启动 Activity 所需的上下文,Activity 本身就是 Context,所以我们直接传入this;第二个参数用于指定目标 Activity的class对象,也就是要跳转到哪个 Activity。

Intent 对象创建好后,就可以调用 Activity 类提供的 startActivity() 方法(方法接收一个 Intent 类型的参数),用于启动目标 Activity。

FirstActivity 中,修改按钮的点击事件,代码如下:

binding.button1.setOnClickListener {
    // 创建一个显式 Intent 对象
    val intent = Intent(this, SecondActivity::class.java)
    startActivity(intent) // 启动目标 Activity
}

注意: Kotlin中 SecondActivity::class.java 的写法相当于Java中的 SecondActivity.class

现在运行程序,点击按钮后,就能从 FirstActivity 跳转到 SecondActivity了,如果要回到上一个界面也很简单,按下返回键就会销毁当前 Activity,从而回到前一个 Activity。

使用隐式 Intent

隐式Intent相对显式Intent则含蓄得多。它并不明确地指定要启动哪一个组件,而是指定动作(action)和类别(category)等信息,让系统找出符合要求的组件并启动。

什么是符合要求的组件? 简单来说就是能够响应这个隐式 Intent 的组件。

配置 Intent Filter

我们现在就来配置 SecondActivity,使它能够响应隐式 Intent。

首先在 AndroidManifest.xml 文件中,我们通过 <activity> 标签下的 <intent-filter>,来指定当前 Activity 能够响应的action和category。这里我们对 SecondActivity 进行配置,代码如下:

<activity
    android:name=".SecondActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="com.example.noactivity.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

在 <action> 标签中我们指定了 SecondActivity 可以响应 com.example.noactivity.ACTION_START 这个action,而 <category> 标签则更精确地指明了 SecondActivity 能够响应的 Intent 中还可能带有的category。

只有 <action> 和 <category> 中的内容同时匹配 Intent 中指定的 action 和 category 时,这个 Activity 才能响应该 Intent。

注意:这里要将 <activity> 标签的 exported 属性设为 true,否则当尝试启动 SecondActivity 时,会报错:android.content.ActivityNotFoundException: No Activity found to handle Intent { act=com.example.noactivity.ACTION_START }

这是Android 12+的安全要求,如果一个Activity包含<intent-filter>,意味着它可能被其他应用调用,这时必须显式声明android:exported="true",否则应用会崩溃。

发起隐式 Intent

我们修改 FirstActivity 中按钮的点击事件,代码:

binding.button1.setOnClickListener {
    val intent = Intent("com.example.noactivity.ACTION_START")
    startActivity(intent)
}

我们使用了 Intent 的另一个构造函数,传入 action 的字符串,就能启动响应 com.example.noactivity.ACTION_START 这个 action 的 Activity。

为什么没有指定我们前面要求的 category 呢?

因为 android.intent.category.DEFAULT 是一种默认的category,会在调用 startActivity() 方法时,自动被添加到 Intent 中,所以任何希望接收隐式Intent的Activity,都要在 <intent-filter> 中添加此类别。

现在我们重新启动程序,点击按钮,还是能够跳转到 SecondActivity,只不过这一次使用的是隐式 Intent。

一个 Intent 中只能指定一个 action,但能同时指定多个 category,我们再来添加一个,修改按钮的点击事件,代码如下:

binding.button1.setOnClickListener {
    val intent = Intent("com.example.noactivity.ACTION_START")
    intent.addCategory("com.example.noactivity.MY_CATEGORY")
    startActivity(intent)
}

我们通过调用 Intent 中的 addCategory() 方法来添加一个 category,值为 com.example.noactivity.MY_CATEGORY

因为我们新增了 category,所以为了 SecondActivity 能够响应我们的Intent,我们需要在 SecondActivity 的 <intent-filter> 标签中再添加一个category的声明,如下所示:

<activity
    android:name=".SecondActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="com.example.noactivity.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="com.example.activitytest.MY_CATEGORY" /> <!--新增这一行-->
    </intent-filter>
</activity>

再次运行程序,还是能够跳转到 SecondActivity

隐式 Intent 的更多用法

使用隐式Intent,不仅可以启动当前应用中的 Activity,还可以启动其他应用中的 Activity,可以进行多个应用程序之间的功能共享。

比如你的应用需要展示一个网页,你大概率是不会自己去实现一个浏览器,所以这时我们可以调用系统的浏览器来实现这个需求。

修改 FirstActivity 中按钮的点击回调:

binding.button1.setOnClickListener {
    val intent = Intent(Intent.ACTION_VIEW)
    intent.data = Uri.parse("https://juejin.cn/")
    startActivity(intent)
}

当前指定的 action 是 Intent.ACTION_VIEW,这是系统内置的 action,其常量值为 "android.intent.action.VIEW"。然后我们通过 Uri.parse() 方法将一个网址解析成了 Uri 对象,再调用Intent的 setData() 方法,将这个对象赋给了传递了进去,用于指定当前 Intent 正在操作的数据。

这里也是一个语法糖,看上去像是给 Intent 的 data 属性赋值一样。

重新运行程序,点击按钮,发现跳转到了系统浏览器,并打开了掘金首页:

我们还可以在 <intent-filter> 标签中配置一个 <data> 标签,用于更精确地指定当前 Activity 能够响应的数据。我们可以在 <data> 标签中做以下配置:

  • android:scheme。用于指定数据的协议部分,如 https。

  • android:host。用于指定数据的主机名部分,如 juejin.cn。

  • android:port。用于指定数据的端口部分,位于主机名之后。

  • android:path。用于指定主机名和端口之后的部分。

  • android:mimeType。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。

只有当 Intent 中携带的 Data 与 <data> 标签中指定的内容完全一致时,当前Activity才能够响应 该Intent。

为了加深理解,我们来新建一个 Activity,命名为 ThirdActivity,对应的布局文件名称为 third_layout,使它能响应网页的 Intent。

首先修改 third_layout.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/button3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 3" />
</LinearLayout>

在 AndroidManifest 清单文件中,修改 ThirdActivity 的注册信息:

<activity
    android:name=".ThirdActivity"
    android:exported="true">
    <intent-filter  tools:ignore="AppLinkUrlError">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="https" />
    </intent-filter>
</activity>

我们配置了当前 Activity 能够响应的 action 是Intent.ACTION_VIEW的常量值“android.intent.action.VIEW”,并且通过 android:scheme 属性指定了数据的协议必须是https协议,这样我们的 ThirdActivity 就能够响应一个打开网页的 Intent了。

另外能够响应 ACTION_VIEW 的 Activity 都应该加上 BROWSABLE 的 category,否则会有警告。而加上BROWSABLE的category是为了实现deep link功能,我们的目的并不是实现这个功能。所以直接在 <intent-filter> 标签中通过 tools:ignore 属性将警告忽略。

重新运行,点击按钮,预期会弹出一个应用选择器对话框,显示目前能够响应这个Intent的所有程序,其中就包括了我们当前的应用,但好像并没有,而是直接跳转到了浏览器。

最后除了https协议外,我们还可以指定其他协议,比如 geo 表示显示地理位置、tel 表示拨打电话。比如这样就可以调用系统的拨号界面:

binding.button1.setOnClickListener {
    val intent = Intent(Intent.ACTION_DIAL) // 指定action为Intent.ACTION_DIAL
    intent.data = Uri.parse("tel:10086") // 协议为tel,号码为10086
    startActivity(intent)
}

运行程序后,点击按钮:

image.png

向下一个 Activity 传递数据

其实 Intent 在启动 Activity 的时候,还可以传递数据。

只需在启动 Activity 之前,使用 Intent 提供了一系列 putExtra() 方法,将我们想要的数据存放在 Intent 中,另一个 Activity 被启动后,就能从 Intent 中将数据取出就可以了。

比如我们要将一个字符串从 FirstActivity 传递到 SecondActivity 中,你可以这样写:

binding.button1.setOnClickListener {
    val data = "how are you"
    val intent = Intent(this, SecondActivity::class.java)
    intent.putExtra("extra_data", data)
    startActivity(intent)
}

我们使用显式 Intent 来启动 SecondActivity,通过 putExtra() 方法传递了一个字符串。注意:putExtra() 方法的第一个参数是键,用于之后取值;第二个参数是值,是真正要传递的数据。

然后在 SecondActivity 中,在 onCreate() 方法中将传递的数据取出,代码如下:

class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.second_layout)

        val extraData = intent.getStringExtra("extra_data")
        Log.d("SecondActivity", "extra data is $extraData")
    }
}

intent 实际上调用的是父类的 getIntent() 方法,该方法会获取在 FirstActivity 用于启动 SecondActivityIntent 对象,然后调用该对象的 getStringExtra() 方法并传入相应的键值,就可以得到传递的数据了。

如果传递的是整型数据,则使用 getIntExtra() 方法;如果传递的是布尔型数据,则使用 getBooleanExtra()方法,以此类推。

重新运行程序,点击按钮会跳转到 SecondActivity,然后查看 Logcat:

D/SecondActivity          extra data is how are you

可以看到,在 SecondActivity 中成功获取到了从 FirstActivity 中传递的数据。

返回数据给上一个 Activity

有时我们需要返回数据给上一个 Activity,比如用户在第二个页面选择了一个选项,我要在第一个页面知道用户选择了什么。

只需调用 ActivityResultCaller 接口中的 registerForActivityResult() 函数就行了,它能注册对一个 Activity 返回结果的回调,这样我们就能在回调中获取 Activity 的结果。

registerForActivityResult() 函数接收两个参数,第一个参数的类型是 ActivityResultContract,表示当前的作用,因为我们是要获取下一个 Activity 的返回数据,所以使用内置的 ActivityResultContracts.StartActivityForResult(),它表示我们要启动一个Activity并期待返回一个ActivityResult对象;第二个参数是 Lambda 表达式结果回调,当启动的 Activity 关闭后,会执行回调中的代码,参数是返回结果,类型是 ActivityResult,其中有两个属性,一个是Int 类型的结果码(resultCode),另一个是 Intent 类型的数据(data)。

方法的返回值是一个ActivityResultLauncher对象,我们可以使用该对象的launch()方法可以用于去启用Intent,把Intent传入即可。

我们现在来实现,首先在 FirstActivity 中使用 registerForActivityResult() 函数对 Activity 结果进行监听,并处理返回的结果:

private val requestDataLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> // 返回结果
    if (result.resultCode == RESULT_OK) { // 成功返回
        val data = result.data?.getStringExtra("data") // 根据键获取返回的数据
        Log.d("FirstActivity","returned data is $data")
    }
}

然后修改启动 Activity 的方式,改为使用 ActivityResultLauncher 对象的 launch() 方法启动:

binding.button1.setOnClickListener {
    val intent = Intent(this, SecondActivity::class.java)
    requestDataLauncher.launch(intent)
}

最后在 SecondActivity 界面中的按钮添加点击事件,在点击按钮后返回数据并关闭当前 Activity:

binding.button2.setOnClickListener {
    val intent = Intent()
    intent.putExtra("data", "Hello FirstActivity") // 存放数据
    setResult(RESULT_OK, intent) // 设置结果码和数据Intent
    finish()
}

重新运行程序,在FirstActivity界面点击按钮1打开SecondActivity,然后在SecondActivity界面点击按钮2会回到FirstActivity,运行结果:

D/FirstActivity           returned data is Hello FirstActivity

有时,用户是按下返回键回到 FirstActivity 的,这时系统会自动将结果码设置为 Activity.RESULT_CANCELED 并关闭Activity,这通常代表着取消,但我们还是要返回数据。所以我们需要在 SecondActivityonCreate() 方法中注册一个按下返回键的回调,它会拦截默认的返回键行为:

onBackPressedDispatcher.addCallback(this ) {
    val intent = Intent()
    intent.putExtra("data", "Hello FirstActivity")
    setResult(RESULT_OK, intent)
    finish()
}

这样,用户按下返回键后,也能将数据进行返回。