Android Intents Kotlin 教程

原文:Android Intents Tutorial with Kotlin
作者:Steve Smith
译者:kmyhy

更新说明:本教程由 Steven Smith 更新为 Kotlin、Android 26(Oreo) 和 Android Studio 3.0,原文作者是 Darryl Bayliss。上一版更新作者是 Artem Kholodnyi。

人们不会漫无目的地乱逛,绝大部分人在做事情时,比如看电视、购物和写代码都会带有自己的目的和意图。

Android 也是一样。在 app 能够执行一个动作之前,它需要知道这个动作的目的或意图,才能正确执行这个动作。

最终发现,人和机器人竟然有那么多的不同 :]

在本文,你将通过 Intent 创建你自己的米姆生成器。同时,你将学习到:

  • 什么是 Intent 以及它在 Android 中的广泛使用。
  • 如何用 Intent 创建内容以及从其它 app 获得内容并用在你的 app 中。
  • 如何接受并响应从其它 app 发过来的 Intent。

如果你是一个 Android 菜鸟,建议你阅读 Beginning Android Development Kotlin for Android ,以了解基本的工具和概念。同时,你需要使用 Android Studio 3.0 以上。

准备好你最佳的米姆表情。本文会将你的 Android 开发级别提升到 9000 以上!!!:]

开始

这里下载本文的开始项目。

在项目中,你将看到 XML 布局文件和相关的 Activity 中已经填充了一些 app 的模板代码,以及一个助手类用于拉升位图,一些项目中用到的 Drawables 和 Strings 资源。

如果你已经打开了 Android Studio,请点击 File\Import Project 并选择已经下载好的项目文件夹。如果未打开 Android Studio,请打开 Android Studio 并在欢迎界面选择 Open an existing Android Studio project,然后选择下载好的开始项目根文件夹。确保接受任何升级到最新 Gradle 插件的提示,或者下载正确的 build 工具。

先来熟悉一下项目。TakePictureActivity 中包含了一个 image view,你可以点击它来打开设备相机。当你点击 LETS MEMEIFY! 时,你会将 image view 中位图文件的地址传递给 EnterTextActivity,那里是真正有趣的地方,因为你输入的文字会将图片变成一张病毒式的米姆。

创建第一个 Intent

Build & run。你会看到:

它还没有什么功能;如果你点击 image view,什么也不会发生。

你将添加代码,让它变得更有趣。

打开 TakePictureActivity.kt 在类的底部 companion 对象下方添加:

const private val TAKE_PHOTO_REQUEST_CODE = 1

这将用于在 intent 返回时识别 intent 是哪一个——在后面你会具体学习。

注意:本文假设你知道如何处理 import 警告,因此不会专门讲述需要添加的 import 语句。这里简单提示一下,如果你没有设置 on-the-fly imports 选项,你可以将光标置于出现 import 警告的类上,用 option+回车(Mac)或者 alt+回车(PC)来添加 import。

在 onClick() 之后添加下列代码,请自行导入必要的 imports :

  private fun takePictureWithCamera() {
    // 1
    val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)

    // 2
    val imagePath = File(filesDir, "images")
    val newFile = File(imagePath, "default_image.jpg")
    if (newFile.exists()) {
      newFile.delete()
    } else {
      newFile.parentFile.mkdirs()
    }
    selectedPhotoPath = getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", newFile)

    // 3
    captureIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, selectedPhotoPath)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    } else {
      val clip = ClipData.newUri(contentResolver, "A photo", selectedPhotoPath)
      captureIntent.clipData = clip
      captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    }

  }

代码有点长,我们将分成几部分来说明。

首先,声明一个 Intent 对象。听起来不错,但什么是 Intent?

一个 Intent 是一个工作或功能的抽象概念,它可以被你的 app 在未来某个时候执行。总之,它就是 app 必须要做的事情。最简单的 Intent 由下面几部分构成:

  • Actions:表示 Intent 需要完成的动作,比如拨打电话号码,打开一个 URL 或者编辑某些数据。一个 Action 是一个简单字符串常量,描述了将要完成的事情。
  • Data:Intent 所要操作的资源。在 Android 中它用 Uri 来进行表示——Uri 是特定资源的唯一标识。数据类型(如果有的话)根据 Action 而变。你肯定不会希望一个拨号动作从一张图片中获取电话号码吧!:]

这种将动作和数据关联在一起方式允许 Android 知道这个 Intent 想做什么以及用什么去做。就是这么简单!

