原文:Android: Intents Tutorial
作者:Darryl Bayliss
译者:kmyhy
人不会漫无目的地瞎逛,他们所做的大部分事情——比如看电视、购物、编写下一个杀手级 app —— 都带有特定的目的或者意图,即 intent。
Android 也是同样的。在一个 app 干某件事情之前,它需要知道这件事情的目的或 intent,才能正确地完成整件事情。
这说明人和 Android 并无不同。
在本文,你将利用 Intent 去创建一个模因软件(一种用于恶搞的图片制作软件,在国外非常流行,可以利用这款软件来简单的PS之后来黑别人或自娱自乐)。通过这个 demo,你可以学到:
- Intent 是什么,以及它在 Android 中广泛用途。
- 如何用 Intent 创建和检索其它 app 中的内容并使用到你的 app 中。
- 如何接收和响应一个别的 app 发来的 Intent。
如果你是一个新手,极度建议你阅读Android Tutorial for Beginners教程,以了解最基本的工具和概念。
准备好要模因的照片。本教程将你的 Android 开发技能提升到 9000 +以上!
开始
从这里下载开始项目。
在开始项目中,你会找到 XML 布局和相关的 activity、一些模式化的代码、用于拉伸位图的助手类、以及后面会用到的一些资源如 Drawable 和 String 等。
如果你已经打开了 Android Studio,点击 File\Import Project 然后选择你所下载的开始项目的最上层目录。 否则,请打开 Android Studio 并从欢迎屏中选择 Open an existing Android Studio project,并选择开始项目的最上层目录。
花点时间浏览下开始项目。TakePictureActivity 包含了一个 ImageView,点击它会打开设备摄像头。当你点击 LETS MEMEIFY!,你会将 ImageView 中位图的文件路径传递给 EnterTextActivity,这个 activity 中真正的乐子开始了,你可以在这里输入一段恶搞的文本并将你的照片转换成下一个病毒式的模因!
创建第一个 Intent
运行 app。你会看到:
现在还没有什么功能,如果你根据提示去做并点击 ImageView,什么也不会发生!
你会添加一些代码让事情变得有趣些。
打开 TakePictureActivity.java 在类中加入常量定义:
private static final int TAKE_PHOTO_REQUEST_CODE = 1;
这个常量用于在返回时唯一标识你的 intent——后面你就知道了。
注意:本文假设你会解决类未引入警告,不需要专门说明需要引入某个类。简单提示一下,如果你忘记引入某个类,你可以将鼠标放在提示“类未引入警告”的类上,按 alt+回车键来导入它。
在 onClick() 方法下添加如下代码,请自行添加必要的 import 语句:
private void takePictureWithCamera() {
// 创建一个 intent 用于从摄像头拍照
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File photoFile = createImageFile();
selectedPhotoPath = Uri.parse(photoFile.getAbsolutePath());
captureIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile));
startActivityForResult(captureIntent, TAKE_PHOTO_REQUEST_CODE);
}
这个方法的代码有点多,因此分成几步来添加代码。
第一行声明了一个 Intent 对象。说起来简单,但那究竟是啥子意思呢?
一个 Intent 是一个任务或功能的抽象,它会被 app 在未来某个时刻执行。简单说,它告诉你的 app 需要做的事情。最基本的 intent 由一下几个部分组成:
- Actions: 也就是 intent 需要做的事情,比如拨打某个电话号码,打开一个 URL,编辑某些数据。一个 action 是一个简单字符串常量描述应该完成的事情。
- Data: 即 intent 需用使用的资源。在 Android 中它会用一个 URI(唯一资源标识符) 或者 Uri 对象来表示。数据类型需要根据 action 而变。例如,在一个打电话的 intent 中,你不能将电话号码搞成一张图片吧?
将 action 和 data 组合在一起,Android 就能够知道这个 intent 是干什么的以及用什么来干的问题。
就是这样简单!
回到 takePictureWithCamera() 方法,创建 intent 时我们使用 ACTION_IMAGE_CAPTURE 来作为 intent 的 action。你可能猜到这个 intent 是用来拍照的了,这恰巧是一个模因软件必需要用到的东西!
后面两行创建了一个临时文件以便保存照片。开始项目中已经有创建临时文件的代码,你可以看一眼这些代码。理解它是怎么实现的。
了解 Extra
第4行代码将一个 extra 添加到新建的 intent 中。
你说的 extra 是啥子东东?
Extra 是一种传递给 intent 的包含额外信息的键值存储对象,以便让 intent 用于完成特定的动作。比方说,如果事先准备一些东西的话,人才能更好地完成某个任务,Android 也是同样的。一个好的 intent 总是需要准备好必要的附属物(extra)。
一个 intent 的 extra 类型是已知的,并且根据 action 而定;这和提供给 action 的 data 的类型是一样的道理。
一个极好的例子是创建 action 为 ACTION_WEB_SEARCH 的 intent。这种 action 接收一个键为 QUERY 的 extra,用于表示你想搜索的查询字串。这个 key 通常是一个字符串常量,表示你不应该改变它。用这样的 action 和 extra 打开一个 intent 会显示一个 Google 的搜索页,并列出你的搜索结果。
再来看一下 captureIntent.putExtra() 这句,EXTRA_OUTPUT 表示你会将摄像头获取的照片保存到文件——这样,Uri 就用来指向早先创建的空文件。
调用 intent
现在一个功能正常的 intent 已经准备就绪,附上一个经典的 intent 的心智模型:
只剩一件事情,就是在 takePictureWithCamera() 方法最后一行让 intent 去完成它的使命。也就是这句:
startActivityForResult(captureIntent, TAKE_PHOTO_REQUEST_CODE);
这句让 Android 打开一个 activity 并执行 captureIntent 指定的 action:拍照并保存到文件。一旦 activity 完成了 action,你需要获得捕获的照片。TAKE_PHOTO_REQUEST_CODE 是我们预先定义的常量,它会被用于在 intent 返回时标识这个 intent 。
在 onClick() 的 switch case 语句的 R.id.picture_imageview 分支中,在 break 语句之前加入:
takePictureWithCamera();
当你点击 image view 时,会调用 takePictureWithCamera 方法。
来检查一下你的工作成果!运行 app,点击 ImageView 打开摄像头:
这时你可以拍张照,但你无法用它来做什么,我们将在下一节来解决这个。
注意:如果你在模拟器中运行,你需要编辑 AVD 中的相机设置。打开 Tools\Android\AVD Manager 菜单,点击你想设置的虚机右边的绿色铅笔图标。然后点击窗口左下角的 Show Advanced Settings。在 Camera 一节,确保所有 enabled camera 下拉框中的选项都设置为 Emulated。
隐式 intent
如果你在真机上运行 app,而这个设备上安装了许多相机类 app,则你会发现有时候会出现这种情况:
会询问你需要用哪个 app 来处理这类 intent。
当你创建一个 intent 时,你可以显示地指定或者不指定由哪种 app 来完成 intent 的 action。ACTION_IMAGE_CAPTURE 是一个极好的隐式 intent 的例子。
隐式 intent 告诉 Android 由用户进行选择。如果用户已经有一个 app,他们就是喜欢用这个 app 来完成某种任务,那么利用这个 app 的功能来为你服务又有什么错呢?至少,这避免了在你的 app 中重复发明轮子。
一个隐式 intent 告诉 Android 它需要某个 app 为它执行 action。Android 系统会询问每个已安装的 app,谁能够处理这类 action,然后由这个 app 进行处理。如果不止一个 app 能处理这类 intent,则提示由用户进行选择:
如果只有一个 app 能够处理,intent 自动用它来执行 action。如果没有任何 app 能够处理,Android 什么也不会返回,给你一个空,并导致 app 崩溃!
要避免这个问题,我们可以检查返回结果,以确保在打开 intent 之前,至少有一个 app 能处理该 action。或者在 AndroidManifest.xml 中声明要安装这个 app 必须在设备上有一个摄像头。
这个 demo 中使用了第二个方法。
你用一个隐式 intent 来进行拍照,但你没有在 app 中获取照片。而没有照片,你的模因 app 就无法继续下去。
在 TakePictureActivity 的 takePictureWithCamera() 后添加方法:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == TAKE_PHOTO_REQUEST_CODE && resultCode == RESULT_OK) {
setImageViewWithImage();
}
}
在 takePictureWithCamera() 方法中用 startActivityForResult() 方法打开的activity 被关闭并返回 app 时会执行这个方法。
上面的 if 语句判断返回的 requestCode 是否和你传入的 TAKE_PHOTO_REQUEST_CODE 匹配,以确保返回的 intent 是我们所希望的 intent。同时还要判断 resultCode 是等于一个 Android 常量 RESULT_OK,这个常量表示操作成功的意思。
如果一切 OK,说明我们的照片已经准备就绪,因此调用 setImageViewWithImage()。
这个方法现在定义。
首先,在 TakePictureActivity 顶部,添加一个 Boolean 变量:
private Boolean pictureTaken = false;
这个变量记录是否拍过照,如果你连续拍了几张照片的话,这个变量就有用了。很快我们就会用到它。
然后,在 onActivityResult()后面添加:
private void setImageViewWithImage() {
Bitmap pictureBitmap = BitmapResizer.ShrinkBitmap(selectedPhotoPath.toString(),
takePictureImageView.getWidth(),
takePictureImageView.getHeight());
takePictureImageView.setImageBitmap(pictureBitmap);
lookingGoodTextView.setVisibility(View.VISIBLE);
pictureTaken = true;
}
BitmapResizer 是开始项目中附带的一个助手类,确保你从相机中获得的照片压缩到适合于你的设备屏幕的尺寸。虽然设备也会拉伸照片,但压缩照片更能节省内存。
运行 app,选择你喜欢的相机 app——如果询问你的时候——然后拍照。
这次,照片会被压缩并显示在 ImageView 中:
你还会在下方 TextView 中,看到一句话,赞美了你的拍摄技巧!有教养是一件很好的事情!:]
显式 Intents
接下来进入这个模因 app 的第二阶段,但首先得让你的照片传给另一个 activity,因为你担心这里的屏幕空间有点紧张。
仍然是 TakePictureActivity, 在其它常量后面添加:
private static final String IMAGE_URI_KEY = "IMAGE_URI";
private static final String BITMAP_WIDTH = "BITMAP_WIDTH";
private static final String BITMAP_HEIGHT = "BITMAP_HEIGHT";
这些键会用于你要传给下一个 activity 的 extra 中。
然后,在 TakePictureActivity 添加下列方法, 请自行导入相关类:
private void moveToNextScreen() {
if (pictureTaken) {
Intent nextScreenIntent = new Intent(this, EnterTextActivity.class);
nextScreenIntent.putExtra(IMAGE_URI_KEY, selectedPhotoPath);
nextScreenIntent.putExtra(BITMAP_WIDTH, takePictureImageView.getWidth());
nextScreenIntent.putExtra(BITMAP_HEIGHT, takePictureImageView.getHeight());
startActivity(nextScreenIntent);
} else {
Toast.makeText(this, R.string.select_a_picture, Toast.LENGTH_SHORT).show();
}
}
这里判断了 pictureTaken 是否为 true,这表明你的 ImageView 已经从相机获得了一张照片。如果没有,你的 activity 会显示一个 Toast 信息,让你去拍张照片。如果为 true,则创建一个 intent,使用先前定义的常量 key 设置它的 extra。
接着,在 onClick() 方法中的 R.id.enter_text_button 分支的 break 之前调用这个方法:
moveToNextScreen();
运行 app,点击 LETS MEMEIFY! ,如果你不拍照的话,你会看到一个 Toast 显示:
如果已经拍过照,moveToNextScreen() 方法会走到创建 intent 并进入输入文本的 activity 这一步。同时会附加 extra 到这个 intent,比如照片的 Uri 和宽高。这将在下一个 activity 中用到。
你已经创建了你的第一个显式 intent。和隐式 intent 比较,显式 intent 要稳妥得多,因为它描述了一个组件,这个组件会用来创建和开启 Intent。它可能是来自于你的 app 的另一个 activity,也可能是 app 中的某个服务,比如在后台下载一个文件。
这个 intent 在构造时会指定一个上下文(例如,这里的 this)以及 intent 想打开的 class(EnterTextActivity.class)。因为你已经显式地说明 intent 如何从 A 找到 B,Android 进行简单的调用就可以了。用户无法控制 intent 如何完成:
运行 app。拍照,但这次点击 LETS MEMEIFY!,你的显式 intent 会将 action 交给下个 activity :
在开始项目中已经在 Manifest 中声明过这个 activity 了,你不必做这个步骤。
模因的第二阶段
看起来这个 intent 真棒。但你的 extra 传递到哪了?它们有没有在最后一块内存处拐错弯了?我们该把它们找出来让它们干活了!
在 EnterTextActivity 顶部加入下列常量:
private static final String IMAGE_URI_KEY = "IMAGE_URI";
private static final String BITMAP_WIDTH = "BITMAP_WIDTH";
private static final String BITMAP_HEIGHT = "BITMAP_HEIGHT";
我们简单地将前一个 activity 中的常量复制到这里。
然后,在 onCreate() 方法中的最后一行后面加入:
pictureUri = getIntent().getParcelableExtra(IMAGE_URI_KEY);
int bitmapWidth = getIntent().getIntExtra(BITMAP_WIDTH, 100);
int bitmapHeight = getIntent().getIntExtra(BITMAP_HEIGHT, 100);
Bitmap selectedImageBitmap = BitmapResizer.ShrinkBitmap(pictureUri.toString(), bitmapWidth, bitmapHeight);
selectedPicture.setImageBitmap(selectedImageBitmap);
在创建 activity 时,将上一个 activity 传入的 Uri 赋给 pictureUri,要获得当前 intent 请使用 getIntent() 方法。获得 intent 后,就可以访问 extra 中存储的值了。
因为变量和对象以不同的形式存储,要从 intent 中访问它们需要用不同的方法。例如要访问 Uri 对象,需要使用 getParcelableExtra()。其他 extra 方法用于访问其他类型的变量比如字符串和原始数据类型。
原始的 getExtra() 方法也可以让你指定一个默认值。如果要访问的值没有提供,或者 key 缺失,则使用默认值。
获取到所需的 extra 之后,就可以用 Uri 和 BITMAP_WIDTH、BITMAP_HEIGHT 指定的大小创建位图。最后设置 ImageView 的 image 以显示照片
除了 ImageView,屏幕上还有 2 个 EditText view,用户可以输入他们的恶搞文字。开始项目已经为你完成了这些工作,将恶搞的文字 PS 到照片上。
你需要做的仅仅是修改 onClick()。在 switch case 语句的 R.id.write_text_to_image_button 分支加入:
createMeme();
当当当当! 运行 App。拍照,在第二个 activity 输入你的恶搞文字,点击 LETS MEMEIFY!:
你编写了你自己的模因 app! 但别高兴得太早——你还要为 app 进行一些“抛光工程”。
怎么保存?
如果能将你的恶搞图片保存成图片并分享给全世界就好了!它自己是不会进行病毒式传播的!:]
幸运的是开始项目已经完成了大部分工作——你只需要将导线连接起来。
在 saveImageToGallery() 方法中加入以下代码,就在 try 块后面,第二个 Toast.makeText() 之前:
// 创建一个 intent,请求扫描新建的文件,将照片 uri 传递给 intent,然后广播 intent。
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(Uri.fromFile(imageFile));
sendBroadcast(mediaScanIntent);
这个 intent 使用了一个 ACTION_MEDIA_SCANNER_SCAN_FILE 的 action 去请求 Android 的媒体库,将 image 的 url 添加到媒体库。这样,app 就能够使用 image 的 Uri 访问媒体库了。
ACTION_MEDIA_SCANNER_SCAN_FILE action 需要 intent 提供额外数据,数据需要是一个 Uri,这个 Uri 我们用已保存的位图文件的 File 对象来构造。
最后,通过 Android 系统来广播 intent,以便所有有兴趣的人——这里即 media scanner——去执行。因为 media scanner 没有用户界面,你无法启动一个 activity,所以我们只能用广播 intent 的方式替代。
现在,在 onClick() 方法的 R.id.save_image_button 分支的 break 之前添加:
saveImageToGallery(viewBitmap);
当用户点击 SAVE IMAGE,上述代码会进行一些错误处理,如果一切正常,则打开 intent。
运行 app,拍照,输入恶搞文字,点击 LETS MEMEIFY!,当图片合成后,点击 SAVE IMAGE。
关闭 app,打开 Photos 程序。如果用模拟器测试,则打开 Gallery 程序。你会看到一张带有恶搞文字的新照片:
你的恶搞照片能够不受 app 的限制并可以发送到社交媒体或用你想用的任何方式分享出去。你的模因 app 做好了!
Intent 过滤
现在,你已经充分了解针对不同的任务使用不同的 intent 了。但是,这个关于诚实的 intent 故事里还有另一个情节:当发送一个隐式 intent 时,你的 app 怎么知道哪个 intent 需要处理。
在 app/manifests 文件夹下,打开 AndroidManifest.xml 在第一个 activity 标签你会看到:
关键是 intent-filter 节点。一个 Intent Filter 允许你的 app 的组件能够响应隐式 intent。
当 Android 试图实现一个其他 app 发来的隐式 intent 时,这就好像一面旗帜。一个 app 可以拥有多个 intent filter,它们像旗帜一样挥舞着,希望 Android 要找的人就是它。
这就像是 intent 和 app 们在进行一场网上约会!:]
为了证明自己就是和某个 intent 匹配的 app,intent 过滤器需要提供三样东西:
- Intent 的 Action: 这个 app 能够完成的 action;例如相机 app 会为你的 app 执行 ACTION_IMAGE_CAPTURE 操作。
- Intent 的 Data: 这个 intent 能够接受的数据类型。取值范围可能是某个文件路径、端口、MIME 类型比如 image 和 video。你可以一个或多个属性,可以严格也可以宽泛地控制 app 从 intent 中获得的数据类型。
- Intent 的 Category: 能够接受的 intent 的类别。这是一种用于指定隐式 intent 中哪个 action 能被处理的附加方式。
让 Memeify(示例 app) 通过一个隐式 intent 将图片和其它 app 共享是一个不错的主意——这个过程会简单到令你惊奇。
在 Manifest 文件的第一个 intent 过滤器之后添加代码:
<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>
这个新的过滤器指明你的 app 会处理 action 为 SEND 的隐式 intent。同时将 intent 类别指定为 DEFAULT,表明在这种情况下我们不需要对类别进行任何特殊的设置。同时只需要提供 MIME 类型为 image 的数据。
打开 TakePictureActivity.java 在类中添加如下方法:
private void checkReceivedIntent() {
Intent imageRecievedIntent = getIntent();
String intentAction = imageRecievedIntent.getAction();
String intentType = imageRecievedIntent.getType();
if (Intent.ACTION_SEND.equals(intentAction) && intentType != null) {
if (intentType.startsWith(MIME_TYPE_IMAGE)) {
Uri contentUri = imageRecievedIntent.getParcelableExtra(Intent.EXTRA_STREAM);
selectedPhotoPath = getRealPathFromURI(contentUri);
setImageViewWithImage();
}
}
}
我们在这里获取了开启这个 activity 的 Intent 并找出它的 action 和 类型,将它们和 intent filter 中的定义进行比较,看它的 data 是不是 MIME_TYPE_IMAGE 类型。
如果是,继续获取图片的 Uri,通过开始项目中提供的助手方法获取位图,并让 ImageView 显示位图。
然后在 onCreate() 方法后加入:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
checkReceivedIntent();
}
当你的 app 的窗口将焦点切换到你的 activity 上时,对 intent 进行检查。这使得 activity 一显示到屏幕上就显示位图。
运行 app。立即回到 Home 屏,然后再回到 Photos 程序,或者 Gallery 程序——如果你正在使用模拟器的话。选择一张照片,点击分享按钮,从弹出菜单中选择 Memeify:
Memeify 正准备接受你的照片!点击 Memeify 看看会发生什么—— Memeify 会打开,并在 ImageView 中显示你选中的照片。
你的 app 现在毫不客气地接受了这个 intent!
结束
你可以从这里下载完成的项目。
Intent 是构建 Android 的砖石之一。没有它,Android 引以为傲的开放和连通根本无法做到。学会如何利用好 intent,你会获得一个强大的盟友。
如果你想学习更多关于 intent 和 intent 过滤器的内容,请参考Google 的 Intents 文档。
有任何问题及建议,请在下面留言。