回到 takePictureWithCamera(),你会看到这个 Intent 使用了动作 ACTION_IMAGE_CAPTURE 来创建。你会猜到这个 Intent 会为你拍一张照片,这正是一个模因生成器所需要的!

第二段代码用于创建一个保存图片的临时文件。开始项目已经为你做了这些,但如果你想了解具体的话请看一眼这个 activity 中的代码。

注意:你可能看到 selectedPhotoPath 变量被添加了一个 .fileprovider 字符串。File Provider 是一种特殊的向 app 提供文件的安全方法。如果你查看 Android Manifest 文件,你会看到 Memeify 使用了 File Provider。具体请阅读这里

了解 Extras

第三段代码是添加一个 Extras 到你新创建的 Intent 中。

你会问:什么是 extra?

Extras 是一种提供给 Intent 完成 Action 所需的额外信息的键值对。打个比方,人类在做足准备工作之后,总是能更好地完成某个动作,这对于 Android Intent 也是一样的。一个好的 Intent 总是要附带上它要用的 Extras。

一个 Intent 总是知道 Extras 的类型并根据 Action 而变;这就和你提供给 Action 的 Data 的类型是类似的。

用一个 ACTION_WEB_SEARCH 动作来创建一个 Intent 是另外一个例子。这种动作接收一个叫做 QUERY 的 Extra 键值对,也就是你想查找的查询字符串。key 通常使用字符串常量,因为它的名字不允许改变。启动执行带有上述 Action 和 Extra 的 Intent,会显示 Google 的搜索页,同时列出搜索结果。

回到 captureIntent.putExtra() 这句上,EXTRA_OUTPUT 表示应当将相机拍下的照片放在哪里——这里,也就是前面创建的空文件的 Uri 位置。

启动 Intent

现在一个有效的 Intent 已经就绪,一个典型的 Intent 的完整心理模型应该是这样的:

没什么好说的了,开始启动这个 Intent 吧,也就是takePictureWithCamera() 的最后一句。在方法的最后添加一句:

startActivityForResult(captureIntent, TAKE_PHOTO_REQUEST_CODE)

这句代码请求 Android 启动一个 activity 来执行 captureIntent 中定义的动作:拍张照片并存入文件。当这个 activity 完成这个 action,你需要从返回结果中获得这张图片。TAKE_PHOTO_REQUEST_CODE 是你早先定义好的常量,将用于在 Intent 返回时识别是哪一个 Intent。

然后,在 onClick() 函数中,将针对 R.id.picture_imageview 条件的 when 语句中的空闭包替换成对 takePictureWithCamera() 的调用,变成:

R.id.pictureImageview -> takePictureWithCamera()

当你点击 image view,就会调用 takePictureWithCamera()。

来检查一下我们的劳动成果!Build & run。点击 image view 打开相机:

这时你可以拍一张照;但你还不能用来做任何事情!我们将在下一节进行处理。

注意:如果你在模拟器上运行 app,你可能需要编辑 AVD 的相机设置。这需要点击 Tools\Android\AVD Manager,然后再点击对应虚拟设备右边的绿色铅笔图标。 然后点击左下角 Show Advanced Settings。在相机一栏, 将所有打开的相机下拉框中都设置为 Emulated 或者 Webcam0。

隐式 Intent

如果你在安装了多个相机类 app 的真机上运行 app,你可能发现情况有点不同:

会提示你选择用哪个 app 来处理这个 Intent。

在创建 Intent 的时候,你可以选择显式或隐式 Intent 来执行这个 Action。ACTION_IMAGE_CAPTURE 是一种典型的隐式 Intent。

隐式 Intent 让 Android 开发者将选择的权利交给用户。如果他们喜欢用某个 app 来执行这个任务,那么为什么要用你自己的 app 来实现这个功能呢?至少,这明显减少了重新制造轮子。

一个隐式 Intent 告诉 Android,它需要一个 app 来处理这个 Action。Android 系统向设备上已安装的 app 进行查询,看有哪个 app 能够处理这种 Action,然后再处理这个 Intent。如果不止一个 app 能够处理这种 Intent,就提示用户选择其中的某个:

如果只有一个 app 能够响应,Intent 就自动跳到这个 app 执行动作。如果没有 app 能够执行该动作,Android 会返回一个空,给你一个 null 值,最终导致 app 崩溃:]

你可以在启动 Intent 之前检查这个结果,确保起码有一个 app 能够响应这个动作,或者也可以说明这个 app 只能安装在拥有相机的设备上,也就是在 AndroidManifest.xml 中添加这句来说明硬件需求:

<uses-feature android:name="android.hardware.camera" />

开始项目使用的是设备限制方法。

总之,你有一个用于拍照的隐式 Intent,但你仍然不能在你的 app 中访问相册。你的模因生成器没有照片的话派不了什么大用场。

在 takePictureWithCamera() 之后添加方法:

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == TAKE_PHOTO_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
      //setImageViewWithImage()
    }
  }

这个方法会在 takePictureWithCamera() 方法的 startActivityForResult() 打开一个 activity 并返回 app 时调用。

if 语句中的 requestCode 必须匹配 TAKE_PHOTO_REQUEST_CODE 常量,以确保这个 Intentn 就是我们指定的 Intent。你还需要检查 resultCode 是否等于 RESULT_OK;这是一个简单的 Android 常量,用于表示 Intent 执行成功。

如果一切正常,你可以认为图片已经可以使用了,然后再调用 setImageViewWithImage()。

让我们来实现这个方法!

首先,在 TakePictureActivity 顶端,添加一个布尔变量:

private var pictureTaken: Boolean = false

这个变量用于保存是否进行了拍照,当你进行多次拍照时这是非常必要的。很快你就要用到这个变量。

然后,在 onActivityResult() 后添加方法:

  private fun setImageViewWithImage() {
    val photoPath: Uri = selectedPhotoPath ?: return
    pictureImageview.post {
      val pictureBitmap = BitmapResizer.shrinkBitmap(
          this@TakePictureActivity,
          photoPath,
          pictureImageview.width,
          pictureImageview.height
      )
      pictureImageview.setImageBitmap(pictureBitmap)
    }
    lookingGoodTextView.visibility = View.VISIBLE
    pictureTaken = true
  }

BitmapResizer 是开始项目中的一个助手类,用于将相机返回的照片缩小到正常的适合屏幕大小的尺寸。尽管设备会自动缩放图片,但我们用它来缩小图片会更能够节省内存。

准备好 setImageWithImage() 方法之后,将 onActivityResult() zho 调用它的那句代码取消注释:

// setImageViewWithImage()

Build & run。选择你喜欢的相机 app ——当提示你的时候——然后拍一张照。

这回,照片会被缩放到适当大小并显示在 image view 中:

你还会看到下面有一个 TextView,上面对你的拍摄技巧大拍马屁。礼多人不怪嘛 :]

显式 Intent

接下来到第二阶段了,但首先你必须将照片传递到下一个 activity,因为你的屏幕资源相当紧张。

在 Constants.kt 中,在注释之后添加下列常量:

const val IMAGE_URI_KEY = "IMAGE_URI"
const val BITMAP_WIDTH = "BITMAP_WIDTH"
const val BITMAP_HEIGHT = "BITMAP_HEIGHT"

它们将作为 Extra 的 key,你将这个 Extra 传递到下一个屏幕。

现在,在 TakePictureActivity 底部添加下列方法,注意添加必要的 import 语句:

  private fun moveToNextScreen() {
    if (pictureTaken) {
      val nextScreenIntent = Intent(this, EnterTextActivity::class.java).apply {
        putExtra(IMAGE_URI_KEY, selectedPhotoPath)
        putExtra(BITMAP_WIDTH, pictureImageview.width)
        putExtra(BITMAP_HEIGHT, pictureImageview.height)
      }

      startActivity(nextScreenIntent)
    } else {
      Toaster.show(this, R.string.select_a_picture)
    }
  }

这里你判断 pictureTaken 是否为 true,这个变量表明你有没有从相机获得了一张位图。如果没有,你的 activty 会显示一个 Toast 告诉用户拍一张照片——为了简单起见,这里用 Toast 类的 show 方法来显示一个 toast。如果 pictureTaken 为 true,你会创建一个 Intent 传递给下一个 activity,然后设置 Extras,就用你刚刚定义的那些 key。

然后,在 onClick() 函数中,将针对 R.id.enter_text_button 分支条件的 when 语句中的空闭包替换成对 moveToNextScreen() 方法调用,变成:

R.id.enterTextButton -> moveToNextScreen()

Build & run。点击 LETS MEMEIFY! ,先不拍照,你会看到 toast 出来了:

如果进行了拍照,moveToNextScreen() 方法会创建一个 Intent 给下一个 activity。Intent 中也包含了一些 Extras,比如图片的 Uri,图片在屏幕上的宽高。这对于第二个 activity 来说是有用的。

你刚才第一次创建了一个显式 Intent。和隐式 Intent 相比,隐式 Intent 相对要保守一些,因为它会描述即将创建的特定组件并用于启动 Intent。这是一个新的 activity,同属于你的 app,或者在你的 app 中的某个服务,比如用于在后台下载某个文件。

这个 Intent 构造时要提供一个 Context,也就是 Intent 被创建的地方(在这个例子中,也就是 this),以及一个要执行这个 Intent 的类(EnterTextActivity::class.java)。因为你显式地描述了一个如何从 A->B 的 Intent,Android 只需要照办就行了。用户不需要控制这个 Intent 如何被完成:

Build & run。再次拍一张照,但这次点击 LETS MEMEIFY!。你的显式 Intent 会将 Action 传递并跳转至下个 activity:

这个开始项目已经创建好了这个 activity,也在 AndroidManifest.xml 中进行过声明,因此你不必再创建它了。

处理 Intent

看起来这个 Intent 能够顺畅运行。但你发送过去的 Extras 呢?它们在内存中迷路了吗?让我们来找到它们并使其发挥作用。

在 EnterTextActivity 的 onCreate() 的中添加代码:

pictureUri = intent.getParcelableExtra<Uri>(IMAGE_URI_KEY)
val bitmapWidth = intent.getIntExtra(BITMAP_WIDTH, 100)
val bitmapHeight = intent.getIntExtra(BITMAP_HEIGHT, 100)

pictureUri?.let {
  val selectedImageBitmap = BitmapResizer.shrinkBitmap(this, it, bitmapWidth, bitmapHeight)
  selectedPictureImageview.setImageBitmap(selectedImageBitmap)
}

当 activity 一被创建,我们将上一个 activity 中传递来的 Uri 保存到 pictureUri 中。通过 intent 对象,我们可以访问到它的 Extra 值。

因为变量和对象具有各种类型,你需要采用不同的方法来访问它们。例如,要访问 Uri 对象要使用 getParcelableExtra()。其它 Extra 方法则用于访问其它变量,比如字符串或原始数据类型。

getIntExtra() 和返回其它原始类型的方法一样,允许你指定一个默认值。默认值用于值不存在或者这个 key 在 Extra 中不存在的情况。

一旦你拿到所需的 Extras,创建一个宽等于 BITMAP_WIDTH、高等于 BITMAP_HEIGHT 的 Bitmap。最后,将 image view 的 image 设置成这张位图,从而显示出这张照片。

除了要显示 image view,屏幕上还包含两个 EditText,用于给用户输入他们的模因文字。开始项目已经实现了从相关视图中获得文字并合成到照片中的工作。

你还需要修改 onClick() 方法。修改条件为 R.id.write_text_to_image_button 的分支语句为:

R.id.writeTextToImageButton -> createMeme()

请来点音乐。Build & run,进行正常的操作,拍一张照片,在第二个屏幕中输入你的搞笑文字,然后点击 LETS MEMEIFY!:

你编写了一个自己的模因生成器!别高兴得太早——你还要对这个 app 进行一些抛光和打磨!

广播 Intent

保存你的新模因图片是很有必要的,因为你可以向全世界分享!它自己是不可能进行病毒式传播的 :]

幸好我们的开始项目已经为你做了这个工作——你只需要将几样东西组装在一起就可以了。

在 saveImageToGallery() 方法的第二个 Toaster.show() 语句的 try 语句块下面添加:

val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
mediaScanIntent.data = Uri.fromFile(imageFile)
sendBroadcast(mediaScanIntent)

这个 Intent 用到了 ACTION_MEDIA_SCANNER_SCAN_FILE 动作,请求 Android 将图片的 Uri 添加到媒体数据库。这样,所有能够访问媒体数据库的 app 都可以通过 Uri 来调取这张图片。

ACTION_MEDIA_SCANNER_SCAN_FILE 动作需要 Intent 提供一些额外的数据,这些数据必须是 Uri 的形式,这个数据来自于你所保存的位图的 File 对象。

最后,你通过 Android 广播该 Intent,以便感兴趣的人——这里指的是媒体扫描程序——能够采取相应的动作。因为媒体扫描程序不具备用户界面,你无法打开一个 activity,因此你简单地广播这个 Intent 即可。

现在,修改 onClick() 方法中 R.id.save_image_button 这里的条件分支为:

R.id.saveImageButton -> askForPermissions()

当用户点击 SAVE IMAGE 按钮,上述代码会检查 WRITE_EXTERNAL_STORAGE 权限。在 Android Marshmallow 以上的系统中,如果这个权限未被授权,这个方法会提示用户进行授权。否则,如果你有权对外部存储进行写入的话,这个方法会直接将跳到 saveImageToGallery() 执行。

在 saveImageToGallery() 中的代码会进行一些错误处理,如果一切顺利,会打开这个 Intent。

Build & run。拍一张照,添加几句模因文字,点击 LETS MEMEIFY!,当图片生成后,点击 SAVE IMAGE。

关闭 app,打开照片程序。如果你使用的是模拟器,请打开 Gallery app。你会看到这张已经被模因好的图片:

现在,你的模因图片能够在你的 app 之外的地方使用了,你可以将它张贴到社交网络或者以其它任何方式进行分享。你的模因生成器大功告成!

Intent 过滤

现在你已经了解如何在正确的任务中使用合适的 Intent。但是,对于这个老实巴交的 Intent 来说,还有另一个问题:当一个隐式 Intent 出现时,你的 app 如何知道要响应哪个 Intent?

在 app/manifests 下,打开 AndroidManifest.xml ,找到第一个 activity:

<activity
    android:name=".TakePictureActivity"
    android:label="@string/app_name"
    android:screenOrientation="portrait">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

重要的是 intent-filter 元素。一个 Intent Filter 将使你的 app 的某些部分能够响应隐式 Intent。

当 Android 试图满足一个另外的 app 所发送的隐式 Intent 时,这些元素就像是一个小旗子。一个 app 可以有多个 Intent Filter,它疯狂地挥舞着,希望它的 intent filter 能够符合 Android 想要的:

有点像 Intent 和 app 正在在线约会 :]

要确保这个 app 正是这个 Intent 所需要的,这个 Intent Filter 需要提供 3 个信息:

  1. Intent Action: 这个 app 能够完成的动作,就像相机 app 能够为你的 app 完成 ACTION_IMAGE_CAPTURE 动作一样。
  2. Intent Data: 能够接收的 Intent 能够的数据的类型。取值范围包括某个文件路径、端口、MIME 类型比如图片和视频等。你可以提供 1 个或多个属性,去严格控制或宽泛指定你的 app 能够处理的 Intent 中的数据。
  3. Intent Category: 能够接收的 Intent 的 category。这是一种用于指定隐式 Intent 的哪种 Action 能够被响应的另类方式。

将模因功能以隐式 Intent 的方式来处理来自其它 app 的图片,是一种不错的做法——其实这非常简单。

在 AndroidManifest.xml 的第一个 Intent Filter 之后添加:

<intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="@string/image_mime_type" />
</intent-filter>

新的 Intent Filter 表示你的 app 可以从一个隐式 Intent 中查找 SEND 动作。你使用了默认的 category 类型,因为你不会对 category 采取任何特殊的用法,同时你只接收类型为 image 的 MIME 数据类型。

现在,打开 TakePictureActivity.kt 在类的最后添加:

  private fun checkReceivedIntent() {
    val imageReceivedIntent = intent
    val intentAction = imageReceivedIntent.action
    val intentType = imageReceivedIntent.type

    if (Intent.ACTION_SEND == intentAction && intentType != null) {
      if (intentType.startsWith(MIME_TYPE_IMAGE)) {
        selectedPhotoPath = imageReceivedIntent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
        setImageViewWithImage()
      }
    }
  }

这里你获取了打开这个 activity 的 Intent,然后检查它的 Action 和类型。然后将它们和你在 Intent Filter 中声明的内容进行比较,这应该一个 MIME image 类型的数据源。

如果比较成功,你就获取图片的 Uri,通过项目中现成的助手方法查询位图的 Uri,最后让 ImageView 显示这张图片。

然后在 onCreate() 方法最后一行添加:

checkReceivedIntent()

上述代码确保当这个 activity 创建时都会检查是否有一个 Intent。

Build & run。返回 Home 屏,打开 Photos 程序,或者如果你用模拟器的话打开 Gallery 程序。任意选择一张图片,点击 share 按钮。你会看到 Memeify 已经被列出:

Memeify 已经准备接收你的照片!点击 Memeify 看看会发生什么—— Memeify 会启动,选中的图片会显示在 ImageView 中。

你的 app 现在像一个 boss 一样接收了 Intent。

接下来做什么?

你可以从这里下载完整项目。

Intent 是 Android 中的基础材料中的一种。如果没有了它,Android 引以为傲的诸多开放性和交互性就不复存在了。学习如何使用 Intent,你将拥有一个非常强大的帮手。

如果你想进一步学习 Intent 和 Intent Filter,请阅读 Google 的 Intent 文档。

如果对本文有任何疑问和建议,请在下面留言。

猜你喜欢

转载自blog.csdn.net/kmyhy/article/details/79354762