安卓意图学习手册-全-

安卓意图学习手册(全)

原文:zh.annas-archive.org/md5/0C85816AE60A0FB2F72BC5A43007FCC3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Android 是一项新兴技术,Google Play 市场上有很多应用程序。到目前为止,它是智能手机技术中最大的奇迹,推动着越来越多的开发者从事 Android 开发。意图是任何 Android 应用程序的重要组成部分,没有使用意图的 Android 应用程序是不完整的。通过使用意图,你的 Android 应用程序可以轻松地实现监听广播、发送消息、通过社交网络分享、发送通知以及访问摄像头、传感器和 Wi-Fi 等硬件组件等功能。

《学习 Android 意图》专注于使用意图来充分利用 Android 平台的各种功能。它非常适合那些想要了解 Android 意图的框架和领域、它的强大功能以及在 Android 应用程序内部的需求的开发者。全书使用实用的深入示例来帮助理解使用意图的关键概念。

本书从介绍 Android 的基本概念及其各种事实和数据开始,例如不同的 Android 版本、它们的发布日期以及 Android 设备的演变。在介绍基本技术概念的同时,从最简单的 Android 意图介绍途径逐步过渡到从组件和功能的角度更实际地看待 Android 意图。

在这本书中,你将学习如何使用不同的组件和功能,例如在活动之间传输数据、调用 Android 的各种功能和组件、执行不同的内置和自定义服务、访问 Android 设备的硬件和软件组件以及发送通知和闹钟。你将获得关于 Android 意图背后概念的理论知识,以及使用 Android 意图以移动高效方式执行特定任务的实际知识。

在书的最后,你将对 Android 意图及其功能有一个清晰的认识和实际掌握。

本书内容

第一章,理解 Android,涵盖了 Android 系统的基本知识和关键概念,它的版本、Android 操作系统的简要历史、Google Play 市场和 Android Studio。这一章还从开发的角度涵盖了主题,包括 Android 应用程序的构建块、活动生命周期及其回调方法。

第二章,Android 意图介绍,涵盖了意图的介绍、意图的基本关键概念、意图在 Android 应用程序中的作用、意图的技术概述、android.content.Intent类中使用对象及其结构。此外,这一章还解释了两个实际示例,说明如何使用意图从一个活动导航到另一个活动。

第三章,意图及其分类,更详细地介绍了意图,并扩展了它们的类别,如显式意图和隐式意图。本章还提供了使用意图的实际实现示例,例如与其他应用共享数据、从其他 Android 应用获取共享数据、从图库中选择图片以及通过意图启动活动或服务。

第四章,移动组件的意图,涵盖了每个 Android 设备中最常见硬件组件的基本知识,如 Wi-Fi、蓝牙、蜂窝数据、全球定位系统(GPS)、地磁场和运动位置传感器。之后,本章详细介绍了意图与这些硬件组件的角色,以及使用意图的实际示例,包括打开/关闭蓝牙、使设备可被发现、打开/关闭 Wi-Fi、打开 Wi-Fi 设置、拍照、录视频以及执行语音识别和文本到语音转换。

第五章,使用意图进行数据传输,深入探讨了使用意图进行数据传输的细节。本章讨论了通过不同方法在活动之间传输数据,使用Intent类的putExtra()方法进行简单的数据传输,通过ParcelableSerializeable类对象发送自定义数据对象,以及 Android 系统中数据传输的一些场景。

第六章,使用意图访问 Android 功能,涵盖了 Android 操作系统中最常见的软件功能,如布局、显示、连接性、通信、可访问性、触摸和硬件支持。本章讨论了两个重要的AndroidManifest标签,<uses-feature><uses-permission>,它们的使用,以及将这些标签与移动硬件组件和 Android OS 功能进行比较。本章提供了在 Android 应用程序中使用意图的实际示例实现,如拨打电话、发送短信/MMS 消息、确认消息送达、接收消息以及使用自定义布局发送通知。

第七章,意图过滤器,详细介绍了意图和意图过滤器,以及它们如何向 Android 操作系统提供关于应用内部存在的活动信息。本章还涵盖了过滤器测试的细节,例如动作测试、数据测试、类别测试,以及在使用意图时这些测试的便利性。

第八章,广播意图,涵盖了 Android 中的广播以及广播意图。本章讨论了 Android 操作系统的系统广播意图,例如电量低、电源连接/断开、启动完成以及耳机插入/拔出等,并附有一些意图的实际示例实现。此外,本章还介绍了自定义广播意图及其在各种情况下的实际应用示例。

第九章,意图服务与待定意图,涵盖了意图的一些最先进的话题,例如与常见的ThreadServiceAsyncTask方法相比,使用IntentService对象。本章介绍了PendingIntent对象及其在实际示例实现中的使用。

您阅读本书所需的内容

为了执行书中各种示例,所需的软件包括任何 Android 开发 IDE,最好是带有最新 Android SDK 的 Eclipse IDE 或 Android Studio(在撰写本书时处于预览发布状态)。

本书的目标读者

《学习 Android 意图》面向希望扩展他们对 Android 意图知识的初学者或中级开发者。我们期望读者具备 Android 开发的基本了解,包括如何使用不同的 Android IDE 以及如何使用原生 Android SDK API 开发应用程序。

这本书对每个 Android 应用开发者都很有用。从最初几章开始,读者将开始学习意图的基础知识,即使是中级开发者也会在整本书中找到有用的提示。随着读者章节的深入,将涵盖更难的主题;因此,初学者不要跳过内容非常重要。

建议读者具备 Java 编程语言和 Android 开发的基本理解。

约定

在这本书中,您会发现多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。

文本中的代码字如下所示:"为了创建一个活动,我们将从Activity类扩展我们的类并覆盖onCreate()方法。"

代码块设置如下:

约定

还可以是以下格式:

public class Activity1 extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_first);

新术语重要词汇以粗体显示。例如,在菜单或对话框中屏幕上看到的单词,会在文本中以这样的形式出现:"点击下一步按钮会将您带到下一个屏幕"。

注意

警告或重要注意事项会以这样的框出现。

提示

提示和技巧会像这样出现。

读者反馈

我们非常欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或可能不喜欢的地方。读者的反馈对我们来说非常重要,它帮助我们开发出您真正能够充分利用的图书。

如果您想要发送一般反馈,只需发送电子邮件到<feedback@packtpub.com>,并在邮件的主题中提及书名。

如果您在某个主题上有专业知识,并且对撰写或参与书籍有兴趣,请查看我们在www.packtpub.com/authors上的作者指南。

客户支持

既然您已经拥有了 Packt 的一本书,我们有一些事情可以帮助您最大限度地利用您的购买。

下载示例代码

您可以从您的账户www.packtpub.com下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,可以访问www.packtpub.com/support注册,我们会直接将文件通过电子邮件发送给您。

错误更正

尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——我们非常感激您能向我们报告。这样做,您可以避免其他读者感到沮丧,并帮助我们改进该书的后续版本。如果您发现任何错误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误更正提交表单链接,并输入您的错误详情。一旦您的错误更正得到验证,您的提交将被接受,错误更正将会上传到我们的网站,或添加到该标题下的现有错误更正列表中。任何现有的错误更正都可以通过从www.packtpub.com/support选择您的标题来查看。

盗版

互联网上版权材料的盗版问题在所有媒体中持续存在。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上以任何形式遇到我们作品非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

若发现疑似盗版材料,请通过<copyright@packtpub.com>联系我们,并提供相关链接。

我们感谢您帮助我们保护作者以及我们为您带来有价值内容的能力。

常见问题

如果您在书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章:了解安卓

本章为您提供了关于安卓的强大理论知识。很明显,这个术语对于任何新手技术用户来说都不陌生。由于这个伟大的操作系统普及,许多开发者开始从网页开发和其他平台转移过来。这次大规模的迁移为安卓应用市场带来了显著变化,并为新的移动应用开发者开启了无限的新大门。安卓是苹果公司 iOS 操作系统的一个强大的对手。然而,正如统计数据所示,在收入方面,安卓正在追赶 iOS 市场,因为谷歌 Play 是下载总量方面增长最快的应用市场。

本章包括以下主题:

  • 介绍安卓

  • 了解安卓的为什么和何时

  • 安卓开发者的官方谷歌 IDE——安卓工作室

  • 安卓应用程序的结构

  • 展示安卓活动生命周期

介绍安卓

安卓是一个基于 Linux 的操作系统,因此它是一款开源软件。谷歌将其许可协议在 Apache 许可协议下发布。安卓代码的可用性使得它成为一个易于修改的操作系统,供应商也可以对其进行定制。由于其高度灵活的设计,一些批评者称它不安全,在一段时间内这是正确的,但现在,安卓已经是一个具有高级安全架构的成熟操作系统。据说最新的安卓版本(即果冻豆)是谷歌有史以来生产的最安全的操作系统。下面让我们通过了解不同版本的安卓操作系统来进一步了解。

探索不同的安卓版本

自从发布以来,安卓通过不同版本的发布一直在进行自我转变。不仅仅是 UI,每个新版本都增加了许多功能,进行了修改和增强。第一个正式使用甜点名称的版本是安卓 1.5 纸杯蛋糕,基于 Linux 2.6.27。每个新的安卓版本都伴随着一组新的 API 级别,这基本上是对以前的 API 进行一些修改,过时处理,以及新增新的控件。

从开发者的角度来看,发布安卓新版本带来了一些旧方法/功能的过时。然而,这将带来警告而不是错误;你仍然可以在新的 API 级别中使用以前的方法调用。

下表展示了不同安卓版本及其 API 级别和主要亮点:

安卓版本 版本名称 主要功能 API 级别 发布日期
安卓 4.1/4.2/4.3 果冻豆 谷歌即时语音搜索锁屏小部件速度提升键盘中的手势输入(仅限开发者)安全的 USB 调试 OpenGLES 3.0 支持改进的相机用户界面从右至左语言支持 16, 17 和 18 2012 年 7 月 9 日,2012 年 11 月 13 日,2013 年 7 月 24 日
安卓 4.0 Ice Cream Sandwich 重大 UI 变化增强锁屏操作屏幕方向动画带 EAS v14 的邮件应用面部解锁增强的网页浏览器支持平板和手机 14 和 15 2011 年 10 月 19 日
安卓 3.x Honeycomb 首个为平板设计的操作系统增加系统栏和动作栏快速访问相机及其功能双窗格邮件 UI 视图多核支持 11、12 和 13 2011 年 2 月 22 日
安卓 2.3 GingerBread 增强的 UI 原生 VoIP/SIP 支持 Google Talk 和 Google Wallet 视频通话支持 9 和 10 2010 年 12 月 6 日
安卓 2.2 Froyo 提升了速度 USB 网络共享 JIT 实现 8 2010 年 5 月 20 日
安卓 2.0/2.1 Eclair 更新的 UI 动态壁纸蓝牙 2.1 5、6 和 7 2010 年 1 月 12 日
安卓 1.6 Donut 手势识别 4 2009 年 9 月 15 日
安卓 1.5 Cupcake 键盘文本预测录制和观看视频 3 2009 年 4 月 30 日

注意

有趣的是,安卓系统的版本是按字母顺序排列的。从 Apple Pie 1.0 开始,然后是 Banana Bread 1.1,按字母顺序连贯地发展到了 Jelly Bean,并保持了这一传统;预计下一个版本将是 Key Lime Pie。

如前所述,由于安卓的开源性质,厂商可以对其进行修改,许多著名的手机制造商都在自己的手机中安装了定制版的安卓系统。例如,三星在安卓上制作了自定义触摸界面,称之为 TouchWiz(三星 Galaxy S4 配备了 TouchWiz Nature UX 2.0)。同样,HTC 和索尼 Xperia 也推出了自己的定制用户界面,分别称之为 HTC Sense 和 TimeScape。

探索不同版本的安卓系统

Google Play – 安卓官方应用商店

与其他著名的移动操作系统一样,安卓也有自己的应用商店,名为 Google Play。此前,应用商店被称为 Android Market,在 2012 年初,更名为 Google Play,用户体验得到了新的改进。该更新将整个娱乐世界统一在 Google Play 之下。音乐、应用、书籍和电影,都变得像苹果著名的 App Store(iTunes)一样易于用户访问。您可以在play.google.com/about/上详细了解安卓商店的信息。

注意

Google Movies & TV、Google Music、Google Books 和 Google Magazines 仅在部分国家可用。

Google Play 提供各种应用、电影、电子书和音乐。最近,他们还在同一应用商店下推出了 Google Play TV 服务。谈到应用方面,Google Play 提供了不同的类别供用户选择应用。从游戏到漫画,再到社交应用,应有尽有。用户可以享受许多付费应用,并通过 Google Play 提供的应用内购买服务解锁许多功能。

还有不同的厂商特定的应用商店,如 Kindle 的亚马逊应用商店、Nook 商店等,它们根据自身的条款和条件提供许多应用程序。

理解 Android 的为何与何时

Android 是基于 Linux 的开源操作系统,主要针对触屏手机和平板电脑。安迪·鲁宾、Rich Miner、Nick Sears 和 Chris White 在 2003 年 10 月创立了这个操作系统。Android 背后的基本意图是开发一个用于数字内容的操作系统。这是因为当时,手机使用的操作系统是 Symbian 和 Windows Mobile。

注意事项

2007 年 6 月,苹果公司推出了 iPhone。2007 年 11 月,谷歌公司推出了 Android。

然而,当他们意识到像相机这样的设备市场并不大时,他们将注意力转向了对抗 Symbian 和 Windows Mobile 的手机。当时 iPhone 还没有上市。作为当今智能手机操作系统市场的佼佼者,占据 75%市场份额的 Android Inc.当时还在秘密运行。除了他们在开发手机软件之外,没有向市场透露任何信息。同年,Android 的联合创始人鲁宾资金耗尽,他的好朋友 Steve Perlman 给他带来了一封装有 10,000 美元现金的信封。

2005 年 8 月,谷歌公司收购了 Android Inc.,使其成为谷歌公司的子公司。收购后,Android 的主要员工留在了 Android Inc.。安迪·鲁宾开发了一个由 Linux 内核提供支持的移动设备平台。谷歌向手机制造商和运营商承诺提供一个灵活且可升级的操作系统。由于谷歌没有在媒体上发布关于 Android 的消息,谣言开始传播。流传的猜测包括谷歌正在开发谷歌品牌的手机,谷歌正在定义手机原型和技术规格。这些猜测和谣言一直持续到 2006 年 12 月。

后来,在 2007 年 11 月,开放手机联盟透露他们的目标是开发移动设备的开放标准。Android 作为他们的首款产品发布,这是一个基于 Linux 内核版本 2.6 构建的移动设备平台。开放手机联盟是由 65 家参与移动领域的公司组成的联盟,倡导移动行业的开源标准。

2008 年 10 月,首款搭载 Android 操作系统的商业手机由 HTC 推出,名为 HTC Dream。下图展示了 HTC Dream。自那时起,Android 一直在进行升级。谷歌在 2010 年推出了 nexus 系列。

理解 Android 的为何与何时

HTC Dream,首款使用 Android Activity 堆栈的 Android 手机

Android 操作系统的演变

安卓操作系统在 HTC Dream 首次亮相后,迅速在消费者中获得了普及。谷歌不断升级安卓系统。每个主要版本都包括修复上一个版本的 bug 和新增功能。

安卓在 2008 年 9 月发布了第一个版本,设备为 HTC Hero。安卓 1.1 是一个修复了 bug 和问题的更新,没有主要版本发布。在安卓 1.1 之后,发布了名为 Cupcake 的安卓 1.5 版本,增加了视频上传、文本预测等功能。2009 年底发布了安卓 1.6 Donut 和安卓 2.0/2.1 Éclair 版本,2010 年 1 月发布的 2.1 版本引入了包括 Google Maps、增强的照片视频功能、蓝牙、多点触控支持、动态壁纸等重大更新。2010 年 5 月,名为 Frozen Yogurt(或 Froyo)的安卓 2.2 版本成为主要发布,增加了对 Wi-Fi 热点连接的支持。

这个版本在开发者中变得非常流行,通常被用作安卓应用的最小 API 级别。2010 年 5 月发布的安卓 2.3 Gingerbread 版本引入了近场通信(NFC)功能,允许用户执行如移动支付和数据交换等任务。这个版本的安卓成为了开发者中最受欢迎的版本。专为平板设备优化的安卓 3.0/3.1 Honeycomb 版本,为开发者提供了更多的 UI 控制,这是一个很大的优势。2011 年 10 月发布的安卓 4.0 Ice Cream Sandwich 版本。由于安卓 3.0/3.1 仅适用于平板电脑,Ice Cream Sandwich 版本弥补了这一差距,同时支持手机和平板电脑。最新的安卓版本,安卓 4.2 Jelly Bean 进一步提升了 UI,优化了软件,还有其他改进。

注意

谷歌从安卓 1.1 版本开始,按照字母顺序以甜点命名安卓版本。

下图以视觉格式展示了所有版本:

安卓操作系统的演变

下面的截图显示了 2013 年 3 月安卓版本的当前分布情况。从截图中可以看出,安卓 2.3 Gingerbread 是最受欢迎的版本,其次是安卓 4.0 Ice Cream 版本。

安卓操作系统的演变

安卓版本的当前分布

谷歌官方 IDE - 安卓工作室

在 2013 年谷歌 I/O 大会之前,安卓官方一直使用 Eclipse 作为其开发 IDE。官方安卓支持明确提到了这款 IDE 与安卓开发工具ADT)和安卓软件开发工具包SDK)及其文档的使用。

谷歌官方 IDE - 安卓工作室

安卓工作室(Windows 7)的加载屏幕

在 2013 年的谷歌 I/O 大会上,谷歌推出了一款专为安卓应用开发设计的新 IDE。这个 IDE 被称为 Android Studio,它是一款基于 IntelliJ 的软件,为开发者提供了许多有前景的功能。

安卓工作室的特点

Android Studio 在 IntelliJ-based IDE 的基础上提供了各种功能。以下是 Android Studio 中引入的功能列表:

  • Android Studio 内置了 Android 开发工具

  • Android Studio 提供基于 Gradle 的构建支持

  • 用于构建 Android UI 的灵活控制,并支持不同屏幕尺寸的同步视图

  • Android 重构、快速修复以及技巧和窍门

  • 带有拖放功能的 Android 应用高级 UI 制作工具

下面的截图展示了带有 UI 制作器的 Android Studio 多屏幕查看器:

Android Studio 的功能

注意事项

当前 Android Studio 的版本是 v0.1.1。

此外,Android Studio 还提供了许多其他功能。谷歌在发布时提到,版本(v0.1)不稳定,需要在进行 100% 准确使用之前进行各种修复。

Android Studio 的限制

Android Studio 处于早期阶段,这使得它成为一款成熟度低且有限制的软件。根据谷歌的说法,他们正在努力更新该软件,并很快将纠正这些问题。在版本 0.1.1 中,开发者面临的问题如下:

  • Android Studio 只能编译为 Android 4.2 Jelly Bean

  • 用户界面只能使用 Android 4.2 Jelly Bean 的 UI 和小部件来制作

  • Eclipse 项目不能直接导入到 Android Studio(参考 developers.android.com/

  • 导入库项目时的错误

Android 应用的构建块

Android 应用由多种构建块组成,帮助开发者保持事物的组织性。它提供了维护资产、图片、动画、视频片段以及实现本地化功能的灵活性。此外,还有一些组件包含有关您的应用程序支持的最小和最大 Android 版本的信息。同样,菜单在 Android 应用项目中是单独处理的。

Android 应用的构建块

在 Android Studio 中显示的 Android 应用程序的各种组件

与 Eclipse IDE 类似,Android Studio 提供了各种便捷的功能来操作这些特性。展望 Android 应用的构建块,我们可以将这些组件分为以下部分:

  • 编码组件

  • 媒体组件

  • XML 组件

  • 引用组件

  • 库组件

编码组件

将组件分解有助于更容易理解 Android 应用的结构。编码组件与 Android 项目的源代码直接相关。为了编写应用程序,开发者需要编写一些代码,以响应用户的期望。

在编码组件中,保存所有开发者代码的主要文件夹是src。该文件夹包含一个或多个 Java 包,开发者根据所完成工作的类型对代码进行分类。编写包名称的默认方式是用点分隔(例如,com.app.myapplicationproject),这样可以轻松将其与其他任何项目的任何其他包区分开来。

注意

Android 应用程序的包名称用于在 Google Play 上唯一标识它。

在这些包内,有.java文件,供开发者从 Android 库引用并继续到期望的输出。这些 Java 类可能继承自 Android API,也可能不继承。我们还可以在编写代码时使用大多数 Java 函数。

媒体组件

由于高度配置的硬件,用户需要具有良好图形、动画、声音和视频文件的应用程序。因此,你可以轻松引入任何一种,但应确保它们都不应影响应用程序的质量,因为市面上有数千种不同类型的 Android 设备。Android 提供了一种灵活的方法,你可以使用它将媒体文件放置在项目内。按分类,有两种在应用程序项目中维护媒体文件的方法:

  • Assets文件夹

  • res文件夹

Assets文件夹

Android 项目包含一个名为assets的文件夹。这个文件夹负责保存所有媒体文件,包括音乐、图片等。开发者可以通过在继承的Activity类中编写getAssets()函数直接从代码访问文件夹。这个函数返回AssetManager,可以轻松地用来访问主assets文件夹内的子文件夹和文件。

assets文件夹的主要优点是无需为放置的文件保持引用,这对于开发者需要进行测试和运行时更改的情况非常方便。尽管它没有任何引用,但由于输入错误,它可能会引入错误。使用资源的另一个优点是开发者可以根据自己的意愿来安排文件夹;同样,这些文件夹的命名约定也可以根据开发者的方便轻松选择。

res文件夹

res文件夹用于管理 Android 应用程序中的应用程序资源,如媒体文件、图片、用户界面布局、菜单、动画、颜色和字符串(文本);换句话说,你可以认为这是处理媒体文件的最智能方式。它包括许多子文件夹,如drawabledrawable-ldpidrawable-mdpidrawable-hdpidrawable-xhdpidrawable-xxhdpirawlayoutanimmenuvalues

Drawable与 Android 项目中使用的图片直接相关。这是一种在项目中保存图片的智能方式。我们知道市场上存在各种支持 Android OS 的设备。为了区分这些设备,低分辨率的图片被放在ldpi文件夹中,供低分辨率设备使用。同样,mdpi文件夹适用于中等屏幕密度的设备,hdpi适用于高密度设备,xhdpi适用于超高密度设备,依此类推。

提示

放在这些 drawable 文件夹中的图片应该有唯一的名称,以便从代码中通过单一引用来访问它们。

同样,为了放置音乐和声音内容,我们使用raw文件夹以便从代码中访问它们。除了音乐和声音之外,任何其他文件也可以放在raw文件夹中(例如,JSON 文件)。animvaluesmenuslayout文件夹也是如此,分别用于放置动画、值、自定义菜单和不同类型的布局。

XML 组件

在 Android 中,开发者需要使用 XML 来创建用户界面。布局、菜单、子菜单以及许多其他内容都是以不同的 Android 标签形式基于 XML 定义的。除了布局,你还可以以 XML 文件的形式存储字符串、颜色代码等许多其他内容。该组件支持维护应用程序的层次结构,使所有开发者易于理解。

让我们看一下一些最重要的 XML 文件,它们是任何 Android 应用程序的支柱。

布局文件夹

res文件夹内,有一个名为layout的文件夹,其中包含了所有活动的布局。需要注意的是,这个文件夹也有一些扩展,就像 drawable 文件夹一样。layout-landlayout-port方法分别用于在横屏和竖屏模式下保持布局的良好组织。

提示

XML 还可以用于创建自定义 drawable,这些 drawable 可以在不同场景下作为图片使用。例如,可以使用 XML 制作自定义按钮的图片,它会在点击和未点击状态下呈现不同的 UI 行为。

前面的截图是 Android Studio 的界面,你可以看到一个activity_main.xml文件,该文件用于描述一个活动的布局。有一些 Android 定义的 XML 标签用于RelativeLayoutTextView(阅读下面的信息框)。同样,还有一些其他标签供开发者使用,以便在布局中包含不同类型的控件。

注意

RelativeLayout是一个布局,其中子元素按照相对位置进行放置。这个布局经常被 Android 移动开发者使用。

TextView是用于显示文本(包括数字、字符串和可编辑内容)的视图之一。

菜单文件夹

Android 提供了不同类型的菜单,以便在活动中快速访问常用的功能。可用的不同菜单如下:

  • 上下文菜单

  • 选项菜单(带操作栏)

  • 弹出菜单

  • 自定义菜单

由于本章的重点有限,我们无法完全展开讨论各种菜单的功能并给出示例。然而,所有类型的菜单都基于 XML 文件,在这些文件中,使用 Android 定义的标签如<menu><item><group>来引入应用程序中的菜单。以下截图供参考:

菜单文件夹

Android ICS 选项菜单在左侧,自定义弹出菜单在右侧

values文件夹

values文件夹包含各种 XML 文件,开发者在许多场景下都可以使用它们。这个文件夹中最常见的文件是styles.xmlstrings.xmlstyle文件包含与任何 UI 样式相关的所有标签。同样,strings.xml文件包含在 Android 项目的源代码中使用的所有字符串。除此之外,strings.xml文件还包含<color>标签的哈希编码,用于在 Android 应用程序的源代码中识别许多颜色。

AndroidManifest.xml

与前面提到的文件夹不同,AndroidManifest.xml是一个包含关于 Android 应用程序重要信息的文件。清单文件由各种标签组成,如<application><uses-sdk><activity><intent-filter><service>等,它们都被包含在<manifest>主标签内。

正如标签所示,这个 XML 文件包含了关于活动、服务、SDK 版本以及与应用程序相关的所有信息。如果你在AndroidManifest.xml文件中输入的信息不正确或遗漏了任何内容,可能会出现各种错误。

AndroidManifest.xml文件的另一个主要优点是,它是跟踪任何 Android 应用程序结构最佳方式。通过这个文件,可以轻松查看活动、服务和接收器的总数。除此之外,我们只需调整AndroidManifest.xml文件,就可以更改样式、字体、SDK 限制、屏幕尺寸限制以及许多其他功能。

在签名.apk构建时,我们会提到包名、版本名称和版本代码,Google Play 通过这些信息来唯一标识应用程序并将其发布到市场。应用程序将通过这个包名被识别,后续的发布基于在AndroidManifest.xml文件中更改版本代码和版本名称。

AndroidManifest.xml

引用组件

安卓应用程序的另一个基本组件是引用组件。简单来说,这个组件帮助基于 XML 的文件与 Java 代码进行交互。在 Android Studio 中,R.java文件位于源文件夹下,该文件夹是项目层次结构中构建文件夹的子文件夹。R.java文件包含了所有在 XML 文件中用于布局、菜单、可绘制资源、动画等的引用。然后,此文件向活动文件公开,以获取引用并获取执行各种功能和参数的对象。

通常,这个R.java文件是作为项目导入的一部分获得的,并用作R.layout.main。在这个例子中,它清楚地表明我们需要获取res布局文件夹中的一个名为main的布局。因此,它将返回一个资源 ID,这个 ID 对开发者是隐藏的,并直接引用res文件夹中的特定布局。

注意

在构建项目时,R.java文件会自动生成。因此,它不应该被推送到仓库中。确保不要手动修改R.java文件的内容。项目gen文件夹中存在的R.java文件是在项目创建或编译时由安卓定义的。

库组件

库是预构建的 Java 文件/项目,任何人都可以使用它们在应用程序内执行特定任务。有许多第三方付费/免费的库,为开发者提供各种功能。库组件本身不是库;它们是保存库的项目文件夹。

在安卓项目中,主应用文件夹(Android Studio)内有一个名为libs的文件夹,用作库组件。任何.jar库文件都可以放在这个文件夹下,以便从代码中引用。在使用这些库的 Java 代码中,需要导入.jar文件中存在的相应包名称,才能使用该特定类的功能。

同样,你可以通过将其他安卓项目作为一个模块并导入到你的项目中,来使用它作为库。这个功能在 Eclipse 中以前被称为库项目,通过项目属性 | 安卓 | 库引用进行导入。

库组件

安卓 Studio 模块导入窗口

安卓活动生命周期

一个 Android 应用程序由一个或多个活动组成。这些活动在执行任务、接收用户输入和向用户展示结果时的转换流程中,是应用程序的可视化表现。每个活动在屏幕上为用户提供了一个可视化的交互界面。Android 按照后进先出的原则,将所有活动保存在后退栈中。每当新活动启动时,当前活动就会被推入后退栈。因此,Android 将焦点转移到新活动上。活动可以占据设备的整个屏幕,也可以只占用屏幕的一部分,或者还可以被拖动。无论活动是占据整个屏幕区域还是屏幕的一小部分,在 Android 中一次只会有一个活动获得焦点。当任何现有活动停止时,它会被推入后退栈,这反过来又导致下一个顶部活动获得焦点。

注意

Android 4.x 版本引入了片段(Fragments)。片段可以被视为子活动,它们被嵌入到活动中,以在单个活动中同时执行不同的任务,与活动不同。

通常,一个 Android 应用程序由多个活动组成。这些活动之间是松散耦合的。最佳实践是为每个要执行的具体任务创建一个活动。例如,在一个简单的电话拨号应用中,可能会有一个活动显示所有联系人,一个显示任意特定联系人的完整联系信息,一个用于拨号,等等。在所有应用中,都有一个主活动,它作为应用的启动点。当应用启动时,这个活动就会开始运行。然后这个活动会启动另一个活动,后者再启动其他活动,依此类推。Android 通过后退栈管理所有活动。

Android 活动生命周期

Android 活动后退栈

前面的图示展示了后台堆栈如何工作的简单表示。堆栈中突出显示的顶部活动代表前台活动,有时也称为焦点活动或运行活动。当一个新活动被创建时,它被推入堆栈;当一个现有活动被销毁时,它从堆栈中移除。这个被推入堆栈和从堆栈中移除的过程由 Android 的活动生命周期管理。这个生命周期被称为活动生命周期。生命周期管理堆栈中的活动,并通过回调方法通知活动状态的变化。由于状态变化,活动会接收到诸如活动创建、活动销毁等不同类型的状态。开发者重写这些回调方法以执行相应状态变化所需的步骤。例如,当活动开始时,应加载必要的资源;当活动被销毁时,应卸载这些资源以获得更好的应用性能。所有这些回调方法在管理活动生命周期方面都扮演着关键角色。开发者可以选择不重写、重写部分或全部方法。

活动的基本状态

基本上,活动存在三种状态:Resumed(恢复),Paused(暂停),和Stopped(停止)。当活动恢复时,它显示在屏幕上并获得用户的焦点。这个活动保持在后台堆栈的前台部分。当另一个活动开始并在屏幕上可见时,这个活动就会被暂停。这个活动仍然在前台任务中,仍然存活,但它没有获得任何用户焦点。也有可能是新活动部分覆盖了屏幕。在这种情况下,暂停活动的部分仍然会在屏幕上可见。当活动完全从屏幕上消失并被前台另一个活动替换时,它进入停止状态。在这个停止状态下,活动仍然存活,但在后台堆栈的背景部分。暂停状态和停止状态之间的区别在于,在暂停状态下,活动附着在窗口管理器上,但在停止状态下,它没有附着在窗口管理器上。

注意

在极端低内存的情况下,Android 系统可能会通过要求其完成或直接杀死进程来杀死任何暂停或停止的活动。为了避免这个问题,开发者应该在暂停和停止的回调中保存所有必要的数据,并在恢复回调中检索这些数据。

活动生命周期的回调方法

当任何活动的状态发生改变时,会有各种回调方法被调用。开发者在这些方法中执行必要的任务和操作,以提高应用的性能。为了展示活动生命周期的实际应用,我们将在本节中创建一个小的 Android 应用程序。以下是逐步操作的步骤:

  1. 启动Android Studio

  2. 创建一个空项目,详细信息如下截图所示:活动的回调方法

    Android Studio 中的新建项目对话框

  3. 在项目的MainActivity.java文件中添加以下代码:

    package com.learningandroidintents.callbacksdemo;
    import android.os.Bundle;
    import android.app.Activity;
    import android.view.Menu;
    import android.widget.Toast;
    public class MainActivity extends Activity {
    
     @Override
            public void onCreate (Bundle savedInstanceState){
                super.onCreate(savedInstanceState);
                Toast.makeText( this, "Activity Created!", Toast.LENGTH_SHORT
                ).show();
            }
    @Override
            protected void onStart ()
            {
                super.onStart();
                Toast.makeText(this, "Activity Started!", Toast.LENGTH_SHORT
                ).show();
            }
    
     @Override
            protected void onResume()
            {
                super.onResume();
                Toast.makeText(this, "Activity Resumed!", Toast.LENGTH_SHORT
                ).show();
            }
    
    @Override
            protected void onPause()
            {
                super.onPause();
                Toast.makeText(this, "Activity Paused!", Toast.LENGTH_SHORT
                ).show();
            }
    
    @Override
            protected void onStop()
            {
                super.onStop();
                Toast.makeText(this, "Activity Stopped!", Toast.LENGTH_SHORT
                ).show();
            }
    
    @Override
            protected void onDestroy()
            {
                super.onDestroy();
                Toast.makeText(this, "Activity Destroyed!", Toast.LENGTH_SHORT
                ).show();
            }
        }
    
  4. 在模拟器中运行项目,你会看到以下顺序在屏幕上打印提示:

    • 活动创建

    • 活动启动

    • 活动恢复

    • 活动暂停

    • 活动停止

    • 活动销毁

让我们看看之前提到的代码是如何工作的。

当你运行项目时,模拟器将按照之前给出的顺序在屏幕上显示所有的提示。在项目开始时,会创建一个活动,然后启动该活动。活动启动后,它会在屏幕上显示,并且模拟器会打印恢复。现在,我们通过按后退键返回,Android 系统准备结束该活动。因此,活动首先会被暂停,然后停止,最后被销毁。所有这些回调一起被称为活动生命周期。活动生命周期从onCreate()方法开始,在onStop()方法结束。活动从onStart()方法到onStop()方法是可见的,并且从onResume()方法到onPause()方法活动保持在前台。下图展示了这个周期的分布:

活动的回调方法

活动生命周期流程

到目前为止,我们已经讨论了使用的生命周期回调方法、它们的状态和目的。现在,我们将探讨回调方法的流程。在 Android 中,当一个活动被启动时,已经打开的活动会被停止,这种活动的变化是按流程发生的。下图展示了活动生命周期的可视化流程图:

活动生命周期流程

回调方法用矩形表示。活动生命周期的第一步是创建一个活动。如果同一任务中没有运行该活动的实例,Android 会创建一个活动。noHistory标签不允许有多个活动,它会决定活动是否会有历史痕迹(参考developer.android.com/guide/topics/manifest/activity-element.html#nohist),您可以通过android:launchmode标志标签确定多个实例。将此标签的值设为true意味着在堆栈中只创建一个活动实例,并且每次调用活动意图时,都会将同一实例推送到堆栈顶部以在屏幕上显示活动。

onCreate()方法之后,会调用onStart()方法。这个方法负责初始化设置,但最佳实践是在onResume()方法中进行配置,该方法是继onStart()之后被调用的。请记住,前台阶段是从onResume()方法开始的。假设用户在电话中接到一个电话,那么这个活动将通过onPause()方法暂停。因此,在活动暂停时涉及存储必要数据的所有步骤都应该在这里完成。在关键内存情况下,这个方法可能非常有用,因为在这种情况下,Android 可能会停止暂停的活动,这反过来可能会导致应用程序出现意外行为。如果活动因关键内存情况被杀死,那么会调用onCreate()方法,而不是onResume()方法,这将导致创建活动的新实例。

但是,如果一切顺利,活动将通过onResume()方法返回到其之前的状态。在这个方法中,用户可以重新加载在onPause()方法中存储的所有数据,可以让活动恢复生机。在onResume()启动后关闭活动,会调用onStop()方法。这会根据用户行为触发onRestart()方法或onDestroy()方法。简而言之,开发者可以利用回调方法控制活动的生命周期。一种良好的实践是使用onPause()onResume()方法进行数据管理,无论活动是否保持在前台,而onCreate()onDestroy()应分别只用于初始数据管理和清理资源。

注意

除了onCreate()方法之外的所有回调方法都不带参数或论据。在关键内存情况下,如果活动被销毁,那么在创建该活动时,该实例状态会传递给onCreate()方法。

并没有必要重写所有的方法。用户可以根据需要重写任意数量的方法,因为没有这样的限制。用户应该在onCreate()方法中设置视图。如果你没有为内容设置任何视图,将会显示一个空白屏幕。在每一个回调中,首先应该调用超类的同名回调方法,然后再进行其他操作。这个超回调方法通过 Android 系统开发的标准流程来操作 Activity 的生命周期。

总结

在本章中,我们探讨了 Android 的关键概念。我们讨论了 Android 及其以糖果命名的版本,回顾了 Android 的历史,以及其创始人如何与谷歌一起发布 Android。我们还讨论了 Google Play,这是 Android 应用的官方商店;Android Studio,这是谷歌推出的官方 IDE,以及它的功能和局限性。然后我们从开发的角度进行了讨论,并介绍了任何 Android 应用的基础构建模块。我们还讨论了 Activity 生命周期,这在任何 Android 应用中都扮演着非常重要的角色,其流程、回调方法,并查看了一个示例。

在下一章中,我们将讨论 Android 中意图的作用、技术概览、结构及其在 Android 中的用途。

第二章:Android 意图介绍

复习上节课的内容——Android 活动是控件、小部件以及用户交互的许多其他事物的可视化表示。一个 Android 应用是由许多相互交互的活动组合而成,以便执行一个或多个应用所专门指定的任务。大多数情况下,在特定时间屏幕上只显示一个活动。某些操作(如按钮点击或手势)可能导致从当前活动导航到活动堆栈上的一个新活动。

Android 意图帮助开发者执行两个活动之间的交互,但这并不是意图唯一的功能。这种交互包括从一个活动移动到另一个活动,从一个活动向另一个活动推送数据,以及在任何特定活动关闭后带回结果。简而言之,可以说意图是 Android 中一个抽象的术语,指的是需要执行的任务。随着你阅读这本书,随着时间的推移,我们将探索其他各种事物。

本章包括以下主题:

  • 意图在 Android 应用中的作用

  • 意图——技术概览

  • 意图的结构

提示

如前一章所讨论的 Android 活动、活动生命周期和活动堆栈的概念,是理解本章及后续章节的前提。如果你对这些基础知识没有概念,我们建议你先阅读第一章《理解 Android》,以便继续前进。

意图在 Android 应用中的作用

在本节中,我们将了解 Android 意图的范围。到目前为止,我们已经完全了解了为什么需要活动,以及为什么有必要维护和跟踪从一个活动到另一个活动的流程。

意图在 Android 应用中的作用

按钮点击(使用意图)在不同活动之间的导航及其通过活动堆栈的表示

可以说,这部分书籍是我们从 Android 意图中获得好处的总结。其范围涉及 Android 活动、服务、数据传输以及许多其他因素。我们将在以下列表中看到这一点:

  • 从一个活动过渡到另一个活动

  • 从一个活动向另一个活动传输数据

  • 与 Wi-Fi 和蓝牙建立连接

  • 访问 Android 摄像头

  • 使用 GPS 传感器获取当前位置

  • 发送短信和彩信

  • 自定义移动通话

  • 发送电子邮件和社交媒体帖子

  • 启动和控制 Android 服务

  • 处理广播消息

  • 更改时区

  • 通知栏提醒

  • 以及更多内容

我们现在将看看 Android 意图的每个关键角色。以下各节给出了关于 Android 意图这些主要特性的简短描述。

意图在 Android 活动中的作用

Intents 最重要的广泛应用是在 Android 活动中。Android 应用程序由许多活动组成,要在这些活动之间过渡,我们需要使用 Android Intents。在之前的图中,你可以看到在活动 1(左侧)内容填写完毕,用户点击输入数据按钮后,Android 将使用 Intents 导航到活动 2(右侧)。

除了之前提到的 Intents 的作用外,还可以用来调用其他应用程序,例如浏览器(从你的活动中打开特定网站)和电子邮件客户端(如 Gmail 或其他,通过发送捆绑信息,带上合适的主题和邮件正文)。

Intents 在活动间数据传输中的作用

现在很清楚,我们使用 Intents 从一个活动导航到另一个活动。但正如我们都知道数据在 Android 应用程序中的巨大作用,用户需要获取、操作和显示数据以执行特定任务。处理这些数据,并确保其安全地在活动之间传输,也是 Android Intents 的另一个目的。

在之前的图中,一旦用户在活动 1 中填写了表单,点击输入数据按钮后,Intents 将执行两项任务。一项是将用户从一个活动带到另一个活动,第二个任务是传输填写的数据到下一个活动,以便显示/计算结果。

Intents 在 Wi-Fi 和蓝牙传输中的作用

在应用程序内部,如果你想实现一个功能,让用户能够更改当前的 Wi-Fi/蓝牙连接,你需要使用 Android 的 Intents。通过 Android Intents,你可以轻松地提供内部接口,让用户在停留在应用内的同时,切换 Wi-Fi 和蓝牙连接。

Intents 在 Android 相机中的作用

当谈论 Android 应用程序时,Android 硬件可能非常重要。这些组件的使用可能是你 Android 应用程序的基本部分。以 1D 或 2D 条形码阅读器为例,应用程序需要扫描条形码并解码以提取信息。这个操作只能通过从应用程序内部打开相机来完成。相机的开启同样由 Android Intents 处理。

Intents 在 GPS 传感器中的作用

Android 应用程序市场在许多类别中都取得了惊人的成绩。如今市场上存在各种类型的 Android 应用程序,包括基于位置的应用程序,它们通过追踪用户的位置执行各种任务。开发者可以通过 Intents 轻松获取用户当前的地理位置,以满足计算需求。

Intents 在发送短信/彩信中的作用

Android 意图可以用来使你的应用程序能够发送短信/彩信。这个任务可以通过从你的活动中设置短信/彩信正文并将其设置在捆绑包中,以调用发送短信/彩信的本机内置应用程序来完成。然后可以通过实现广播接收器来增强此短信/彩信发送功能,这使得你的活动能够知道消息何时已发送或何时已投递。

意图在移动呼叫中的作用

应用程序中触发移动呼叫的任何条件都可以通过 Android 意图实现。Android 应用程序将使用内置应用程序拨打意图中以数据形式提供的任何特定号码。

意图在电子邮件和社交网络帖子中的作用

从你的应用程序访问 Gmail 发送电子邮件是由 Android 意图管理的。有一个意图调用,我们在其中放入收件人的电子邮件、附件、邮件正文和主题,并在活动上启动意图。这将打开带有这些参数填充的本机 Gmail 应用程序,用户可以将其发送给期望的收件人。

同样,我们可以通过意图发送各种社交网络更新,如 Facebook、Twitter 和 Google+。这个任务是通过使用共享意图完成的。我们将内容以文本或捆绑包的形式放入,并通过 Android 意图发送。然后 Android 打开所有可用的共享应用程序(如前图所示),并让用户选择最佳的共享应用程序。唯一条件是,需要有预先安装的应用程序,以便 Android 意图与之交互并发送所需的帖子,如下所示:

Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);

注意

Android 意图除了在墙上发帖或推送到时间线之外,没有透露社交网络的任何其他功能。要观察完整的功能集,互联网上有许多作为免费许可软件或付费的第三方 API。

电子邮件和社交网络帖子中意图的作用

使用共享意图在各种平台上分享帖子的意图

下一个屏幕截图显示了当您向用户提供了某个功能,例如用户想要通过 Gmail 分享时会发生什么。在这种情况下,屏幕将显示您的内容,并如下所示:

电子邮件和社交网络帖子中意图的作用

通过共享意图传递数据后的 Gmail 界面

意图在 Android 服务中的作用

与 Android 活动(Activity)类似,intents 也用于启动Android 服务(Services)。Android 服务基本上是 Android 应用在不对用户界面产生影响的情况下执行的长运行任务。即使用户切换应用,这一后台任务也可以继续运行。换句话说,服务可以与活动绑定,也可以独立运行以执行任务。不过,在所有情况下,都会使用 intent 来启动服务。

Intent 在广播接收器中的作用

Intent 在广播接收器中有着广泛的应用。广播接收器用于响应任何其他应用甚至系统发起的广播消息。在这个背景下,我们捕获 Android 消息并提取数据,以便在我们的应用中显示。例如,当我们需要接收系统启动完成信号时,会使用 Intent.ACTION_BOOT_COMPLETED。同样,许多其他的 intent 值和 intent 对象可以在应用的不同位置使用,以便执行与广播接收器相关的各种任务。

另一个例子可以是发送短信/彩信,你可以创建一个广播接收器来查看发送是否完成或消息是否已投递。

Intent 在时区中的作用

你的应用可能需要做与时区相关的操作。一旦你在旅行中遇到时区变化,你可能希望应用能做出不同的响应。在这种情况下,我们可以使用广播接收器来检测时区的变化,从 intent 中获取数据以访问当前时区,并执行特定任务。这是一种非常方便的方法,可以根据你的时区来维护应用结构和数据。

Intent 在状态栏中的作用

Android 状态栏用于向用户提供即时通知,同时不会占用屏幕过多空间。从上至下滑动的通知面板包含许多功能,例如一些快速访问项,如无线连接管理器(目前仅在少数手机上可用)等。我们可以将通知放在状态栏中,以告知用户任何信息。Android Intents 用于放置内容,并在其中提供状态栏通知。

Intent 在状态栏中的作用

Android 通知面板和通知

Intent – 技术概览

在前几节课中,我们已经对 Android Intents 进行了理论上的概述。现在让我们深入了解一下这一 Android 特性的技术细节。在本部分,我们将从更宏观的角度来看待 Android Intents,包括示例代码及其解释。

从技术上讲,Android Intent 由两个组件组成,这两个组件都可以独立工作。这两个组件如下:

  • 编码组件

  • XML 组件

编码组件

在编写类时,会在 Java 代码中实现 Android Intents。通常在 Android 项目中,我们会为处理活动(activity)创建一个单独的包。如前所述,为了记录应用程序的完整追踪,有一个AndroidManifest.xml文件,其中应包含每个活动(activity)、服务(service)、权限(permission)以及其他内容的记录。

提示

在实现代码时,我们需要确保所有活动(activity)都在AndroidManifest.xml文件中声明,以便从代码中访问它们。否则,Android 将抛出ActivityNotFoundException错误。

为活动(activity)、服务(service)和广播接收器(Broadcast Receivers)实现 Android Intents 的方式是相同的。在活动(activity)中实现 Android Intents 时,我们需要注意以下事项:

  • 在实现 intent 之前导入android.content包(这是 Android 的父包,其中包含 intent 类)。

  • Intent 构造函数应该在 Android 活动的上下文中。如果不是,它应该具有上下文对象,以确定在哪个活动中调用 intent。

  • 应导入目标活动类(如果它位于源活动所在包的其他包中)。

  • 只有在 intent 处于 Android 活动的上下文中,或者如果源活动的上下文存在于上下文对象中,你才能调用startActivity()方法。

XML 组件

intent 依赖的第二个也是最重要的组件位于AndroidManifest.xml文件中。回顾一下这个文件包含的内容——AndroidManifest是包含有关应用程序所有信息的文件。它包含所有活动(activity)、服务(service)、权限(permission)、版本代码(version codes)、sdk 版本等许多其他信息。

同样,此文件中也提到了 Intent Filters。此时,我们只想涵盖 Intent Filters 的主要用途。以下是 Intent Filter 的简要介绍:

  • Intent Filters 有一些条件,必须满足这些条件才能处理 Android Intents。

  • Intent Filters 包含有关 Android Intent 的数据和类别(category)的额外信息。

简而言之,Intent Filter 描述了 Android 系统如何识别在特定 Android Intent 上应采用的行为。

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

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

提示

你可以在AndroidManifest.xml中的单个活动中包含许多 Intent Filters。

如前述代码所示,明确指出<intent-filter>标签包含有关类别(category)和动作(action)的信息。当应用程序尝试执行系统未知任务时,这些标签是AndroidManifest中活动(activity)的重要组成部分。

要理解为什么在 Intent Filter 的代码中会出现<category>,可以举个例子:我们开发一个应用,其中有两个活动。如果我们没有告诉系统哪个是启动应用时的第一个活动;系统就会混淆并显示错误No Launcher Activity Found,并且不会启动应用。因此,为了完成这项任务,我们需要将其中一个活动的类别设置为android.intent.category.LAUNCHER。这将帮助系统识别应用启动的基础活动,并继续流程。

实现 Android Intents 进行活动导航

在这一节中,我们将看看 Android Intent 的实现。让我们开始吧。

要开始这个示例,你需要构建一个 Android 项目。你可以使用 Android Studio 或 Eclipse(根据你的方便),但如果你使用 Eclipse,确保你已经正确安装了 JDK、ADT 和 Android SDK,以及它们的兼容性包。如果你不知道这些 IDE 之间的区别,可以参考本书的第一章,《理解 Android》。

在 Android Studio 中创建项目的内容在上一章已经介绍过。重复这些步骤可以帮助你创建一个带有一些预定义文件和文件夹的完整 Android 项目。

要开始实现 Android Intents,你需要执行以下步骤:

  1. 创建一个新的 Android 项目,或者选择任意一个现有的你想要在其中实现 Android Intents 的项目。

  2. 打开你想要实现意图的源活动。

    注意

    提醒一下,当活动中有事件调用发生时,会调用意图。例如,点击按钮时,下一个活动应该出现。所以按钮点击就是事件。

  3. 实现以下代码以实现此结果:

    //-------------------------------------------------------------------------
    Part One - MainActivity Class
    
    public class MainActivity extends Activity {
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button1);
        button.setOnClickListener(new OnClickListener() {
    
          @Override
          public void onClick(View v) {
            // TODO Auto-generated method stub
            Intent myIntent = new Intent(MainActivity.this,MySecondActivity.class);
            startActivity(myIntent);
          }
        });
      }
    }
    
    //---------------------------------------------------------------------------
    Part Two - MySecondActivity Class
    
    public class MySecondActivity extends Activity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_two_layout);
        Toast.makeText(this, "The Intent has been called...",Toast.LENGTH_LONG).show();
      }
    }
    
    //----------------------------------------------------------------------------
    Part Three - activity_main.xml File
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical" >
    
      <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Press to navigate"
      />
    
    </LinearLayout>
    
    //----------------------------------------------------------------------------
    Part Four - activity_two_layout.xml File
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical" >
    
    </LinearLayout>
    
    //---------------------------------------------------------------------------
    Part Five - AndroidManifest.xml File
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest 
      package="com.app.fragmenttestingapplication"
      android:versionCode="1"
      android:versionName="1.0" >
    
      <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="16" />
    
      <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
      <activity
        android:name="com.app.fragmenttestingapplication.MainActivity"
          android:label="@string/app_name" >
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
    
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>
      <activity
        android:name="com.app.fragmenttestingapplication.MySecondActivity"
        android:label="@string/app_name" >
      </activity>
    </application>
    
    </manifest>
    
  4. 运行项目,会出现一个按钮。点击按钮,通过意图导航到下一个活动。

实现 Android 意图进行活动导航

指示应用从第一个活动到第二个活动的流程

理解流程

之前的代码分为五个部分;我们将逐一描述它们。请记住,这些部分指的是 Android 项目中的五个不同文件。考虑到你将在这个预定义项目中使用这些代码,我们将从新创建项目的角度来描述,以便使其更加详尽和清晰。

第一部分 – MainActivity.java

MainActivity.java是在创建项目时生成的第一个类。由于它是一个 Android 活动,因此它带有一个onCreate方法,该方法在活动创建后立即被调用(如第一章,理解 AndroidAndroid 活动生命周期所述)。与此活动关联的布局名为activity_main.xml。因此,在onCreate()方法中,行setContentView(R.layout.activity_main)指的是那个 XML,并用于根据activity_main.xml中的布局设置该活动的视图。

现在,在第二步中,通过在Activity类中使用findViewById(int id)方法,在代码中获取activity_main.xml布局中具有 ID button1的按钮。它将返回View类的对象,因此我们可以轻松地将其转换为按钮以获得按钮对象。

一旦提取了按钮对象,我们就在其上实现setOnClickListener()方法。setOnClickListener()方法属于View类,它需要一个View.OnClickListener(一个接口)作为参数。这个接口要求我们重写onClick()功能以实现。每当在 UI 中点击按钮时,都会触发这个事件。

在这个onClick()方法内部,将进行意图的实际实现。由于我们希望在点击按钮时调用我们的意图,因此我们将在该方法中处理所有与意图相关的操作。使用带有上下文和目标类参数的构造函数声明意图对象。MySecondActivity.class文件是我们想要导航到的目标活动,而我们通过获取MainActivity(或者你可以使用getContext()方法,它基本上返回相同的活动上下文)来访问当前活动的上下文。这是因为我们现在处于OnClickListener的上下文中。

在此阶段,我们拥有了意图对象。我们可以用这个对象做更多的事情,但目前我们的任务是仅将其导航到下一个活动。这就是为什么我们要调用startActivity()方法并将此意图作为参数传递。这将把应用导航到下一个活动,该活动将从活动堆栈顶部出现,而前一个活动将位于其下方。

第二部分 – MySecondActivity.java

MySecondActivity.java活动是意图的目标活动。这是一个简单的活动,其中包含一个onCreate()方法,该方法在屏幕上设置内容视图。在布局创建后,我们通过show()方法在屏幕上显示一个吐司消息,以便识别第二个活动已被加载并在显示消息The intent has been called

注意

吐司消息是一个简单的消息框,显示几秒钟后消失。默认情况下,这个消息包含文本,但可以轻松定制吐司以包含图片和其他许多内容。

第三部分 – activity_main.xml

这个 XML 文件是MainActivity.java类的布局。如您所见,我们试图从布局中获取按钮的引用。这个按钮在activity_main文件中声明,并带有用于从 XML 中提取的 ID。

注意

所有在 XML 文件中声明的布局引用都放在 R 文件中,Java 代码/类通过它来在代码中获取对象。

描述activity_main.xml文件,有一个带有关于其长度、宽度和方向的某些参数的线性布局。在线性布局的标签内,你可以看到被带入 Java 代码中的按钮声明。这个标签还带有关于高度、宽度、ID 以及将在布局中显示的文本的某些参数。

注意

为了给任何视图分配 ID,请采取预防措施。所有这些 ID 都存在于 R 文件中,该文件在一个类中处理所有内容。尝试自定义你的 ID,以避免将一个活动的视图与另一个混淆。

第四部分 – activity_two_layout.xml

这是一个简单的布局文件,包含父级线性布局,以创建一个简单的空白活动。这个布局文件被分配给第二个活动,用户点击按钮后意图会带用户到这个活动,并在这里显示吐司消息。

第五部分 – AndroidManifest.xml

没有完整的AndroidManifest.xml文件,因为该文件包含有关应用程序的所有信息。在这个 XML 文件中,有一个 manifest 父标签,其中包含关于项目的三个最重要的属性:

  • 包名:这是项目的名称,应用程序将由此名称进入谷歌应用市场(或其他任何市场)。这个名称应该为整个应用程序唯一定义,并且不应与市场上其他任何包名匹配。

  • 版本代码:这是一个整数值,表示与之前版本应用程序相比的版本号。

  • 版本名称:这是一个字符串值,用于向用户显示。它是应用程序的发布版本名称。

Manifest标签内,有一个名为<uses-sdk>的标签,在其属性中定义了应用程序可访问的最低和最高 Android API 版本。在此之后是一个应用程序标签,其中存储有关应用程序的信息,包含图标、标签和应用程序的主题。

在主标签中,描述了应用程序中的活动,有<activity>标签。它的数量应与应用程序中使用的活动数量相等。如你所见,意图的 XML 组件存在于第一个活动中,即<intent-filter>,它告诉系统MainActivity.java是应该用作启动器的活动类。与第一个活动不同,第二个活动不包含意图过滤器的标签。你可以分析出,第二个活动无需包含这些标签,因为它与第一个活动在流程中是一致的。

从第一个活动到第二个活动,前景意图应该是连续的。这就是为什么它不需要包含<category><action>标签的原因。即使没有这些,应用程序也能正常工作。

未来的考量

在本书中,我们将详细了解我们还可以用意图做些什么。到目前为止,我们只介绍了意图的基本功能以及它在活动栈中的流程。在接下来的内容中,你将遇到很多相关知识。

android.content.Intent 类的其他构造函数

Intent 类提供了多种构造函数,帮助开发者在各种场景中。在上一节中,我们只使用了一种构造函数类型。其他构造函数的多态形式在 Google 上也是可用的。

以下各节将解释各种构造函数。

Intent()

这是默认构造函数,它返回一个空的意图对象。但是,这里的“空”并不意味着它是一个 null 对象。

Intent(intent o)

这个构造函数用于创建一个现有意图的克隆。我们传递想要克隆的意图对象,并将其作为构造函数的参数。在这种情况下,返回的意图是原始意图的副本。它还将映射原始意图中放入的每个值(额外数据)并返回该意图的副本。

Intent(Context c, Class<?> cls)

这是我们在前面示例中使用的构造函数。它基本上接收两个参数:源上下文和目标类。源上下文是你当前正在使用的活动的上下文,而第二个参数是你想要导航到的类。

Intent(String action)

带有动作的构造函数用于创建一个写入动作的意图对象。动作的正确使用和定义,如在意图中的使用,将在接下来的章节中介绍。还要记住,这个构造函数用于广播动作。

Intent(String action, URI uri)

这个构造函数用于创建一个带有期望动作以及一些数据 URL 的意图。例如,我们传递参数new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.google.com"));。这个构造函数清楚地表示意图将支持用于查看的动作,并通过另一个参数解析 URL 的 URI www.google.com。这将打开浏览器,其中将加载 Google 的网站。再看一个这种构造函数的例子:new Intent(Intent.ACTION_DIAL, Uri.parse("tel: (+1) 123456789"));。通过编写这个语句,我们清楚地表明我们想要创建一个用于激活电话拨号器的意图,并通过 URI 的值来执行通过意图拨打电话的功能。

注意

这种形式的构造函数主要用于调用隐式意图。关于代码的更多信息可以在developer.android.com/reference/android/content/Intent.html找到。

从 Android Intent 获取结果

正如我们之前所见,Android Intent 用于从一个活动导航到另一个活动,但在实际场景中,这种导航需要许多其他东西。这里将讨论 Android Intent 最重要的特性之一。我们将在这里看到的是,一旦目标活动关闭,源活动将获得什么响应。

为了进一步解释前面的陈述,我们有一个场景:

  • 有一个源活动(将从这里开始导航)

  • 一个目标活动(将进行导航的地方)

  • 目标活动关闭时,它将向源活动返回一个结果。

这是 Android Intent 中一个非常方便的选项,用于从活动中带回结果。我们将通过示例代码详细讨论这个特性。

通过示例理解

在这个例子中,我们将查看有两个活动的情况。第一个活动将作为启动活动。第二个活动将作为目标活动,它还将向第一个活动返回一些结果。第一个活动将捕获结果,并根据该代码决定第二个活动中完成了或失败的任务类型。最后,根据返回的结果,在第一个活动中显示一些对话框消息。

深入示例

再次,以下代码分为五部分。这是上一个示例的修改,其中描述了意图的正常使用。为了实现这个示例,请执行以下步骤:

  1. 创建一个新项目或打开任何现有项目,以便进行更改。

  2. 打开源活动,在其中实现意图。

  3. 实现以下代码:

    //----------------------------------------------------------------
    //Part One - MainActivity Class
    
    public class MainActivity extends Activity {
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button1);
        button.setOnClickListener(new OnClickListener() {
    
          @Override
          public void onClick(View v) {
            // TODO Auto-generated method stub
            Intent myIntent = new Intent(MainActivity.this, MySecondActivity.class);
            startActivityForResult(myIntent, 1);
          }
        });
      }
    
      @Override
      protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    
      if(requestCode == 1){
        if(resultCode == RESULT_OK){      
        Toast.makeText(this, "The Processing is succesfully done...", Toast.LENGTH_LONG).show();       
        }
        if (resultCode == RESULT_CANCELED) {    
          Toast.makeText(this, "The Processing failed,try again later..", Toast.LENGTH_LONG).show();
        }
      }
    }
    }
    
    //----------------------------------------------------------------
    //Part Two - MySecondActivity Class
    
    public class MySecondActivity extends Activity {
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_two_layout);
        Toast.makeText(this, "The Intent has been called...", Toast.LENGTH_LONG).show();
    
        Intent returnIntent = new Intent();
    
        CheckBox yesCheckBox = (CheckBox)findViewById(R.id.checkBox1);
        CheckBox noCheckBox = (CheckBox)findViewById(R.id.checkBox2);
        Button button = (Button) findViewById(R.id.button2);
        button.setOnClickListener(new OnClickListener() {
    
          @Override
            public void onClick(View v) {
            // TODO Auto-generated method stub
            if(yesCheckBox != null && yesCheckBox.isChecked()){
              setResult(RESULT_OK, returnIntent);
            finish();
          }else if(noCheckBox != null &&noCheckBox.isChecked()){
           setResult(RESULT_CANCELED, returnIntent);
           finish();
           }
          }
        });
      }
    }
    }
    
    //----------------------------------------------------------------------------
    Part Three - activity_main.xml File
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical" >
    
      <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Press to navigate"
      />
    
    </LinearLayout>
    
    //----------------------------------------------------------------------------
    Part Four - activity_two_layout.xml File
    
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout 
    
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      tools:context=".MainActivity" >
      <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/checkBox1"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="24dp"
        android:text="@string/return_string" />
    
      <CheckBox
        android:id="@+id/checkBox2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBox1"
        android:layout_alignBottom="@+id/checkBox1"
        android:layout_toRightOf="@+id/button1"
        android:text="@string/no_string" />
    
      <CheckBox
        android:id="@+id/checkBox1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_marginTop="50dp"
        android:layout_toLeftOf="@+id/button1"
        android:text="@string/yes_string" />
    
    </RelativeLayout>
    
    //---------------------------------------------------------------------------
    Part Five - AndroidManifest.xml File
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest 
      package="com.app.fragmenttestingapplication"
      android:versionCode="1"
      android:versionName="1.0" >
    
      <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="16" />
    
      <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
          android:name="com.app.fragmenttestingapplication.MainActivity"
          android:label="@string/app_name" >
          <intent-filter>
          <action android:name="android.intent.action.MAIN" />
    
          <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
      </activity>
      <activity
        android:name="com.app.fragmenttestingapplication.MySecondActivity"
          android:label="@string/app_name" >
          </activity>
      </application>
    
    </manifest>
    
  4. 运行项目,会出现一个按钮。点击按钮,通过意图导航到下一个活动。

  5. 当意图成功执行后,新的活动将会出现。点击模拟器或你正在测试的 Android 手机上的返回键,系统将会带你回到第一个活动。

  6. 返回第一个活动后,它会显示吐司通知,以便从目标活动中传达结果。深入示例

    指示应用程序从第一个活动到第二个活动,并将结果返回到第一个活动的流程

解释代码

在这个例子中,如前所述,我们将了解如何获取结果,以验证(在第一个活动中)在第二个活动中执行的任务是否成功完成。

在继续之前,请记住这是之前定义例子的扩展;因此,我们之前讨论过的内容在这个例子中不会解释。如果你在这里发现任何难以理解的内容,请阅读之前的例子。

让我们一步一步开始。

第一部分 – MainActivity.java

与之前一样,这是在点击按钮后开始导航的第一个活动。这个活动的大部分内容与之前的例子相同,但如果你仔细观察,会发现使用了startActivityForResult()方法来启动活动。这是因为我们启动的活动将在第二个活动关闭后返回一个结果。

startActivityForResult()方法有两个参数。第一个是意图,与之前描述的startActivity()方法类似。让我们看看第二个参数是什么。设想一个场景,第一个活动调用了多个将返回结果的活动。现在,在捕捉结果时,我们需要确定它来自哪个活动。因此,我们分配一个请求码,当意图将用户带回第一个活动时,它将返回结果。然后我们将进行检查,以了解结果来自哪个活动。

接下来,如果你仔细查看代码,会发现有一个被重写的方法名为onActivityResult()。当第二个活动关闭时,这个方法将被调用。正如你在代码中所见,它有三个参数。第一个是requestCode,它与发送结果返回的活动相符合。除此之外,我们还有resultCode,它会告诉你在第二个活动中执行的任务是否成功。第三,我们有意图,基本上是这个对象导致活动返回到第一个活动。我们还可以通过这个意图将一些数据发送回第一个活动。

第二部分 – MySecondActivity.java

这与第一个示例相同;实际上,它包含了布局文件中的两个复选框对象,这些对象用于在第二个活动中生成结果。我们通过 ID 找到视图,并根据选中的复选框判断哪个是启用的。当按下按钮时,它会检查哪个复选框被启用,并根据启用的优先级调用setResult()函数。这个函数为意图设置一个结果,这将帮助活动返回到第一个活动。

设置结果后,我们将结束第二个活动,并带着结果返回到第一个活动。在活动结束后,第一个活动的onActivityResult()方法将被执行,以便查看由第二个活动发送的结果。

第三部分 – activity_main.xml

这部分代码与第一个示例相似;请参考第一个示例的解释。

第四部分 – activity_two_layout.xml

这个布局文件指的是第二个活动。我们放置了两个复选框,根据这些复选框的决定,将决定要发送哪个结果返回到第一个活动。有一些属性与上一个示例中的相同,因此可以参考那个示例。

第五部分 – AndroidManifest.xml

同样,此文件未作修改,与第一个示例相同。请参考第一个意图的简单示例。

未来考虑事项

我们可以关注两种未来考虑的场景。第一种是当所有活动都在发送结果回来时如何处理两个或更多活动。这当然是通过requestCode参数的帮助。第二重要的是,在只发送响应码的场景中,可能还需要发送一些字符串、整数值或自定义对象作为结果。在这种情况下,我们需要其他方法通过意图将这些对象发送回第一个活动。

注意

在第五章,使用意图进行数据传输,我们将全面了解如何在意图中传输不同类型的数据。

意图的构造

在本节中,我们将研究在 Android 中使用的意图对象的结构。意图是包含多个事物以方便的信息包。它包含了在执行意图时应采取的操作信息。同样,它也包含了将处理意图的类别信息。数据在意图中起着至关重要的作用。因此,意图具有以 URI 形式执行的数据信息。

注意

由于这里空间有限,我们无法解释每个组件中的每个常量。为了获取 Android 使用的完整常量列表,您可以在这里查看:

developer.android.com/reference/android/content/Intent.html

我们现在将逐一了解这些组件,以了解它们的真正含义。

组件

这将解释哪个组件将受到特定意图执行的影响或处理。例如,有一个负责执行与调用它的活动相关的动作的意图。同样,有一个由广播接收器处理的意图,在执行某些与系统相关的任务时会报告。如果没有设置组件名称,它将使用其他信息自行识别组件。下表显示了在 Android 意图中使用的不同类型的组件:

常量 描述
CATEGORY_BROWSABLE 表示该意图可以安全地由浏览器执行以显示数据。
CATEGORY_LAUNCHER 表示在应用程序启动时,该活动应作为启动器执行。
CATEGORY_GADGET 活动可以放入从任何小工具启动的另一活动中。

操作

操作基本上指明了这个意图将引发什么动作。例如,如果我们初始化一个名为ACTION_CALL的动作的意图对象,这个意图将通过以 URI 形式传递带有ACTION_CALL动作的数据字符串来启动通话功能。再以ACTION_BATTERY_LOW为例,它与广播接收器组件相关。将此动作放在意图过滤器中,如果电池电量低于阈值,它将触发事件(或简而言之,弹出低电量提示)。

Android 中存在各种类型的操作。下表显示了一些意图操作及其描述:

常量 组件 描述
ACTION_CALL 活动 开始电话通话
ACTION_EDIT 活动 显示用户数据以进行编辑
ACTION_MAIN 活动 作为初始活动启动,无数据
ACTION_SYNC 活动 将服务器上的数据与移动设备同步
ACTION_BATTERY_LOW 广播接收器 显示电池电量低警告
ACTION_HEADSET_PLUG 广播接收器 当插入或拔出耳机时显示警告
ACTION_SCREEN_ON 广播接收器 当屏幕开启时触发
ACTION_TIMEZONE_CHANGED 广播接收器 当时区设置更改时

数据

这不应被视为独立的组件;相反,它是用来辅助动作组件的。如前所述,有些组件需要传递一些数据。例如,ACTION_CALL函数需要通过一个数据值来识别应该拨打哪个电话号码。在这种特定情况下,我们需要将tel: xxxxxxxxxxx URI 放入数据中,并将其转发给动作。同样,当执行ACTION_EDITACTION_VIEW动作时,它们需要提供一个文档或 HTTP URL 以完成动作。数据以URI通用资源标识符)的形式提供给意图。

附加信息

这些基本上是 Android Intent 所需的附加数据的键值对。我们可以从代码中获取这些值(当我们创建意图对象时),并将这些数据传递到下一个活动。就动作而言,有些动作需要额外的数据来完成任务。例如,ACTION_TIMEZONE_CHANGED动作需要一个额外的时区,该时区描述了新的时区,基于此可以执行进一步的任务。

总结

在本章中,我们讨论了意图的介绍及其角色、技术概览、在 Android 应用程序中的基本实现,以及基于意图的结构,我们将进一步探索可以执行的不同类型的任务。本章还提供了两个非常重要的 Android Intent 实现,其中一个是活动到另一个活动的导航,而在第二个中,将特定活动的结果发送回第一个活动。本章讨论的概念是理解 Android Intent 高级概念的关键工具,这将在本书的后面讨论。在下一章中,我们将学习关于 Android Intent 的分类及其理论,以及在各种实用示例中的实现,这些示例可以轻松地在 Eclipse 环境中实现。

第三章:意图及其分类

意图 是用来激活一个 Android 组件的异步消息,通过另一个组件来实现。这些意图用于在发生某些事件时触发 Android 操作系统,并采取一些行动。根据接收到的数据,Android 操作系统确定意图的接收者并触发它。

通常,有两种类型的意图:显式隐式。顾名思义,显式意图由开发者明确指定触发 Android 操作系统的特定组件。然而,隐式意图触发的是 Android 操作系统任何类别的一般组件。由 Android 操作系统决定需要激活哪个组件。如果有一个以上的通用组件,系统会要求用户从所有组件的列表中选择一个。Android 操作系统中意图的这项功能使得应用程序更具交互性,因为其他应用程序和开发者也可以访问它。例如,你正在开发一个图片编辑的 Android 应用程序,用于编辑任何图片,应用滤镜等。所以,如果应用程序接收到来自 Android 系统任何来源的图片,比如电子邮件附件、图库图片、其他图片工具等,与仅从应用程序本身加载图片的应用程序相比,该应用程序将变得更加互动和响应迅速。这种应用程序的互动,无论是通过电子邮件发送图片还是在编辑应用程序中接收图片,都是通过隐式意图实现的。

在本章中,你将了解到以下主题:

  • 意图的类型

  • 显式意图

  • 在 Android 应用程序中使用显式意图

  • 隐式意图

  • 在 Android 应用程序中使用隐式意图

  • 意图和 Android 晚期绑定

提示

如前一章所讨论的意图及其结构的概念,是理解本章及后续章节的前提条件。如果你对这些内容没有基本的了解,请阅读 第二章 Android 意图简介,以便继续学习。

这两种意图,即显式和隐式意图,在功能上有很大的不同。让我们从最简单的意图类型开始说起。

显式意图

最简单的意图类型是显式意图。当开发者知道要使用哪个组件,并且不希望向用户提供自由访问时,显式意图是最佳选择。在这里,开发者在意图声明中明确指定了要触发的组件。这个组件可以是任何活动、服务或广播接收器。例如,一个 Android 应用程序通常包含一个以上对应功能的活动。为了从一个活动导航到另一个活动,会使用显式意图。以下代码段展示了这样一个显式意图的简单声明,它从活动 A 指向活动 B:

显式意图

意图是android.content.Intent类的实例。显式组件被指定为 Java 类标识符,它们被 Android 操作系统用于在开发者发送意图时激活它们。如前几章所述,意图对象接收两个参数:上下文。上下文参数接收触发其他组件激活的源上下文。类参数取特定类的类对象,用于指定目标组件。在前面的代码段中,intent对象以ActivityA类作为源组件,以ActivityB作为要激活的目标组件。现在,这个在代码段中声明的具有特定组件作为目标的intent对象可以在任何地方使用。例如,它可以用于以ActivityA作为父活动启动ActivityB。以下部分描述了使用显式意图触发各种组件(如活动、服务等)的用途。

在 Android 应用程序中使用显式意图

在本节中,我们将讨论在 Android 应用程序中显式意图的各种用途。如前所述,显式意图可以用于激活其他组件,如活动、服务和广播接收器。我们将讨论两个显式意图的示例;第一个是从一个活动启动另一个活动,另一个是在后台启动服务。

通过显式意图启动一个活动

任何 Android 应用程序至少包含一个或多个活动。因此,在存在多个活动的情况下,如何在它们之间导航对开发者来说变得很重要。在本节中,我们将开发一个包含两个活动的示例,我们将从一个活动启动另一个活动,并在停止/完成当前打开的活动后返回到它。下图展示了我们即将开发的示例的简单原型:

通过显式意图启动一个活动

如前图所示,我们有两个活动:主活动(Main Activity)第二个活动(Second Activity)。这两个活动都有一个用于导航的单一按钮和一个显示活动名称的标题。现在,让我们开始开发第一个示例。但是,要开始这个示例,你需要构建一个 Android 项目。你可以使用 Android Studio 或 Eclipse(根据你的方便),但在使用 Eclipse 的情况下,请确保你已经正确安装了 JDK、ADT 和 Android SDK 及其兼容性。如果你不知道这些 IDE 之间的区别,请参考本书的第一章。在上一章中已经解释了如何在 Android Studio 中创建项目。重复这些步骤将给你一个带有一些预定义文件和文件夹的完整 Android 项目。

创建一个空的 Android 项目后,我们将实现显式意图的使用。在示例中,我们将编写或修改许多不同类型的文件,如 Java、XML 等。每个文件都有其自身的目的,并在示例中执行其自身的功能。现在,让我们逐一探索这些文件。

MainActivity.java 类

项目的主文件是MainActivity.java。以下是在此文件中需要实现的代码片段:

MainActivity.java 类

拥有一个以上活动的应用程序必须有一个主活动。这个主活动定义了应用程序的启动点。在我们的示例中,MainActivity.java类是项目的主活动。为了向活动提供布局文件以进行视觉展示,我们将在活动的onCreate()方法中调用setContentView(R.layout.activity_main)activity_main.xml文件是项目目录中layout文件夹里的布局文件,代表了应用的主屏幕。设置活动的视图后,我们可以通过从布局文件获取它们的视图来获取活动中将使用的所有组件。要获取任何组件的视图,我们将使用findViewById()方法,该方法接收视图的 ID 并返回一个View对象,根据我们的需求进行类型转换。在这个文件中,我们从布局文件中获取了 ID 为button1的按钮的View对象,并将其类型转换为Button并引用到我们的按钮。任何按钮都应该有监听器以便与视图进行用户交互并自定义视图的行为。在我们的文件中,我们只为按钮设置了View.OnClickListener以获取按钮的点击/轻触。

注意

Android SDK 中有两个OnClickListener类。一个位于View类中,用于诸如按钮、文本字段等视图。另一个位于DialogInterface类中,用于检测对话框中的点击和轻触。开发者在导入类及其包时应小心谨慎。

我们使用了Button对象的setOnClickListener()方法来设置按钮的OnClickListener对象。我们在方法的参数中引用了一个匿名监听器,并覆盖了onClick()方法。在onClick()方法中,我们需要提供当按钮被点击时我们想要展示的行为。

提示

匿名对象是开发者没有指定对象名称的对象。由于这个原因,开发者在代码中不能直接访问该对象。你也可以通过创建接口对象,并在setOnClickListener()方法中传递它来设置视图的OnClickListener对象。

我们已经创建了一个意图对象,其源上下文为MainActivity,而SecondActivity类是要激活的目标组件。这里需要注意的是,我们不是传递SecondActivity的对象,而是显式传递了SecondActivity的 Java 类表示。这就是我们声明一个显式意图对象的方式。现在,通过这个意图对象,我们可以根据需求执行许多不同的任务,但在我们这个案例中,选项是有限的。我们创建了一个包含非常具体信息的显式意图,因此,这个意图仅可用于有限的目的。在这个例子中,意图被用来通过调用startActivity()方法在返回栈上启动另一个活动。这个方法接收一个显式意图对象的参数,该参数具有源上下文和目标活动的信息。

提示

要在一个除了正在运行的活动之外的类中声明意图,我们可以使用应用上下文来传递意图构造函数的上下文参数。这可以通过getApplicationContext()方法来获取。

因此,总结这个文件的功能,该文件表示应用程序的启动点,显示了一个按钮。点击按钮后,应用程序将导航到另一个活动。这种活动之间的导航是通过一个显式意图对象实现的。在实现了MainActivity文件之后,让我们在SecondActivity.java文件中实现我们的第二个活动。

SecondActivity.java

SecondActivity.java类是我们显式意图的目标活动。以下是这个文件中实现代码片段:

类

同样,这个类是从Activity类扩展而来的,遵循活动生命周期。它也应该重写生命周期回调方法。在onCreate()方法中,我们通过调用setContentView()方法来设置活动的视图,这次,我们通过R.layout.activity_main2传递了activity_main2.xml文件的引用。这个 XML 文件放在了res目录下的layout文件夹中。我们再次通过调用findViewById()方法从布局文件中获取按钮的View组件,并将其类型转换为Button。然后,我们将按钮的OnClickListener()设置为一个匿名监听器,并重写onClick()方法来定义点击按钮时的行为。这次,我们在onClick()方法中调用了Activity.finish()方法。这个方法只是简单地将位于返回栈顶部的活动移除。

由于我们是直接从MainActivity启动的SecondActivity,当我们结束SecondActivity时,我们将会再次看到MainActivity

提示

安卓设备上的返回按钮仅仅调用了活动(Activity)的finish()方法,或者是对话框(Dialog)的dismiss()方法。

我们还可以创建一个带有SecondActivity上下文和目标类MainActivity.java的意图对象。但这会在返回栈中创建MainActivity的新实例,并将其推到SecondActivity的顶部。在这种情况下,我们在返回栈中会有两个MainActivity实例和一个位于它们之间的SecondActivity实例。

注意

如果我们在AndroidManifest.xml文件中MainActivity<activity>标签里将android:noHistory属性设为true,那么启动MainActivity的新实例将会导致已创建的实例被置于返回栈的顶部,从而避免了新对象的创建。开发者在构建应用流程和导航控制时应该更加小心,因为这类流程可能导致应用中的循环。这可能会造成应用无休止的问题。

我们应该注意到,这两个活动文件MainActivity.javaSecondActivity.java几乎包含相同的代码,除了MainActivity的按钮监听器使用显式意图启动新活动,以及SecondActivity的按钮监听器只是通过自动按下安卓手机的返回按钮来将应用拉回。

到目前为止,我们已经了解到如何使用显式意图导航到另一个活动。但是,应该记住,这两个活动使用了不同的布局文件进行可视化表示,包含执行两项任务的操作按钮。需要注意的是,布局文件在显式意图的导航中不起任何作用。这些文件只是展示活动的视觉内容,以简化用户交互。现在,让我们关注这些布局文件。

activity_main.xml文件

activity_main.xml文件是MainActivity.java的布局文件,采用 XML 编写。以下代码展示了布局文件的实现:

activity_main.xml 文件

在布局文件中,我们有LinearLayout<Button>视图在布局内。请记住,这个按钮是在将活动的视图设置为OnClickListener之后,由MainActivity文件提取的。

注意

在 XML 文件中声明的所有布局和视图的引用都会在R.java文件中自动生成。在 Java 中,我们可以使用R类以静态方式访问组件。例如,我们可以使用R.layout.layout_name来引用布局。然而,在 XML 中,可以通过放置@来访问R。例如,访问颜色时可以使用android:name="@color/my_custom_color"

描述activity_main.xml文件,其中有一个带有关于heightwidthorientation参数的<LinearLayout>。正如你所见,在<LinearLayout>标签内部,声明了通过活动中的findViewById()方法引入 Java 代码的<Button>标签。这个标签还带有如idwidthheighttext等参数,这些参数将显示在布局中。

activity_main2.xml文件

activity_main2.xml文件布局是SecondActivity类的视觉表示。以下代码展示了这个文件:

activity_main2.xml 文件

这个布局与activity_main布局相同,只是按钮的文本值不同。这被用在SecondActivity.java类中。同样,按钮是从 XML 通过活动中的findViewById()方法引用到 Java 的,并且设置了OnClickListener以定义点击按钮时要执行的自定义操作。

没有包含AndroidManifest文件的 Android 应用程序是不完整的。我们已经实现了示例应用的视觉布局以及它们使用显式意图从一个活动导航到另一个活动的功能。然而,当应用程序有多个活动时,必须通知 Android 操作系统所有这些活动及其属性。为了告知 Android 操作系统使用了多少个活动类,使用了AndroidManifest文件。让我们看看这个文件是如何通知 Android 操作系统有关活动的。

AndroidManifest.xml文件

AndroidManifest.xml文件包含应用程序的所有设置和偏好。在处理多个活动时,开发者在声明活动时应小心。只有一个定义应用程序启动点的活动应该具有启动器的意图过滤器。所有活动都应该在这里保存。关于活动的信息可以包括活动的标签、活动的主题、方向等。对于活动来说,AndroidManifest.xml文件就像一个注册中心,应用中的所有组件,如Activity类、Service类等,都应该在这里注册。如果任何活动没有在AndroidManifest.xml文件中注册,Android 操作系统在调用该活动时将抛出ActivityNotFoundException异常。

关于我们的显式意图应用,两个活动MainActivitySecondActivity都在AndroidManifest.xml文件中注册。需要在此注意的是,MainActivity有一个<intent-filter>子标签,它将MainActivity声明为整个应用程序的启动或入口活动。以下是AndroidManifest.xml文件中实现的代码:

AndroidManifest.xml 文件

通过AndroidManifest.xml文件,我们的显式意图示例就完成了。在这个例子中,我们定义了两个活动;其中一个是主活动。主活动通过显式声明另一个活动的名称,使用显式意图来启动该活动。该示例包含了两个布局文件,用于两个活动以及清单文件的视觉表示,并注册所有活动以及应用程序的基本设置。当你运行项目时,你应该能看到如下所示的屏幕转换:

AndroidManifest.xml 文件

在下一节中,我们将了解显式意图在服务中的另一种用途。我们将学习如何从活动中显式使用意图来启动服务。

通过显式意图启动服务。

与活动不同,服务在后台执行特定的任务和动作。服务没有任何视觉布局或 UI。需要注意的是,服务运行在应用的主线程上;因此,当 Android 需要内存时,它会停止那些不在运行或处于暂停状态的服务。在下一个示例中,我们将使用显式意图启动服务,并使用相同的显式意图停止它。因此,为了开始这个示例,请使用任何 Android IDE(如带有 ADT 插件的 Eclipse 或 Android Studio)创建一个空项目。在这个项目中,我们将实现使用显式意图来启动/停止服务。由于我们的主要关注点是显式意图,我们将创建一个非常简单的服务,在被活动启动或停止时显示吐司通知。我们在示例应用程序中修改/实现了四个部分。下面,我们逐一看看这些文件的作用。

ServiceExample.java 类

由于我们的示例应用包含一个服务,这个文件是我们应用中使用的服务类的表示。以下代码展示了服务的实现:

public class ServiceExample extends Service {

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Toast.makeText(this,"Service Created",300);
    }

    @Override
    public void onStart(Intent intent, int startId) {
        super.onStart(intent, startId);
        Toast.makeText(this,"Service start",300);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Toast.makeText(this,"task perform in service",300);
        return super.onStartCommand(intent, flags, startId);
    }
}

在 Android 开发中,我们必须从Service类扩展我们的类,并根据我们的自定义覆盖和实现所需的方法,以创建任何服务。首先,开发者必须实现onBind()方法,这是必须的。这个方法是Service的一个抽象方法。该方法用于在运行时绑定正在运行的服务。然后,onCreate()onStart()方法与活动类中的相同。开发者在这些方法中进行必要的初始化设置。在我们的示例中,我们只是将显示通知用户方法调用的吐司。onStartCommand()方法是一个非常重要的方法,我们在这里完成所有后台工作。需要注意的是,服务在主线程中运行。因此,如果你想要执行繁重的处理任务,你应该在这个方法中创建一个单独的线程并启动它。这就是标准方式下进行后台处理的方法。

注意

我们也可以在主线程中创建线程,在后台执行繁重的处理;那么为什么我们需要服务来创建线程呢?在 Android 操作系统中,服务是进行后台处理的标准方法。当 Android 操作系统内存不足时,它可以停止空闲的服务以获取其内存。但它不能停止线程;因此,使用服务是在后台持续执行任务的一种更好的方式。

onStartCommand()方法中,我们除了像服务类的其他方法一样显示一个吐司外,没有做任何事情。关于服务类没有特别需要提及的地方。与上一个示例一样,这个示例应用中最重要的部分是主活动。现在让我们详细看看主活动类。

ServiceDemoActivity.java 类

ServiceDemoActivity.java 类是我们应用的主活动,定义了应用程序的启动点。以下代码展示了类的实现:

ServiceDemoActivity.java 类

这个类是从 Activity 类扩展而来的,我们覆盖了一些其方法。在 onCreate() 方法中,我们通过引用存储在应用 res/layout 文件夹中的布局来设置活动的 ContentView 布局。然后,我们在布局中提取按钮并为这些按钮设置 OnClickListener。在早期的示例中,我们为按钮使用了 OnClickListener 接口的匿名对象。在这个示例中,我们通过在 setOnClickListener() 方法参数中传递 this 来提供 Activity 类作为我们的 OnClickListener。传递给参数的对象必须实现 OnClickListener 接口并覆盖 onClick() 方法。因此,我们的活动类实现了这个接口以及扩展的活动。我们还在这个类中覆盖了 onClick() 方法。由于这次我们有两个按钮并且只有一个 OnClickListener 接口,我们必须首先找出哪个按钮被按下,然后相应地采取行动。

一种实现方式是获取按下视图的 ID,并与资源中的 ID 进行比较。因此,getId() 方法为我们返回视图的 ID;我们在 switch 代码块中传递 ID,并与我们按钮的 ID 进行比较。在这两种情况下,我们都在创建一个显式意图,传递活动的上下文和我们的服务类名作为要激活的目标组件,就像我们在活动示例中所做的那样。需要注意的是,服务是通过 startService() 方法启动并通过 stopService() 方法停止的。这些方法采用显式意图,包括有关需要启动或停止哪个服务的信息。这个类向我们展示了如何容易地使用显式意图从任何活动中启动或停止任何服务。像往常一样,这个主活动使用了两个按钮,StartStop,这两个按钮是从位于 Android 项目目录资源文件夹中的布局中提取的。让我们看看这个布局文件包含什么。

activity_main.xml 文件

activity_main.xml 文件是 ServiceDemoActivity 的布局文件,采用 XML 编写。以下代码显示了该文件的实现:

 文件

我们在布局中有一个 <LinearLayout> 元素和两个按钮视图。请记住,这些按钮是由 ServiceDemoActivity 文件提取的,以在设置活动的视图后为两个按钮设置 OnClickListener

注意

我们可以在 XML 以及 Java 中创建布局。Android 建议在 XML 中创建所有布局,因为 Java 在 Android 中用于处理。如果我们用 Java 创建布局,布局的创建也将被处理;这可能导致应用程序更加耗电。只有在动态布局中,例如在游戏中使用的布局,才使用 Java。

在描述activity_main.xml文件时,有一个<LinearLayout>元素,它具有关于height(高度)、width(宽度)和orientation(方向)的特定参数。如您所见,在<LinearLayout>标签内,声明了按钮,这些按钮被引入到 Java 代码中。此标签还带有关于height(高度)、width(宽度)、idtext(将出现在布局中的文本)的特定参数。最后但同样重要的是,Android 的 manifest 文件用于应用程序设置。与活动一样,开发者必须在 manifest 文件中注册应用程序中实现的所有服务。让我们看看文件,了解我们是如何在示例应用的 manifest 文件中注册服务的。

AndroidManifest.xml文件

要注册一个服务,我们必须在<application>标签内提供代码。以下代码展示了AndroidManifest.xml文件的完整实现:

文件

可以注意到,在<activity>标签之后,我们放置了带有属性名称的<service>标签,以定义我们正在注册哪个服务。如果我们不在AndroidManifest.xml文件中注册我们的服务,我们将遇到ServiceNotFoundException异常抛出,并且我们会得到错误日志,例如 "Unable to start service (service package with name): not found"。

注意

LogCat位于 Android Studio 的 DDMS 视图中。LogCat 记录了连接设备或模拟器中执行的所有活动。任何抛出的Force Close崩溃异常都会在 LogCat 中记录,开发者可以通过它找到崩溃的原因并解决。

到目前为止,我们关注的是显式意图,并创建了两个使用显式意图的简单应用。现在,让我们转向另一种称为隐式意图的意图类型。

隐式意图

与显式意图不同,当开发者不知道要使用哪个组件时,隐式意图是一个很好的选择。隐式意图不会像显式意图那样直接指定要激活的 Android 组件,而只是指定需要执行哪些操作。Android 操作系统将选择要触发的组件。如果有多個可以触发的组件,Android 操作系统会为用户提供选项,让用户选择一个组件。例如,我们想要在浏览器中打开一个网页链接。如果我们使用显式意图,我们有一个选项是开发我们自己的自定义浏览器,并从我们的应用程序中明确触发它来查看网页链接。另一种更可取的方法是使用隐式意图打开手机中已安装的任何浏览器。如果手机中安装了不止一个浏览器,用户将被给予选择一个来执行操作,即查看网页链接。隐式意图的这项功能作为在 Android 操作系统中执行任何操作的一般形式。我们在意图中指定数据和操作,Android 根据该数据选择合适的组件。以下代码段展示了一个隐式意图的简单声明,它提供了一个链接,由 Android 在最适合的组件或浏览器中浏览:

隐式意图

与显式意图一样,隐式意图的构造函数中传递了两个参数。你可以在第二章《Android 意图简介》中阅读更多关于意图构造函数的内容。第一个参数是 Android 操作系统要执行的操作;我们在这里指定为ACTION_VIEW。这将告诉 Android 系统,即将查看某物。另一个参数是数据,通常以URI格式定义。我们使用了 PacktPub 网站作为示例。Android 操作系统将在手机默认浏览器中打开这个网页地址。如果找到多个浏览器,用户将看到一个所有可用浏览器的列表,以选择一个查看地址。

注意

这里可以传递任何 URI,不仅限于网页地址。例如,手机中任何联系人的 URI 或图库中的任何图片也可以传递,Android 操作系统将为传递在 URI 中的数据采取最合适的操作。

这种意图的通用行为在 Android 开发中显得尤为重要。开发者通过制作通用应用节省了大量时间。这对开发者来说很有利,因为他们只需发送信息即可。其余的由 Android 操作系统定义,用户可以根据自己的意愿选择执行操作。开发者不仅可以为用户提供选择其他应用执行隐式意图中动作的功能,还可以开发自己的自定义应用,并将其添加到选择列表中。例如,我们开发了一个图片编辑应用。我们希望该应用能够实现在用户从任何其他应用中选择图片时,我们的应用能出现在选项列表中,这样用户就可以轻松地从手机中的任何位置导航到我们的应用来编辑图片。这可以通过隐式意图实现。但这次的差别是,我们不会发送隐式意图;相反,我们将从其他应用接收隐式意图。我们可以通过在我们的AndroidManifest.xml文件中注册意图过滤器来实现这一点,在这里我们需要定义应用将执行的操作。这个特性使得应用与其他应用的互动性更强,不同应用与 Android 操作系统之间的集成对开发者来说变得非常简单。在以下各节中,我们将开发两个隐式意图的示例,并看看我们可以用这些示例做什么。

在 Android 应用程序中使用隐式意图

本节将讨论在 Android 应用程序中隐式意图的各种用途。如前所述,隐式意图可以以通用形式用于与其他 Android 组件进行通信,与显式意图不同。让我们通过以下两个示例来看隐式意图的实际应用:一个用于共享内容,另一个用于从其他 Android 应用获取内容。

使用隐式意图共享内容

如今,社交网络是使任何应用程序病毒式传播并推广给其他用户的原因。由于社交网络的种类繁多,将所有共享功能都放入应用中变得困难。大多数情况下,开发者在他们的应用中添加 Facebook、Twitter 和 Instagram,但有时在应用中添加这些 SDK 不仅给开发者带来麻烦,也给用户带来麻烦。例如,多个 SDK 会在构建文件中增加一些大小;应用由于功能过多而变得复杂。

幸运的是,我们可以通过几行代码使用隐式意图来解决此问题。让我们通过创建一个简单的内容共享示例来看看这是如何可能的。首先,使用任何 Android IDE(如带有 ADT 插件的 Eclipse 或 Android Studio)创建一个空项目,或者打开您想要添加共享功能的任何现有项目。我们现在将实现使用隐式意图在社交网络上共享任何数据。

我们实现了一个简单的单行分享应用,该应用会要求用户选择分享方式,并在该网络上分享这一行内容。在任何空项目中,都有两个主要文件会被修改。让我们逐一探索这两个文件。

activity_main.xml 文件

activity_main.xml 文件是我们简单的行分享应用的视觉布局。以下代码片段展示了这个文件的实现:

activity_main.xml 文件

我们有一个相对布局,在其中放置了两个视图:一个带有textShare的按钮和一个<EditText>标签,用于获取用户的输入行。在之前的例子中,我们使用了<LinearLayout>来在屏幕上对齐视图。这次,我们使用了<RelativeLayout>来添加视图。在相对布局中,我们根据其他视图来放置我们的视图。例如,android:layout_alignLeft获取任何视图的 ID,并将此视图放在主视图的左侧。同样,在Share按钮中,我们也使用了android:layout_below属性,将其放置在文本字段下方。关于文本字段,这是屏幕上的第一个视图;因此,它可以相对于父视图放置。

注意

layout_ android:layout_alignParentLeftandroid:layout_alignParentTop布尔标志将EditText视图放置在父视图的左上角,即屏幕的左上角。在 Android 操作系统中,使用相对布局来对齐视图是最推荐的方法。

这就是我们的单行分享应用的视觉表示。现在,让我们看看在主活动文件中是如何使用隐式意图的。

MainActivity.java 类

MainActivity.java 类文件是行分享应用的主活动。以下代码展示了该文件的实现:

public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Button shareBtn = (Button) findViewById(R.id.button1);
    shareBtn.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View arg0) {
        // TODO Auto-generated method stub
        String msg = ((EditText) findViewById(
          R.id.editText1)).getText().toString();

        Intent in = new Intent(Intent.ACTION_SEND);
        in.putExtra(Intent.EXTRA_TEXT, msg);
        in.setType("text/html");
        startActivity(Intent.createChooser(in, "Share via..."));
      }
    });
  }
}

这个类是从Activity类扩展而来的,目的是使其成为一个活动。像往常一样,我们重写了onCreate()方法,通过setContentView()方法设置活动的 Content View,并引用了一个布局,这个布局就是位于res/layout目录下的activity_main.xml文件。设置布局后,我们通过findViewById()方法从布局中获取按钮的引用,并将其View.OnClickListener接口设置为一个匿名对象。

设置视图后,让我们设置监听器并在可触摸视图上定义功能。为此,我们重写了监听器的onClick()方法,并在该方法中放置了我们的主要功能代码,因为我们要在按钮点击时分享行内容。我们首先通过获取其引用从文本字段中获取消息文本,然后获取字段中的文本。我们创建了一个Intent对象,并在构造函数中传递了ACTION_SEND作为其类别。ACTION_SEND意图将数据传递给 Android 操作系统。

请记住,我们没有像显式意图中那样明确指定数据的接收者。这个动作的接收者会通过选择器对话框询问用户数据应该发送到哪里。

在创建意图实例后,我们在意图中添加了消息行的额外数据。我们选择了EXTRA_TEXT键来添加我们的数据。例如,如果我们想发送短信,我们必须将数据插入到短信正文中;如果我们想发送电子邮件,我们必须将数据放入电子邮件正文中。因此,我们选择了一般文本类型,Android 操作系统将检测到合适的位置放置数据。

到目前为止,我们已经设置了类别和数据,但我们还必须设置数据的类型。Android 操作系统将根据数据的类型在我们的选择器对话框中放置应用。例如,我们将类型设置为text/html;这将把所有支持文本或 HTML 数据格式的应用,如电子邮件、Facebook、Twitter 等,放在选择器中。如果我们将其设置为image/png,所有支持图像编辑、图库等的应用将被放入选择器列表中。此外,我们可以通过放置一个斜杠后跟一个星号来定义支持的图像的一般类型。例如,image/*将把所有支持图像的应用,不仅限于 PNG,放在选择器列表中。

最后,我们通过调用startActivity()方法启动这个意图的活动。你应该小心,在startActivity()方法或Intent.chooser()方法中传递意图。如果我们通过startActivity传递意图,我们将得到ActivityNotFoundException异常。我们不知道为了共享应用而具体要启动哪个活动;在这种情况下,我们将创建一个选择器列表,用户将决定要启动哪个活动。这就是我们为用户提供选择他或她喜欢的共享内容方式的方法。

现在,运行项目,你会看到一个屏幕,上面有一个共享按钮和一个输入要共享内容的文本字段。按下共享按钮后,你会看到一个对话框,询问你选择共享的方法。这个对话框将包括所有用于共享文本的应用,如短信、SMS、MMS、电子邮件和 Facebook。以下截图展示了应用屏幕:

MainActivity.java 类

在本例中,我们使用隐式意图与 Android 操作系统的其他应用进行通信。在下一个示例中,我们将看到其他应用如何与我们的应用通信。

为共享意图注册你的应用

在上一个示例中,我们从我们的应用触发了其他共享应用。在本例中,我们将为共享意图注册我们的应用,以便其他应用开发者和用户可以使用隐式意图与我们的应用通信。两个示例都使用了隐式意图,但使用方法在这两种情况下是不同的。

在上一个示例中,我们在 Java 文件中使用了隐式意图,并在OnClickListener中的按钮点击时触发了意图。在此示例中,为了注册我们的应用以接收任何意图,例如此例中的分享意图,我们必须在AndroidManifest.xml文件的 XML 文件中放置我们的代码。稍微复习一下,AndroidManifest.xml文件管理我们应用的所有设置。现在让我们学习如何实现应用。

要开始此示例,请使用任何 Android IDE(如带有 ADT 插件的 Eclipse 或 Android Studio)创建一个空项目,或打开你想要接收发送意图的任何现有项目。在这个项目中,我们将探索在应用中获取其他应用共享数据时隐式意图的使用。我们首先将我们的应用注册为文本共享应用。然后,所有在 Android 中共享任何text/html类型文件的应用都可以在用户选择时触发我们的应用。为了执行这个任务,我们修改了三个文件中的代码:一个布局文件、一个活动文件和清单文件。现在让我们逐一看看这些文件。

activity_main.xml文件

activity_main.xml文件是我们共享应用的视觉布局。以下代码展示了此文件的实现:

activity_main.xml 文件

我们有一个包含文本视图组件的相对布局。我们将文本视图的初始文本值设置为“Hello World”;记住,我们从其他应用共享时得到的数据将打印在此文本视图内容中。在视觉表现之后,接下来总是应用的处理和编码逻辑。现在让我们看看下一个活动文件,它执行主要任务并实现使用隐式意图的逻辑。

MainActivity.java

MainActivity.java类文件是显示来自任何其他应用共享数据的活动。以下代码展示了此文件的实现方法:

MainActivity.java 类

当用户从分享对话框中选择我们的应用时,此活动将被打开。我们还已将此活动设置为我们的主活动。应用中的任何活动都可以注册以接收分享意图。主活动不一定需要注册以接收共享数据。我们已设置活动的布局,之后我们获取意图数据。这是将从任何其他分享应用抛出的数据。我们首先通过调用getIntent()方法获取意图。有许多类型的意图;我们必须确保我们的活动只对我们在AndroidManifest.xml文件中注册的意图类型起作用。为了检测此意图是否用于分享,我们必须检查意图动作。因此,我们通过调用Intent.getAction()方法获取了意图的动作。我们可以使用Intent.getType()方法获取意图的类型。然后,我们检查类型和动作。

如果意图动作是Intent.ACTION_SEND且类型为text/html,这意味着我们可以在应用中显示那种类型的数据。如果两个条件都为true,我们将文本视图内容设置为从意图中得到的数据。我们可以通过Intent.getStringExtra()方法从意图中获取数据。这个方法将数据类型作为输入参数或参数。在这个例子中,我们获取了Intent.EXTRA_TEXT数据类型,它表示意图中通常用于电子邮件正文、Facebook 帖子或短信的消息或文本数据。

通过理解MainActivity类,我们得知我们只是接收了意图并检查了它。但还有一个问题,就是其他应用如何识别我们的应用,以及它们如何知道我们的应用可以显示text/html数据。如果我们从同一应用的另一个活动中显式打开这个活动,同样会接收到相同的意图,并检查相同的条件。但这次,没有条件会为true,因此布局中的文本不会改变。为了让其他应用看到我们的应用,我们必须注册一个意图过滤器。这是在AndroidManifest.xml文件中完成的。

AndroidManifest.xml文件

为了让我们的应用在分享内容时可见,我们必须在文件中为应用的接收活动注册一个意图过滤器。以下代码展示了此文件的实现:

文件

MainActivity<activity>标签中,我们插入了两个意图过滤器。第一个意图过滤器是为了让这个活动成为主活动,也就是应用的启动器。第二个意图过滤器是一段执行应用核心功能的代码。我们插入了一个意图过滤器,其动作为android.intent.action.SENDmimeTypetext/html。这告诉安卓操作系统,每当触发具有Send动作的意图,并且包含text/html类型的数据时,应用都可以处理这个意图。这样我们的应用就会显示在应用的 chooser 对话框中。

现在,运行项目,你会看到Hello World的屏幕。关闭应用并运行我们之前的示例应用ShareImplicitIntent。在文本字段中写些东西,然后点击分享按钮。在 chooser 对话框中,你会在列表中看到我们的应用GettingSharedData。选择这个应用会打开活动,这次,文本字段中不会显示Hello World,而是会显示从另一个应用共享的数据。以下截图展示了应用的演示效果:

文件

到目前为止,我们已经看到了两个隐式意图的例子。在一个例子中,我们与其他应用如电子邮件、短信、Facebook 等共享了一些数据。在另一个例子中,其他应用与我们的应用共享内容,我们接收了这些数据并进行了展示。但是,隐式意图并不仅限于共享内容。使用隐式意图可以执行很多操作和选择,包括拨打电话、发送短信、显示地图、搜索任何内容、拍照、显示和编辑联系人等等。

在下一个例子中,我们将学习如何从图库中选择任何图片并在我们的活动中显示它。

通过隐式意图选择图片

在这个项目中,我们将实现使用隐式意图从图库中选择任何图片的功能。我们将放置一个 ImageView,以在我们的应用中显示图片。这张图片将由用户从图库中选择。现在就让我们来实现它吧!为了开始这个例子,请使用任何 Android IDE(如带有 ADT 插件的 Eclipse 或 Android Studio)创建一个空项目,或者打开任何你想要添加图片选择功能的项目。

我们将注册我们的应用作为一个图片分享应用,这样在 Android 操作系统中所有分享图片的应用都可以在用户选择的情况下触发我们的应用。我们修改了三个文件中的代码:一个布局文件、一个活动文件和清单文件。让我们看看这些文件的作用。

activity_main.xml文件

与所有 Android 应用一样,activity_main.xml文件代表了主活动的布局文件。以下代码展示了此文件的实施例:

文件

我们放置了两个视图组件;一个按钮视图,用于点击时打开图库,以及一个用于显示图片的 ImageView。与其他应用不同,我们在布局文件中设置了按钮监听器。回顾最后一种方法,我们通过在活动类文件中调用button.setOnClickListener()方法来设置我们的点击监听器。在这个例子中,我们在<Button>标签中使用了android:onClick属性,并在属性另一侧提供了监听器的名称。我们必须提供一个方法名,该方法应该在此布局使用的活动文件中定义。

提示

Android 操作系统建议你在 XML 布局文件中设置监听器。但是,如果布局被多个活动使用,开发者应该注意,因为属性值是一个方法名,并且应该在活动文件中定义。这意味着要么所有使用布局文件的活动都定义该方法,要么所有活动都应该在 Java 文件中设置监听器,而不是在 XML 中。

我们布局文件中的另一个视图组件是 ImageView。这个 ImageView 将显示从图库或其他图片分享应用中选择的图片。我们将 ImageView 的来源设置为启动器图标图像作为默认图片。

在开发应用程序的布局之后,让我们关注应用程序的逻辑。MainActivity文件展示了应用程序如何从其他应用程序获取图像并显示它。

MainActivity.java 类

MainActivity.java 类是主活动 Java 文件,它执行应用程序中的所有功能。以下代码段是此文件的实现:

MainActivity.java 类

我们从onCreate()方法开始,首先将活动的 Content View 设置为我们布局文件。我们在类中创建了三个私有字段。第一个是REQUEST_CODE常量,它是一个整数值。这个常量用作从任何其他 Android 应用获取数据的请求代码。由于我们要从图库中选择图片,因此需要一个请求代码来识别正确的结果和数据。第二个字段是位图。这个位图用于以位图格式存储选中的图片。活动类的第三个也是最后一个字段是 Image View。这用于在 XML 文件中引用 Image View。

pickImage()方法是设置在 XML 布局文件的<Button>标签中的按钮监听器。这个方法应该带有View参数。这个参数包含了在运行时被点击的视图。根据我们应用程序的要求,我们希望在按钮点击时打开图库;因此,为了打开图库,在这个方法中将触发一个隐式意图。我们使用无参数构造函数创建一个空的意图对象。然后,我们使用image/*将其类型设置为任何图像格式。之后,我们将它的意图动作设置为Intent.ACTION_GET_CONTENT。这告诉 Android 操作系统显示所有共享内容的应用程序。

现在,我们已经告诉 Android 操作系统,我们只需要图像内容,通过设置类型;因此,Android 操作系统只显示那些共享图像的应用程序,如图库。我们将类别设置为Intent.CATEGORY_OPENABLE。这用于指示GET_CONTENT意图只需要可以用ContentResolver.openInputStream打开的 URIs。

最后,我们通过调用startActivityForResult()方法来启动活动。请记住,在我们之前的应用程序中使用了startActivity()方法。

提示

startActivity()方法和startActivityForResult()方法之间的区别在于,startActivityForResult()方法在停止后会将一些结果返回给父活动,而startActivity()则不返回任何内容。

因为我们需要从图库中获取任何图片,图库应用将返回我们将在应用中使用的图片 URI 以显示它。为了在我们的活动中获取结果,我们需要在类中覆盖onActivityResult()方法。这个方法有三个参数。第一个是请求码,是一个整数值。这个值定义了我们用于启动活动的请求 ID。我们在类中使用了一个常量私有字段REQUEST_CODE作为这个值;因此,在我们的onActivityResult()方法中,我们将请求码与这个常量值进行比较以确认。第二个参数RESULT_CODE是一个整数值。这个值告诉我们得到的结果是否正确且可以使用。第三个参数是意图,它包含我们将要在应用中使用的结果数据。

onActivityResult()方法中,我们创建了一个InputStream对象,然后通过比较请求码和结果码来确定是否应该处理意图数据。如果一切顺利,我们可以通过调用Intent.getData()方法获取选定的图片 URI,并将其传递给本活动的ContentResolveropenInputStream()。任何活动的ContentResolver都可以通过调用Activity.getContentResolver()方法获得。获取到 URI 的数据流后,我们通过调用BitmapFactory.decodeStream()方法将其解码为位图,并将输出位图设置到我们的活动位图字段中。然后,我们在图像视图中设置位图。在try/catch块的最后一部分,我们关闭了流。

现在,运行项目,您将看到如下截图所示的屏幕。用户点击按钮,将显示图库。然后,用户选择他喜欢的照片以显示,应用将在应用屏幕上展示它:

MainActivity.java 类

总结关于隐式意图的整个部分,我们实现了三个示例。在第一个示例中,我们学习了如何与其他应用共享数据。在第二个示例中,我们了解了其他应用如何与我们的应用共享数据。最后,在第三个也是最后一个示例中,我们学习了如何从图库获取图片并在我们的应用中使用它。在下一节也是最后一节中,我们将讨论 Android 晚期绑定。

意图和 Android 晚期绑定

众所周知,Android 应用程序最核心的三个组件是活动(activities)、服务(services)和广播接收器(broadcast receivers)。这些组件通过消息传递进行通信和触发。这种消息传递是通过意图(intents)完成的。意图消息传递是同一应用程序或不同应用程序中组件之间的晚期运行时绑定(晚期绑定)的机制。在每种情况下,Android 系统都会找到正确的组件,例如要触发的活动、服务或接收器,并在必要时实例化它们。这些意图之间没有重叠。例如,广播接收器意图只发送给广播接收器,而不会发送给任何活动或服务。另一个例子是,通过startActivity()startActivityForResult()方法传递的意图永远不会发送给任何如服务或接收器这样的组件,而只发送给一个活动。

在本章使用的示例中,隐式意图总是执行开发者不确定这些操作将如何执行以及使用什么应用程序的动作。这种将动作分配给组件的运行时行为称为 Android 晚期运行时绑定,这可以通过隐式意图轻松完成。

总结

在本章中,我们讨论了意图的分类,包括隐式意图、显式意图和晚期绑定。本章还提供了 Android 意图的一些重要实现,我们通过这些实现与其他应用程序共享数据,其他应用程序与我们的应用程序共享数据,从图库中选择任何图片,通过显式意图启动活动或服务等等。

在下一章中,我们将学习如何通过意图触发移动组件,例如摄像头,以及它们在我们的应用程序中的使用方式。

第四章:移动组件的意图

在上一章中,我们讨论了意图的分类以及不同类别意图的使用方法。我们还讨论了隐式意图和显式意图等类别的优缺点。但是,除了我们至今讨论的关于意图的理论之外,现在是我们讨论一些更实用的意图应用的时候了。在本章中,我们将讨论所有安卓手机中常见的移动组件,并了解如何通过意图非常容易地访问和使用这些移动组件。安卓提供了丰富的库和功能,开发者可以利用这些来使用移动组件,这就像在公园里散步一样简单。本章主要包括四类不同的组件:视觉组件如摄像头,通信组件如 Wi-Fi 和蓝牙,媒体组件如视频和音频录制、语音识别以及文本到语音转换,最后是运动组件,如接近传感器。本章将讨论以下主题:

  • 常见移动组件

  • 组件与意图

  • 通信组件

  • 通过意图使用蓝牙

  • 通过意图使用 Wi-Fi

  • 媒体组件

  • 通过意图拍照和录制视频

  • 使用意图进行语音识别

  • 意图在文本到语音转换中的作用

  • 运动组件

  • 通过意图设置接近提醒

本章以及后续章节的理解需要依赖于前几章中讨论的意图的概念和结构。如果你对这些内容没有基本的了解,我们建议你阅读第二章,安卓意图简介和第三章,意图及其分类,以便继续深入学习。

常见移动组件

由于安卓操作系统的开源性质,许多不同的公司如 HTC 和三星在他们的设备上移植了具有许多不同功能和风格的安卓操作系统。每款安卓手机在某种程度上都是独一无二的,拥有许多与其他品牌和手机不同的独特功能和组件。但在所有安卓手机中,有一些组件是共通的。

注意

这里我们使用了两个关键术语:组件和功能。组件是安卓手机的硬件部分,如摄像头、蓝牙等。而功能是安卓手机的软件部分,如短信功能、电子邮件功能等。本章全部关于硬件组件,以及如何通过意图访问和使用这些组件。

这些通用组件可以独立于任何手机或型号进行普遍使用和实现。毫无疑问,意图是激活这些 Android 组件的最佳异步消息。这些意图用于在发生某些事件时应采取某些操作时触发 Android 操作系统。Android 根据接收到的数据,确定意图的接收者并触发它。以下是每部 Android 手机中都存在的几个常见组件:

Wi-Fi 组件

每部 Android 手机都配备了完整的 Wi-Fi 连接组件支持。具有 Android 版本 4.1 及以上的新 Android 手机还支持 Wi-Fi Direct 功能。这使得用户无需连接热点或网络接入点即可连接到附近的设备。

蓝牙组件

Android 手机包括蓝牙网络支持,允许 Android 手机用户与其他设备在低范围内无线交换数据。Android 应用程序框架为开发者提供了通过 Android 蓝牙 API 访问蓝牙功能的方法。

蜂窝组件

没有手机是不完整的,每个 Android 手机都有一个用于通过短信、通话等进行移动通信的蜂窝组件。Android 系统提供了非常高级、灵活的 API 来利用电话和蜂窝组件创建非常有趣和创新的应用程序。

全球定位系统(GPS)和地理位置

GPS 是任何 Android 手机中非常有用但耗电的组件。它用于为 Android 用户开发基于位置的应用程序。谷歌地图是与 GPS 和地理位置相关的最佳功能。开发者已经提供了许多利用谷歌地图和 Android 中的 GPS 组件的创新应用程序和游戏。

地磁场组件

地磁场组件在大多数 Android 手机中都可以找到。此组件用于估计在地球给定点的 Android 手机的磁场,特别是计算从北磁偏角。

注意事项

地磁场的组件使用由美国国家地理空间情报局生产的世界磁模。目前用于地磁场的模型有效期至 2015 年。较新的 Android 手机将拥有地磁场的新版本。

传感器组件

大多数 Android 设备内置有测量运动、方向、环境条件等的传感器。这些传感器有时充当应用的大脑。例如,它们根据手机的周围环境(天气)采取行动,并允许用户与应用自动互动。这些传感器为测量相应传感器值提供高精度和准确性的原始数据。例如,重力传感器可以用于在任何应用或游戏中追踪手势和动作,如倾斜、震动等。同样,温度传感器可以用来检测手机温度,或者如前所述的地磁传感器可以在任何旅行应用中使用来追踪指南针方位。总的来说,Android 中有三大类传感器:运动传感器、位置传感器和环境传感器。以下小节将简要讨论这些类型的传感器。

运动传感器

运动传感器允许 Android 用户监控设备的运动。既有基于硬件的传感器,如加速度计、陀螺仪,也有基于软件的传感器,如重力、线性加速度和旋转矢量传感器。运动传感器用于检测设备的运动,包括倾斜效果、震动效果、旋转、摆动等。如果使用得当,这些效果可以使任何应用或游戏变得非常有趣和灵活,并能证明提供极佳的用户体验。

位置传感器

两个位置传感器,地磁传感器和方向传感器,用于确定移动设备的位置。另一个传感器,近距离传感器,允许用户确定设备的面部与物体的距离有多近。例如,当我们在 Android 手机上接到任何电话时,将手机放在耳朵上会关闭屏幕,当我们把手机拿回手中时,屏幕显示会自动出现。这个简单的应用使用近距离传感器来检测耳朵(物体)与设备面部(屏幕)的接触。

环境传感器

这些传感器在 Android 应用中使用不多,但被 Android 系统广泛用于检测许多小事物。例如,温度传感器用于检测手机的温度,并可用于节省电池和延长手机寿命。

注意

在撰写本书时,三星 Galaxy S4 Android 手机已经发布。该手机通过允许用户通过无需触摸的手势,如移动手或脸在手机前执行操作,展现了环境手势的极大使用,例如拨打电话。

组件和意图

Android 手机包含大量的组件和功能。这对 Android 开发者和用户都有利。Android 开发者可以使用这些移动组件和功能来定制用户体验。对于大多数组件,开发者有两个选择;他们可以扩展组件并根据应用程序需求进行定制,或者使用 Android 系统提供的内置接口。由于扩展组件超出了本书的范围,我们不会讨论第一个选择。但是,我们将研究使用移动组件的内置接口的另一种选择。

通常,为了从我们的 Android 应用程序中使用任何移动组件,开发者会向 Android 系统发送意图,然后 Android 根据意图调用相应的组件。正如我们之前所讨论的,意图是发送给 Android 操作系统的异步消息,以执行任何功能。大多数移动组件只需使用几行代码就可以通过意图触发,并且开发者可以在他们的应用程序中充分利用这些组件。在本章的以下部分,我们将通过实际示例看到一些组件以及如何通过意图使用和触发它们。我们将组件分为三种方式:通信组件、媒体组件和运动组件。现在,让我们在以下各节中讨论这些组件。

通信组件

任何手机的核心用途都是通信。除了通信功能外,Android 手机还提供了许多其他功能。Android 手机包含用于通信目的的短信/彩信、Wi-Fi 和蓝牙。本章关注硬件组件;因此,我们将在本章中仅讨论 Wi-Fi 和蓝牙。Android 系统提供了内置 API 来管理和使用蓝牙设备、设置、可发现性等。它不仅为蓝牙,也为 Wi-Fi、热点、配置设置、互联网连接等提供了完整的网络 API。更重要的是,通过意图编写少量代码,可以非常容易地使用这些 API 和组件。我们将从讨论蓝牙开始,在下一节中,我们将介绍如何通过意图使用蓝牙。

通过意图使用蓝牙

蓝牙是一种通信协议,旨在实现短距离、低带宽的点对点通信。在本节中,我们将讨论如何与本地蓝牙设备进行交互和通信,以及如何通过蓝牙与附近的远程设备进行通信。蓝牙是一种非常短距离的协议,但可以用来传输和接收文件、媒体等数据。由于数据加密,截至 Android 2.1 版本,只有配对的设备才能通过蓝牙设备相互通信。

注意

从 Android 2.0 版本(SDK API 级别 5)开始,蓝牙 API 和库变得可用。还应该注意的是,并非所有 Android 手机都必然包含蓝牙硬件。

安卓系统提供的蓝牙 API 用于执行许多与蓝牙相关的操作,包括打开/关闭蓝牙、与附近设备配对、与其他蓝牙设备通信等等。但并非所有这些操作都可以通过意图执行。我们将仅讨论那些可以通过意图执行的操作。这些操作包括从我们的安卓应用中设置蓝牙的开启/关闭、跟踪蓝牙适配器状态以及使我们的设备在短时间内可被发现。无法通过意图执行的操作包括向其他蓝牙设备发送数据和文件、与其他设备配对等。接下来,让我们在以下各节中逐一解释这些操作。

一些蓝牙 API 类

在本节中,我们将讨论安卓蓝牙 API 中一些在所有使用蓝牙的安卓应用中都会用到的类。理解这些类将帮助开发者更容易理解以下示例。

BluetoothDevice

这个类代表与用户通信的每个远程设备。这个类是对手机蓝牙硬件的薄封装。要对此类的对象执行操作,开发者必须使用BluetoothAdapter类。这个类的对象是不可变的。我们可以通过调用BluetoothAdapter.getRemoteDevice(String macAddress)并传递任何设备的 MAC 地址来获取BluetoothDevice。这个类的一些重要方法包括:

  • BluetoothDevice.getAddress(): 它返回当前设备的 MAC 地址。

  • BluetoothDevice.getBondState(): 该方法返回当前设备的绑定状态,例如未绑定、正在绑定或已绑定。

注意

MAC 地址是一个由 12 个字符组成的字符串,以 xx:xx:xx:xx:xx:xx 的格式表示。例如,00:11:22:AA:BB:CC。

BluetoothAdapter

这个类代表当前运行我们安卓应用的设备。需要注意的是,BluetoothAdapter类代表当前设备,而BluetoothDevice类代表其他可能与我们的设备配对或未配对的设备。这个类是一个单例类,不能被实例化。要获取这个类的对象,我们可以使用BluetoothAdapter.getDefaultAdapter()方法。要执行与蓝牙通信相关的任何操作,这个类是主要的起点。这个类的一些方法包括BluetoothAdapter.getBondedDevices(),它返回所有已配对的设备,BluetoothAdapter.startDiscovery(),它搜索附近所有可发现的设备等等。还有一个名为startLeScan(BluetoothAdapter.LeScanCallback callback)的方法,用于在发现设备时接收回调。这个方法在 API 级别 18 中引入。

注意

BluetoothAdapterBluetoothDevice 类中的某些方法需要 BLUETOOTH 权限,有些还需要 BLUETOOTH_ADMIN 权限。因此,当在您的应用中使用这些类时,不要忘记在 Android 清单文件中添加这些权限。

到目前为止,我们已经讨论了 Android OS 中的一些蓝牙类以及这些类中的某些方法。在下一节中,我们将开发我们的第一个 Android 应用,它将要求用户打开蓝牙。

打开蓝牙应用

要执行任何蓝牙操作,必须先打开蓝牙。因此,在本节中,我们将开发一个 Android 应用,如果蓝牙设备尚未打开,它会要求用户打开蓝牙设备。用户可以接受并打开蓝牙,或者用户也可以拒绝。在后一种情况下,应用程序将继续运行,而蓝牙将保持关闭状态。可以说,使用意图可以非常容易地执行此操作。让我们通过查看代码来看看如何做到这一点。

首先,在您喜欢的 IDE 中创建一个空的 Android 项目。我们是在 Android Studio 中开发的。在撰写这本书的时候,项目处于预览模式,预计不久将进行测试版发布。现在,我们将修改项目中的几个文件,以创建我们的 Android 蓝牙应用。我们将修改两个文件。让我们在以下章节中看看这些文件。

MainActivity.java 文件

这个类代表我们 Android 应用的主活动。以下代码是在此类别中实现的:

MainActivity.java 文件

在我们的活动中,我们声明了一个名为 BLUETOOTH_REQUEST_CODE 的常量值。这个常量作为请求代码或请求唯一标识符,在我们的应用与 Android 系统之间的通信中使用。当我们请求 Android 操作系统执行某些操作时,我们会传递任何请求代码。然后,Android 系统执行操作并将相同的请求代码返回给我们。通过比较我们的请求代码与 Android 的请求代码,我们就可以知道已执行的操作。如果代码不匹配,则意味着此操作是针对其他请求的,不是我们的请求。在 onCreate() 方法中,我们通过调用 setContentView() 方法设置活动的布局。然后,在接下来的几行中,我们执行实际的任务。

我们创建了一个字符串enableBT,它获取与BluetoothAdapter类相关的ACTION_REQUEST_ENABLE方法的值。这个字符串在意图构造函数中传递,以告诉意图它的目的是启用蓝牙设备。与蓝牙启用请求字符串一样,安卓操作系统还包含许多其他请求,用于各种动作,如 Wi-Fi、传感器、相机等。在本章中,我们将了解一些请求字符串。在创建请求字符串之后,我们创建我们的意图,并将请求字符串传递给它。然后,我们通过在startActivityForResult()方法中传递它来启动我们的意图。

需要注意的是,在之前的章节中,我们使用了startActivity()方法,而没有使用startActivityForResult()方法。基本上,startActivity()方法仅通过意图启动任何活动,但startActivityForResult()方法启动任何活动,在执行某些操作后,它会返回到原始活动并呈现操作结果。因此,在这个例子中,我们调用了请求安卓系统启用蓝牙设备的活动。安卓系统执行操作并询问用户是否应该启用设备。然后,安卓系统将结果返回给之前启动意图的原始活动。为了从其他活动获取任何结果到我们的活动,我们重写了onActivityResult()方法。此方法在从其他活动返回后调用。该方法包含三个参数:requestCoderesultCodedataIntentrequestCode参数是一个整数值,包含开发者提供的请求代码值。resultCode参数是操作的结果。它告诉开发者操作是否已成功执行,是正面响应还是负面响应。dataIntent对象包含原始调用意图数据,例如哪个活动启动了意图以及所有相关信息。现在,让我们详细看看我们重写的方法。我们首先检查requestCode,我们的请求代码,是否为BLUETOOTH_REQUEST_CODE。如果两者相同,我们比较结果代码以检查我们的结果是否正常。如果正常,这意味着蓝牙已被启用;因此,我们显示一个通知用户关于它的吐司,如果结果不正常,这意味着蓝牙尚未启用。这里我们也通过显示吐司来通知用户。

这就是执行我们蓝牙启用应用核心功能的活动类。现在,让我们在下一节中看看 Android 的清单文件。

AndroidManifest.xml 文件

AndroidManifest.xml 文件包含应用的所有必要设置和偏好。以下是此清单文件中包含的代码:

AndroidManifest.xml 文件

任何使用蓝牙设备的 Android 应用都必须有使用蓝牙的权限。因此,为了向用户提供权限,开发者会在 Android 清单文件中声明<uses-permission>标签,并写入必要的权限。如代码所示,我们提供了两个权限:android.permission.BLUETOOTHandroid.permission.BLUETOOTH_ADMIN。对于大多数启用蓝牙的应用,仅BLUETOOTH权限就可以完成大部分工作。BLUETOOTH_ADMIN权限仅适用于那些使用蓝牙管理设置的应用,例如使设备可被发现、搜索其他设备、配对等。当用户首次安装应用程序时,他会收到有关应用程序需要哪些权限的详细信息。如果用户接受并授予应用权限,应用将被安装;否则,用户无法安装应用。文件的其余部分与书中其他示例中的相同。

讨论了 Android 清单和活动文件之后,我们将通过编译和运行项目来测试我们的项目。当我们运行项目时,我们应该看到如下截图所示的屏幕:

AndroidManifest.xml 文件

启用蓝牙应用

当应用启动时,用户会看到一个对话框,以启用或禁用蓝牙设备。如果用户选择,蓝牙将被打开,并且一个提示会通过显示蓝牙状态来更新状态。

跟踪蓝牙适配器状态

在上一个示例中,我们看到了如何仅通过将蓝牙请求的意图传递给 Android 系统,在几行代码内打开蓝牙设备。但是启用和禁用蓝牙是耗时的异步操作。因此,我们可以使用广播接收器来监听状态变化,而不是轮询蓝牙适配器的状态。在这个示例中,我们将看到如何使用广播接收器中的意图来跟踪蓝牙状态。

这个示例是之前示例的扩展,我们将使用相同的代码并添加新的代码。现在让我们看看代码。我们有三个文件,MainActivity.javaBluetoothStateReceiver.javaAndroidManifest.xml。让我们逐一讨论这些文件。

MainActivity.java 文件

这个类表示我们 Android 应用的主活动。以下代码在这个类中实现:

public class MainActivity extends Activity {
  final int BLUETOOTH_REQUEST_CODE = 0;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    registerReceiver(new BluetoothStateReceiver(), new IntentFilter(
      BluetoothAdapter.ACTION_STATE_CHANGED));
    String enableBT = BluetoothAdapter.ACTION_REQUEST_ENABLE;
    Intent bluetoothIntent = new Intent(enableBT);
    startActivityForResult(bluetoothIntent,
      BLUETOOTH_REQUEST_CODE);
  }
  @Override
  protected void onActivityResult(int requestCode, int resultCode,
    Intent data) {

    // TODO Auto-generated method stub
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == RESULT_OK) {
      if (requestCode == BLUETOOTH_REQUEST_CODE) {
        Toast.makeText(this, "Turned On", Toast.LENGTH_SHORT).show();
      }
    }
    else if (resultCode == RESULT_CANCELED) {
      Toast.makeText(this, "Didn't Turn On", Toast.LENGTH_SHORT).show();
    }
  }
}

从代码中可以看出,这段代码与之前的示例几乎相同。唯一的区别是在设置活动的内容视图后,我们增加了一行代码。我们调用了registerReceiver()方法,它以编程方式向 Android 系统注册任何广播接收器。我们也可以通过在 Android 清单文件中声明它们,通过 XML 注册接收器。广播接收器用于接收来自 Android 系统的广播。

在执行诸如打开蓝牙、打开/关闭 Wi-Fi 等常规操作时,安卓系统会发送广播通知,开发者可以利用这些通知来检测手机状态的变化。有两种类型的广播。正常广播是完全异步的。这些广播的接收者以无序的方式运行,多个接收者可以同时接收广播。与另一种类型的有序广播相比,这些广播更有效。有序广播一次发送给一个接收者。每个接收者收到结果后,它会将结果传递给下一个接收者或完全终止广播。在这种情况下,其他接收者不会收到广播。

尽管Intent类用于发送和接收广播,但意图广播是一种完全不同的机制,并且与startActivity()方法中使用的意图是分开的。广播接收器无法查看或捕获与startActivity()方法一起使用的意图。这两种意图机制之间的主要区别在于,startActivity()方法中使用的意图执行用户当前正在进行的前台操作。然而,与广播接收器一起使用的意图执行一些用户不知道的后台操作。

在我们的活动代码中,我们使用了registerReceiver()方法来注册在BluetoothStateReceiver类中定义的自定义广播接收器对象,并且我们根据接收器的类型传递了一个意图过滤器BluetoothAdapter.ACTION_STATE_CHANGED。这个状态告诉意图过滤器,我们的广播接收器对象用于检测应用中蓝牙状态的变化。注册接收器后,我们创建了一个意图,传递BluetoothAdapter.ACTION_REQUEST_ENABLE,告诉应用打开蓝牙。最后,我们通过调用startActivityForResult()启动我们的操作,并在onActivityResult()方法中比较结果,以查看是否打开了蓝牙。你可以在本章的上一示例中阅读这些过程。

注意事项

当你在活动的onCreate()onResume()方法中注册接收器时,你应在onPause()onDestroy()方法中注销它。这种方法的好处是,当应用暂停或关闭时,你不会收到任何广播,这可以减少安卓不必要的开销操作,从而提高电池寿命。

现在,让我们看看我们自定义的广播接收器类的代码。

BluetoothStateReceiver.java 文件

这个类表示我们的自定义广播接收器,用于跟踪蓝牙设备的状态变化。以下代码显示了该文件的实现:

BluetoothStateReceiver.java 文件

正如我们对活动和服务的操作一样,要创建自定义广播接收器,我们需要从BroadcastReceiver类继承并重写方法以声明自定义行为。我们重写了onReceive()方法,并在该方法中执行了跟踪蓝牙设备状态的主要功能。首先,我们将创建一个字符串变量来存储当前状态的字符串值。为了获取字符串值,我们使用了BluetoothAdapter.EXTRA_STATE。现在,我们可以将这个值传递给意图的get()方法来获取我们需要的数据。由于我们的状态是整数并且也是额外的信息,我们调用了Intent.getIntExtra()并在其中传递了我们需要的字符串以及默认值-1。现在,既然我们已经得到了当前状态码,我们可以将这些码与BluetoothAdapter中预定义的码进行比较,以查看蓝牙设备的状态。有四个预定义的状态。

  • STATE_TURNING_ON:此状态通知用户蓝牙正在打开的操作正在进行中。

  • STATE_ON:此状态通知用户蓝牙已经打开。

  • STATE_TURNING_OFF:此状态通知用户蓝牙设备正在关闭。

  • STATE_OFF:此状态通知用户蓝牙已经关闭。

我们将当前状态与这些常量进行比较,并根据得到的结果显示提示信息。Android 的清单文件与之前的示例相同。

因此,简而言之,我们讨论了如何启用蓝牙设备,并通过意图让用户打开或关闭它。我们还看到了如何在广播接收器中使用意图跟踪蓝牙操作的状态并显示提示信息。以下屏幕截图显示了应用程序的演示:

BluetoothStateReceiver.java 文件

启用蓝牙应用

设置为可发现

到目前为止,我们仅通过打开或关闭与蓝牙进行交互。但是,要通过蓝牙开始通信,一个设备必须设置为可发现以开始配对。我们不会为此意图的应用创建任何示例,但只会解释如何通过意图完成这一操作。要打开蓝牙,我们使用了BluetoothAdapter.ACTION_REQUEST_ENABLE意图。我们将意图传递给startActivityForResult()方法,并在onActivityResult()方法中检查结果。现在,要使设备可发现,我们可以在意图中传递BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE字符串。然后,我们将此意图传递给startActivityForResult()方法,并在onActivityResult()方法中跟踪结果以比较结果。

下面的代码片段展示了使设备可发现的意图创建过程:

设置为可发现

在代码中,你可以看到没有什么新内容是没有在之前讨论过的。只是意图动作字符串类型发生了变化,其余部分保持不变。这就是意图的力量;你只需用几分钟和几行代码就可以完成几乎任何事情。

监控可发现性模式

正如我们追踪蓝牙状态变化一样,我们也可以使用本章前面介绍的确切方法来监控可发现性模式。我们必须通过扩展BroadcastReceiver类来创建一个自定义的广播接收器。在onReceive()方法中,我们将得到两个额外的字符串:BluetoothAdapter.EXTRA_PREVIOUS_SCAN_MODEBluetoothAdapter.EXTRA_SCAN_MODE。然后,我们将这些字符串传递给Intent.getIntExtra()方法以获取模式的整数值,然后我们将这些整数与预定义的模式进行比较以检测我们的模式。下面的代码片段展示了代码示例:

监控可发现性模式

通过蓝牙进行通信

蓝牙通信 API 只是围绕标准的RFCOMM(标准蓝牙射频通信协议)的封装。为了与其他蓝牙设备通信,它们必须配对。我们可以使用BluetoothServerSocket类通过蓝牙进行双向通信,该类用于建立监听套接字以启动设备间的链接,以及BluetoothSocket用于创建一个新的客户端套接字来监听蓝牙服务器套接字。一旦建立连接,服务器套接字就会返回这个新的客户端套接字。我们将不讨论蓝牙在通信中的使用,因为这超出了本书的范围。

通过意图使用 Wi-Fi

在互联网时代,以及其在手机上的广泛应用,使得全球信息触手可及。几乎每一位安卓手机用户都期望所有应用能充分利用互联网。因此,为应用添加互联网接入成为了开发者的责任。例如,当用户使用你的应用时,他们可能会想要与朋友分享在应用中的活动,如完成一个游戏关卡或阅读新闻应用中的文章,通过各种社交网络分享,或者发送短信等等。所以,如果用户不能通过你的应用连接到互联网、社交平台或全球信息,那么这个应用就会变得非常局限,甚至可能令人感到无聊。

要执行任何使用互联网的活动,我们首先必须处理互联网连接本身,比如手机是否有任何活动的连接。在本节中,我们将了解如何通过我们的核心主题——意图——访问互联网连接。像蓝牙一样,我们可以通过意图完成许多与互联网连接相关的任务。我们将实现三个主要示例:检查手机的互联网状态、选择任何可用的 Wi-Fi 网络以及打开 Wi-Fi 设置。让我们开始第一个示例,使用意图检查手机的互联网连接状态。

检查互联网连接状态

在我们开始编写示例代码之前,我们需要了解一些重要的事情。任何连接到互联网的 Android 手机都可能拥有任何类型的连接。手机可以通过数据连接连接到互联网,也可以是任何开放的或安全的 Wi-Fi。数据连接被称为移动连接,通过 SIM 卡和服务提供商提供的移动网络连接。在这个例子中,我们将检测手机是否连接到任何网络,如果连接了,它连接的是哪种类型的网络。现在让我们实现代码。

有两个主要的文件负责应用程序的功能:NetworkStatusReceiver.javaAndroidManifest.xml。你可能对MainActivity.java文件感到好奇。在以下示例中,由于应用程序的需求,这个文件没有被使用。我们在这个例子中要实现的功能是,无论手机的互联网连接状态如何改变,比如 Wi-Fi 开启或关闭,这个应用程序都会显示一个吐司来展示状态。该应用将在后台执行其工作,因此不需要活动和布局。现在,让我们逐一解释这些文件:

NetworkStatusReceiver.java 文件

这个类表示我们自定义的广播接收器,用于跟踪设备网络连接状态的变化。以下代码展示了该文件的实现:

NetworkStatusReceiver.java 文件

正如我们对活动和服务的处理一样,要创建自定义广播接收器,我们需要从BroadcastReceiver类继承并重写方法以声明自定义行为。我们重写了onReceive()方法,并在该方法中执行了跟踪 Wi-Fi 设备状态的主要功能。我们已经在 Android 清单文件中将此接收器注册为网络状态更改,我们将在下一节讨论该文件。这个onReceive()方法只在网络状态改变时被调用。因此,我们首先显示一个吐司,声明网络连接状态已改变。

注意

需要注意的是,任何广播接收器都不能在Toast的上下文参数中使用this来传递,就像我们在Activity类中所做的那样,因为BroadcastReceiver类没有像Activity类那样扩展Context类。

我们已经通知了用户关于网络状态的变化,但我们还没有通知用户具体发生了哪种变化。因此,在这个时候,我们的意图对象变得非常有用。它包含了网络的所有信息和数据,以extra对象的形式存在。回顾前面的章节,extraBundle类的一个对象。我们创建一个本地的Bundle引用,并通过调用getExtras()方法将意图的extra对象存储在其中。同时,我们还将在一个boolean变量中存储无连接的extra对象。EXTRA_NO_CONNECTIVITY是一个boolean变量的查找键,用于指示是否完全缺乏网络连接,即是否有任何网络可用。如果这个值为真,意味着没有网络可用。

在存储了我们需要的extra对象之后,我们需要检查extra对象是否存在。因此,我们用 null 检查了extra对象,如果extra对象可用,我们会从中提取更多的网络信息。在 Android 系统中,开发者会以常量字符串的形式被告知感兴趣的数据。所以,我们首先获取我们网络信息的常量字符串,即EXTRA_NETWORK_INFO。我们将其存储在一个字符串变量中,然后将其作为extra对象的get()方法的键值参数使用。Bundle.get()方法返回一个Object类型的对象,我们需要将其类型转换为所需的类。我们正在寻找网络信息,因此我们使用NetworkInfo类对象。

提示

Intent.EXTRA_NETWORK_INFO字符串在 API 级别 14 中已被弃用。由于NetworkInfo可以根据用户 IDUID)而有所不同,应用程序应始终通过getActiveNetworkInfo()getAllNetworkInfo()方法获取网络信息。

我们已经得到了我们感兴趣的所有值和数据;现在,我们将比较并检查数据以找到连接状态。我们检查这个NetworkInfo数据是否为null。如果不是null,我们通过检查NetworkInfogetState()方法的值来确认网络是否已连接。NetworkInfo.State枚举表示粗粒度的网络状态。如果NetworkInfo.State枚举等于NetworkInfo.State.CONNECTED,意味着手机已连接到任何网络。请记住,我们仍然不知道我们连接的是哪种类型的网络。我们可以通过调用NetworkInfo.getTypeName()方法来找到网络的类型。此方法将根据相应的情况返回MobileWi-Fi

注意

粗粒度的网络状态在应用中比DetailedState使用得更广泛。这两种状态映射之间的区别在于,粗粒度网络只显示四种状态:CONNECTING(连接中)、CONNECTED(已连接)、DISCONNECTING(断开中)和DISCONNECTED(已断开)。然而,DetailedState提供了更多细节的状态,例如IDLE(空闲)、SCANNING(扫描中)、AUTHENTICATING(认证中)、UNAVAILABLE(不可用)、FAILED(失败)以及前面提到的四种粗粒度状态。

剩下的部分是一个if-else代码块,用于检查网络状态并在屏幕上显示相应的状态提示。总的来说,我们首先从意图中提取了extra对象,将它们存储在局部变量中,从额外信息中提取网络信息,检查状态,并最终以提示信息的形式显示出来。接下来,我们将在下一节讨论 Android 的清单文件。

AndroidManifest.xml 文件

由于我们的应用中使用了一个广播接收器来检测网络连接状态,因此有必要在应用中注册和注销广播接收器。在我们的清单文件中,我们执行了两项主要任务。首先,我们添加了访问网络状态的权限,使用了android.permissions.ACCESS_NETWORK_STATE。其次,我们使用接收器标签注册了我们的接收器,并添加了类的名称。

同时,我们添加了意图过滤器。这些意图过滤器定义了接收器的目的,比如应该从系统接收哪种类型的数据。我们使用了android.net.conn.CONNECTIVITY_CHANGE过滤器动作来检测网络连接变化广播。除了这两点之外,这个文件中没有新的内容,其余代码与我们在前面章节中讨论的一致。以下是该文件的代码实现:

AndroidManifest.xml 文件

总结前面应用的细节,我们创建了一个自定义的广播接收器,并定义了网络变化的自定义行为,即显示提示信息,然后在清单文件中注册了我们的接收器,并声明了所需的权限。以下截图显示了在手机开启 Wi-Fi 时应用的一个简单演示:

AndroidManifest.xml 文件

网络状态变化应用

在前面的截图中,我们可以看到当开启 Wi-Fi 时,应用会显示一个提示框告知网络状态已改变。提示框之后,它会显示变化;在我们的案例中,Wi-Fi 已连接。你可能会好奇在这个应用中意图(intents)的作用。没有使用意图,这个应用是不可能实现的。首先在清单文件中使用意图来注册接收器,以便筛选网络状态变化。另一个意图的使用是在接收器中,当我们收到更新并想知道变化时。因此,我们使用了意图,并以extra对象的形式从中提取数据,用于我们的目的。在这个例子中,我们没有创建自己的意图;而是仅使用了提供的意图。在下一个例子中,我们将创建自己的意图,并使用它们从应用中打开 Wi-Fi 设置。

打开 Wi-Fi 设置应用

到目前为止,我们仅将意图用于网络和 Wi-Fi 目的。在这个例子中,我们将创建意图对象并在我们的应用中使用它。在之前的示例应用中,我们检测了手机的 network change 状态并在屏幕上显示。在这个例子中,我们将在同一个应用中添加一个按钮。点击或轻触按钮,应用将打开 Wi-Fi 设置。用户可以从那里打开或关闭 Wi-Fi。当用户执行任何操作时,应用将在屏幕上显示网络状态变化。对于网络状态,我们使用了NetworkStatusReceiver.javaAndroidManifest.xml文件。现在,让我们打开同一个项目,并更改我们的MainActivity.javalayout_main.xml文件,为它们添加一个按钮及其功能。让我们逐一看看这两个文件:

activity_main.xml文件

这个文件是我们主活动文件的视觉布局。我们将在这个 XML 文件中添加一个按钮视图。该文件的代码实现如下:

文件

我们在布局中添加了一个按钮,其视图 ID 为btnWifiSettings。我们将使用这个 ID 在布局文件中获取按钮视图。我们已经在之前的章节中讨论过布局。现在,让我们看看将使用此布局作为视觉内容的主活动文件。

MainActivity.java文件

这个文件表示作为应用启动点的活动主文件。我们将在本文件中实现我们按钮的核心功能。该文件的代码实现如下:

文件

如同之前多次讨论的那样,我们从Activity类扩展了我们的类,并覆盖了类的onCreate()方法。在调用超类方法之后,我们首先使用setContentView()方法引用了我们的布局文件(在上一节中解释),并将布局 ID 作为参数传递。在获取布局文件之后,我们通过调用findViewById()方法从布局中提取了我们的 Wi-Fi 设置按钮。记住,我们将按钮视图的 ID 设置为btnWifiSettings;因此,我们将此 ID 作为参数在方法中传递。我们在一个本地Button object.reference对象中存储了按钮的引用文件。现在,我们将设置本地按钮的View.OnClickListener,以在按钮点击时执行我们的任务。我们在button.setOnClickListener()方法中传递了一个OnClickListener的匿名对象,并覆盖了匿名对象的onClick()方法。

至今为止,我们仅完成了创建应用设置的一些初始步骤。现在,让我们集中精力打开 Wi-Fi 设置任务。我们将创建一个Intent对象,并且必须传递一个常量字符串 ID 来告诉意图要启动什么。我们将使用Settings.ACTION_WIFI_SETTINGS常量,它展示了允许配置 Wi-Fi 的设置。创建Intent对象后,我们将在startActivity()方法中传递它,以打开包含 Wi-Fi 设置的活动。这非常简单,完全没有火箭科学那么复杂。当我们运行这个应用时,我们将看到与以下截图类似的内容:

MainActivity.java 文件

打开 Wi-Fi 设置应用

如从前面的截图中所见,当我们点击或轻触 Wi-Fi 设置按钮时,它将打开 Android 手机的 Wi-Fi 设置屏幕。在更改设置时,比如打开 Wi-Fi,它将显示吐司通知来展示更新的更改和网络状态。

我们已经讨论了使用意图进行通信的组件,在其中我们通过意图使用了蓝牙和 Wi-Fi,并看到了它们在各种示例和应用中的使用方法。现在,我们将讨论如何通过意图使用媒体组件,以及我们在以下各节中可以为媒体组件做些什么。

媒体组件

前一节我们讨论了通信组件的全部内容。但旧手机与新型智能手机的区别在于多媒体功能,比如高清音视频特性。手机的多媒体功能已成为许多消费者更为重视的考量因素。幸运的是,Android 系统为许多功能提供了多媒体 API,比如播放和记录各种图像、音频和视频格式,无论是本地还是流式传输。如果用简单的语言来描述媒体组件,这个主题只能在一个专门的章节中详细讲解,这超出了本书的范围。我们仅讨论那些可以通过意图触发、使用和访问的媒体组件。本节将要讨论的组件包括使用意图拍照、使用意图录制视频、使用意图的语音识别以及意图在文本到语音转换中的作用。前三个主题使用意图执行操作;但文本到语音转换的最后一个主题并不完全使用意图。我们还将开发一个示例应用程序来实际观察意图的使用。让我们逐一在以下小节中讨论这些主题。

使用意图拍照

现在,几乎每部手机都有数码相机组件。嵌入在手机中的数码相机的普及使得它们的价格和体积都下降了。Android 手机也包含从 320 万像素到 3200 万像素不等的数码相机。从开发的角度来看,可以通过许多不同的方法拍照。Android 系统也提供了相机控制和拍照的 API,但我们只关注一种使用意图的方法。这是 Android 开发中拍照最简单的方式,代码不超过几行。

我们将首先创建一个带有图像视图和按钮的布局。然后,在Activity类中,我们从布局文件中获取我们视图的引用,并设置按钮的点击监听器。点击按钮时,我们将创建捕获图像的意图,并以子类的形式启动另一个活动。在得到结果后,我们将在我们的图像视图中显示那捕获的图像。

那么,准备好基本的空 Hello World 项目后,我们将修改三个文件并在其中添加我们的代码。这些文件是activity_main.xmlMainActivity.javaAndroidManifest.xml。让我们逐一解释每个文件中的更改:

activity_main.xml 文件

这个文件代表了视觉布局。我们将添加一个ImageView标签以显示捕获的图像,以及一个Button标签以拍照并触发相机。该文件的代码实现如下:

activity_main.xml 文件

如代码所示,我们在相对布局中放置了一个带有imageView1 ID 的ImageView标签。这个 ID 将在主活动文件中使用,从布局中提取视图以在 Java 文件中使用。我们将视图通过在android:layout_centerHorizontal标签中赋值为true来放置在布局的水平中心。最初,我们将应用启动图标的默认图像设置到我们的图像视图中。在图像视图下方,我们放置了一个按钮视图。点击按钮时,将启动相机。按钮的 ID 通过图像视图布局下的android:layout_below标签设置为btnTakePicture。这种相对性是相对布局与线性布局相比的主要优势。现在,让我们看看执行主要功能并使用此布局作为视觉部分的应用的活动。

MainActivity.java 文件

此文件表示应用的主要启动活动。该文件使用layout_main.xml文件作为视觉部分,并从Activity类扩展而来。文件的代码实现如下:

MainActivity.java 文件

我们通过重写活动的onCreate()方法来开始我们的类。通过调用setContentView()方法,我们将活动的视觉布局设置为activity_main.xml布局。现在,由于布局已设置,我们可以获取布局文件中视图的引用。

我们在类中创建两个字段;ImageView类的takenImage用于显示捕获的图像,以及Button类的imageButton用于点击触发相机。当点击按钮时,将调用onClick()方法。因此,我们将在该方法中定义相机触发的代码。在这个方法中,我们正在创建Intent类的一个实例,并在构造函数中传递MediaStore.ACTION_IMAGE_CAPTURE常量。这个常量将告诉 Android,该意图是为了图像捕获,当启动这个意图时,Android 将启动相机。如果用户安装了不止一个相机应用,Android 将呈现所有有效相机应用的列表,用户可以选择任何应用来拍摄图像。

创建意图实例后,我们将这个意图对象传递给startActivityForResult()方法。在我们的拍照应用中,点击按钮将启动相机的另一个活动。当我们关闭相机活动时,它将返回到我们应用的原始活动,并给我们一些捕获图片的结果。因此,要在任何活动中获取结果,我们必须重写onActivityResult()方法。当子活动完成后,父活动开始时,将调用此方法。当调用此方法时,意味着我们已经使用了相机,现在回到了父活动。如果结果成功,我们可以在图像视图中显示捕获的图像。

首先,我们可以了解此方法是在使用相机后调用,还是发生了另一个动作。为此,我们必须比较方法的requestCode参数。记住,当调用startActivityForResult()方法时,我们将TAKE_IMAGE_CODE常量作为另一个参数传递。这就是要比较的请求代码。

之后,为了检查结果,我们可以查看方法的resultCode参数。由于我们使用这段代码来启动相机拍照意图,我们将我们的resultCodeRESULT_OK常量进行比较。两个条件都成功后,我们可以得出我们已经收到图像的结论。因此,我们通过调用getExtras().get()方法使用意图获取我们的图像数据。这将给我们Object类型的数据。我们进一步将其类型转换为Bitmap,以便为ImageView准备。

最后,我们调用setImageBitmap方法,将新的位图设置到我们的图像视图中。如果你运行代码,你会看到一个图标图像和一个按钮。点击按钮后,将启动相机。当你拍照时,应用会崩溃并关闭。你可以在以下截图中看到:

MainActivity.java 文件

拍照后应用崩溃了

你可能想知道为什么会发生崩溃。我们忘记提到一件事;每当任何应用使用相机时,我们必须在我们的清单文件中添加uses-feature标签,告诉应用它将使用相机功能。让我们看看我们的 Android 清单文件,了解uses-feature标签。

AndroidManifest.xml 文件

此文件定义了在我们的应用程序中要使用的所有设置和功能。只有一个我们没见过的新东西。文件的代码实现如下:

AndroidManifest.xml 文件

你可以看到我们添加了uses-feature标签,并在android:name属性中指定了android.hardware.camera。这个标签告诉应用程序它将如何使用相机,Android 操作系统允许我们的应用程序使用外部相机。

在清单文件中添加这行代码并运行后,如果你手机里有多个相机应用,你会看到类似以下截图的内容:

AndroidManifest.xml 文件

通过意图应用拍照

在截图中,你可以看到用户被要求选择相机,拍照后,图像会在应用中显示。

当我们总结代码时,我们首先创建了一个带有图像视图和按钮的布局。然后在Activity类中,我们从布局文件中获取我们视图的引用,并设置了按钮的点击监听器。点击按钮后,我们创建了捕获图像的意图,并启动了另一个作为子活动的活动。获取结果后,我们在图像视图中显示了捕获的图像。这就像在公园里散步一样简单。在下一节中,我们将了解如何使用意图录制视频。

使用意图录制视频

到目前为止,我们已经看到了如何使用意图拍照。在本节中,我们将看到如何使用意图录制视频。我们不会在本节中讨论整个项目。使用意图录制视频的流程与拍照几乎相同,只有一些小的变化。我们只在本节中讨论这些变化。现在,让我们看看应用是如何录制视频的。

我们做的第一个更改是在我们的布局文件中。我们移除了图像视图部分,并放置了VideoView标签。以下代码实现展示了该标签:

使用意图录制视频

你可以看到,这与ImageView中的内容是一样的。现在,由于我们在布局中将图像视图更改为视频视图,因此我们也必须在活动中进行相应的更改。正如我们对ImageView所做的那样,我们将创建一个VideoView的字段对象,并在活动的onCreate()方法中获取它的引用。以下代码示例展示了VideoView字段对象的行:

使用意图录制视频

一切都保持不变,我们已经讨论过这部分内容。现在,在我们的onClick()方法中,我们将看到如何发送触发视频录制的意图。要放在onClick()方法中发送意图的代码实现如下:

使用意图录制视频

你可以看到我们创建了一个意图对象,并且在构造函数中,我们没有传递MediaStore.ACTION_IMAGE_CAPTURE,而是传递了MediaStore.ACTION_VIDEO_CAPTURE。此外,我们还通过调用putExtra()方法在意图中放置了一个extra对象。我们通过将MediaStore.EXTRA_VIDEO_QUALITY值赋为1来定义视频质量为highextra对象。然后,我们再次将意图传递给startActivityForResult()方法,以启动相机活动。

下一个变化是在我们从意图获取视频的onActivityResult()方法中。以下代码展示了一些获取视频并将其传递给VideoView标签并播放的示例代码:

使用意图录制视频

在拍照的情况下,我们从意图中恢复了原始数据,将其类型转换为Bitmap,然后将我们的ImageView设置为Bitmap。但是在这里,在录制视频的情况下,我们只获取了视频的 URI。Uri对象声明了手机中数据的引用。我们获取视频的 URI,并使用setVideoURI()方法将其设置在我们的VideoView中。最后,我们通过调用VideoView.start()方法播放视频。

从这些部分,你可以看到使用意图捕获图像或录制视频是多么容易。通过意图,我们使用了已经内置的相机或相机应用。如果我们想要自己的自定义相机来捕获图像和视频,我们就必须使用 Android 的 Camera API。

我们可以使用 MediaPlayer 类来播放视频、音频等。MediaPlayer 类包含 start()stop()seekTo()isLooping()setVolume() 等方法。要录制视频,我们可以使用 MediaRecorder 类。这个类包含 start()stop()release()setAudioSource()setVideoSource()setOutputFormat()setAudioEncoder()setVideoEncoder()setOutputFile() 等方法,还有更多。

注意

在您的应用中使用 MediaRecorder API 时,别忘了在您的清单文件中添加 android.permission.RECORD_AUDIOandroid.permission.RECORD_VIDEO 权限。

若要不用意图(intents)拍照,我们可以使用 Camera 类。这个类包含了 open()release()startPreview()stopPreview()takePicture() 等方法,还有更多。

注意

当您在应用中使用 Camera API 时,别忘了在您的清单文件中添加 android.permission.CAMERA 权限。

到目前为止,我们已经使用意图来处理视频和图片的可视媒体组件。在下一节中,我们将使用意图来处理手机的音频组件。我们将在接下来的章节中看到如何使用意图来支持语音识别和文本转语音。

使用意图进行语音识别

智能手机引入了语音识别功能,这对残疾人士来说是一大成就。安卓在 API 级别 3 的版本 1.5 中引入了语音识别。安卓通过 RecognizerIntent 类支持语音输入和语音识别。安卓的默认键盘上有一个带有麦克风图标的按钮。这让用户可以选择说话而不是输入文本。它为此目的使用语音识别 API。以下截图显示了带有麦克风按钮的键盘:

使用意图进行语音识别

带有麦克风按钮的安卓默认键盘

在本节中,我们将创建一个示例应用,其中包含一个按钮和文本字段。点击按钮后,将显示安卓的标准语音输入对话框,并要求用户说些什么。应用将尝试识别用户所说的话并将其输入到文本字段中。我们将从在 Android Studio 或其他 IDE 中创建一个空项目开始,并修改其中的两个文件。下一节我们将从布局文件开始。

activity_main.xml 文件

这个文件代表了应用的可视内容。我们将在该文件中添加文本字段和按钮视图。该文件的代码实现如下:

activity_main.xml 文件

如你所见,我们放置了一个EditText字段。我们将android:inputType设置为textMultiLine以在多行中输入文本。在文本字段下方,我们添加了一个带有 ID 为btnRecognizeButton视图。当点击此按钮时,它将用于启动语音识别活动。现在,让我们讨论一下主要活动文件。

MainActivity.java 文件

此文件代表了项目的主要活动。该文件的代码实现如下:

MainActivity.java 文件

与往常一样,我们重写了onCreate()方法,并通过setContentView()方法设置的布局获取按钮引用。我们将按钮的监听器设置为此类,并在活动中实现OnClickListener,同时重写onClick()方法。在onClick()方法中,我们创建了一个意图对象,并在构造函数中将RecognizerIntent.ACTION_RECOGNIZE_SPEECH作为动作字符串传递。这个常量会告诉 Android,该意图是为了语音识别。然后,我们需要添加一些extra对象,向 Android 提供有关意图和语音识别的更多信息。要添加的最重要的extra对象是RecognizerIntent.EXTRA_LANGUAGE_MODEL。这会告诉识别器在识别语音时使用哪个语音模型。识别器使用这个额外信息来更精确地调整结果。这个extra方法在调用语音识别意图时是必须提供的。我们传入了RecognizerInent.ACTION_LANGUAGE_MODEL_FREE_FORM模型进行语音识别。这是一个基于自由形式语音识别的语言模型。现在,我们有一些可选的extra对象可以帮助识别器得到更准确的结果。我们添加了RecognizerIntent.EXTRA_PROMPTextra,并在其中传递了一些字符串值。这将通知用户语音识别已开始。

接下来,我们添加了RecognizerIntent.EXTRA_MAX_RESULTSextra,并将其值设置为1。语音识别的准确性总是有所变化。因此,识别器将尝试更精确地识别。它会创建不同准确性和可能不同含义的不同结果。通过这个extra,我们可以告诉识别器我们感兴趣的结果数量。在我们的应用中,我们将其设置为1。这意味着识别器将只为我们提供一个结果。不能保证这个结果足够准确;这就是为什么建议传递大于1的值。对于简单的情况,你可以传递一个值高达5。记住,你传递的值越大,识别所需的时间就越长。

最后,我们添加了最后一个关于语言的可选extra。我们将Locale.ENGLISH作为RecognizerIntent.EXTRA_LANGUAGE的值传递。这将告诉识别器语音的语言。因此,识别器无需检测语言,这使语音识别的准确性更高。

注意

语音识别引擎可能无法理解Locale类中所有可用的语言。同样,也不是所有设备都支持语音识别。

添加所有extra对象后,我们确保了意图对象已准备好。我们将其传递给startActivityForResult()方法,并设置requestCode1。当调用此方法时,会显示一个标准的语音识别对话框,并带有我们给出的提示信息。说话结束后,我们父活动的onActivityResult()方法将被调用。我们首先检查requestCode是否为1,以确保这是我们语音识别的结果。之后,我们将检查resultCode以确定结果是否正常。成功的结果后,我们会得到一个字符串数组列表,其中包含识别器识别的所有单词。我们可以通过调用getStringArrayListExtra()方法并传递RecognizerIntent.EXTRA_RESULTS来获取这些单词的列表。当resultCode正常时,才会返回此列表;否则,我们会得到一个 null 值。在完成语音识别的相关操作后,我们现在可以将文本值设置为结果。为此,我们首先从布局中提取EditText视图,并通过调用setText()方法将我们的结果设置为文本字段的值。

注意

运行语音识别需要活跃的互联网连接。语音识别过程是在谷歌的服务器上执行的。安卓手机接收语音输入,将其发送到谷歌服务器,并在那里进行处理以识别。识别完成后,谷歌将结果发送回安卓手机,手机会通知用户关于结果,这样整个循环就完成了。

如果你运行这个项目,你会看到类似于以下截图的内容:

MainActivity.java 文件

使用意图进行语音识别

在图片中,你可以看到点击识别按钮后,会显示一个标准的语音输入对话框。说话之后,我们会返回到父活动,并在识别语音后,它将在文本字段中打印所有文本。

意图在文本到语音转换中的作用

在上一节中,我们讨论了 Android 系统如何识别我们的语音并执行操作,例如通过语音命令控制手机。我们还使用意图开发了一个简单的语音到文本示例。这一节与上一节相反。在本节中,我们将讨论 Android 系统如何将我们的文本转换成优美的语音叙述。我们可以称之为文本到语音转换。Android 在版本 1.6(API 级别 4)中引入了文本到语音TTS)转换引擎。我们可以使用这个 API 在应用程序内部产生语音,从而允许我们的应用与用户对话。如果我们添加语音识别,它将像是与应用程序对话一样。文本到语音转换需要预装语言包,由于手机存储空间有限,不能保证手机已经预装了任何语言包。因此,在使用文本到语音引擎创建任何应用时,检查语言包是否已安装是一个好习惯。

我们不能通过意图使用文本到语音转换。我们只能通过称为 TTS 的文本到语音引擎使用它。但在文本到语音转换中,意图有一个小作用。意图仅用于检查语言包是否预装。因此,任何使用文本到语音的应用在创建时,首先必须使用意图来检查语言包的安装状态。这就是意图在文本到语音转换中的作用。下面是检查语言包安装状态的示例代码:

在文本到语音转换中意图的作用

在进行文本到语音转换时,我们首先要检查语言包。在代码中,我们可以看到我们正在创建一个意图对象。我们传递了Engine.ACTION_CHECK_TTS_DATA常量,这会告诉系统该意图将检查文本到语音(TTS)数据和语言包。然后我们在startActivityForResult()方法中传递该意图,并附上作为requestCode使用的VAL_TTS_DATA常量值。现在,如果安装了语言包且一切正常,我们将在onActivityResult()方法中得到resultCodeRESULT_OK。所以,如果结果正常,我们可以使用文本到语音转换。下面是onActivityResult()方法的代码示例,如下截图所示:

在文本到语音转换中意图的作用

因此,我们首先检查传递的代码的requestCode。然后,我们将resultCode检查为Engine.CHECK_VOICE_DATA_PASS。这个常量用于判断是否有语音数据。如果我们的手机中有数据,我们可以在那里进行文本到语音转换。否则,很明显,在进行文本到语音转换之前,我们必须先安装语音数据。你会很高兴知道,安装语音数据也非常简单;它使用意图来完成这个目的。以下代码片段展示了如何使用意图安装语音数据:

意图在文本到语音转换中的作用

我们创建了一个意图对象,并在构造函数中传递了Engine.ACTION_INSTALL_TTS_DATA。这个常量将告诉 Android,这个意图是用来安装文本到语音语言数据包的。然后,我们将意图传递给startActivity()方法以开始安装。在语言包安装之后,当我们想要进行文本到语音转换时,我们必须创建一个TextToSpeech类的对象,并在onActivityResult()方法中调用其speak()方法:

protected void onActivityResult(int requestCode, int resultCode,
  Intent data) {

  if (requestCode == VAL_TTS_DATA) {
    if (resultCode == Engine.CHECK_VOICE_DATA_PASS) {
      TextToSpeech tts = new TextToSpeech(this,
        new OnInitListener() {
        public void onInit(int status) {
          if (status == TextToSpeech.SUCCESS) {
            tts.setLanguage(Locale.US);
            tts.setSpeechRate(1.1f);
            tts.speak("Hello, I am writing book for Packt",
              TextToSpeech.QUEUE_ADD, null);
          }
        }
      });
    }
    else {
      Intent installLanguage = new Intent (
        Engine.ACTION_INSTALL_TTS_DATA);
      startActivity(installLanguage);
    }
  }
}

如代码所示,在成功安装语言数据包后,我们创建了一个TextToSpeech实例,并传递了一个匿名OnInitListener对象。我们实现了onInit()方法。这个方法将设置TextToSpeech对象的初始设置。如果状态是成功的,我们将设置语言、语速,最后调用speak()方法。在这个方法中,我们传递了一个字符串,Android 将会大声读出这些字母。

总结整个话题,意图在文本到语音转换中的作用是检查和安装语音数据包。意图并不直接贡献于文本到语音转换,但它们只是为文本到语音转换设置了初始设置。

通过文本到语音转换,我们已经完成了关于媒体组件的讨论。在媒体组件中,我们讨论了拍照、录制视频、语音识别和文本到语音转换。在下一节中,我们将讨论运动组件,并看看意图在这些组件中扮演的角色。

运动组件

安卓手机的运动组件包括许多执行不同任务和动作的不同类型的传感器。在本节中,我们将讨论运动和位置传感器,如加速度计、地磁传感器、方向传感器和接近传感器。所有这些传感器都参与了安卓手机的运动和位置。我们将只讨论那些使用意图来触发的传感器。只有一个这样的传感器使用意图,那就是接近传感器。让我们在下一节中讨论它。

意图和接近警报

在了解意图在近距离警报中的作用之前,我们将讨论近距离警报是什么以及它们在各种应用程序中如何有用。

什么是近距离警报?

近距离传感器让用户确定设备与物体的接近程度。当你的应用程序对手机屏幕移向或远离任何特定物体时做出反应时,这通常很有用。例如,当我们在 Android 手机上接到来电时,将手机放在耳朵上会关闭屏幕,将其拿回手中会自动开启屏幕。这个应用正在使用近距离警报来检测耳朵与设备近距离传感器之间的距离。下图以视觉形式展示了这一点:

什么是近距离警报?

另一个例子可能是当手机闲置一段时间并且屏幕关闭时,如果有未接电话,它将震动或者给出通知提示我们检查手机。这也可以通过使用近距离传感器来完成。

近距离传感器使用近距离警报来检测手机传感器与任何物体之间的距离。这些警报允许你的应用程序设置触发器,当用户移动到或超出设定的地理位置距离时,这些触发器会被触发。本节不讨论近距离警报使用细节,但将涵盖一些基本信息以及意图在近距离警报中的作用。例如,我们为特定的覆盖区域设置一个近距离警报。我们选择一个经纬度形式的点,围绕该点的半径(米),以及警报的过期时间。现在,设置近距离警报后,如果设备越过该边界,将触发警报。可能是设备从外部移动到半径内,或者从半径内移动到外部。

意图在近距离警报中的作用

当近距离警报被触发时,它们会激活意图。我们将使用一个PendingIntent对象来指定要触发的意图。让我们看看前面章节中讨论的距离应用的一些代码示例,以下是其实现:

意图在近距离警报中的作用

在前面的代码中,我们正在实现使用应用程序中近距离警报的第一步。首先,我们通过PendingIntent创建一个近距离警报。我们将警报的名称定义为DISTANCE_PROXIMITY_ALERT,然后通过调用当前活动中编写的代码的getSystemService()方法来获取位置管理服务。然后,我们为纬度、经度、半径和过期时间设置了一些随机值,过期时间设为无限。应该记住,这些值可以根据你正在创建的应用程序类型设置为任何值。

现在我们来到了创建接近警报最重要的部分。我们创建意图,并在构造函数中传递我们自己的警报名称以创建我们自己的意图。然后,我们通过使用getBroadcast()方法获取一个广播意图来创建一个PendingIntent对象。最后,我们通过调用addProximityAlert()方法,在位置管理服务中添加接近警报。

这段代码仅创建了警报并为其设置了初始值。现在,假设我们已经完全完成了我们的距离应用。因此,无论何时我们的设备穿过我们在应用中指定的边界或进入该边界内,LocationManager将会检测到我们已经越过了边界,并且会触发一个带有LocationManager.KEY_PROXIMITY_ENTERING额外值的意图。这个值是一个布尔值。如果它的值是true,意味着我们已经进入了边界;如果是false,则我们离开了边界。为了接收这个意图,我们将创建一个广播接收器并执行操作。以下代码段展示了接收器的示例实现:

public class ProximityAlertReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    Boolean isEntered = intent.getBooleanExtra(
      LocationManager.KEY_PROXIMITY_ENTERING, false);
    if (isEntered)
      Toast.makeText(context, "Device has Entered!",
        Toast.LENGTH_SHORT).show();
    else
      Toast.makeText(context, "Device has Left!",
        Toast.LENGTH_SHORT).show();
  }
}

在代码中,你可以看到我们使用getBooleanExtra()方法获取LocationManager.KEY_PROXIMITY_ENTERING的额外值。我们比较这个值,并相应地显示吐司提示。正如你所见,这是相当简单的。但是,像所有接收器一样,这个接收器在注册到AndroidManifest.xml或通过 Java 代码注册之前是不会工作的。注册接收器的 Java 代码如下:

在接近警报中意图的作用

这里除了我们调用了Activity类的registerReceiver()方法外,没有什么需要解释的。我们将在接下来的章节中更详细地讨论IntentFilter

简而言之,意图在获取接近警报方面起着次要的作用。意图仅用于告诉 Android 操作系统已添加了哪种类型的接近警报,它何时触发,以及它应该包含哪些信息,以便开发人员可以在他们的应用中使用它。

概述

在本章中,我们讨论了几乎所有 Android 手机中常见的移动组件。这些组件包括 Wi-Fi 组件、蓝牙、蜂窝网络、全球定位系统、地磁场、运动传感器、位置传感器和环境传感器。然后,我们讨论了这些组件中使用意图的作用。为了更详细地解释这个作用,我们使用了蓝牙通信、打开/关闭蓝牙、使设备可被发现、打开/关闭 Wi-Fi 以及打开 Wi-Fi 设置的意图。我们还看到了如何通过意图拍照、录制视频、进行语音识别和文本到语音的转换。最后,我们看到了如何通过意图使用接近传感器。

在下一章中,我们将看到如何在不同活动、服务和其他移动组件之间通过意图传输数据。

第五章:使用意图进行数据传输

到目前为止,我们已经学习了意图的分类、它们在安卓组件中的用途以及在安卓应用程序中逐步实现它们的方法。现在是查看安卓应用程序最重要的部分的时候了。在安卓应用程序中,从一项活动向另一项活动传输数据(无论是隐式还是显式)是必不可少的。本章的主要焦点是数据的安全传输和检索。

本章包括以下主题:

  • 传输数据的必要性

  • 活动之间的数据传输——一种有意的方式

  • 在显式意图中进行数据传输

  • 使用意图显式传输数据的方法

  • 在隐式意图中进行数据传输

寻找需要传输数据的需求

从技术上讲,一个安卓应用程序是由不同的活动组合而成的。这些活动包括布局、视图和一些内容。这些内容大多数不是动态的,也不是预先决定的。例如,如果一个安卓布局包含一个按钮,那么该按钮中的文本可以是静态的或预定义的。同样,如果一个活动中有任何文本字段或列表视图,它通常包含来自任何服务器或其他手段的动态数据。

在这些情况下,我们需要一些动态数据,这些数据我们的应用程序可以从服务器(或其他地方)获取,并在活动之间传输。正是在这种场景下发生数据传输。此外,在其中一个活动对数据进行一些操作,而另一个活动需要在其视图中显示它的情况下,数据传输的可能性非常高。

举一个简单的例子

为了更好地理解这张图片,让我们从理论上举例说明为什么需要在活动之间进行数据传输。读者应用程序可以作为一个很好的例子,来理解数据传输的原因。

读者应用程序是一个包含不同种类新闻的列表视图的应用程序,点击新闻可以进入描述页面,页面中会展示整条新闻以及图片和其他文本。让我们一步步了解这个应用程序的流程(以 TechCrunch 安卓应用为例)。应用程序将以一个启动屏幕开始,向读者或开发者描述谁制作了这个应用。

下面的截图是启动屏幕;应用程序将搜索互联网连接,以便将新闻源显示到应用程序的屏幕上。一旦本地获取了数据,它就会解析数据并将其放入列表视图中。请注意,以下列表视图的截图基本上是自定义列表视图,并不是直接通过安卓的内置布局获得的。我们需要为此制作一个自定义布局,然后在常规列表视图中填充它。为此,需要使用适配器(请参考互联网,了解如何在安卓中创建基本的列表视图)。

举一个简单的例子

读者应用的活动,其中新闻在列表视图中列出。

现在,有两种可能的数据传输方式,如下所述:

  • 获取包括描述在内的所有数据

  • 一旦点击了列表视图中的馈送,将立即获取该馈送的描述。

举一个简单的例子

显示前一个活动中点击的故事描述的活动

无论哪种情况,这一步都需要将数据从第一个活动传输到下一个活动。如果是第一种情况,由第一个活动解析的描述数据将被传递到第二个活动,以便在视图中填充它。否则,在第二种情况下,它将传递一些 URL 给第二个活动,从那里它可以获取新闻的描述。请参考前面的屏幕截图。

活动之间的数据传输——一种 INTENTed 方式

当我们讨论活动之间的数据传输时,我们需要记住,管理与活动流程交互的唯一方式是通过意图。在上一章中,我们详细讨论了如何使用意图从一个活动移动到另一个活动。在这里,我们将看到如何将数据与这些意图一起传输,以及如何在目标活动中安全地捕获传输的数据。

显式意图中的数据传输

你可以通过注意显式意图中数据传输的使用来开始理解意图中的数据传输。回想一下显式意图的定义,它们是指向另一个活动(在同一应用内或另一个应用内)的意图。显式意图通常指向同一应用内的活动,但根据应用需求,它们也可以指向属于其他应用的活动(例如,设备相机)。

活动间数据传输的方法

在本节中,我们将开始了解在 Android 应用程序中使用的各种数据传输技术。这些技术各有优缺点。总共有三种方法可以从一个活动显式地传输数据到另一个活动。我们很快就会看到它们以及它们的示例。这三种方法如下:

  • 使用putExtras()进行数据传输

  • 使用Parcelable进行数据传输(仅适用于自定义数据对象)

  • 使用Serializable进行数据传输(仅适用于自定义数据对象)

使用 putExtras()进行数据传输

在 Android 中,将数据从一个活动传输到另一个活动的最简单方式是通过 extras 发送。意图 extras 支持原始数据类型以发送数据。这意味着你可以以不同的数据类型如StringBooleanIntegerFloat的形式发送数据。

从理论上讲,意图额外信息可以在 Android API 的 Intent 类中找到。开发者需要创建一个 Intent 类的对象。这个对象将用于在活动之间导航。有了这个对象,就会有 putExtras() 函数的多种多态形式。这些多态形式接受不同的数据类型(如前所述)作为参数,并将意图对象加载到该数据中。这样,对象就完成了。现在,从 Activity 类中调用 startActivity() 方法开始执行意图。

这只是问题的一面。这个意图将应用程序的流程引导到第二个活动;在显式调用的情况下,它是同一应用程序的活动,或者在隐式意图的情况下,它可以是其他应用程序。这个新活动将接收意图对象并从中提取数据。因此,Intent 类中还有另一个方法,称为 getExtras()。结果,它将给出源活动在意图对象中添加的所有额外信息,并使用它,我们可以轻松提取意图中额外信息中所需的数据。

这个理论解释可能不会让你完全理解使用意图进行数据传输的每一个细节。我们将在下一节通过示例更多地了解使用意图进行数据传输,其中将给出数据传输的分步解释。

putExtras() 的实现

在这一节中,我们将逐步学习如何借助额外信息从一个活动向另一个活动传输数据。正如你可能之前读到的,考虑到活动之间的数据传输,这种方法是最简单的。为了理解这个方法的运作和实现,你必须了解活动生命周期、不同活动的处理以及意图的实现,以便在活动之间导航作为前提条件。

为了开始第一个示例,第一步是创建一个 Android 项目。在 Android Studio 中创建项目的步骤在之前的章节中已经描述;如果你需要,可以参考它们。你最终会创建一个带有许多文件和文件夹的项目(这是 Android 项目的默认设置)。

实现一个现成的教程

为了简单起见,我们将源活动命名为 Activity1,目标活动命名为 Activity2。现在,按照以下步骤成功实现示例。

  1. 首先,创建一个新的 Android 项目,或者选择你想要使用意图实现数据传输的任何现有项目。在你新创建的项目中,相应类里实现以下代码:

    //---------------------------------------------------------------
    //Activity1 Class
    
    public class Activity1 extends Activity {
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_first);
    
        final EditText editTextFieldOne = (EditText) findViewById(R.id.edittext1);
        final EditText editTextFieldTwo  = (EditText) findViewById(R.id.edittext2);
        final EditText editTextFieldThree  = (EditText) findViewById(R.id.edittext3);
    
        Button transferButton = (Button) findViewById(R.id.button);
    
        String valueOne = editTextFieldOne.getText().toString();
        String valueTwo = editTextFieldTwo.getText().toString();
        String valueThree = editTextFieldThree.getText().toString();
    
        transferButton.setOnClickListener(new OnClickListener() {
    
          @Override
          public void onClick(View v) {
            // TODO Auto-generated method stub
    
            Intent intent = new Intent(Activity1.this,Activity2.class);
            intent.putExtra("EDITTEXT_ONE_VALUE",valueOne);
            intent.putExtra("EDITTEXT_TWO_VALUE",valueTwo);
            intent.putExtra("EDITTEXT_THREE_VALUE",valueThree);
            Activity1.this.startActivity(intent);
    
          }
        });
    
      }
    }
    
    //----------------------------------------------------------------
    //Activity2.java
    
    public class Activity2 extends Activity {
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_second);
    
        Intent intent = getIntent();
    
        String valueOne = intent.getExtras().getStringKey("EDITTEXT_ONE_VALUE");
        String valueTwo = intent.getExtras().getStringKey("EDITTEXT_TWO_VALUE");
        String valueThree = intent.getExtras().getStringKey("EDITTEXT_THREE_VALUE");
    
        TextView textViewOne = (TextView) findViewbyId(R.id.textView1);
        TextView textViewTwo = (TextView) findViewbyId(R.id.textView2);
        TextView textViewThree = (TextView) findViewbyId(R.id.textView3);
    
        textViewOne.setText(valueOne);
        textViewTwo.setText(valueTwo);
        textViewThree.setText(valueThree);
    
      }
    }
    
    //----------------------------------------------------------------
    //main_first.xml File
    
    <RelativeLayout
    
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:paddingBottom="@dimen/activity_vertical_margin"
      android:paddingLeft="@dimen/activity_horizontal_margin"
      android:paddingRight="@dimen/activity_horizontal_margin"
      android:paddingTop="@dimen/activity_vertical_margin"
      tools:context=".Activity1" >
    
      <EditText
        android:id="@+id/edittext1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/textView1"
        android:layout_alignBottom="@+id/textView1"
        android:layout_alignParentRight="true"
        android:layout_toRightOf="@+id/textView1"
        android:ems="10"
        android:inputType="textPersonName" >
    
        <requestFocus />
      </EditText>
    
      <EditText
        android:id="@+id/edittext2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/textView2"
        android:layout_alignBottom="@+id/textView2"
        android:layout_alignLeft="@+id/edittext_enter_name"
        android:layout_alignRight="@+id/edittext_enter_name"
        android:ems="10" />
    
      <EditText
        android:id="@+id/edittext3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/edittext_enter_sirname"
        android:layout_alignRight="@+id/edittext_enter_sirname"
        android:layout_alignTop="@+id/textView3"
        android:ems="10" />
    
      <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignRight="@+id/edittext_enter_address"
        android:layout_below="@+id/textView3"
        android:layout_marginRight="10dp"
        android:layout_marginTop="33dp"
        android:text="@string/enter_button_text" />
    
    </RelativeLayout>
    
    //----------------------------------------------------------------
    //main_second.xml File
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
    
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical" >
    
      <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/null_string"
        android:textAppearance="?android:attr/textAppearanceMedium" />
    
      <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/null_string"
        android:textAppearance="?android:attr/textAppearanceMedium" />
    
      <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/null_string"
        android:textAppearance="?android:attr/textAppearanceMedium" />
    
    </LinearLayout>
    
    //----------------------------------------------------------------
    //AndroidManifest.xml File
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest
    
      package="com.app.application"
      android:versionCode="1"
      android:versionName="1.0" >
    
      <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />
    
      <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
          android:name="com.app.application.Activity1"
          android:label="@string/app_name" >
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
    
            <category android:name=
              "android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>
        <activity
          android:name="com.app.application.Activity2"
          android:label="@string/app_name" >
        </activity>
      </application>
    
    </manifest>
    
  2. 运行项目,屏幕上会出现以下截图:实现一个现成的教程

    Activity1.java布局用于通过PutExtra()从用户处获取输入。

  3. 填写EditText字段并点击按钮以传输数据。Activity2屏幕将出现,并显示在Activity1屏幕中输入的表单数据:实现一个现成的教程

    Activity2.java文件的视图,显示数据已成功捕获并显示。

理解代码

前一节“实现一个现成的教程”包含五个部分,我们将在以下各节中详细查看。像本书中呈现的每个示例一样,我们已根据新项目描述此示例,以使其灵活且易于理解。你可以轻松地将此示例放入你自己的应用程序中;一旦你正确理解了意图额外信息,这样做不会花费额外的努力。

Activity1.java

开始,这是源活动,它将启动意图以导航到下一个活动。

这是一个在创建新的 Android 项目时构建的简单活动。回顾基础知识,它将有一个onCreate()方法,该方法将在活动由 Android 创建时首先执行。一旦创建了活动,在Layout文件夹中的main_first.xml文件中定义的布局将在屏幕上呈现。

现在,是获取布局文件中所有EditText字段对象的时候了。为此,我们将在代码中添加以下几行,通过 ID 查找视图并返回对象:

EditText editTextFieldOne = (EditText) findViewById(R.id.edittext1);

提示

为你的对象使用有意义的名称是很好的。由于这本书面向初学者,他们通常不处理大型应用程序,因此给出的对象名称是为了使代码尽可能简单。

findViewById()方法属于Activity类,其目的是找到可以是任何布局子视图的特定视图并返回对象。同样,通过编写以下几行代码,我们将获得另外两个EditText对象:

EditText editTextFieldTwo = (EditText) findViewById(R.id.edittext2);
EditText editTextFieldThree = (EditText) findViewById(R.id.edittext3);

目前,我们拥有Activity1.java类中所有输入字段的对象。下一步是实现按钮的功能,该功能将从这些字段获取输入,将它们添加到意图对象中,并发送出去。

注意

findViewById()方法的返回类型是View类的对象。因此,在使用findViewbyId()时,我们需要将返回的对象强制转换为特定的类类型。为了理解这一点,你可以看到在前面的代码中,视图被转换为EditText

现在,下一步是在按钮上实现OnClickListener()方法。为此,第一步是使用与输入字段类似的方法获取按钮对象。

Button transferButton = (Button) findViewById(R.id.button);

一旦我们获得了按钮对象,我们将使用参数OnClickListener()实现setOnClickListener()方法及其实现:

transferButton.setOnClickListener(new OnClickListener(){});

如你所见,在前面的代码行中,我们已经为transferButton附加了一个OnClickListener()对象,以及它自己的setOnClickListener方法。请记住,它仍然是一个原始方法。现在,是时候重写onClick()方法了。

在前面的代码中,你可以看到给出了onClick()方法的定义,在这个方法中,我们将从EditText字段获取数据并将其放入意图额外数据中。如代码所述,通过在每个EditText对象上调用以下行来从EditText字段获取数据:

String valueOne = editTextFieldOne.getText().toString();

这将获取输入字段中当前存在的值。我们获取所有EditText字段的值,并依次存储在valueOnevalueTwovalueThree中。

现在,我们已经有了要放入意图对象中的数据,我们使用之前描述的方法创建一个意图对象。我们设置源活动和目标活动(即Activity1.java作为源活动,Activity2.java作为目标活动)。下一步是在代码中传递值。Intent.putExtra(String name, String data)方法最适合将字符串值放入额外数据中。

putExtra()的参数有些像键值对。第一个参数name,基本上是到达目标活动后用来识别它的键。另一个就是与该键相关联的额外传输的值。因此,在下面的代码行中,我们使用一个键将字符串放入意图对象中:

intent.putExtra("EDITTEXT_ONE_VALUE",valueOne);

现在,我们已经将第一个EditText字段的值使用键EDITTEXT_ONE_VALUE放入了意图对象中,对于其他两个值,我们重复这一信息。一旦值被加载到意图对象中,我们就调用startActivity()方法来执行这个意图。

Activity2.java

这是处理传入意图的目标类。这个类包含一个简单的布局文件,其中包含三个TextView视图,以显示来自前一个活动的值。在onCreate()方法中,意图是通过getIntent()方法接收的。这个方法属于Activity类,用于获取将导航到它的意图。

Intent类中有一个方法,用于获取与特定意图对象一起到来的所有额外数据。以下方法用于识别带有描述键的特定数据集:

String valueOne = intent.getExtras().getStringKey("EDITTEXT_ONE_VALUE");

与键EDITTEXT_ONE_VALUE关联的值将从意图对象中提取,并保存到valueOne字符串中。同样,所有的数据都将从意图对象中取出并保存在这个目标类中。

数据保存到变量后,是获取TextView视图的对象,并将这些值设置给它们的时候了。如前所述,使用findViewById()方法获取TextView视图。

textViewOne.setText(valueOne);

setText()方法用于在TextView中设置文本,因此它保存的是来自第一个活动传入的值。这就是目标活动如何通过使用putExtras()功能从源活动中获取数据。

main_first.xml 文件

main_first.xml 文件是一个简单的 XML 文件,其中包含三个EditText字段,用于活动从中获取输入。此外,它还包含一个按钮,用于触发事件以便导航到下一个活动。这些视图的 ID 分别被指定为edittext1edittext2edittext3button1

提示

你可以通过在 GUI 中拖放来制作你想要的布局文件。布局文件的 XML 代码很简单,并且在之前的章节中也有解释。但是,请记住,拖放特别不推荐给新的 Android 开发者使用;因此,实现它的最佳方式是通过 XML 文件。

main_second.xml 文件

这是第二个活动的布局文件,实际上它是目标活动和数据接收活动。布局包括三个TextView视图,用于显示从Activity1即源活动发送的valueOnevalueTwovalueThree

AndroidManifest.xml 文件

AndroidManifest.xml文件是 Android 应用程序的基本部分。它跟踪整个应用程序的结构。它包含所有的活动、接收器、权限、与版本相关的问题、最小和最大 SDK 以及其他许多内容。由于我们的项目中有两个活动,Activity1Activity2,因此AndroidManifest.xml文件中包含了这些活动以及像应用程序版本名称和版本代码等不同的内容。

注意

从一个活动向另一个活动发送数据不需要特殊权限,但在 SD 卡或内部存储上进行数据读写时,我们确实需要某些权限来完成这项任务。

未来的考虑事项

通过包裹发送数据是 Android 意图使用的基本技术之一。它有许多改进和效率提升,我们将在以下数据传输方法中进行进一步研究。我们还应该记住,这种方法仅限于某些特定的数据类型(在下一节中给出)。为了从从一个活动向另一个活动传输自定义对象,我们需要使用下一个数据传输方法。

支持的数据类型

Intent 的putExtra()方法支持多种数据类型,可以传输到目标活动。在之前的示例中,我们只使用了一种数据类型(String),但除此之外,我们还可以添加其他各种数据类型。除了putParcelable()putSerializable()之外,这些方法都是不言自明的,它们是本章的下一个主要话题。请查看以下截图,展示了各种数据类型:

支持的数据类型

可以在 putExtras() 意图中添加的不同的数据类型

安卓捆绑包(Android Bundles)的概念

安卓数据 Bundle 是一个可以添加和一起发送各种值的捆绑包。例如,如果我们想通过putExtra()发送多个值,我们会创建一个 Bundle,将所有这些值添加到 Bundle 中,然后使用intent.putExtras()方法发送此 Bundle。

注意

你可以直接将所有值分别添加到意图中,以直接向意图提供数据,或者第二种方法是将所有值添加到 Bundle 中,并通过意图发送它。

我们现在将探讨如何将数据提供给 Bundle,并通过修改上一个活动来将此 Bundle 发送到下一个活动。在从Activity1发送数据时,我们不是直接将不同的值添加到意图中,而是进行以下操作:

安卓捆绑包的概念

如你在代码中所见,valueOnevalueTwovalueThree通过使用newBundle.putString()函数和每个数据值统一的关键字添加到 Bundle 中。现在,这个 Bundle 通过intent.putExtras(newBundle)函数添加到意图中,然后我们像在之前的例子中一样调用startActivity()函数。

在目标活动中,我们可以通过首先使用getIntent().getExtras()函数提取数据捆绑包直接捕获数据。这将返回 Bundle 对象,通过引用我们在源活动中添加的特定关键字,我们可以使用以下函数提取所有三个值的数据:

Bundle.getString("EDITTEXT_ONE_VALUE","DEFAULT_VALUE");

注意

Bundle.getString(key, defaultValue)中的第二个参数是默认值,如果找不到指定键的值,将返回该默认值。

查看以下截图。你将看到可以同时添加到 Bundle 中的不同数据类型:

安卓捆绑包的概念

在 Bundle 中添加不同数据类型的各种函数

使用 Parcelable 进行数据传输

在活动之间传输数据的第二个也是最重要的方法是Parcelable()。前面的方法有一个限制,即我们只能发送基本数据类型,如StringsIntegersDoublesFloats。在实际项目中,我们需要在活动之间传输自定义对象。这些自定义数据对象根据应用程序的需求保存信息。因此,应该相应地传输它们。

现在很清楚,上一个版本仅用于基本数据类型的数据传输,而 Parcelable 可以被称为前一个类型的子类型。

在此方法中,数据类通过实现Parcelable类接口来继承,以使其对象与putExtra()意图方法兼容。我们还需要覆盖Parcelable类接口中的一些方法,以赋予其功能。一旦完成,该类的对象可以放入意图或 Bundle 中,以便在活动之间传递。

Parcelable的实现

在本节中,我们将学习如何在数据类上实现Parcelable,然后如何在不同活动之间传输该对象。对此有两种情况:

  • 只有一个对象从源类发送到目标类

  • 自定义对象数组正在从源类发送到目标类

按照传统,我们将从创建一个新的 Android 项目开始。在给定的示例中,我们将其称为 Parcel 应用。该项目在创建时将创建Activity1.java作为默认活动,它也将作为源活动。第二个活动将是Activity2.java,它将作为接收 parcel 的目标活动。

实现一个现成的教程

完成新项目的创建后,将此代码插入到你的应用程序中。这将影响Activity1.javaActivity2.javalayout_activity1.xmllayout_activity2.xmlAndroidManifest.xml,并引入另一个名为Person.java的类。

//----------------------------------------------------------------
//The Activity1 Class

public class Activity1 extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.layout_activity1);

    final EditText nameText = (EditText) findViewById(R.id.edittext_enter_name);
    final EditText sirnameText = (EditText) findViewById(R.id.edittext_enter_sirname);
    final EditText addressText = (EditText) findViewById(R.id.edittext_enter_address);

    Button enterButton = (Button) findViewById(R.id.button1);
    final Person firstPerson = new Person();
    enterButton.setOnClickListener(new OnClickListener() {

      @Override
      public void onClick(View v) {
        // TODO Auto-generated method stub
        firstPerson.setFirstName(nameText.getText().toString());
        firstPerson.setSirName(sirnameText.getText().toString());
        firstPerson.setAddress(addressText.getText().toString());

        Intent parcelIntent = new Intent(Activity1.this,Activity2.class);
        parcelIntent.putExtra("FIRST_PERSON_DATA", firstPerson);
        Activity1.this.startActivity(parcelIntent);

      }
    });

  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it//is present.

    getMenuInflater().inflate(R.menu.activity1, menu);
    return true;}

}

//----------------------------------------------------------------
//The MySecondActivity Class

public class Activity2 extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // TODO Auto-generated method stub
    super.onCreate(savedInstanceState);
    setContentView(R.layout.layout_activity1);

    Person incomingPersonObj = getIntent().getParcelableExtra("FIRST_PERSON_DATA");
    TextView nameTextView= (TextView) findViewById(R.id.person_name_text);
    TextView sirnameTextView= (TextView) findViewById(R.id.person_sirname_text);
    TextView addressTextView= (TextView) findViewById(R.id.person_address_text);

    nameTextView.setText(incomingPersonObj.getFirstName());
    sirnameTextView.setText(incomingPersonObj.getSirName());
    addressTextView.setText(incomingPersonObj.getAddress());

  }
}

//----------------------------------------------------------------
//The layout_activity1.xml File

<RelativeLayout
  xmlns:android=http://schemas.android.com/apk/res/android
  xmlns:tools=http://schemas.android.com/tools
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingBottom="@dimen/activity_vertical_margin"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin"
  tools:context=".Activity1" >

  <TextView
    android:id="@+id/textView1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_alignParentTop="true"
    android:layout_marginTop="20dp"
    android:text="@string/name_text"
    android:textAppearance="?android:attr/textAppearanceMedium" />

  <EditText
    android:id="@+id/edittext_enter_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/textView1"
    android:layout_alignBottom="@+id/textView1"
    android:layout_alignParentRight="true"
    android:layout_toRightOf="@+id/textView1"
    android:ems="10"
    android:inputType="textPersonName" >

  <requestFocus />
  </EditText>

  <TextView
    android:id="@+id/textView2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/edittext_enter_name"
    android:layout_marginTop="20dp"
    android:layout_toLeftOf="@+id/edittext_enter_sirname"
    android:text="@string/sirname_text"
    android:textAppearance="?android:attr/textAppearanceMedium" />

  <EditText
    android:id="@+id/edittext_enter_sirname"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/textView2"
    android:layout_alignBottom="@+id/textView2"
    android:layout_alignLeft="@+id/edittext_enter_name"
    android:layout_alignRight="@+id/edittext_enter_name"
    android:ems="10" />

  <EditText
    android:id="@+id/edittext_enter_address"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@+id/edittext_enter_sirname"
    android:layout_alignRight="@+id/edittext_enter_sirname"
    android:layout_alignTop="@+id/textView3"
    android:ems="10" />

  <TextView
    android:id="@+id/textView3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/edittext_enter_sirname"
    android:layout_marginTop="20dp"
    android:layout_toLeftOf="@+id/edittext_enter_address"
    android:text="@string/address_text"
    android:textAppearance="?android:attr/textAppearanceMedium" />

  <Button
    android:id="@+id/button1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignRight="@+id/edittext_enter_address"
    android:layout_below="@+id/textView3"
    android:layout_marginRight="10dp"
    android:layout_marginTop="33dp"
    android:text="@string/enter_button_text" />

</RelativeLayout>

//----------------------------------------------------------------
//The activity_two_layout.xml File

<?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" >

  <TextView
    android:id="@+id/person_name_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/null_string"
    android:textAppearance="?android:attr/textAppearanceMedium" />

  <TextView
    android:id="@+id/person_sirname_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/null_string"
    android:textAppearance="?android:attr/textAppearanceMedium" />

  <TextView
    android:id="@+id/person_address_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/null_string"
    android:textAppearance="?android:attr/textAppearanceMedium" />

</LinearLayout>

//----------------------------------------------------------------
//The Person.java File

public class Person implements Parcelable {

  private String firstName;
  private String sirName;
  private String address;

  public Person(){
    firstName = null;
    sirName = null;
    address = null;
  }
  public Person(String fName, String sName, String add) {
    firstName = fName;
    sirName = sName;
    address = add;
  }

  public Person(Parcel in) {
    firstName = in.readString();
    sirName = in.readString();
    address = in.readString();
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getSirName() {
    return sirName;
  }

  public void setSirName(String sirName) {
    this.sirName = sirName;
  }

  public String getAddress() {
    return address;
  }

  public void setAddress(String address) {
    this.address = address;
  }

  @Override
  public int describeContents() {
    // TODO Auto-generated method stub
    return 0;
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    // TODO Auto-generated method stub
    dest.writeString(firstName);
    dest.writeString(sirName);
    dest.writeString(address);
  }

  public static final Parcelable.Creator<Person> CREATOR =new Parcelable.Creator<Person>() {

    public Person createFromParcel(Parcel in) {
      return new Person(in);
    }

    public Person[] newArray(int size) {
      return new Person[size];
    }
  };
}

//----------------------------------------------------------------
//The AndroidManifest.xml File

<?xml version="1.0" encoding="utf-8"?>
<manifest

  package="com.app.parcelapplication"
  android:versionCode="1"
  android:versionName="1.0" >

  <uses-sdk
    android:minSdkVersion="8"
    android:targetSdkVersion="17" />

  <application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    <activity
      android:name="com.app.parcelapplication.Activity1"
      android:label="@string/app_name" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity
      android:name="com.app.parcelapplication.Activity2"android:label="@string/app_name" >
    </activity>
  </application>

</manifest>

运行此应用程序,它将在你的设备上显示Activity1的输出屏幕。查看以下截图,了解应用程序在 Android 屏幕上的显示效果:

实现一个现成的教程

Activity1包含三个EditText字段以接收用户输入,以及一个按钮用于将数据传输到下一个活动

当你处于第一个屏幕时,输入数据并按下按钮,迁移到下一个活动,该活动将显示输入的数据。屏幕将如下所示:

实现一个现成的教程

Activity2Parcelable的形式显示从Activity1传输的数据

理解Parcelable实现

为了理解此示例的工作原理,我们首先需要了解Parcelable的工作方式。在 Android 中,需要将自定义数据(即自定义对象和自定义对象数组)从一个活动传输到另一个活动。通常,自定义数据类与 extras 不兼容;因此,我们为该类实现Parcelable接口。

Parcelable对自定义数据类的作用是它与 extras 兼容。使用Parcelable实现的类的对象可以轻松地添加到意图的putExtra()方法中。同样,它也可以成为 Bundle 对象的一部分,稍后可以通过意图进行传输。

我们现在可以解释前面的代码。

Activity1.java

这是Parcelable对象开始迁移的源类。它从实现onCreate()方法开始。在这个方法中,在设置主视图之后,我们通过它们的 ID 找到视图并将它们的对象带到活动中。这些视图包括三个EditText字段和一个按钮。它们用于获取用户的输入并触发事件,以便开始将数据传输到下一个活动。

button.setOnClickListener()方法中,我们传递了一个新的OnClickListener()对象,在其中覆盖了onClick()方法。我们希望一旦点击按钮就启动意图,因此我们在onClick()方法中实现了意图,并从字段中获取数据。

现在,我们不希望该方法直接将数据传输到意图中。这就是为什么我们要创建一个Person.java类的对象,该对象将保存从字段中获取的值。我们将这个对象命名为firstPerson。为了将值设置到这个对象中,我们实现了以下代码行:

firstPerson.setFirstName(nameText.getText().toString());

前面的行将把该对象的名字设置为从第一个EditText字段中获取的值。第一个EditText字段,nameText,保存了名字的值。因此,使用nameText.getText()方法,它将返回可以轻松通过调用其上的toString()方法转换的Editable对象。

为了从第二个和第三个EditText字段获取值,将重复相同的方法。它们将被设置在同一个Person对象中。你可以通过以下代码行看到这一操作:

firstPerson.setSirName(sirNameText.getText().toString());.
firstPerson.setAddress(addressText.getText().toString());.

在这个阶段,firstPerson对象已经准备好从Activity1传递到Activity2。由于该对象是通过实现Parcelable来继承的,我们可以直接将其添加到额外数据中。我们将在接下来的部分学习如何实现Parcelable。在这里,我们将看到如何将Parcelable添加到意图对象中。

创建一个Intent类的对象,并为其提供源和目标,即源上下文和目标类的.class引用,以让它知道从哪里启动这个意图以及在哪里结束。我们可以通过调用parcelIntent.putExtra()来添加Parcelable。请看以下代码行:

parcelIntent.putExtra("FIRST_PERSON_DATA", firstPerson);

使用这个方法,我们可以轻松地将自定义的Parcelable数据对象添加到意图对象中,然后在下一行简单地调用startActivity()函数以启动意图。

Activity2.java 类

在这个类中,我们将学习如何在目标类中捕获传递的Parcelable对象。为此,首先按照实现活动的onCreate()方法的正常流程开始。设置内容视图并通过查找布局中的 ID 引入三个文本视图。这三个文本视图将显示第一个活动对象的接收值。

getIntent()方法将接收由Activity1.java类传输的意图对象,该对象持有数据。一旦获取到对象,我们可以通过调用getExtras()方法来获取其额外信息,这将返回一个包含数据的Bundle。在那个Bundle上调用getParcelable()函数,并带上键,以获取对象。现在,这个对象被一个新的Person类对象incomingPersonObj接收。

现在,我们有了一个在从startActivity()调用意图时从源类初始化的相同对象。我们现在将通过调用以下代码行来设置文本视图的文本:

nameTextView.setText(incomingPersonObj.getFirstName());
sirnameTextView.setText(incomingPersonObj.getSirName());
addressTextView.setText(incomingPersonObj.getAddress());

incomingPersonObj.getFirstname()方法将从incomingPersonObj获取人的名字,并在第一次调用方法时直接将其值设置给nameTextView。对于sirnameTextViewaddressTextView对象,过程相同。

layout_activity1.xml文件

这是包含Activity1.java视图的布局文件。如代码中所述,它包含三个EditText字段,其 ID 分别为:edittext_enter_nameedittext_enter_sirnameedittext_enter_address。除此之外,还有三个文本视图,用于简单地指示哪个EditText字段包含哪个值。

每个活动都需要一个事件触发器,用于启动任何进程。在这个布局中,按钮将执行任务;因此,它也被放置在EditText字段下方。

layout_activity2.xml文件

这个布局文件创建了Activity2.java的布局,即目标活动。这个活动负责提取数据并在其布局中显示。布局包括三个TextView视图,其 ID 分别为person_name_textperson_sirname_textperson_address_text。这些 ID 用于将这些视图带到代码中(如代码的第二部分所示)。

Person.java

Person类基本上是一个数据持有类,其对象将在应用程序的任何地方创建。这也被称为豆类,用于接收来自服务器以 JSON、XML 或其他任何格式的数据。在我们的Person类中,有三个字段。所有字段都是私有的,并具有它们各自的公共获取器和设置器。firstNamesirNameaddress对象表示它们将持有的信息类型。Activity1.java类创建了此类的一个对象,从EditText字段获取数据,并将其添加到对象内部。

这个类通过实现Parcelable接口来继承。这个Parcelable接口需要实现一些重要的事情。首先,我们将实现一个接受Parcel作为参数的此类构造函数。这个构造函数将在实现Parcelable接口时从类内部使用。in.readString()方法用于从包裹中读取值。

提示

为了使这项技术工作,需要按照在writeToParcel()方法中写入的顺序读取 Parcel。查看代码中写入 Parcel 的顺序。它是firstNamesirNameaddress。在构造函数中也可以观察到同样的顺序。

重写writeToParcel()方法是为了通过Parcelable生成同类对象,以便可以使用。Parcelable.Creator<Person>用于创建类的实例,供Parcel使用;它使用writeToParcel()方法来完成这项工作。

一旦准备好对象,就会将其转发到下一个活动,并由Activity2.java类捕获,如给定代码的第一和第二部分所解释的那样。

AndroidManifest.xml 文件

当谈论开发 Android 应用程序时,这个文件的重要性不容忽视。我们需要在这个文件中添加这两个活动,以便它们被 Android 应用程序识别。如您在文件中所见,这两个活动在清单文件中都有自己的标签,以及它们的参数和意图过滤器。

未来考虑

前述实现Parcelable的方法是用于在 extras 或 Bundle 中传输单个Parcelable对象的。同样,我们可以通过实现Parcelable来传输自定义数据豆的数组或 ArrayList。

使用 Serializable 进行数据传输

在意图中使用的第三种数据传输方法是Serializable。许多 Java 开发人员已经熟悉Serializable这个词,因为它在 Android 引入之前就已经使用了。Android 最大的优势在于,其开发在 SDK 中是 Java,在 NDK 中是 C++。这使得它非常灵活且强大。

功能方面也是如此;Java.io.Serializable是纯 Java 功能,可以原封不动地在 Android 开发中使用。putExtras()意图有一个选项,可以在不进行任何特定努力的情况下将 Java 序列化对象从一个活动传输到另一个活动。我们从Serializable的介绍开始这一部分,供非 Java 用户了解。

什么是 Serializable?

Java 带有一个机制,可以将对象表示为字节数组。这里不仅序列化了数据,而且还可以找到关于对象类型以及对象内部放置了什么类型的数据的信息。

这些序列化对象可以写入文件并存储在任何外部存储中(例如 SD 卡)。重新制作对象的过程称为反序列化,在这个过程中,可以收集隐藏在字节数组中的信息,以便在需要时在内存中重新生成对象。

制作和重新制作任何序列化对象的过程完全独立于 JVM。这意味着可以在 Java 中序列化对象,并且可以在支持与序列化时相同版本的 Java 的 JVM 的任何语言中重新制作。

在 Java 中,ObjectOutputStream类用于序列化对象,而ObjectInputStream类用于当我们从现有的序列化对象重新生成对象时。这些类分别包含writeObject()readObject()方法。这些方法实际上开始了序列化或反序列化的过程。

Serializable的一个例子

在本节中,我们将了解 Java 中如何实现Serializable。这是 Android 处理这些对象的内部机制。这个例子包含两种方法,用于执行序列化和反序列化的任务。

首先,序列化的方法如下:

Serializable 的一个例子

Person类实现了Serializable接口。这将使对象能被ObjectOutputStream类的对象识别。首先,我们将通过设置名称和地址创建一个序列化的对象,如代码所示。

一旦创建了对象,它就可以被序列化。我们从创建一个FileOutputStream对象开始,该对象用于将数据写入文件;同时添加将引用序列化文件所在位置的路径。创建一个将引用该文件的ObjectOutputStream对象,即ObjectOutputStream out = new ObjectOutputStream(fileOut)。现在我们可以通过调用out.writeObject(person)来写入对象。这将开始序列化对象(将其转换为字节数组)并将其添加到给定位置。然后我们将关闭outfileOut对象。

下一步,我们将看到如何从序列化源读取数据。请看以下代码:

Serializable 的一个例子

代码很容易理解,因为它几乎包含了将对象写入序列化文件所需的所有步骤。我们将创建一个Person类的实例以保存重建的对象。创建FileInputStream对象。它指向要反序列化的文件的位置。使用ObjectInputStream对象获取该文件路径并使其准备好读取。通过这种方式,调用in.readObject()方法以反序列化对象,并将返回person对象。完成此操作后,我们将关闭infileIn对象。

现在我们有了反序列化Person类的对象,可以在日志中或打印在控制台上。

注意事项

在 Java 和 Android 中编写可序列化对象的方法是相同的。当我们在 Android 中执行此操作时,我们可以将文件写入 SD 卡。之后,该文件可以被任何其他活动或应用程序获取并反序列化。

Serializable的实现

到目前为止,我们已经理解了使用 Serializable 的主要原因。快速复习总是好的。Serializable 技术用于将对象转换为字节数组;我们可以将其写入文件,并将其存储为 SD 卡或其他存储设备上的 .ser 文件。然后可以在任何位置读取此序列化对象,而与活动无关。

注意

Serializable 是执行数据传输的最简单方法。它还用于从一个活动向另一个活动传递一个或多个自定义数据对象。

序列化文件不一定总是需要 .ser 扩展名。

与其他示例和实现一样,我们将通过创建一个全新的项目来开始这一过程。这个项目将包含两个活动;一个作为源活动,另一个作为目标活动。序列化的对象将从其中一个活动开始导航,目标活动将捕捉它以便从中提取数据。Android 原生支持 Java 的对象序列化和反序列化过程;因此,我们不需要做任何额外的工作,因为序列化的现象由 Android 自身处理。

传递可序列化对象 —— 教程

在本章中,将通过创建一个新项目来开始实现 Serializable。默认情况下,这个项目将包含一个活动(假设为 Activity1.java)。实施以下步骤,您将制作一个实现了 Serializable 的项目。然后我们将了解其解释。

从第一步开始,在您新创建的 Android 项目中实现以下代码。这将引入三个新文件:

  • Activity2.java:这将作为目标活动。

  • layout_activity2.xml:这个文件将保存目标活动的布局。

  • Person.java:这是负责提供数据豆(data beans)对象的序列化类。

    // =============================================================
    //The Activity1.java file
    
    public class Activity1 extends Activity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.layout_activity1);
    
    final EditText edittext1 = (EditText) findViewById(R.id.editText1);
    final EditText edittext2 = (EditText) findViewById(R.id.editText2);
    final EditText edittext3 = (EditText) findViewById(R.id.editText3);
    
    Button button = (Button) findViewById(R.id.button1);
    button.setOnClickListener(new OnClickListener() {
    
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
    Person person = new Person();
    person.setName(edittext1.getText().toString());
    person.setSirname(edittext2.getText().toString());
    person.setAddress(edittext3.getText().toString());
    
    Intent intent = new Intent(Activity1.this, Activity2.class);
    intent.putExtra("PERSON_OBJECT", person);
    startActivity(intent);
    
    }
    });
    }
    
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
    }
    
    }
    
    //================================================================
    //The Activity2.java file
    
    public class Activity2 extends Activity {
    
    String TAG = "MainActivity2";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.layout_activity2);
    Person person = (Person) getIntent().getExtras().getSerializable("PERSON_OBJECT");
    TextView tView = (TextView) findViewById(R.id.data_text);
    
    if(person != null){
    tView.setText("Data Successfully catched");
    Log.d(TAG, person.getName);
    Log.d(TAG, person.getSirName);
    Log.d(TAG, person.getAddress);
    
    }else{
    tView.setText("Data Object is null");
    } 
    }
    
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
    }
    
    }
    
    //================================================================
    //The Person.java
    public class Person implements Serializable {
    String name;
    String sirname;
    String address;
    
    private static final long serialVersionUID = 1L;
    
    public String getName() {
    return name;
    
    }
    public void setName(String name) {
    this.name = name;
    }
    
    public String getSirname() {
    return sirname;
    }
    
    public void setSirname(String sirname) {
    this.sirname = sirname;
    }
    
    public String getAddress() {
    return address;
    }
    
    public void setAddress(String address) {
    this.address = address;
    }
    }
    
    //================================================================
    //The layout_activity1.xml file
    
    <RelativeLayout 
    
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context=".MainActivity" >
    
        <EditTextandroid:id="@+id/editText1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="26dp"
            android:ems="10" >
    
            <requestFocus />
        </EditText>
    
        <EditText
            android:id="@+id/editText2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/editText1"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="41dp"
            android:ems="10" />
    
        <EditText
            android:id="@+id/editText3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@+id/button1"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="22dp"
            android:ems="10" />
    
        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="153dp"
            android:text="Enter Data" />
    
    </RelativeLayout>
    
    //================================================================
    // The layout_activity2.xml file
    
    <RelativeLayout 
    
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context=".MainActivity" >
    
        <TextView
            android:id="@+id/data_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/hello_world" />
    
    </RelativeLayout>
    
    //================================================================
    //The AndroidManifest.xml file
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest 
        package="com.app.serializable"
        android:versionCode="1"
        android:versionName="1.0" >
    
        <uses-sdk
            android:minSdkVersion="8"
            android:targetSdkVersion="17" />
    
        <application
            android:allowBackup="true"
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name"
            android:theme="@style/AppTheme" >
            <activity
                android:name="com.app.serializable.Activity1"
                android:label="@string/app_name" >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <activity
                android:name="com.app.serializable.Activity2"
                android:label="@string/app_name" >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.DEFAULT" />
                </intent-filter>
            </activity>
        </application>
    
    </manifest>
    

完成实现后,运行项目。将出现以下屏幕,您需要在字段中添加您的数据。然后点击 Enter Data 按钮:

传递可序列化对象 —— 教程

源活动要求输入数据,以便通过序列化发送它

当您按下 Enter Data 按钮时,将打开 Activity2.java,并显示以下屏幕:

传递可序列化对象 —— 教程

Activity2 显示数据已成功输入

同时,当您查看项目的 LogCat 时,它将显示我们在 Activity2.java 文件中记录的数据。请看以下截图:

传递可序列化对象 —— 教程

LogCat 显示在 Activity2 中成功捕捉到数据

漫步于 Serializable 代码中

为了理解 Android 中 Serializable 的工作原理,你需要回顾一下之前描述的 Java 中Serializable的细节。如果你还没有看过这部分内容,我们建议你查看一下。在本节中,我们将重点解释前面的示例以及如何在 Android 项目中实现它。

为了简化,我们将其分为六个部分。每个部分都有详细的解释。

Activity1.java

Activity1.java类将作为启动意图的源活动。它还将作为源活动,因为它负责创建并发送自定义数据对象。让我们从代码的第一部分开始,即实现Activity1.java类。

如前所述,这个类负责从用户那里接收数据输入,并创建一个数据对象。在onCreate()方法中,我们首先会通过setContentView()方法设置一个特定的布局,该布局包含视图。布局设置完成后,我们的下一个任务是将这些视图作为对象带入到我们的 Java 代码中,如下面的代码所示。这将帮助我们执行这些对象上绑定在布局中的视图的各种任务。

final EditText edittext1 = (EditText) findViewById(R.id.editText1);
final EditText edittext2 = (EditText) findViewById(R.id.editText2);
final EditText edittext3 = (EditText) findViewById(R.id.editText3);

调用findViewById()函数会带来与 ID 相关联的特定视图。我们将它强制转换为EditText类,并分别将其放入edittext1edittext2edittext3对象中。

这三个字段将用于从用户那里获取输入,但我们需要一个事件触发器,用于从一个活动导航到另一个活动,并负责数据传输。为此,我们在布局中实现了一个按钮,并通过调用findViewById()方法在 Java 代码中获取它:

Button button = (Button) findViewById(R.id.button1);

现在,我们的 Java 代码中有了所有必要的视图。下一步是实现按钮的功能,即它被点击时将执行什么操作。为此,我们需要在这个按钮上实现OnClickListener接口:

Button.setOnClickListener(new OnClickListener())

上述代码行负责为按钮设置点击监听器。它接收一个OnClickListener参数,在其中我们实现了onClick()方法。这个onClick()方法将负责给按钮分配一个任务:

button.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    // TODO Auto-generated method stub
  }

  });

完成这些后,我们将创建一个Person类对象(Serializable对象),并将其值设置为从EditText字段获取的输入值。现在,它有两个部分;第一部分是创建Person类的新对象。我们将通过调用其构造函数来实现这一点:

Person person = new Person();

在第二部分,我们将通过调用getText()方法获取EditText对象的值。getText()方法返回一个Editable对象;因此,为了将其转换为字符串,我们在其上调用toString()方法。当你观察代码时,我们会一起执行所有这些任务:

person.setName(edittext1.getText().toString())

首先,从edittext1对象获取值并将其转换为String。其次,我们通过其值设置人员的姓名。我们将使用类似的过程进一步设置person对象的sirNameaddress

person.setSirname(edittext2.getText().toString());
person.setAddress(edittext3.getText().toString());

现在,我们已经有一个准备传输的对象。我们将创建一个Intent对象,并为其分配源活动和目的地活动的上下文。这将表示意图从哪个活动启动,以及它将迁移到哪个活动。我们将通过调用构造函数来实现这一点:

Intent intent = new Intent(Activity1.this, Activity2.class);

一旦创建了意图对象,我们将在其中添加数据对象并调用startActivity()方法。为了将序列化对象放入意图对象中,我们将调用intent.putExtra()方法。这部分最后的步骤是调用startActivity()方法,该方法将启动导航过程。

Activity2.java

目的地活动的主要目的是捕捉意图,从中提取数据对象,并在视图中显示。我们从实现onCreate()方法开始。首先,通过调用Activity类的setContentView()方法来设置布局。现在,是时候捕捉从Activity1.java类启动的intent对象了。

与本章前面的示例一样,我们通过调用getIntent()函数来获取意图。这个函数返回一个用于启动此活动的intent对象。一旦有了intent对象,我们就调用getExtras()函数。这将返回一个包含发送活动添加到这个intent对象的所有额外内容的 Bundle。

在这个 Bundle 上,我们现在将调用getSerializable()方法,它将借助发送活动赋予的key值带来一个Serializable对象。那个key值应与发送活动的值相同;否则,它将返回一个可能导致应用因NullPointException而崩溃的空值。

现在Person对象已经包含了所有的值。我们接下来的任务是把这些值在 LogCat 中记录下来,以便我们可以验证它。实现了一个空指针检查,以查看对象是否为空。如果不为空,我们从person.getNameperson.getSirNameperson.getAddress获取并记录其值。如果对象为空,它会显示Data Object is null,因此不会崩溃。

Log.d(TAG, person.getName);
Person.java

当我们谈论从Serializable传输数据时,Person.java类是我们需要实现的最重要的类。我们创建了一个 Java 类,其中包含三个私有字符串变量。每个变量都有自己获取和设置外部值的 getter 和 setter 函数。与前面的方法一样,我们使用Parcelable接口实现了我们的数据豆类;在这里,我们将使用Serializable实现我们的类。

一旦实现,Android 就可以将这个类的对象视为Serializable。现在,每当它被添加到意图对象中时,Android 将以字节数组的形式传输它。与Parcelable相比,这是一个较慢的过程,但当我们只处理几个对象时,这种缓慢是不明显的。如果我们想在有数千个数据对象的地方应用这种方法,可能会花费一些时间。

layout_activity1.xml文件

布局文件属于Activity1.java类。当你第一次运行代码时,将显示这个布局文件中描述的布局。在Activity1.java类的onCreate()方法中,我们使用了setContentView()方法来粘贴用户界面。

在这个 XML 文件中,有四个视图;其中三个是EditText字段,用于在 Activity1 中接收用户输入。除此之外,还有一个按钮,用于在字段中完全填充数据后触发事件。给它们分配的 ID 是Activity1.java类默认使用的,用于在.java类中获取这些视图,edittext1edittext2edittext3分别是它们各自字段的 ID。

layout_activity2.xml文件

这个布局文件包含了Activity2.java类所需的视图。它由一个TextView视图组成,该视图将告诉我们从Activity1.java类传来的值是否正确获取。这个文本视图将根据数据对象显示数据成功捕获数据对象为空的消息。

AndroidManifest.xml文件

AndroidManifest.xml文件包含所有的活动、权限、SDK 信息、版本代码等许多其他内容。简而言之,它用于保存有关应用程序的所有信息。在这里,我们有我们的活动,Activity1.javaActivity2.java类以及它们的意图值。如果你忘记在这个文件中提及任何活动,它将在 LogCat 中产生异常,显示ClassNotFoundException

提示

ParcelableSerializable是两种用于从一个活动传输数据对象到另一个活动的方法。Serializable是最简单实现的,而Parcelable是所有方法中最快的。如果你需要基于较少的对象执行任务,并且想要一个简单的解决方案,你应该选择Serializable。但是,如果你想要一个无论实现复杂性如何都完美的方法,你应该选择Parcelable

数据和隐式意图

在前面的示例中,描述了应用程序内部数据传输的需求。现在非常清楚,没有数据的传输和处理,任何应用程序都是不完整的。在本章的这一部分,我们将看到需要将数据传输到隐式意图的场景。

回顾隐式意图的定义,它们通常不会指向一个专门的目标,而是将应用程序的流程交给外部应用程序,或者换句话说,它们启动另一个活动以执行特定任务。

外部应用程序需要从你的应用程序中获取一些数据以执行任务。现在,我们将看到在哪种情况下,以 URI 形式的数据传递会传递给隐式意图。

查看地图

你可以从自己的应用程序启动谷歌地图,并定位到一个特定的地点。这意味着你可以通过隐式意图发送需要打开的任何位置到谷歌地图。基于纬度和经度,你可以打开谷歌地图上的一个特定点。这个纬度和经度通过 URI 传递给谷歌地图,这是向隐式意图发送数据的一种方式。

为了执行这项任务,我们需要编写以下代码行:

查看地图

根据特定的语法,我们需要编写 URI 字符串。为了打开特定位置的地图,URI 字符串包含geo关键字,后面跟着纬度和经度(如代码所示,以逗号分隔)。这个 URI 值通过android.content.Intent.ACTION_VIEW动作传递给隐式意图。这个视图动作将获取 URI 并打开最佳的应用程序来执行任务。然后,我们将通过调用startActivity(intent)方法启动意图。

查看地图

安卓操作系统中的谷歌地图视图。

打开网页

如果你想打开网页,数据传递的隐式需求也可以按类别划分,你需要用特定的加载页面打开默认的网页浏览器。在这个过程中,我们需要传递应用程序想要在浏览器中打开的 URL。这个 URL 也是通过Uri.parse()方法传递的。

提示

请记住,在此场景中我们使用的是默认的网络浏览器,并不是作为安卓应用程序一部分的网页视图。

为了在默认的网络浏览器中发送并打开 URL,请按顺序实现以下代码行:

打开网页

如代码所示,有一个字符串包含要在网络浏览器中打开的值(URL)。这个值然后通过Uri.parse()函数的意图构造器中,使用Intent.ACTION_VIEW动作输入。这将选择最佳选项来打开 URL。

打开网页

浏览器打开我们应用程序调用的 Google.com 网页的视图。

发送电子邮件

您可能需要一种功能,在您的应用程序中调用默认的 Google 邮件应用程序,并指定一个特定的收件人和电子邮件内容。在这种情况下,我们同样需要将数据添加到意图对象中,即发件人姓名、电子邮件正文和电子邮件主题,然后启动该活动。

为了执行此任务,我们需要编写以下代码行:

发送电子邮件

创建一个带有Intent.ACTION_SEND动作的Intent对象。其作用是打开发送选项的意图。现在,是时候向这个对象中添加数据了。Android API 考虑到了所有可能发生的场景;因此,在Intent类中有一些定义的常量,可用于由 Android 唯一标识数据。Intent.EXTRA_EMAIL是在使用putExtra()方法向意图提供电子邮件地址时使用的关键字常量。同样,在额外信息中提及主题也有一个关键字常量;Intent.EXTRA_SUBJECTIntent.EXTRA_TEXT将用于添加电子邮件的正文。

一旦我们使用这些参数调用应用程序,它将以这些参数填充字段的方式打开 Gmail。它看起来会类似于以下截图:

发送电子邮件

由我们的应用程序调用的 Gmail 应用程序视图

拨打电话

如果您想在应用程序中启动一个特定号码的呼叫,您需要调用拨号器意图。使用Intent.ACTION_DIAL,您可以启动带有给定 URI 的特定号码的拨号器意图。按照以下代码,在您的应用程序中实现拨号器功能:

拨打电话

URI 字符串包含了拨号器应该启动的电话号码。一旦打开拨号器,它会显示号码原文,用户现在可以拨打这个号码。

其他杂项场景

本章还可以包含其他各种场景(例如日历和时间小部件),但由于篇幅和限制,我们只考虑了四个场景。在隐式意图间实现数据传输至关重要,且可以非常轻松地完成。

总结

在本章中,我们详细学习了如何在 Android 应用程序内操作数据。我们了解到如何使用不同的方法将数据从一个活动传输到另一个活动,以及如何使用意图的putExtra()函数简单传递默认数据结构。自定义数据对象或自定义数据对象数组可以通过ParcelableSerializable发送到另一个活动。我们还学习了如何在我们的 Android 应用程序中实现所有这些数据传输方法。在章节的最后,我们简要介绍了四种场景,在这些场景中,当通过意图从我们的应用程序调用其他应用程序时,数据会通过隐式意图发送到其他应用程序。

本章节对于实际应用开发至关重要,因为活动间甚至应用外部的数据传递是任何 Android 应用程序的基本组成部分,而使用 Android 意图可以轻松实现这一功能。

在接下来的章节中,我们将学习如何使用意图来访问 Android 的特性。我们还将了解意图过滤器是如何工作的,广播意图的基础知识是什么,最后,我们将学习意图服务及待定意图的实现。

第六章:使用意图访问 Android 功能

在上一章中,我们讨论了使用组件进行数据传输。我们了解了如何从一个活动向另一个活动传输数据,以及为什么应该在不同的组件之间传输数据。我们还讨论了使用意图进行数据传输的各种方法。Android 系统中有很多组件,意图提供了一种简单的接口,使这些组件能够相互通信。第四章中,我们讨论了使用系统硬件如 Wi-Fi、蓝牙、摄像头、麦克风等的不同的 Android 组件。我们还讨论了如何使用意图利用这些组件,以及如何使用不超过几行代码的 Android 硬件开发许多不同的应用程序。

到目前为止,我们只讨论了硬件组件及意图与这些组件的作用。这一章全是关于 Android 软件功能以及如何使用意图作为主要接口在我们的应用程序中使用这些功能。Android 包含大量的库和 API,开发者可以非常容易地使用不同的 Android 功能。本章将引导我们了解常见的 Android 功能,并且我们还将开发一些示例应用程序,展示如何与这些功能结合使用意图。

本章包括以下主题:

  • Android 操作系统的功能

  • Android 功能与组件对比

  • 常见的 Android 操作系统功能

  • Android 功能和意图

  • <uses-feature><uses-permission>标签

  • 使用 SEND 动作分享

  • 使用意图发送短信/MMS

  • 使用意图发送数据消息

  • 确认消息送达

  • 接收短信

  • 使用意图进行电话通信和拨打电话

  • 使用意图发送通知

  • 其他一些 Android 功能

    小贴士

    本章以及后续章节需要理解意图的概念和结构,这是基本前提。如果你对这些东西的基本概念不了解,我们建议你阅读第三章,意图及其分类和第四章,移动组件的意图以便继续深入学习。

Android 操作系统的功能

Android 是一个开源的操作系统和智能设备(如手机和平板电脑)的中间件框架。这些设备包含了许多提供用户便捷生活方式的功能和功能。这些功能包括硬件功能,如音频、蓝牙、摄像头、网络、麦克风、GSM、NFC 以及诸如加速度计、气压计、指南针、陀螺仪和 Wi-Fi 等传感器。

它不仅包括硬件组件,还包括软件特性,如应用小部件、主屏幕、输入法、动态壁纸、布局、存储、消息传递、多语言支持、浏览器、Java 支持、媒体支持、多点触控、通话、消息传递、多任务处理、可访问性、外部存储等。我们之前已经将硬件特性称为移动组件,并在前面的章节中进行了讨论。我们将在本章中通过实际示例讨论软件特性。

注意事项

在这里我们使用两个关键词:组件和特性。摄像头、蓝牙等组件是安卓手机的硬件部分。特性是安卓手机的软件部分,如短信特性、电子邮件特性等。本章将介绍软件特性、它们的访问方式以及通过意图使用它们的方式。

安卓特性与组件

通常,“安卓特性”和“组件”这两个词可以互换使用。但为了明确起见,我们将关键词组件定义为使用硬件的特性,而将关键词特性定义为在后台使用软件的安卓特性。正如我们在上一节中讨论的,安卓包含许多组件和特性,当这些组件和特性被移植到任何手机上时,就使其成为智能手机。并非所有的组件和特性都可以通过意图使用。因此,我们将详细讨论那些可以通过意图使用的特性。

需要注意的是,直接或间接使用硬件的特性需要用户提供访问权限。这些权限在应用程序安装过程中提供。如果用户没有向应用程序提供权限,应用程序将无法访问硬件;因此,它无法使用该特性。

在本章中,我们将了解那些在后台使用软件但还需要一些权限的特性。我们将在以下部分提供有关权限的更多详细信息。

常见安卓特性

到目前为止,我们只是以一般方式讨论了安卓特性。在本节中,我们将讨论在安卓手机和平板电脑中常见的一些最常用的安卓特性。每个安卓设备在某些方面都是独一无二的,并且拥有许多与其他品牌和手机不同的独特特性和组件。但是在所有安卓手机中都能找到一些共同的特性。这些特性中的许多可以用于我们的应用程序,无论具体的型号或手机如何,意图无疑是最异步且简单的方式来在应用程序中使用这些特性。现在,让我们看看在许多设备中常见的安卓特性及其在手机中的功能。

布局和显示

现在,智能手机的尺寸越来越大,还有一些新的 Android 设备类型,即平板电脑。更大的屏幕和更高分辨率的显示已经将手机转变为多媒体设备。这些设备拥有从 240 x 320 到 1268 x 800 像素的布局尺寸和从 3 英寸到 11 英寸的屏幕尺寸。这些设备屏幕密度有所不同,如低、中、高、大、超大等。以下图片展示了三种不同分辨率设备的比较:

布局和显示

不同屏幕尺寸的 Android 设备

为了显示高质量的图形,Android 提供了用于 2D 画布绘图和 3D 图形的图形库,使用 OpenGL-ES 的 OpenGL。在 Android 3.0 版本之后,引入了一个新的渲染图形库 RenderScript。RenderScript 是一种针对 Android OS 的脚本语言,允许开发者编写高性能图形渲染和原始计算代码。它主要定位于并行数据处理计算,比如在多核处理器(如 CPU、GPU 或 DSP)之间分配处理。

注意

在 Android 3.0 之前,专为 Android 平板电脑开发的版本,Android 使用 2D 画布来渲染其布局、主屏幕和移动 UI。在 Android 3.0 引入 RenderScript 之后,Android 使用 RenderScript 以更美观和优化的图形来渲染其布局、主屏幕和移动 UI。

数据存储与检索

没有哪种 Android 设备在运行时不需要某种形式的存储。为了更好的性能,设备不仅需要像 RAM 这样的易失性内存以便处理和快速访问,还需要像外部 SD 卡这样的永久性存储。Android 设备支持多种数据存储和检索方式,这些方式根据开发者和将使用的应用程序而有所不同。

如果我们的应用程序使用大量数据,开发者可以使用 SQLite 轻量级关系数据库功能为每个应用程序提供支持。开发者可以使用 SQLite 数据库来管理具有保密和高效存储能力的数据。

不仅数据库,Android 设备还提供了文件存储功能。由于保存和加载数据对于几乎每个应用程序来说都是至关重要的,Android 提供了多种不同的方法来存储和检索数据,以确保我们应用程序的持久性。文件存储并非最佳选择,但有时,开发者除了读写文件来处理应用程序的持久数据外,别无他法。幸运的是,Android 提供了功能,让开发者可以在设备的内部或外部媒体上创建、保存和加载文件,比如 SD 卡。这些 microSD 卡采用 FAT32、Ext3 或 Ext4 文件系统格式。不仅有这些文件系统,而且一些 Android 设备,尤其是平板电脑,支持高容量存储媒体,如 USB 闪存盘。

除了在数据库或磁盘文件中存储大量或重型数据外,安卓还提供了一个用于存储简单应用程序数据的功能,如 UI 状态、游戏得分等。这是通过使用SharedPreferences方法实现的。SharedPreferences方法使用名称/值对(NVP)机制来存储应用程序的轻量级数据。

连接和通信

在具备连接功能之前,我们不能将一个安卓设备称为智能手机。支持连接、通信和数据传输的技术包括 GSM/Edge、IDEN、CDMA、EV-DO、蓝牙、Wi-Fi、LTE、近场通信(NFC)、WiMAX 等。安卓提供了一套完整的通信和连接库。这使得开发者可以轻松地在他们的应用程序中使用这些功能。例如,通过蓝牙支持,用户可以发送文件,访问电话簿和语音拨号,以及在不同手机间交换联系人。

注意

在安卓 3.1 及其之后的版本中,安卓包含了通过蓝牙通信将键盘、鼠标和游戏手柄设备与安卓手机连接的原生功能。在安卓 3.1 之前,一些第三方应用程序为此提供了定制的方法。

安卓手机不仅支持数据传输,还支持电话和短信通信。安卓包含了用于短信通信的 SMS 和 MMS,以及线程化文本消息和安卓云到设备消息传递C2DM),新的谷歌云消息传递GCM)也是安卓推送消息服务的一部分。

对于电话功能,安卓支持通话,但在撰写本书时并不支持原生视频通话;然而,一些安卓设备定制的操作系统版本允许开发者和用户通过 UMTS 网络(如三星 Galaxy S)或 IP 进行视频通话。此外,谷歌环聊(Google Hangout),取代了谷歌 Talk,在安卓 2.3.4 及更高版本中可用。这允许用户使用互联网连接进行视频通话。要使用谷歌环聊视频通话,用户需要一个谷歌+账户。微软公司的第三方工具 Skype 也用于在安卓 2.3 及以后版本中进行视频通话。

可访问性和多点触控

安卓设备运行在完全基于触摸的界面上,包含一些硬或软触摸按钮,这些按钮根据设备的不同而有所差异。Android 设备原生支持多点触控。多点触控技术允许开发者和用户使用单点触控、轻敲、双击、长按、捏合缩放手势、旋转手势、各个方向的滑动手势等等。Android 的最新版本(在撰写本书时是 Android 4.4 KitKat)包含一些新的触摸手势,如轻敲和长按手势、滚动手势等。同时,三星推出了无触摸手势,利用他们特定的 API,即 Look API。通过 Look API,用户无需触摸屏幕,只需在空中移动手或头部,Android 就会执行所需的功能。例如,头部向上移动将向上滚动页面,头部向下移动将向下滚动页面。此外,许多 Android 设备制造商,如三星,推出了笔功能,以便用户更轻松、更准确地用笔在手机上书写和使用。

注意

多点触控功能最早是在 HTC Hero Android 手机中推出的。在那之前,由于当时苹果对触摸屏技术的专利,该功能在 Linux 内核层面原本是被禁用的。

除了触摸操作,用户还可以通过原生引入的语音或语音识别引擎来访问他们的手机。同时,Android 包含一个名为 Talkback 的功能,该功能允许视力不佳的人听到他们的 Android 手机在特定时间正在进行的操作。这些人可以使用语音操作如拨打电话、发送短信、导航等来访问他们的手机。这些语音操作是从 Android 2.2 版本开始引入的。在撰写本书时,通过语音操作控制硬件的能力在 Android 中尚未实现。

注意

Android 4.1 及其后续版本在语音操作上进行了增强,当使用特定命令查询时,可以读取来自谷歌知识图谱的答案。

广泛的内容和多媒体支持

安卓设备在高清媒体支持方面不亚于任何电脑。Android 提供了全面的 API 来管理图片、视频和音频。Android 设备支持的格式包括 WebM、H.263、H.264、3GP、MP4、MPEG-4、AMR、MP3、MIDI、OGG、WAV、JPEG、PNG、GIF、BMP 和 WebP。不仅如此,Android 还提供了使用 RTP/RTSP 协议、HTML 渐进式下载(如 HTML5 <video>标签)、HTTP 动态流媒体协议以及 Flash 插件提供的 Adobe Flash 流媒体(RTMP)协议在线流媒体的功能。

注意

新的 Android 设备支持 3D 图像捕捉和 3D 视频支持作为其原生功能。

除了广泛的多媒体支持外,Android 还提供了播放功能、控制选项、播放器、像其他手机一样的声音硬件按钮、全屏播放等。

安卓不仅支持多媒体,还支持内容格式,如文本文件、Word 文档、HTML 等。安卓中可用的网页浏览器基于开源的 WebKit 布局引擎,该引擎最初由苹果公司开发。这与 Chrome 的 V8 JavaScript 引擎相结合,在安卓系统中使用。

虽然大多数安卓应用程序原生是用 Java 编写的,但由于安卓系统中没有 Java 虚拟机,因此不支持 Java 字节码。这个 Java 代码反而被编译成 Dalvik 可执行文件,在专为安卓系统设计的 Dalvik 虚拟机上运行。Dalvik 最重要的特点是,它与 Java 虚拟机相比,针对低电量、有限内存和 CPU 进行了优化。

注意

安卓浏览器在安卓 4.0 版本中 Acid3 测试得到了 100/100 的分数。 Acid3 测试是由 Web Standards Project 提供的网页测试页,用于检查网页浏览器对各种网页标准元素(如文档对象模型(DOM)、JavaScript 等)的符合性。

硬件支持

安卓设备不仅提供了电话功能,如拨打电话、发送短信等,而且还具有许多新硬件组件的多种功能。安卓具有视频摄像头、触摸屏、全球定位系统(GPS)用于基于位置的应用程序、加速度计、陀螺仪、气压计、磁力计、接近传感器、压力传感器、温度计、Wi-Fi、蓝牙和专用的游戏控制功能。

注意

一些新的安卓手机,例如三星 Galaxy S4,提供了新的传感器,如光和颜色传感器,用于捕捉无需触摸的手势。

安卓手机包含了 GPS 和基于位置的技术,因此安卓系统本地支持谷歌地图,以及谷歌基于 GSM 小区的位置技术,用于确定设备的当前位置。为了使地图对开发者和用户更有用,安卓还提供了本地 API,支持正向和反向地理编码,帮助将坐标转换为地址,反之亦然。

后台服务和多任务处理

由于安卓智能手机的屏幕尺寸有限,用户界面屏幕上只能显示一个应用程序。但安卓支持多任务功能,可以让应用程序和服务在后台运行。开发者可以利用后台服务执行不需要用户交互的自动处理。这一特性的应用示例包括生成提醒;监控消息、统计和天气预报;从互联网下载数据;或者在后台播放音频文件。

提示

当安卓设备内存不足时,它会停止后台中优先级较低的应用程序。开发者应该在应用程序进入后台之前存储必要的数据和应用程序状态,这样在被停止时,应用程序可以从保存的状态中恢复其状态。

安卓同样支持通知功能,这是一种传统的标准方式,用于在手机中提醒用户。开发者可以使用安卓的通知库来创建可以发出声音、支持震动或者激活 LED 灯的通知提醒。此外,安卓还允许开发者设置通知界面图标、布局等。

这些后台应用程序可以是独立的,也可以依赖于其他应用程序。安卓提供了如意图和内容提供者等特性,用于应用程序间的通信方法和机制。

增强的主屏幕

主屏幕就像电脑或笔记本电脑的桌面屏幕。安卓用户可以在主屏幕上获得快速链接、应用快捷方式和信息。安卓为主屏幕提供了可定制功能。小部件、活动文件夹和动态壁纸使主屏幕对用户来说更加互动和美观。这些应用程序允许安卓开发者创建动态的应用程序组件,它们可以直接在主屏幕上提供应用程序的窗口或提供有用及时的信息。开发者还可以为用户提供在主屏幕上添加快捷方式的选项。这些快捷方式为用户提供必要的信息,他们无需打开应用程序。例如,我们有一个显示当前时间和天气的应用程序。现在,每当用户想要查看时间和天气时,都需要打开应用。因此,与其为此目的创建应用程序,不如创建一个主屏幕小部件会是一个更好的主意。这个小部件会在主屏幕上显示天气和时间,用户就不需要打开应用程序了。

安卓的其他功能

安卓开发者可以用多种语言开发应用程序,为用户提供应用程序的本地版本。安卓提供了多语言应用程序的特性。

同时,安卓支持网络共享功能,允许用户将设备的网络连接分享给其他手机和电脑。这种共享可以通过 Wi-Fi 热点或 USB 网络共享来实现。

注意

网络共享功能在安卓 2.2 版本中引入;因此,早期版本通过第三方应用程序和制造商支持网络共享。

同时按下电源键和音量减键可以让用户捕获设备的屏幕截图。这个功能最早是在安卓 4.0 中引入的。早期版本使用第三方应用程序,但这些应用程序需要设备已获得根权限作为前提条件。开发者也可以通过 PC 连接使用 DDMS 工具来截图。

注意

任何安卓设备的 root 操作都是被禁止的,这样做会违反所有的保修和保证协议,有时也可能对手机造成风险操作。

安卓功能和意图

迄今为止,我们已经讨论了安卓手机和平板电脑中常见的不同功能,但我们仍然不知道意图(intents)与这些功能之间的联系。有些功能可以通过意图使用,而有些则不能。简单提醒一下,意图是不同应用程序与安卓系统之间的异步消息。

在本章中,我们将讨论一些可以通过意图使用的功能,并了解意图是如何执行各种动作的。我们将功能分为四个部分:消息传递、电话、通知和闹钟。我们将开发一些使用意图并访问这些功能的示例,并讨论如何访问这些功能以及意图在其中的作用。

在开始讨论这些示例应用程序之前,我们将讨论一些在安卓中用于澄清意图和功能之间概念的基本术语。在下一节中,我们将讨论 AndroidManifest 文件中的两个不同标签,uses-featureuses-permission。这些标签用于声明任何安卓应用程序的一些权限和设置。让我们在下一节中看看它们的作用。

<uses-feature><uses-permission> 标签

任何安卓应用程序默认情况下都没有权限执行可能直接影响其他应用程序、系统或用户的行为。这包括读取或写入用户的私人数据,如联系人和短信,读取或写入其他应用程序的文件,或任何其他活动。安卓系统允许应用程序独立运行并被沙盒化,但在共享数据的情况下,应用程序必须明确地相互共享。为了更轻松地实现这一共享目标,安卓允许开发者在他们的应用程序中声明所需执行活动的权限。用户将被通知有关允许他们在设备上安装应用程序的权限。

开发者需要记住关于权限的两件事:设备功能的权限,如访问相机或硬件,以及定义自定义权限。在本主题中,我们将讨论第一种选择,即访问设备功能和硬件,并授予应用程序权限。这可以通过在清单文件中使用两个标签来实现:uses-feature 标签和 uses-permission 标签。

首先,我们将讨论<uses-feature>标签。<uses-feature>标签允许开发者声明应用程序将使用的任何单一硬件或软件特性。这将在应用程序的AndroidManifest文件的<manifest>标签中声明,正如标签名称所暗示的,这会通知应用程序有关要访问的依赖实体。以下代码段展示了<uses-feature>标签的一般声明方式:

和标签

你可以看到<uses-feature>标签中有三个属性:namerequiredglEsVersionandroid:name属性以字符串描述符的形式指定应用程序使用的任何单一硬件或软件特性。<uses-feature>标签中的android:required属性相当重要。它是一个布尔值,表示应用程序是否需要android:name属性中指定的特性。如果开发者对任何特性声明android:required="true",这意味着没有指定特性在设备上可用时,应用程序将无法运行。如果开发者对特性声明android:required="false",这意味着应用程序希望设备上有该特性。如果该特性不可用,应用程序可能无法正常工作,或者在尝试使用不可用的特性时可能会崩溃。此属性的默认值为 true。<uses-feature>标签中的最后一个属性是android:glEsVersion。这是一个 16 位表示的版本号。此属性指定应用程序将使用的 OpenGL ES 版本。例如,我们的应用程序中使用了摄像头。以下代码段展示了如何在清单文件中为摄像头声明权限:

和标签

你可以在代码中看到,我们使用了android.hardware.camera字符串作为android:name属性。这个字符串声明了 Android 的摄像头特性,其他属性声明应用程序需要摄像头特性,并支持 OpenGL ES 1.0 版本以便它能正常工作。开发者必须在应用程序中为每个使用的特性在单独的<uses-feature>标签中指定;因此,如果应用程序需要多个特性,应在清单文件中声明多个标签。声明应用程序中使用的所有特性是一个好习惯。这些声明的<uses-feature>标签只提供信息,Android 系统在应用程序安装前不会检查匹配的特性。

注意

Google Play 使用在清单文件中声明的<uses-feature>标签,来过滤掉不符合其软件和硬件要求的设备上的应用程序。

<uses-feature>标签最早在 API 级别 4 中引入。如果包含<uses-feature>标签的应用在低版本设备上运行,早期版本会忽略此标签。

下表展示了硬件和软件特性的一些特性类型和名称字符串。它们可以用于<uses-feature>标签的android:name属性中:

硬件特性

特性类型 特性描述符(Android 名称) 描述
蓝牙 android.hardware.bluetooth 此特性允许应用使用设备的蓝牙功能。
相机 android.hardware.camera 此特性允许应用使用设备的相机组件。
android.hardware.camera.flash 这是一个子特性,允许应用使用设备的相机闪光灯。
位置 android.hardware.location.gps 此子特性允许应用使用从设备的全球定位系统(GPS)接收器获得的精确位置坐标。
传感器 android.hardware.sensor.accelerometer 此特性允许应用使用设备加速度传感器的运动读数。
android.hardware.sensor.compass 此特性允许应用使用设备指南针的方向读数。
android.hardware.sensor.proximity 此特性允许应用使用设备的距离传感器。
屏幕 android.hardware.screen.landscape 此特性将应用屏幕方向设置为横屏。
android.hardware.screen.portrait 此特性将应用屏幕方向设置为竖屏。
触摸屏 android.hardware.touchscreen.multitouch 此子特性允许应用使用两点触控功能,如捏合。
Wi-Fi android.hardware.wifi 此特性允许应用使用设备的 Wi-Fi 组件。

软件特性

特性类型 特性描述符(Android 名称) 描述
应用小部件 android.software.app_widgets 此特性允许应用包含应用小部件,并且可以安装在具有主屏幕的设备上。
主屏幕 android.software.home_screen 此特性允许应用作为设备主屏幕的替代。
输入法 android.software.input_methods 此特性允许应用提供自定义输入法。
动态壁纸 android.software.live_wallpaper 此特性允许应用提供动态壁纸。

我们在前面表格中并未展示所有特性和描述符。我们只列出了一些最常用的特性。表格展示了每个特性的特性类型,用于android:name标签的特性名称描述符,以及特性将如何影响设备中应用的功能的简短描述。

一些特性被归类为硬件特性,一些则为软件特性。硬件特性是使用后端硬件组件的特性。为了访问这些硬件组件,我们的应用程序应具有访问硬件的权限。需要注意的是<uses-feature>标签只是提供信息,它只告诉用户应用程序正在使用某些特定功能。它并不允许应用程序使用任何特定功能或组件的访问权限。

为了允许应用程序使用任何特定的组件,安卓提供了另一个标签<uses-permission>。如果用户在安装时允许,此标签将提供应用程序访问组件的权限。以下代码片段展示了在清单文件中编写<uses-permission>标签的语法:

软件特性

<uses-permission>标签请求应用程序必须获得的任何特定权限,以便它能正常运作。权限仅在应用程序安装时由用户授予。与<uses-feature>标签不同,<uses-permission>标签只有一个android:name属性。该标签的唯一属性指定了权限的名称。权限名称可以使用<permission>标签定义(这超出了本书的范围,我们不会讨论),或者使用安卓系统提供的标准权限名称。例如,为了允许应用程序读取电话联系人,我们可以编写如下代码片段:

软件特性

你可以看到我们是如何从android.permission包提供了一个标准的权限名称,以便读取手机的联系人。

注意

<uses-feature>标签声明的特性被谷歌应用市场用来筛选应用程序,而通过<uses-permission>标签声明的权限将在安装时向用户展示,以获取访问权限。

一些 <uses-feature> 标签名称描述符是在 <uses-permission> 标签描述符之后添加到 API 中的。因此,一些使用 <uses-permission> 标签的应用程序能够在不声明清单文件中的 <uses-feature> 标签的情况下使用特定硬件。为了防止应用程序因这种不匹配而出现任何意外问题,一些权限与某些功能相关联。Google Play 假定某些与硬件相关的权限默认表示需要底层硬件功能。<uses-feature> 标签允许 Google Play 在市场上筛选应用程序,只向用户展示设备能够运行的应用程序。然而,当用户下载并安装应用程序时,<uses-permission> 标签执行其职责。在安装之前,会要求用户授予应用程序中指定的所有权限。只有当用户授权时,应用程序才会安装。因此,对于那些既有 <uses-feature> 又有 <uses-permission> 标签名称描述符的功能,在应用程序的清单中声明两者是一个好习惯,以确保其正常工作。以下表格展示了一些由权限暗示的功能:

类别 <uses-permission> 描述符 <uses-feature> 描述符
蓝牙 android.permission.BLUETOOTH android.hardware.bluetooth
摄像头 android.permission.CAMERA android.hardware.camera
位置 android.permission.ACCESS_COARSE_LOCATION android.hardware.location``android.hardware.location.network
android.permission.ACCESS_FINE_LOCATION android.hardware.location.gps``android.hardware.location
麦克风 android.permission.RECORD_AUDIO android.hardware.microphone
电话 android.permission.CALL_PHONE android.hardware.telephony
android.permission.PROCESS_OUTGOING_CALLS android.hardware.telephony
android.permission.READ_SMS android.hardware.telephony
android.permission.RECIEVE_SMS android.hardware.telephony
android.permission.SEND_SMS android.hardware.telephony
android.permission.WRITE_SMS android.hardware.telephony
Wi-Fi android.permission.ACCESS_WIFI_STATE android.hardware.wifi

从表中可以看出,所有由权限暗示的功能都是硬件功能,需要硬件组件才能正常运行应用程序。因此,已经明确指出开发者应该声明 <uses-feature><uses-permission> 标签,以便在 Google Play 中筛选并在设备上正确安装,不会给用户和开发者造成任何麻烦。

使用 SEND 动词分享

任何手机的主要目的都是提供一种简单的通信方式。像所有手机一样,Android 智能手机提供了一种更简单的通信方式。在这个互联网和社交网络的时代,Android 手机在共享和社交网络方面被证明是非常高效的。Android 提供了诸如共享图片、状态、发送电子邮件、社交网络(如 Facebook、Twitter 等)的功能。幸运的是,对于开发者来说,所有这些共享功能都可以通过几行意图代码非常容易地实现。意图被证明是在 Android 组件和应用程序内部执行异步通信的一个非常好的方式。

在第三章中,我们讨论了使用意图共享状态的示例。在本章中,我们将更详细地解释同样的SEND意图,并查看如何通过用户选择的任何媒介共享图像和文本。当涉及到在 Android 手机上共享任何东西时,带有SEND动作的意图被广泛使用。在本节中,我们将讨论带有SEND动作的意图,以了解它能够实现的功能。

为了使用SEND动作定义意图,以下代码段展示了其声明:

使用 SEND 动作共享

你可以看到,我们在意图的构造函数中传递了Intent.ACTION_SEND的字符串常量。这个字符串常量告诉 Android 系统,意图是用来在设备上发送任何东西的。我们可以通过调用以下代码段中显示的startActivity()方法来执行以下意图:

使用 SEND 动作共享

startActivity()方法中传递SEND意图将允许用户通过提供所有可能的共享应用程序的对话框,选择他喜欢的发送方式。但是,如果我们没有设置意图类型就在startActivity()方法中传递SEND意图,它将抛出一个运行时异常。以下日志显示了运行时抛出的一些异常行:

使用 SEND 动作共享

在日志中,你可以看到"Unable to start activity",然后抛出了android.content.ActivityNotFoundException。当调用startActivity(intent)方法或其变体失败时,因为找不到执行给定意图的活动,就会抛出此异常。不仅异常类型,而且日志还显示了活动失败的原因。它说"No activity is found to handle the intent"。你可能想知道为什么 Android 找不到适合接收意图的活动。回想一下前面的章节中的隐式意图,Android 会查找所有可能与意图类型匹配的活动,并在对话框中显示所有这些应用程序。在我们的例子中,除了它的Intent.ACTION_SEND动作外,我们没有为意图定义任何类型;这就是为什么我们会得到ActivityNotFoundException的运行时异常。让我们设置动作的类型,看看显示所有可能接收意图的应用程序的对话框:

使用 SEND 动作分享

我们可以看到调用了setType()方法,并传递了一个text/html类型的字符串。这个方法设置了意图的显式 MIME 数据类型。这通常用于创建只指定类型而不指定数据的意图。这是 Android 系统中常用的隐式意图。此方法会清除之前设置的任何意图数据。

注意

Android 框架中的 MIME 类型匹配是区分大小写的。因此,你应始终使用小写字母书写你的 MIME 类型。你也可以使用normalizeMimeType(String)方法确保它被转换为小写。

我们在方法参数中传递了text/html作为 MIME 类型。这个类型告诉 Android 系统,所有支持 HTML 类型数据处理的应用程序都可以接收这个意图。因此,结果就是 Android 会将所有这些应用程序推送到对话框中,让用户选择他/她喜欢的应用程序。以下图片展示了text/html类型的对话框:

使用 SEND 动作分享

你可以看到所有支持 HTML 类型内容的程序都显示在图片中,比如电子邮件Imo MessengerSkype。你可以看到在 Android 手机上使用SEND意图分享内容是多么容易,选择应用程序并启动它们的工作留给了 Android。

注意

你可能已经注意到对话框中没有显示短信/彩信发送应用程序,因为短信/彩信只是纯文本应用程序,它们只支持那种类型的内容。

从列表中选择任何选项后,应用将会启动。由于我们没有设置任何要分享的内容,应用程序将基本上是空的。要在意图中设置内容,我们必须使用额外的信息(extras)。我们会为一些信息如标题、主题或文本添加额外的信息。以下代码片段展示了如何在SEND意图中添加一些额外的信息:

使用 SEND 动作分享

你可以在代码中看到,在设置意图的 MIME 类型之后,我们多次调用了putExtra()方法。这个方法向意图中添加扩展数据。该函数有两个参数:namevaluename参数必须包含一个包前缀;例如,应用com.android.contacts会使用像com.android.contacts.ShowAll这样的名称。我们为意图的主题、标题和文本内容传递了三个字符串。这类数据的名称,如Intent.EXTRA_SUBJECTIntent.EXTRA_TITLEIntent.EXTRA_TEXTIntent类中已经声明,我们可以以静态方式访问它们。你可能会想,既然我们已经传递了标题字符串,为什么还要传递主题呢?嗯,SEND意图是一个隐式意图,Android 显示了所有支持该意图的应用。用户可以选择任何应用,因为不同的应用对不同的数据感兴趣。例如,任何电子邮件应用都会对主题、收件人和正文字符串感兴趣。而任何短信应用只会对收件人和正文字符串感兴趣。因此,为了有效地使用SEND意图,你应该添加所有可能的内容,以便有效地与每个应用共享。以下是一个使用SEND意图发送电子邮件的示例。以下代码段展示了我们如何使用SEND意图来发送电子邮件:

使用 SEND 动作共享

首先,我们通过传递Intent.ACTION_SEND参数给构造函数来声明我们的SEND意图。然后,我们通过调用setType()方法将意图的类型设置为"text/html" MIME 类型。接下来,我们为电子邮件应用添加额外的内容,如下列表所示:

  • Intent.EXTRA_SUBJECT:此名称常量用于添加主题。

  • Intent.EXTRA_EMAIL:此名称常量用于填充收件人字段中的电子邮件地址。

  • Intent.EXTRA_CC:此名称常量用于填充 Cc 字段的电子邮件地址。

  • Intent.EXTRA_BCC:此常量用于填充 Bcc 字段的电子邮件地址。

在调用startActivity()方法之前,我们通过Intent.EXTRA_TEXT这个名称常量来设置电子邮件的正文,并在putExtra()方法的值参数中传递我们的文本。startActivity()方法将显示与之前图像相同的对话框,并在选择电子邮件应用后,显示以下截图:

使用 SEND 动作共享

一个已经使用意图中的内容填充的电子邮件应用

从图片中可以看出,我们放入 extras 的所有数据都已自动填充到电子邮件应用中,如主题、电子邮件、文本等。现在,用户只需轻触发送按钮,电子邮件就会被发送。在这个示例应用中,我们直接通过 To 字段向一个地址发送了电子邮件,并通过 Cc 和 Bcc 间接向另外两个地址发送。安卓允许我们添加多个电子邮件地址。为此,使用了名称常量Intent.EXTRA_EMAIL。我们在代码中传递了一个地址;我们还可以传递包含电子邮件地址的字符串数组,以发送电子邮件。

从本节内容中,我们主要了解到ACTION_SEND意图是如何被使用的,以及仅用几行代码就能通过此意图完成多少工作。如果我们从对话框中选择 Facebook、Twitter 或任何其他应用,我们会看到通过该应用分享数据的相同结果。这就是使用隐式意图的强大之处,几乎在每种可能的情况下都能使其通用,而无需进行任何复杂开发工作。

注意

ACTION_SEND是意图的一个动作。像这个动作一样,还有其他动作,如ACTION_VIEWACTION_SEARCH,可以通过传递意图用于安卓中的其他目的。

使用意图进行电话通讯和拨打电话

不仅安卓手机,从发明之日起,所有电话的主要目的都是提供一种远距离交流的方式。与其他所有电话一样,安卓手机也提供了拨打电话和接收电话、检查通话记录(如未接电话和已拨号码)、存储联系人、编辑/修改/删除联系人等功能。由于安卓手机属于智能手机范畴,其通话功能更为丰富。用户可以进行视频通话、录音通话、电话会议、手机与电脑之间的通话等。所有这些功能为用户提供了非常有效的产品,并让开发者能够利用这些功能实现更大的灵活性和生产力。

安卓为开发者提供了许多电话功能的 API。这些电话 API 让你的应用程序和开发者能够访问底层的电话硬件,从而可以创建自定义拨号器,集成呼叫处理或电话状态监控等功能。

注意

由于安全原因,开发者无法自定义手机的通话界面。当用户拨打电话或接听来电时,会显示通话界面。

由于本书专注于意图,我们只讨论那些可以使用意图利用的电话功能。在许多功能中,如拨打电话、接收电话、检查通话记录、接听/拒绝电话等,只有少数可以直接且仅通过意图利用。幸运的是,拨打电话就是其中之一。让我们在下一节讨论如何使用意图拨打电话。

使用意图拨打电话

在 Android 中拨打电话有两种方法。开发者可以使用 Android 提供的 API 来拨打电话,或者仅通过发送包含必要信息(如电话号码)的意图来启动电话拨打。我们将在本节的后面探讨启动电话拨打的方法。

在前面的章节中,我们了解了如何使用意图中的动作来告诉 Android 系统我们的意图。我们将通过告诉 Android 我们的意图来拨打电话,其余工作留给系统完成。以下代码段允许应用程序启动已预拨指定号码的拨号器,用户可以通过按下其中的拨号按钮明确地拨打电话:

使用意图拨打电话

你可以看到我们在代码中做了非常少的改动。我们声明了一个phoneNumber字符串,用于存储我们想要拨打的号码。你可能会想知道为什么我们在字符串中拼接了一个tel:前缀。这个前缀用于获取号码的通用资源标识符URI)。我们通过调用Uri类的静态方法Uri.parse()来获取这个 URI。这个方法返回 URI,我们反过来将其传递给构造函数的另一个参数。我们通过在意图声明中传递Intent.ACTION_DIAL来提供DIAL动作,最后,我们像往常一样调用startActivity(intent)方法来执行意图,并告诉 Android 系统处理我们的意图。以下屏幕截图显示了之前提到的代码段的拨号器结果:

使用意图拨打电话

通过启动 DIAL 意图来显示一个已拨号码的拨号屏幕

当我们运行之前的代码时,应用程序将启动 Android 手机的默认拨号器,并在其中拨打代码中提供的号码。它不会打电话;它只会拨号,因为我们使用了Intent.ACTION_DIAL。用户可以明确地按下拨号器的拨号按钮并进行通话。

如果用户不想拨号,也可以直接调用号码而不先进入拨号界面。Android 为此提供了Intent.ACTION_CALL动作。以下代码段展示了如何直接拨打电话:

使用意图拨打电话

你可以从代码中看出,除了在意图构造函数中传递的动作外,其他都一样。在最后一个示例中,我们传递了Intent.ACTION_DIAL,而在这个例子中,我们传递了Intent.ACTION_CALL来直接拨打电话。当我们运行这段代码时,应用程序将在 Android 手机上开始拨打电话。以下屏幕截图显示了通话:

使用意图拨打电话

通过启动CALL意图来显示一个通话中的屏幕

这个ACTION_CALL直接拨打电话的操作需要用户向应用程序授予权限。以下代码片段展示了需要放在AndroidManifest文件中的权限,以使应用程序能够完美运行:

使用意图拨打电话

需要注意的是,ACTION_CALL不能使用意图拨打紧急电话;然而,使用ACTION_DIAL可以拨号紧急号码。如果用户在手机上安装了多个拨号器,ACTION_DIAL操作将显示拨号器列表,用户可以选择一个喜欢的拨号器。以下截图展示了多个拨号器的场景:

使用意图拨打电话

从对话框中选择多个拨号器

ACTION_DIALACTION_CALL意图之间几乎没有区别。ACTION_DIAL意图只是拨号,用户可以通过按下拨号按钮明确地拨打电话,但ACTION_CALL会直接拨打电话,而不会向用户显示拨号器。

注意

应用程序直接拨打电话可能会受到限制。因此,除非需要ACTION_CALL,否则在应用中使用ACTION_DIAL是一个好习惯。

这就是我们如何通过意图轻松拨打电话并使用 Android 的通话功能。在下一节中,我们将了解如何使用意图发送短信、彩信和数据消息。除了发送,我们还可以确认消息送达以及接收消息。接下来的一节,我们将详细讨论这些内容。

使用意图发送短信/彩信

除了拨打电话的功能外,手机还支持短信服务,如短消息服务(SMS)、多媒体消息服务(MMS)以及最近的数据消息。SMS/MMS 功能在手机中应用最广泛,许多人更喜欢使用它而不是拨打电话。Android 提供了 API 和框架,让开发人员能够从他们的应用程序中发送和接收消息。开发人员甚至可以替换原生的短信应用程序来发送和接收文本消息。

注意

在撰写本书时,还没有用于从应用程序内部发送彩信的 API 或库,但您可以使用ACTION_SENDACTION_SENDTO意图来发送。

本节将引导您了解各种操作,如使用意图发送短信、发送彩信、发送数据消息、确认消息送达以及接收短信。然后,我们将简要介绍所有这些操作在不使用意图的情况下是如何执行的,以及 Android 的 API 对我们有何帮助。接下来的一小节,我们将看看使用意图发送短信的任务。

使用意图发送短信

使用意图的最大优点是它将我们的需求责任传递给了 Android 系统,而不是我们从核心创建完整的功能。如果我们在我们当前的情况下使用意图,即给某人发送短信,我们只需提供要发送消息的号码和要发送的消息。其余的工作由 Android 自身完成。

关于使用意图发送或分享内容的话题,我们已经进行了很多讨论,幸运的是,这里没有什么是我们需要吸收的新知识。这是我们之前使用的创建ACTION_SEND意图并调用startActivity(intent)方法执行的老方法。以下代码片段展示了我们之前使用的ACTION_SEND意图示例:

使用意图发送短信

现在,如果我们使用这段代码,它对我们来说并不实用,因为它没有执行我们发送短信的动作。它既没有在选择器对话框中显示支持短信的应用,也没有使用EXTRA_TEXT额外参数传递的数据发送任何短信。为了使用ACTION_SEND发送短信,我们需要注意一些额外的事项。使用意图发送短信有两种方法:通过ACTION_SEND意图和通过ACTION_SENDTO意图。让我们看看如何使用ACTION_SEND意图发送短信。

我们需要使用ACTION_SEND动作创建意图,并在其中嵌入消息的额外参数"sms_body"。Android 会自行向用户询问接收者的电话号码。但由于我们还未指定意图的类型,所以在选择器列表对话框中仍然不会显示任何短信支持应用。由于短信是短文本消息,我们应该将意图类型设置为"text/html",但由于大多数短信应用对彩信(MMS)没有原生支持,它们会寻找"image/jpg""image/png"作为意图类型。因此,将意图类型设置为"image/png"后,我们将得到以下代码片段:

使用意图发送短信

当我们执行这段代码时,我们会看到包括短信支持应用、电子邮件应用等在内的各种应用的选择器对话框。当我们选择任何短信应用时,我们会看到类似于以下图片的内容:

使用意图发送短信

发送短信意图后显示默认短信应用

您可能已经注意到,短信应用中的文本部分已经用我们在"sms_body"额外参数中添加的内容填充,用户正在输入消息的接收者数量。

注意

前面的图片展示了 QMobile Noir A10 智能手机的默认短信应用。您的设备将显示您设置为手机默认的短信应用,当然,它不会和这个应用相同。

这样我们就可以使用意图发送短信了。现在,让我们考虑另一个情况,我们希望通过编程设置收件人的数量。为此,我们必须使用ACTION_SENDTO意图而不是ACTION_SEND意图。以下代码段展示了ACTION_SENDTO意图的使用:

使用意图发送短信

在前面的代码中,你可以看到我们在之前发送 SMS 消息的代码中做了一些更改。我们将动作设置为ACTION_SENDTO,而不是ACTION_SEND。同时,在意图的构造函数中传递了另一个电话号码 URI 的参数。我们为电话号码创建了一个字符串,并在号码前拼接了"sms:"标签。这个标签让Uri类明白该字符串代表要发送消息的电话号码,并相应地解析它。你可能还记得,在上一节中,我们使用"tel"标签通过意图拨打任何号码。当你执行代码时,它会要求应用程序选择短信应用。选择任何支持 SMS 的应用程序后,它会直接将短信发送到提供的电话号码,而不是像上一个示例中那样询问电话号码。你可能已经注意到,在这段代码中我们没有设置意图的类型。这是因为当我们使用ACTION_SENDTO意图时,我们不需要明确设置意图的类型。Android 会从如"sms""tel"的标签以及如ACTION_SENDTOACTION_CALL的动作中理解开发者试图做什么。

注意事项

如果你想要使用ACTION_SEND并明确使用代码设置收件人号码,Android 提供了"address"额外项,无需使用任何如"tel""sms"的标签,就可以在其中放入号码字符串。

到目前为止,我们讨论了使用ACTION_SENDACTION_SENDTO发送 SMS 文本消息。在下一节中,我们将看到如何使用意图发送包含嵌入图片的多媒体消息。

使用意图发送 MMS

文本消息与多媒体消息唯一的区别在于嵌入的富媒体内容。MMS 消息包含照片、视频和贺卡等富媒体内容,以及一些作为内容消息的文本。目前,Android 没有提供让开发者原生发送 MMS 的库,与 SMS 不同。但幸运的是,意图为我们提供了一种明确的方式来发送 MMS。正如真正的区别所定义的,我们需要在文本消息意图中添加一些媒体,并将其类型设置为多媒体,如"image/png",然后我们就完成了 MMS 消息的发送。以下代码段展示了如何使用用于 SMS 消息的意图发送任何 MMS 消息:

使用意图发送 MMS

你可以看到代码有两部分。在第一部分,我们获取了存储在外部存储的images文件夹中所需图像的 URI。在第二部分,我们使用ACTION_SEND创建意图。然后,我们使用"sms_body"额外项添加我们的文本,并将类型设置为"image/png",使其对多媒体消息有意义。之后,我们使用Intent.EXTRA_STREAM额外项附加我们的媒体,并在其中传递我们的图像 URI 作为值。最后,我们通过调用startActivity(intent)方法执行意图。唯一的区别是使用EXTRA_STREAM额外项附加媒体 URI,其余与短信中的操作相同。你也应该注意,我们可以使用ACTION_SENDTO指定收件人号码,或者我们也可以使用电话号码作为值的"address"额外项。

注意

在上一个示例中,我们已经设置了"image/png"的类型。这只能发送 PNG 图像。对于其他图像格式,我们可以指定"images/*"

到目前为止,我们只讨论了发送短信和彩信。但我们确定这些消息已经成功送达了吗?好吧,下一节是关于确认消息送达和了解意图在其中扮演的角色。让我们在下一节中看看如何使用意图来确认消息的送达。

使用意图确认消息送达

当我们使用意图发送短信时,无论是短信还是彩信,我们都无法追踪这些消息以执行操作,例如确认送达。背后的原因是隐式地使用意图,并依赖于安卓系统的默认操作。如果我们使用意图发送消息,这意味着我们将发送消息的责任交给了安卓系统。现在,如果我们想要确认消息的送达状态,这意味着我们正在向安卓系统询问我们的消息。不幸的是,要实现这一点,我们缺少两样东西:一是告诉安卓系统我们确认了某条消息,二是安卓系统可能不记得我们谈论的是哪条消息。

为了能够确认送达,我们必须手动使用原生 API 发送消息。这个 API 的任务是跟踪我们正在谈论的消息的送达状态和消息本身。此外,使用这个 API,我们可以轻松发送查询,询问安卓系统关于送达确认的信息。

现在,如果我们使用原生 API 发送消息,我们必须考虑彩信。如前所述,对彩信的原生支持仍然不存在;因此,我们将无法追踪和确认彩信的送达,但我们可以确认短信的送达状态。在本节中,我们将讨论如何使用原生短信 API 检查短信送达状态,以及如何使用意图来实现我们的目标。

意图在 Android 中是一种异步的通信方式,并且被广泛使用。唯一的变化是,它们被用来实现我们的目标和完成需求。关于确认消息送达状态的简短解释,我们将使用名为SmsManager的原生短信 API,通过SmsManager.sendTextMessage()方法发送文本消息。为了跟踪消息,我们将使用两个意图:一个用于发送动作,另一个用于送达动作。除了这两个意图,我们还将创建两个挂起的意图:一个用于发送动作,另一个用于送达动作。最后,为了执行所有四个意图,我们将创建两个广播接收器:一个检查发送动作,另一个检查送达动作。这里看起来可能相当复杂,但实际上非常简单。让我们看看声明我们四个意图的代码片段:两个意图和两个挂起意图:

使用意图确认消息送达

你可以看到,我们在代码中以常规方式声明了我们的意图。这里的唯一区别是,我们使用了代表字符串的自定义动作,如"sent_sms_action""delivered_sms_action"。然后,我们使用PendingIntent类的getBroadcast()工厂方法创建了两个挂起意图。getBroadcast()方法将检索到将执行任何广播的PendingIntent,例如调用Context.sendBroadcast()方法。

因此,在创建完所有四个意图之后,我们现在需要创建并注册广播接收器,这些接收器将执行挂起的意图。以下代码片段展示了两个接收器的实现:

使用意图确认消息送达

如前一段代码所示,我们使用Activity.registerReceiver()方法注册了两个广播接收器,并传递了匿名对象。重写的方法onReceive()实现了我们的目的。当一个消息被发送时,会调用一个onReceive()方法;当一个消息被送达时,会调用另一个onReceive()方法。我们在代码中加入了注释,以展示你可以在哪里使用自定义功能。你可能想知道 Android 如何知道这些是用于发送和送达状态的广播接收器。通过检查意图过滤器,Android 会知道这一点。你可以看到,我们在意图过滤器的构造函数中传递了我们在意图中使用的自定义动作,这些过滤器将过滤广播,接收器只接收它注册的广播。到目前为止,我们已经为确认消息送达做了核心工作。现在剩下的就是去执行它,这时SmsManager API 就派上用场了。我们将创建一个SmsManager的实例,并调用其sendTextMessage()方法来发送消息,并放入所有意图,然后我们就完成了。以下代码片段展示了SmsManager的使用代码:

使用意图确认消息送达

请记住,SmsManager API 使用了android.permission.SEND_SMS权限;因此,不要忘记在您的清单文件中添加它,如下面的代码片段所示:

使用意图确认消息送达

这样,我们就能确认消息的送达。我们只能确认文本消息的送达状态,并且需要用户授予SEND_SMS权限。但是,如果我们使用意图,我们只能发送消息,并且不需要用户任何权限。

注意

安卓模拟器支持发送和接收短信。这可以通过创建多个模拟器实例并向模拟器的端口号发送短信来实现。

总结意图在确认消息送达中的作用,意图在这里并不是执行确认消息送达的核心动作。它们只是提供了一种通信方式,携带必要的信息,比如哪条消息的送达需要被检查等。然后,这些意图被广播接收器使用,它们不断检查消息的送达和发送状态。一旦完成,它们将状态传递到我们的意图中,然后这些意图为我们提供消息是否已发送或送达的更新。

在下一节中,我们将做几乎相同的事情和编码,但这次,我们将这样做是为了接收消息。使用所有这些代码片段后,我们可以开发出能发送和接收消息的短信应用。让我们看看如何接收消息以及意图在背后的作用。

使用意图接收短信

迄今为止,我们讨论了发送短信/MMS 消息以及在应用程序中意图的重要性。在本节中,我们将讨论如何监听传入的消息,以便我们能在应用程序中使用它们。利用这个功能,我们可以开发消息传递应用。意图可以使用ACTION_SENDACTION_SENDTO意图直接发送消息,但这些意图并不直接参与监听传入消息和接收消息。意图与Broadcast Receiver的使用方式相同,用于获取发送者号码、消息内容、消息时间等信息。在讨论如何监听传入消息之前,我们必须了解一些在以下应用程序中使用的类。

SmsManager 类

在前面的子节中,我们已经使用了SmsManager类来确认消息的送达。这个类用于管理如发送数据、短信和 PDU 消息等 SMS 操作。我们不能通过构造函数实例化这个对象;我们可以通过调用SmsManager.getDefault()静态方法来获取它的实例。我们可以使用这个类来发送消息。

注意

有两个不同类名为SmsManagerandroid.telephony.SmsManagerandroid.telephony.gsm.SmsManager。在 GSM 包中的后者在 API 级别 4 及以后版本中已被弃用。

SmsMessage 对象

这个类代表了一个简单的短信消息对象。在接收到来电消息时,我们将得到一个SmsMessage对象的数组。这个类用于获取诸如消息正文、消息时间、发送者号码等信息。

协议数据单元(PDU)

PDU 是短信消息的行业格式。开发者无需担心详细阅读 PDU 或理解其格式,因为 Android 的SmsManager类会读取和写入 PDU,并为开发者提供使用 PDU 的方法。

这些类和概念将在接收来电消息的应用中使用。现在,让我们讨论一下在 Android 中是如何接收消息的。当任何设备接收到新的短信消息时,会触发一个新的广播意图。这个意图的动作是android.provider.telephony.SMS_RECEIVED。我们必须创建一个自定义的广播接收器,它将寻找这个广播意图。每当我们收到任何消息时,广播接收器的onReceive()方法将被调用。以下代码段展示了我们为来电消息实现的自定义广播接收器:

public class IncomingMsgReceiver extends BroadcastReceiver {
  private static final String SMS_RECEIVED = "android.provider.
    Telephony.SMS_RECEIVED";
  public void onReceive(Context _context, Intent _intent) {
    if (_intent.getAction().equals(SMS_RECEIVED)) {
      // SMS Received. Write your code here.
      Bundle msgBundle = _intent.getExtras();
      getMessageData(msgBundle);
    }
  }
}

与往常一样,我们的类从BroadcastReceiver扩展而来,并覆盖了onReceive()方法。当设备接收到任何来电消息时,会调用这个方法。我们首先检查这个意图是否包含任何收到的消息。如果意图动作与我们的SMS_RECEIVED字符串字面量相同,这意味着我们已经收到了消息。

注意

在 Android 中,收到的短信动作android.provider.Telephony.SMS_RECEIVED是不被支持的,并且在未来的平台版本中可能会发生变化。开发者在使用这些 Android 中不被支持的隐藏方法和属性时应谨慎。

在检查并确认动作后,我们必须从意图中获取消息数据并执行应用程序的自定义操作。我们首先通过调用getExtras()方法从意图中获取 extras bundle,然后我们将这个 bundle 传递给我们的getMessageData()方法。这是我们自定义的方法,在这个方法中,我们将了解如何从 bundle 中获取消息数据。以下代码实现展示了方法的定义:

协议数据单元(PDU)

我们首先检查我们的 bundle 不是一个 null 对象。然后我们通过调用get()方法并传递"pdus"键从 bundle 中提取 PDUs。

注意

如果你不清楚在get()方法中应该传递哪个键,你可以调用Set<String> Bundle.keySet()方法来获取 bundle 中使用的所有键。

回顾 PDU,PDU 是短信的行业格式。一旦我们有了数组中的所有 PDU 对象,我们就可以使用SmsMessage.createFromPdu()方法从这些 PDU 创建短信。创建所有消息后,我们遍历数组,并使用SmsMessage.getMessageBody()SmsMessage.getOriginatingAddress()SmsMessage.getTimestampMillis()方法从其中获取消息数据,如消息正文文本、消息发送者号码和消息时间。现在,我们可以将这些数据字符串用于我们的应用程序。需要注意的是,任何长消息都会被分解成许多小消息,这就是为什么我们得到对象数组的原因。

除非我们在应用程序中注册此广播,否则它不会工作。要在我们的应用程序中注册它,我们必须在主活动中编写以下代码:

协议数据单元(PDU)

这里没有新的讨论内容。我们正在创建一个带有SMS_RECEIVED动作和广播接收器实例的意图过滤器。然后,我们将它们都传递给活动的registerReceiver()方法。消息接收器需要android.permission.RECEIVE_SMS权限;因此,不要忘记在您的清单文件中添加这一行:

协议数据单元(PDU)

这样,我们就可以在应用程序中接收来电消息,并以多种不同的方式使用它们。您可能想知道意图在这个应用程序中的作用。如前所述,这个应用程序中没有直接使用意图。当设备收到任何消息时,会触发一个广播意图。我们使用该意图从中提取数据和消息,这些消息在我们的应用程序中使用。在 Android 设备接收到消息后,意图的作用就是提供关于消息的数据。

我们可以使用 Android 调试工具中的Dalvik 调试监控服务器DDMS)面板,在 Android 模拟器上模拟来电消息。以下是 DDMS 视图中用于模拟消息的模拟器控制面板的屏幕截图:

协议数据单元(PDU)

在 DDMS 视图中用于模拟消息的模拟器控制面板

在本节中,我们了解了发送短信、彩信、确认消息送达以及接收来电消息。我们还讨论了在这些应用中使用意图的重要性和方法。在下一节中,我们将学习关于通知的知识,以及如何在制作交互式通知时使用意图。

使用意图的通知

从传统手机到智能手机,每款手机都使用某种方式来通知和提醒用户某些事件,如接收短信或电话。与这些手机一样,Android 手机使用通知系统来提醒用户。通知是显示在应用程序正常 UI 之外的消息。当触发任何新的通知时,它会在通知区域显示。用户可以通过向下拉动手势来随时从通知抽屉和通知区域查看通知。以下屏幕截图展示了 Android 中两种不同通知的示例:

使用意图的通知

Android 手机中的通知

通知就像是在用户忙于其他移动活动(如玩游戏)时,发生重要事件时提醒用户的渠道。

对于任何开发者来说,通知是开发者显示在应用正常用户界面(UI)之外的一个用户界面元素,用以指示并告知用户某个事件已经发生。然后,用户可以在使用其他应用时选择查看通知,并在他们希望的时候进行响应。使用通知是让不可见的应用组件(如广播接收器和服务)告知用户任何事件发生的首选方式。

在本节中,我们将讨论通知、它们的布局、在通知布局中显示附加信息以及启动意图。我们将了解意图的作用,并创建一个具有自定义通知布局的示例应用程序,以及意图在这类应用程序中的重要性。在我们开始开发示例应用程序之前,让我们讨论通知中使用的某些基本概念。

通知形式

通知可以采取不同的形式,例如状态栏中的任何持久图标,可以通过启动器访问。当用户选择此通知时,当发生某些活动或服务时,会触发任何指定的意图。通知还可以用来打开设备的闪烁 LED 灯。此外,设备在接收到通知时也可以振动或播放铃声。

NotificationManager 类

NotificationManager类代表了一个用于处理 Android 中通知系统的系统服务。我们不能实例化这个类,但可以通过调用getSystemService()方法并传递Context.NOTIFICATION_SERVICE来获取它的实例对象。以下代码段展示了如何获取NotificationManager类的实例:

NotificationManager 类

Notification 类

Notification类代表了 Android 中的任何通知。它提供了 API,允许开发者设置通知的图标、标题、时间等。以下代码段展示了如何在 Android 中创建一个通知:

Notification 类

通知布局

每个通知都有一个图标和滚动文本,有时称为状态文本。当启动通知且通知抽屉关闭时,会显示图标。当触发通知时,滚动文本会在状态栏中滚动,当打开通知抽屉时,它会被设置为通知信息文本。下面的截图概述了通知区域的不同方面:

通知布局

通知和通知区域

你可以从前面的截图中看到,当触发任何通知时,其滚动文本会在状态栏中滚动。滚动完整个文本后,其图标将显示在状态栏上。当用户通过下拉打开通知抽屉时,将显示通知的大图标以及通知标题、内容文本和时间戳。这就是在 Android 中触发任何通知的方式。

现在,我们将讨论通知是如何触发的以及通知应用中意图的使用方法。我们将创建一个通知,如下面的截图所示:

通知布局

一个简单的通知

因此,在创建通知之前,我们需要通知的布局。以下代码实现展示了我们通知的布局:

通知布局

我们在RelativeLayout中放置了四个视图:一个用于图标的ImageView,一个用于标题的大型TextView,以及两个分别用于描述和时间戳的小型TextViews。我们使用了RelativeLayouts的对其方式,将视图放置在其他视图的下方、上方、右侧和左侧,以便在不同分辨率智能手机上以相同方式显示。我们将此文件保存为资源目录中 layout 文件夹的notification_layout.xml。这就是我们的通知布局。现在,让我们学习如何创建使用此布局的任何通知。

要创建具有自定义布局的通知,我们在 Android 中有两种不同的方法。第一种方法是使用setLatestEventInfo方法更新标准扩展状态通知显示中显示的详细信息。这是最简单的方法,在更多应用中使用。另一种方法是设置通知的contentViewcontentIntent属性,以使用RemoteView类为扩展状态显示分配自定义 UI 布局。

注意

RemoteView是一种机制,允许开发者在任何独立应用程序中嵌入和控制布局。这通常用于创建主屏幕小部件。

在本节中,我们将使用一个复杂的方法来创建通知,因为此方法在代码中使用了意图。我们首先创建一个RemoteView对象,并将其分配给通知对象的contentView属性。contentView视图表示在展开状态栏中的通知。通知通常表示对操作的请求,当用户点击通知抽屉区域或展开状态栏中的通知时,执行此操作。我们可以指定当用户点击通知项时将被触发的PendingIntent。通常,此意图会打开我们的应用程序,并提供有关我们通知的更多信息。除了设置contentView之外,我们还需要将contentIntent设置为我们创建的PendingIntent对象,其中自定义内容视图被分配给我们的通知。contentIntent意图是在点击展开状态条目时必须执行的目的地。如果这是活动的意图,我们必须包括FLAG_ACTIVITY_NEW_TASK,这将使我们的活动在新任务中启动。

注意

当你手动设置contentView属性时,还必须设置contentIntent属性;否则,在触发通知时将抛出异常,导致应用程序的任何运行时崩溃。

一旦将contentView属性设置为我们的自定义远程视图,我们就不能以常规方式设置所需视图。我们必须使用RemoteView对象上的 set 方法,这些方法会修改在定义的布局中使用的每个视图。这就是任何具有自定义布局的通知的开发方式。以下代码展示了具有自定义布局通知的实现,这可以添加到任何活动中:

通知布局

从代码中可以看出,我们首先创建了一个Notification对象,带有初始图标、ticker 文本和触发通知的时间。然后,我们创建意图对象,IntentPendingIntent,用于指定当通知被点击时的动作。接着,我们设置了通知对象的contentIntentcontentView。我们为contentView创建了一个新的RemoteView对象,并在其中传递了我们的notification_layout.xml引用。这就是通知布局通过RemoteView构造函数设置为我们的自定义布局的方式。然后,我们将我们的待定意图设置为contentIntent。最后,我们使用 set 方法更新我们布局的值,如setImageViewResource()setTextViewText()。到目前为止,我们已经使用自定义布局开发了自己的通知。现在,我们将了解如何触发通知。以下代码段展示了如何触发通知:

通知布局

我们通过调用getSystemService()方法来获取NotificationManager类的实例。为了触发通知,我们调用了NotificationManager.notify()方法,该方法接收两个参数:第一个是通知的 ID,第二个是通知对象本身。以下屏幕截图显示了应用程序的输出:

通知布局

来自我们应用程序的通知触发

到目前为止,我们已经了解了如何创建通知并为它们的视图设置自定义布局。你可能正在思考意图在这个应用中的重要性及其用途。在这个应用中,意图仅用于一个目的,那就是当用户点击通知时,引导用户到我们所需的应用程序或活动。我们创建了一个Intent对象,并由此生成了一个PendingIntent对象,这个对象在通知中被用作contentIntent

概要总结

在本章中,我们讨论了 Android 的特性。我们了解了常见的 Android 特性,如布局、显示、连接性、通信、可访问性、触摸和硬件支持,以及它们与 Android 移动组件的比较。然后,我们看到了AndroidManifest文件中两个最重要的标签<uses-feature><uses-permission>的用途及其如何使用。

我们还讨论了硬件和软件特性与 Android 移动组件之间的关系,以及它们与这些清单标签的联系。然后,我们了解了最常见的意图动作ACTION_SEND,它用于通过隐式意图的方法与其他应用程序分享或发送任何内容。接着,我们将意图的知识扩展到手机更具体的特性,包括拨打电话、发送短信/MMS、确认消息送达和接收消息。我们使用了意图以及 Android 的原生 API 来执行这些操作。然后,我们讨论了通知和警告,并学习了如何在通知中设置自定义布局。我们了解到了两种不同的方法,并在我们的示例应用程序中使用了一种方法。我们了解了这些类型的应用程序中意图的使用,也了解了它们与这些类的作用。

在下一章中,我们将讨论意图过滤器,并了解 Android 如何识别不同的意图并根据调用和应用程序对它们进行过滤。

第七章:意图过滤器

意图过滤器是理解安卓意图微小但重要细节的高级步骤。在本章中,我们将了解意图过滤器的基础知识以及如何在安卓应用程序中有效使用它们。本章还处理意图在传递到期望组件之前应通过的各类测试。

注意

意图过滤器可以在AndroidManifest.xml文件中的activity标签下找到。

在本章中,我们将涵盖以下主题:

  • 意图对象及其分类

  • 理解意图过滤器是什么

  • 理解意图测试是什么

  • 为特定任务实现意图过滤器

意图对象及其分类(重复,不翻译)

意图对象附带大量信息。这个信息包将帮助组件从中提取知识。例如,应该对随意图对象传递的数据采取什么类型的操作;同样,还有关于安卓系统的信息。当系统不知道将处理即将到来的意图的组件时,需要关于安卓系统的信息。

为了更好地理解前一段提到的示例,考虑一个意图被传递以启动电影的场景。在这种情况下,操作系统必须知道执行此动作需要哪个软件。

下文将讨论安卓意图对象中包含的分类。

组件名称

意图对象包含将处理数据的相关组件名称信息。通常,此组件由完整的类名组成,例如com.app.demoactivity.MyActivitycom.example.demoactivity.MainActivity。对于意图对象来说,此组件信息是可选的。如果意图对象知道该组件,安卓会将数据处理指向那个特定的组件;如果不知道,安卓会确定哪个组件最适合处理此事件。

注意

组件名称中的包部分不一定与AndroidManifest.xml文件中的项目名称相同。

组件名称通过安卓 API 提供的setComponent()setClassName()方法设置。

意图解析

安卓意图分为两部分(如第三章所述,意图及其分类),隐式意图显式意图。对于显式意图,不为其分配组件名称并不会造成问题,因为必须将组件包含在意图对象中,然后安卓会自动将显式意图指向所描述的组件。

另一方面,在隐式意图中,如果没有向 Android 系统提供组件名称,它将自动将其指向所有可能处理此传入意图的应用程序。只有当意图具有意图过滤器时,此操作才会发生,否则 Android 不会引导它。这个术语称为 Android 意图解析;当你不需要为隐式意图定义组件时,它将自动显示所有可能接收此意图的应用程序列表。

动作

动作是一个描述在意图上要执行操作的字符串。例如,ACTION_CALLACTION_BATTERY_LOWACTION_SCREEN_ON。你可以在 developer.android.com/reference/android/content/Intent.html 找到其他各种动作的常量。你也可以创建自己的意图动作,但请确保在前面加上项目名称,例如 com.example.myproject.SHOW_CONTACT。当开发者想要创建一个之前未添加到 Android SDK 的事件时,需要自定义动作。当开发者想要触发/检查与该应用程序紧密相关且不在 Android SDK 中的动作时,也可能出现这个需求。

注意

com.example.XXX 是在 Java 和 Android 应用程序开发中不推荐使用的包名。这确保了使用这个包大多是因为理解了本例中的用途。

动作通常可以告诉你意图是如何构建的,尤其是数据和额外信息。它就像方法的现象,有参数并且返回值。最佳实践是始终尽可能具体地使用你的动作名称,并与意图紧密耦合。意图动作可以通过使用 Android API 的 setAction() 方法来设置,你可以通过 getAction() 方法获取它。

下表给出了一些预定义的意图动作常量:

常量 组件关系 动作
ACTION_CALL 活动 初始化一个电话通话
ACTION_EDIT 活动 显示用户数据以进行编辑
ACTION_MAIN 活动 作为初始活动启动,无需数据输入,也没有返回输出
ACTION_SYNC 活动 在服务器上与移动设备上的数据同步
ACTION_BATTERY_LOW 广播接收器 电池电量低的警告
ACTION_HEADSET_PLUG 广播接收器 耳机插入设备
ACTION_SCREEN_ON 广播接收器 屏幕已开启

数据

在 Android 意图中,根据提供的数据类型,采取不同类型的动作。数据是 Android 意图的基本部分之一,尤其是在隐式类别中。让我们通过一些例子来更好地了解如何在 Android 意图中使用与其相关动作的数据。

ACTION_EDIT中使用数据

考虑一个ACTION_EDIT的例子。每当我们在意图中调用这个动作时,很明显,编辑功能需要在某种文档中实现。这个文档路径应以 URI 的形式给出,然后由 Android 意图处理。这个 URI 实际上是我们要放入意图对象中的数据部分。

当开发者希望打开默认的 Android 的添加新联系人屏幕并期待用户进行编辑时,可以使用ACTION_EDIT。在这种情况下,用于打开添加新联系人屏幕的意图应该定义ACTION_EDIT动作。

ACTION_CALL中使用数据

考虑另一个ACTION_CALL的例子。当我们需要通过意图执行呼叫功能时,使用此动作。因此,为了完成这项任务,我们需要通过使用tel:// URI 引用来提供电话号码。这是需要与意图一起提供的数据集,以便 Android 知道需要执行拨号功能的数据。

ACTION_VIEW中使用数据

接下来是我们的第三个例子,即ACTION_VIEW。在大多数情况下,当调用此动作时,会通过 URI 链接到一个网站。这帮助 Android 了解要在其上执行查看动作的数据。通常,ACTION_VIEW动作会附加一个http:// URI,以便 Android 可以处理查看任何网页的功能。

类别

这是向意图提供的信息,以便了解执行该特定意图所需的最佳组件类型。例如,如果我们想使用ACTION_VIEW动作查看一个网页,我们可以将其类别指定为CATEGORY_BROWSABLE,以让 Android 知道与意图关联的数据是安全的,并且可以通过 Android 浏览器轻松执行。

在任何 Android 程序中都可以轻松使用的类别常量列表如下:

常量 说明
CATEGORY_BROWSABLE 活动可以在与意图关联的数据的 Android 浏览器上安全执行。
CATEGORY_GADGET 活动与任何 Android 设备托管的另一个活动相关联。
CATEGORY_HOME 活动显示主屏幕,或者当按下主页按钮时用户看到的第一个屏幕。
CATEGORY_LAUNCHER 特定活动的类别是启动器,这意味着它将成为堆栈顶部的活动。
CATEGORY_PREFERENCE 目的地活动来自偏好设置面板。

附加项

在前面的章节中,我们已经详细了解了 extras 功能以及如何与意图一起使用。与数据类似,一些 extras 与将要启动的意图绑定在一起。例如,ACTION_HEADSET_PLUG动作具有额外的"State",用以指示耳机是否已连接到手机。

这些方法与 bundle 对象的方法类似。因此,可以使用putExtras()getExtras()方法将 extras 安装和读取为 bundle。

意图过滤器

在此时刻,我们对 Android 意图及其实现有了完美的理解。Android 意图负责告诉 Android 已经发生了某个事件,它也用于提供额外数据,以执行某些特定动作。但是 Android 如何知道哪个组件可以促进任何意图的执行呢?为此,引入了意图过滤器的概念。意图过滤器用于识别哪些组件可以响应活动、服务或广播意图的特定调用。

通常,意图过滤器通过AndroidManifest.xml文件提供给活动或服务,该文件包含动作、数据和类别测试。对于广播接收器,意图过滤器也可以通过代码动态定义。

对于隐式意图,为了将其传递给特定组件,必须通过所有三个测试。基于这些情况,可能会出现两种条件:一种情况是意图没有通过任何一项测试,那么它将不会被传递给组件。另一种情况是,当它通过了所有测试,它将直接移交给相应的组件。在第一种情况下,有一个例外,如果没有通过测试,它可以被传递给同一活动的下一个意图过滤器。通过这种方式,它有可能按照预期执行。

提示

AndroidManifest.xml文件中,我们可以在一个活动中拥有多个意图过滤器。

具有内部意图过滤器的活动的普通 XML 标签如下所示:

意图过滤器

如代码所示,它由一个包含所有内容的 activity 标签组成。这个活动仅包含一个意图过滤器,其中包含两个主要组件:动作类别。此意图执行时要采取的动作是 android.intent.action.MAIN,通过调用此动作,任何对活动的先前引用都会被移除,活动会以全新的启动执行。这样,类别被设置为 android.intent.category.LAUNCHER;这表明在 AndroidManifest 文件中编写的活动是启动器的 activity 标签。这意味着,一旦执行应用程序,它就是第一个要启动的活动。如果在 AndroidManifest.xml 文件中有两个或更多描述为启动器的活动,Android 操作系统将询问用户要从哪个活动开始。

注意

<intent-filter>AndroidManifest.xml 文件的一部分,而不是 Java 代码,因为它包含的信息需要在项目应用程序启动之前获取。例如,在项目应用程序启动之前,需要确定它是否为启动器活动。由于 AndroidManifest.xml 文件在项目应用程序启动之前执行,以便提取有关项目的信息,意图过滤器是此文件的一部分。唯一的例外是在广播意图的情况下,其信息可以从 Java 代码动态修改,而不是从 AndroidManifest.xml 文件。

处理多个意图过滤器

并非强制要求任何 Android 活动只能有一个意图过滤器。一个活动可能包含多个意图过滤器,这些过滤器占据了诸如类别、数据和动作等多个子组件。请看下面的截图,展示了带有不同类型参数的两个意图过滤器:

处理多个意图过滤器

前述截图中提到的代码解释将在本章后续内容中介绍。目前,了解活动内部各种意图过滤器的实现是重要的。

意图过滤器的测试组件

过滤器是意图对象中动作、数据和类别字段的代表。每当调用隐式意图时,都会针对这些过滤器进行测试,以便执行。如果该意图不满足任何一个测试组件,它将不会执行,或者更确切地说,它将被引导到同一活动的另一个意图过滤器(如果存在的话)。

现在,为了正确理解意图过滤器的意图,我们需要逐步评估与意图过滤器相关的每个测试组件。存在三个测试组件:

  • 动作测试

  • 数据测试

  • 类别测试

动作测试

操作描述了即将由传入意图执行的操作类型。AndroidManifest.xml文件确定了传入意图需要满足的要求。如果任何意图无法匹配AndroidManifest.xml文件中指定的操作,它将不会被执行。

操作测试基本上是由项目清单文件中提供的信息来执行的一个测试。所有的操作组件都在<intent-filter>标签内定义,然后通过匹配来执行意图。在下面的截图中,你可以看到操作测试时intent-filter标签的样子:

操作测试

在前一个截图中给出的代码中,intent-filter标签内列出了三个操作。这些操作测试将由 Android 操作系统确定,如果传入的意图能够执行这些操作。在前面的代码中列出了三个测试,你可以在Action部分给出的表格中看到它们的描述。需要遵循以下两个条件:

如果intent-filter标签内没有写入操作,Android 操作系统将拒绝处理意图,因为没有可匹配的内容。

如果intent-filter标签包含多个操作,但传入意图中没有列出任何操作,意图仍将毫无问题地继续执行。

<action>的编写约定

在定义操作时,Android 遵循一些约定。需要记住,对于默认操作,我们必须使用 Android API 中给出的预定义常量。在 Android 库中,每个操作字符串都以ACTION_开头,然后是实际的操作名称,例如ACTION_MAINACTION_TIME_ZONE_CHANGEDACTION_WEB_SEARCH

同样,在提到需要在AndroidManifest.xml文件中包含此字符串的约定时,Android 遵循android.intent.action.STRING模式。在这个语句中,单词STRING被替换为要匹配的具体操作,但不包含单词ACTION。为了理解这个语句,以ACTION_MAIN常量为例。如果我们想在AndroidManifest.xml文件中提到它,我们不会写ACTION_,而是会这样写:

的编写约定

ACTION_EDIT也是同样的情况,它使 Android 能够编辑 URI 中给出的任何文档的引用。我们将在下面的截图中编写代码,以便在AndroidManifest.xml文件中容易理解:

的编写约定

当涉及到自定义动作时,动作是由用户定义而不是 Android API。有一个最佳实践是在编写之前始终以您的包名开始,以保持其唯一性。例如,如果您想创建一个名为HIDE_OBJECTS的动作,您必须在 XML 文件中编写如下截图所示的代码:

为  编写约定

类别测试

为了通过类别测试,必须确保传入的意图类别与AndroidManifest.xml中的<category>标签内至少提到的一个类别相匹配。如果在不知道其中类别的情况下创建意图对象,它应该总是通过,无论清单文件中定义了哪些类别。

需要记住,如果我们想使用startActivity()方法在活动之间移动,那么愿意接收隐式意图的活动在AndroidManifest.xml文件中必须有一个默认类别,即CATEGORY_DEFAULT(如 Android API 中所提及)。

提示

这与为动作编写约定一样,类别应该写成android.intent.category.DEFAULT,在AndroidManifest.xml中不提CATEGORY_字符串。

然而,对于启动类别来说,这不是情况;它是一个例外。我们在启动活动标签中提到android.intent.category.LAUNCHER。类别测试的表示在下图中显示:

类别测试

在前一个截图给出的代码中,提到了两个类别。第一个类别是android.intent.category.DEFAULT,这是因为这个特定的活动已经准备好接收隐式意图。在清单文件中提到的另一个类别是android.intent.category.BROWSABLE,它使此活动能够浏览手机中的本地 Android 浏览器或其他用于浏览网站的应用程序。

设置启动活动

设置启动活动主要是类别的一部分。在这里,我们需要确保我们完全理解了与意图相比启动活动的异常情况。由于众所周知,启动活动是在应用程序首次启动后立即启动的活动,因此我们现在可以在此基础上深入了解其类别概念。如果已知活动将接收某些隐式意图,则使用DEFAULT类别;另一方面,LAUNCHER活动是在任何应用程序中首次启动的活动。

在这个意义上,没有一个启动活动可以同时是默认的。结论是,在AndroidManifest.xml中,没有任何活动可以同时具有android.intent.category.DEFAULTandroid.intent.category.LAUNCHER。在清单中呈现的启动活动看起来像下面截图中的代码:

<activity android:name="com.example.android.application.MyList"
  android:label="@string/title_my_list">
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER"/>
  </intent-filter>
</activity>

在前述截图给出的代码中,活动com.example.android.application.MyList是启动活动,它将在应用程序开始时生成一个列表。由于这是应用程序的主要入口点,我们在清单中提供了ACTION_MAIN作为动作。而你可以看到第二个标签,提供的类别名称为android.intent.category.LAUNCHER

数据测试

提到数据标签是为了便于对执行的活动采取行动。这就是为什么在<activity>标签内可以有多个数据标签的原因。标签包含有关特定 URI 或 MIME 媒体类型的信息。例如,一个活动可能包含如下截图所示的数据标签:

数据测试

在前述屏幕截图给出的代码中,意图过滤器包含两个数据标签。在每一个标签中,android:mimeType属性下给出的 MIME 媒体类型指定了活动支持特定操作的数据格式。video/avi值描述了.avi文件的视频格式,这是活动所支持的。同样,如果需要提及音频文件类型,我们可以使用audio/mpeg

我们也可以在视频或音频 MIME 类型后加上星号。例如,请看以下截图:

数据测试

这段代码与之前的相同,除了video/*audio/* MIME 类型。星号表示此活动支持它们所有可能的子类型。

现在,有一些要点我们需要确保:

  • 如果一个意图对象不包含有关 URI 的任何特定信息,它只会在AndroidManifest.xml文件中没有提供数据信息的情况下通过intent-filter标签。

  • 如果一个意图对象只包含 URI 但不包含数据 MIME 类型,只有当它与过滤器中指定的 URI 匹配并且没有为数据类型指定过滤器时,它才会被传递。

  • 如果一个意图对象只包含 MIME 类型,但不包含 URI,只有当它与过滤器中指定的 MIME 类型匹配并且没有为 URI 指定过滤器时,它才会被传递。

  • 当一个意图对象包含 URI 以及 MIME 类型时,只有当它们与AndroidManifest.xml中指定的意图过滤器对应值匹配时,它才会被传递。

标签的典型表示

<data>标签包含许多属性以使其信息完整。以下语法包含了可以在<data>标签中定义的所有属性,这将在处理意图时增加对活动的了解:

典型的标签表示

在前一个屏幕截图给出的代码中,有各种属性,它们都是可选的,但它们相互之间更为依赖。以下是可选属性的列表:

  • scheme

  • host

  • port

  • path

  • pathPrefix

  • pathPattern

现在我们来谈谈它们之间的相互依赖关系。如果在数据标签中没有提到方案,那么在此之后将没有有效的 URI。同样,如果没有定义主机元素,所有的路径标签和主机标签值将无效。

概述

在本章中,我们详细地查看了意图过滤器和意图对象。我们了解了意图对象的基本构建块,在其中我们在 Java 代码中定义元素,而在另一方面,意图过滤器让 Android OS 了解应用程序内部的活动。我们学习了intent-filters标签如何通过匹配传入的意图对象及其属性来工作。然后,它们决定是否应该执行意图。

我们还了解了动作、数据和类别以及它们是如何工作的。不同的数据、类别和动作如何在单个活动的不同意图过滤器中合并,以及如果存在多种过滤器选择,主要机制是什么。我们还研究了某些写作约定,在 Android Manifest 中编写启动活动的典型方式,以及当数据对格式的不同子类型有效时包含多少 MIME 类型。在下一章中,我们将看到意图如何与广播接收器一起使用,它们的实际示例以及可能因为它们而产生的问题类型。

第八章:广播意图

在上一章中,我们了解了意图过滤器以及这些过滤器如何向安卓操作系统提供有关不同活动、服务等的信息。我们还讨论了意图过滤器的工作原理以及它们如何将到来的意图对象与属性相匹配。本章还提供了关于动作、数据和类别测试的信息。

意图是安卓操作系统不同组件之间异步发送消息的方式。到目前为止,我们只学会了如何从一个组件向另一个组件发送和接收这些消息,即意图。但在我们讨论的每个示例中,我们都有关于意图接收者的信息,比如哪个活动或服务将接收意图并使用嵌入在意图中的数据。

在本章中,我们将扩展有关向多个广播接收器发送意图的知识。我们将学习安卓操作系统如何广播意图,以及这些广播意图是如何被接收的。

本章节包括以下主题:

  • 在安卓操作系统中的广播

  • 安卓操作系统中的广播意图

  • 安卓操作系统中的系统广播

  • 使用安卓操作系统的不同系统广播

  • 检测电池电量低广播

  • 检测屏幕开启/关闭广播

  • 检测手机重启完成广播

  • 发送/接收自定义广播意图

提示

要理解本章及后续章节,需要掌握第二章安卓意图简介和第三章意图及其分类中讨论的意图概念和意图结构。如果你对这些基础概念不熟悉,我们建议你阅读第三章意图及其分类和第四章移动组件的意图,以便继续学习。

在安卓操作系统中的广播

任何运行 Android 操作系统的智能手机在特定时间都有很多服务和动作在执行。这些服务和动作可能在前景或后台运行。那么这里我们可能会想,这些服务和动作实际在做什么呢?答案非常简单。这些服务和动作在等待某些事件的发生,或在后台执行一些长时间的操作,或与 Android 操作系统的其他组件进行通信等等。你可能会想知道这些组件如何在事件发生时进行监听,或者它们如何在后台与其他组件进行通信,尤其是当用户不能直接与应用程序交互时。在 Android 操作系统中,这类任务是通过广播来完成的。Android 操作系统不断地广播有关不同动作的信息,例如是否连接了电源和是否开启了 Wi-Fi。我们作为开发者,在我们的应用程序中使用这些广播信息,使我们的应用程序更具互动性和智能。在下一节中,我们将了解 Android 操作系统如何广播不同的信息。

广播意图

广播意图是通过调用任何Activity类的sendBroadcast()sendStickyBroadcast()sendOrderedBroadcast()方法来广播的Intent对象。这些广播意图为不同的应用程序组件之间提供了一个消息和事件系统。此外,Android 操作系统还使用这些意图来通知感兴趣的应用程序关于系统事件,比如电量低或者是否插入了耳机。要创建广播意图的实例,我们必须在其中包含一个动作字符串。动作字符串用于标识广播意图,它是唯一的。这个动作字符串通常使用 Java 包名格式。

在下面的代码片段中,我们将创建一个广播意图的实例并将其广播出去:

广播意图

在前面的代码中可以看到,并没有一个名为BroadcastIntent的特殊类。它只是一个普通的Intent对象。我们曾在startActivity()startService()等方法中使用过这些Intent对象。这次我们将这些Intent对象传递给了Activity类的sendBroadcast()方法。我们通过调用setAction()方法来设置其动作字符串。如前所述,在setAction()方法中我们使用了包名格式。为了广播任何意图,我们使用了sendBroadcast()方法。这个方法可以广播任何给定的意图。记住,这个方法调用是异步的,会立即返回。你不能从任何接收器中得到任何结果,接收器也不能中止任何广播意图。感兴趣的接收器会将意图的动作字符串与它们自己的动作字符串匹配,如果匹配,这些接收器将被执行。

注意

从现在开始,在本章中我们将使用关键词广播广播们,而不是广播意图

Android 系统内置的广播

Android OS 包含不同类型的广播。Android OS 不断广播这些意图,以通知其他应用程序系统中的各种变化。例如,当设备电量低时,Android OS 会广播包含低电量信息的意图;对这一信息感兴趣的应用程序和服务在接收到后会执行相应的操作。这些广播在 Android OS 中是预定义的,我们可以在应用程序中监听这些意图,使我们的应用更具交互性和响应性。

提示

你可以在名为broadcast_actions.txt的文本文件中找到所有可能的广播列表。该文件存储在Android文件夹下的SDK文件夹中。

<ANDROID_SDK_HOME>/platforms/android-<PLATFORM_VERSION>/data/broadcast_actions.txt

下表展示了一些 Android OS 广播及其行为描述的列表:

广播意图动作 描述
android.intent.action.ACTION_POWER_CONNECTED 当手机连接到电源时,会广播此意图。
android.intent.action.ACTION_POWER_DISCONNECTED 当手机从任何电源断开时,会广播此意图。
android.intent.action.BATTERY_LOW 当手机电量低时,会广播此意图。
android.intent.action.BOOT_COMPLETED 当手机启动完成时,会广播此意图。
android.intent.action.DEVICE_STORAGE_LOW 当手机设备存储空间不足时,会广播此意图。
android.intent.action.NEW_OUTGOING_CALL 当新的外拨电话开始时,会广播此意图。
android.intent.action.SCREEN_OFF 当手机屏幕关闭时,会广播此意图。
android.intent.action.SCREEN_ON 当手机屏幕打开时,会广播此意图。
android.net.wifi.WIFI_STATE_CHANGED 当手机的 WIFI 状态改变时,会广播此意图。
android.media.VIBRATE_SETTING_CHANGED 当手机的振动设置改变时,会广播此意图。
android.provider.Telephony.SMS_RECEIVED 当手机收到短信时,会广播此意图。

正如我们前面的表格中所看到的,Android OS 通过发送广播,不断通知不同的应用程序关于设备状态的多种变化。我们可以监听这些变化或广播,并执行自定义操作,使我们的应用更具响应性。

注意

你可能已经注意到,一些前面的意图,如android.provider.Telephony.SMS_RECEIVED,并不包含在SDK文件夹中的列表中。这些意图在 Android 中不受支持,并可能在任何未来的平台版本中有所变动。开发者在他们的应用中使用这些不受支持的隐藏功能时应谨慎。

到目前为止,我们只讨论了广播,但还没有在实际例子中使用它们。在下一节中,我们将开发一些示例,在这些示例中,我们将监听一些 Android 操作系统的预定义广播,并根据情况进行操作。

检测设备的低电量状态

在本节中,我们将实现一个当手机电量低时会显示警报消息的小应用程序。现在,让我们开始第一个示例的开发。但是,为了开始这个示例,你需要构建一个 Android 项目。你可以使用 Android Studio 或 Eclipse IDE(根据你的方便),但如果是 Eclipse,请确保你已经正确安装了 JDK、ADT 和 Android SDK 及其兼容性。如果你不知道这些 IDE 之间的区别,可以参考本书的第一章《理解 Android》。按照这些步骤将帮助你创建一个带有一些预定义文件和文件夹的完整 Android 项目。

创建一个空的 Android 项目后,我们需要修改两个文件:一个主活动文件和一个清单文件。同时我们也添加了一个接收器文件。现在,让我们详细看看这些文件。

BatteryLowReceiver.java 文件

由于我们正在开发一个低电量警报应用,我们首先要做的是检测低电量。为此,我们需要创建一个BroadcastLowReceiver类,它将监听低电量广播。以下代码显示了接收器文件的实现:

BatteryLowReceiver.java 文件

如前述代码所示,我们从BroadcastReceiver类扩展了我们的类,并覆盖了onReceive()方法。当接收到任何广播时,将调用此方法。我们首先要做的是检查这个意图是否是低电量意图或其他广播。为此,我们检查意图的动作字符串是否与标准的 Android 动作Intent.ACTION_BATTERY_LOW相匹配。如果结果为true,则意味着设备的电量低,我们需要执行自定义操作。

接下来,我们创建一个线程,在其中传递一个匿名Runnable对象。我们覆盖了run()方法,在这个方法中,我们使用AlertDialog.Builder接口创建一个AlertDialog实例。我们设置详细信息,比如警报的标题和消息,然后显示它。

你可能会好奇为什么我们要创建一个线程来显示警报。我们本可以在不使用线程的情况下显示警报。需要注意的是,广播接收器运行的时间非常短,大约只有 4 毫秒。开发者在在接收器中执行操作时应该非常小心。从广播接收器中创建线程来执行创建警报、启动活动和服务的操作是一种良好的实践。

现在,我们的 BatteryLowReceiver 类已经准备好了。但是,这个接收器是如何被触发的,这个类如何从 Android 操作系统中接收低电量广播呢?这些问题将在下一节中解释。现在让我们详细看看我们的活动文件。

BatteryLowActivity.java 类

这个类代表了应用程序的主要活动,这意味着每当应用程序启动时,这个活动将首先被启动。以下代码展示了我们活动文件的实现:

BatteryLowActivity.java 类

与往常一样,我们从 Activity 类扩展了我们的类。然后,我们覆盖了活动的 onCreate() 方法。我们创建了一个 IntentFilter 实例,并在其构造函数中传递了 Intent.ACTION_BATTERY_LOW 动作字符串。你可以在第七章,意图过滤器中了解更多关于意图过滤器的信息。之后,我们创建了 BatteryLowReceiver 类的一个实例。最后,我们调用 registerReceiver() 方法,并在其中传递我们的接收器和过滤器对象。这个方法告诉 Android 操作系统,我们的应用程序对低电量广播感兴趣。这样我们就可以监听低电量广播。这里需要注意的是,当调用 registerReceiver() 方法时,开发人员有责任在应用程序不再对低电量广播感兴趣时调用 unregisterReceiver() 方法。如果开发人员没有注销它,无论应用程序是打开还是关闭,它都会监听低电量广播并相应地采取行动。

这对内存和应用程序的优化可能是有害的。我们可以在 Activity 类的 onDestroy()onPause()onStop() 回调中调用 unregisterReceiver() 方法,如下面的代码片段所示:

BatteryLowActivity.java 类

AndroidManifest.xml 文件

开发人员也可以在 AndroidManifest.xml 文件中注册接收器。在清单文件中注册接收器的优点是,开发人员无需通过调用 unregisterReceiver() 方法手动注销它们。Android 操作系统会自行处理这些接收器,开发人员无需再为此担心。下面是我们 AndroidManifest.xml 文件的代码实现,其中注册了我们的低电量接收器:

AndroidManifest.xml 文件

在前面的代码中,您可以看到我们在<application>标签内使用了<receiver>标签来注册我们的广播接收器。我们在<receiver>标签的android:name属性中插入了BatteryLowReceiver的完整包名,作为接收器的名称。正如我们在活动文件中通过创建IntentFilter类实例来设置意图过滤器动作一样,我们在<intent-filter>标签内嵌入了动作名为android.intent.action.BATTERY_LOW的意图过滤器。这个意图过滤器将告诉 Android 操作系统,接收器对设备的低电量状态信息感兴趣。

注意

开发者应注意,注册接收器应该只采用一种方法;要么从活动中调用registerReceiver()方法,要么从他们的AndroidManifest.xml文件中注册。使用AndroidManifest.xml文件来注册应用程序的BroadcastReceiver是一个好习惯。

当我们运行应用程序时,我们会看到一个空白屏幕,因为我们没有为活动设置任何布局。但是当手机电量低时,手机上会显示一个警告框。以下截图展示了来自我们BatteryLowReceiver类的警告框:

AndroidManifest.xml 文件

检测手机的屏幕开启/关闭状态

几乎在所有的 Android 手机中,我们在接听电话时都注意到了一个非常有趣的功能;我们可以看到屏幕会开启或关闭。此外,您可能已经观察到,当您将手机靠近耳朵时,屏幕会关闭,而当你从耳朵旁移开并手持手机时,屏幕会自动开启。这是智能手机的有趣行为。假设我们想要开发一个应用程序,每当屏幕开启时,我们都想开启扬声器模式,以便与我们在一起的其他人可以听到并参与电话对话。而当我们再次将其放在耳朵上,屏幕关闭时,我们想要关闭扬声器模式。下图展示了这个应用程序的概念:

检测手机的屏幕开启/关闭状态

现在,让我们在以下示例中开发这样的应用程序。首先从您喜欢的 IDE 中创建一个 Android 项目。然后,我们首先需要检测屏幕是否已开启或关闭。为了检测这一点,我们将实现我们自定义的BroadcastReceiver类。让我们在下一节中实现我们的广播接收器类。

ScreenOnOffReceiver.java文件

ScreenOnOffReceiver.java文件表示我们用于检测手机屏幕开启/关闭状态的定制广播接收器。以下代码实现展示了我们的屏幕开启/关闭检测接收器:

ScreenOnOffReceiver.java 文件

与之前的示例一样,我们的ScreenOnOffReceiver类从BroadcastReceiver类扩展而来,并覆盖了onReceive()方法。当我们的应用程序接收到任何广播意图时,将调用此方法。我们的应用程序首先通过将意图动作与Intent.ACTION_SCREEN_ONIntent.ACTION_SCREEN_OFF常量进行比较,来检查它是否是屏幕开启/关闭的意图。记住,在之前的示例中,我们只监听一个广播意图。然而在本例中,我们监听两个广播意图:一个用于屏幕开启,另一个用于屏幕关闭。

在 Android 手机中,屏幕不仅在通话期间会开启/关闭。当手机锁定/解锁时,屏幕也会开启/关闭。因此,在设置我们的扬声器开启/关闭之前,我们必须检查当前是否正在通话。我们可以通过检查AudioManager的模式来检测它。如果模式是AudioManager.MODE_IN_CALL,那就意味着我们当前正处于来电或去电的通话中。一旦我们确认了通话模式状态,那么我们就可以设置扬声器的开启/关闭。我们使用AudioManager.setSpeakerphoneOn(boolean)方法来实现这一目的。

到目前为止,我们已经实现了我们的接收器。但我们还没有注册这些接收器。记得在前一个示例中,我们使用了两种方法来注册我们的自定义广播接收器:一种是从活动类中使用registerReceiver()方法,另一种是从AndroidManifest.xml文件中。让我们选择后者,即使用AndroidManifest.xml文件来注册我们的接收器。

AndroidManifest.xml 文件

与前面的示例一样,我们将在该清单文件中注册我们的ScreenOnOffReceiver广播接收器。需要注意的是,在之前电池电量低的应用示例中,我们只为一个过滤器注册了接收器,即手机的低电量状态。然而,在本例中,我们监听两个状态过滤器:屏幕开启和屏幕关闭。但我们只实现了一个广播接收器。下面让我们看看如何在以下AndroidManifest.xml文件的代码实现中,用两个意图过滤器注册一个接收器:

AndroidManifest.xml 文件

在前面的代码中,我们可以看到,我们在<application>标签内放置了<receiver>标签,以注册我们的接收器。同时,需要注意的是,这次我们两次使用了<intent-filter>标签,其中嵌入了两种不同的动作:一个是android.intent.action.SCREEN_ON,另一个是android.intent.action.SCREEN_OFF。你可以在第七章《意图过滤器》中了解更多关于多个意图过滤器的内容。这两个意图过滤器连同我们在AndroidManifest.xml文件中嵌入的接收器一起,将我们的ScreenOnOffReceiver广播接收器注册到 Android 操作系统中,监听手机屏幕开启和关闭状态的变化。

检测手机的重启完成状态

许多安卓应用程序在执行多项任务和操作时会在后台运行服务。例如,一个天气应用会通过后台服务在固定时间间隔后持续检查天气情况。但你有没有想过,当你重启手机或电池耗尽导致手机重启后,这些服务是如何在重启后再次开始运行的?在本节中,我们将了解如何实现这一点。

当一部安卓手机成功重启后,安卓操作系统会广播一个意图,通知其他应用程序重启已完成。然后这些应用程序会再次启动它们的后台服务。在本节中,我们将创建一个监听重启完成广播的应用程序,并从中启动我们的测试服务。

让我们在任何 IDE(如 Eclipse 或 Android Studio)中创建一个空的安卓项目。像往常一样,我们首先实现我们的广播接收器类。

PhoneRebootCompletedReceiver.java 文件

PhoneRebootCompletedReceiver.java 类表示我们的重启完成广播接收器文件。以下代码展示了该文件的实现:

PhoneRebootCompletedReceiver.java 文件

你可以在前面的代码中看到,我们没有做任何新的操作。我们的类是从BroadcastReceiver类扩展而来的。然后,我们检查意图的Intent.ACTION_BOOT_COMPLETED动作是否为真。如果是,我们通过调用Context.startService()方法来启动我们的临时服务。现在,让我们在下一节中看看TempService类的作用。

TempService.java 文件

TempService.java 类表示我们将在安卓系统启动完成后开始运行的服务。

注意

在 Android 3.0 中,用户至少需要启动一次应用程序,之后应用程序才能接收到android.intent.action.BOOT_COMPLETED的广播。

以下代码展示了我们TempService类的实现:

TempService.java 文件

与任何常规服务类一样,我们的类是从Service扩展而来的。我们重写了两个方法:onBind()onStartCommand()。在onStartCommand()方法中,我们将通过调用Toast.makeText()方法并传入"Service started"文本来显示一个提示框。当我们的手机启动完成后,将显示这个提示框。我们可以在该方法中实现我们自己的自定义操作。

现在,我们还需要通知安卓操作系统,我们的应用程序想要监听 Boot Completed 广播。与之前的程序一样,我们将在AndroidManifest.xml文件中注册我们的接收器。下一节我们将看到这一点。

AndroidManifest.xml 文件

AndroidManifest.xml 文件通知安卓操作系统我们的应用程序想要监听 Boot Completed 广播。以下代码展示了该清单文件的实现:

AndroidManifest.xml 文件

与之前的示例应用几乎相同。我们使用 <application> 标签内的 <receiver> 标签注册了我们的接收器,并带有 android.intent.action.BOOT_COMPLETED 动作的意图过滤器。我们还通过使用 <application> 标签内的 <service> 标签注册了 TempService。必须注意,Boot Completed 广播需要用户授予 android.permission.RECEIVE_BOOT_COMPLETED 权限。我们可以通过添加 android:name 属性设置为 android.permission.RECEIVE_BOOT_COMPLETED<uses-permission> 标签,请求用户授予此权限。这样,当手机重启时,我们就可以启动自定义服务了。

发送和接收自定义广播

到目前为止,我们只接收广播。我们实验过的所有意图都是 Android 系统广播。在本节中,我们将讨论自定义广播。我们将了解如何向其他应用程序发送我们自己的自定义广播,以及其他应用程序如何监听我们的自定义广播意图。

在下一节中,我们将创建一个示例,该示例将向其他应用程序发送自定义广播。现在让我们为应用程序创建活动和布局文件。

activity_main.xml 布局文件

activity_main.xml 文件表示我们活动的布局文件。以下代码展示了清单文件的实现:

activity_main.xml 布局文件

如您在布局文件中所见,我们放置了一个带有 ID btnSendBroadcastIntent 的按钮。我们将在活动文件中使用此按钮向其他应用程序发送广播。现在让我们看看活动文件。

MainActivity.java 文件

MainActivity.java 文件是我们应用程序的主要启动点。此活动将使用 activity_main.xml 布局文件作为其视觉部分。以下代码展示了文件的实现:

MainActivity.java 文件

你可以从前面的代码中看到,我们通过调用findViewById()方法从我们的布局文件中获取了Button对象。然后我们设置了它的OnClickListener()方法,并在重写的onClick()方法中执行了我们的主要操作,即向其他应用程序发送广播。我们创建了一个Intent对象,并通过调用Intent.setAction()方法设置其动作字符串。需要注意的是,这次我们定义了自己的自定义动作值,即com.packt.CustomBroadcast字符串。创建自己的自定义广播接收器时,我们应该遵循包命名约定。最后,我们使用该意图进行广播,通过调用Activity类的sendBroadcast()方法。这就是我们的自定义广播意图发送到 Android 操作系统和其他应用程序的方式。现在,所有监听这种类型广播的应用程序和接收器都将接收到它,因此可以执行它们的自定义操作。在下一节中,我们将实现我们的自定义广播接收器类,它将接收这种类型的意图,并通过显示提示来通知用户。

CustomReceiver.java 文件

CustomReceiver.java 文件代表我们的自定义广播接收器类,它将接收我们的自定义广播。这个类可以在这个应用程序中,也可以在任何其他想要监听这种自定义类型广播的应用程序中。与之前的所有示例一样,这个类将相同,并且从BroadcastReceiver类扩展而来。与之前示例的唯一区别在于,我们之前使用的是 Android 操作系统的标准预定义常量动作字符串来检测系统广播,但在这个示例中,我们正在监听具有自定义动作字符串设置的自定义广播。以下是文件实现的代码:

public class OurCustomReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    // TODO Auto-generated method stub
    if (intent.getAction() == "com.packt.CustomBroadcast") {
      Toast.makeText(context, "Broadcast Intent Detected.",
        Toast.LENGTH_LONG).show();
    }
  }
}

从前面的代码中可以看出,我们并没有做任何你还不熟悉的新操作。我们的类是从BroadcastReceiver派生出来的,并重写了onReceive()方法。然后我们将意图的 action 字符串与我们的自定义字符串com.packt.CustomBroadcast动作进行比较。如果为true,我们将显示一个提示“检测到广播意图”。我们可以在该方法中执行自定义操作。最后,我们必须注册这个接收器,以便 Android 操作系统可以通知我们的应用程序关于广播的信息。

AndroidManifest.xml 文件

与往常一样,AndroidManifest.xml 告诉 Android 操作系统我们的应用程序正在监听自定义广播。以下是文件实现的代码:

AndroidManifest.xml 文件

你可以看到,我们以与注册 Android 系统广播接收器相同的方式注册了我们的自定义广播接收器。现在,当我们运行这个应用程序时,我们会看到一个名为发送广播意图的按钮。当我们点击这个按钮时,我们的自定义广播将在 Android 操作系统中广播。由于我们也为此自定义意图创建了一个接收器,因此我们也将接收到这个意图。在接收到意图时,我们的自定义接收器将显示一个提示消息。以下屏幕截图展示了这个应用程序的执行情况:

AndroidManifest.xml 文件

总结

在本章中,我们讨论了广播。我们还了解了 Android 操作系统的不同系统广播意图,例如电量低、电源连接和开机完成。我们也学习了如何通过注册自定义接收器来接收这些广播,以及如何在那些接收器中执行我们自己的自定义操作。最后,我们学习了如何发送我们自己的自定义广播以及接收这些自定义意图。

在下一章中,我们将探索两种特殊的意图类型:IntentServicePendingIntent。我们还将学习如何使用这些意图以及通过这些意图可以实现什么功能。

第九章:意图服务与待定意图

从本书一开始,我们就一直在研究意图可以执行的不同任务,以促进 Android 及其类型。我们已经看到意图可以帮助在活动之间导航。它们还用于在它们之间传输数据。我们了解到如何设置过滤器以验证传入的意图是否可以通过组件测试,最后我们学习了意图在广播接收器中的作用。在本章中,我们将深入了解如何使用意图服务和待定意图进行便捷操作。

在本章中,我们将探讨以下主题:

  • 意图服务是什么?

  • 意图服务的使用和实现

  • 待定意图是什么?

  • 待定意图的使用和实现

  • 概括

意图服务

意图服务是一个简单的服务类型,用于处理与主线程无关的异步工作。如果客户端通过startService(Intent intent)方法发送请求,就可以这样做。这个新任务将由工作线程处理,并在没有工作可做时停止。

注意

意图服务继承了 Android API 中的Service

意图服务用于卸载工作线程,使其不会成为瓶颈。它有助于使事情与主应用程序线程分开进行。需要注意的是,尽管它独立于主线程工作,但一次只能处理一个请求。

意图服务是从应用程序的 UI 线程卸载工作到工作队列的最佳方式。无需为每个处理创建异步任务并管理它们。相反,你定义一个意图服务,使其能够处理你想要发送进行处理的数据,然后简单地启动服务。最后,你可以通过在意图对象中广播数据并将其从广播接收器捕获来将数据发送回应用程序,以便在应用程序中使用。

四个基础组件的比较

本节展示了 Android 开发中四个最重要的元素(服务、线程、意图服务和异步任务)之间的基本区别,包括意图服务。

最佳使用情况

服务、线程、意图服务和异步任务的最佳情况场景如下表所示:

最佳情况场景
服务 当任务不是很长,并且与主线程无关时
线程 当有一个长任务需要执行,并且需要并行完成多个任务时
意图服务 当有不需要主线程干预的长任务,并且需要回调时
异步任务 当有需要与主线程通信的长任务,并且需要并行工作

注意

如果 Intent Service 需要与主线程通信,我们需要使用 Handler 或广播 Intent。

触发条件

下表讨论了 Service、Thread、Intent Service 和 Async Task 之间的触发条件差异:

Service Thread Intent Service Async Task
触发方式 使用onStartService()方法 使用start()方法 使用 Intent 使用execute()方法
触发原因 可以从任何线程调用 可以被任何其他线程调用和运行 只能从主线程调用 只能从主线程调用
运行环境 可以从主线程调用 在自己的线程上运行 在独立的工作线程上运行 在独立的工作线程上运行,尽管主线程的方法可能会在中间运行
限制条件 在某些场景下可能会阻塞主线程 需要手动处理,代码可能不易理解 不能同时处理多个任务,所有任务都在同一个工作线程上运行 只能有一个任务实例,不能在循环中运行

Intent Service 的使用和实现

从本章的前几部分,我们清楚地了解了 Intent Service 的定义以及它与 Thread、Async Task 和 Service 之间的基本区别。现在是开始实现和使用 Intent Services 的时候了。为此,我们将从一个示例开始,这将帮助我们学习如何从 Intent Service 生成假通知。

从 Intent Service 生成假通知

在这个示例中,我们将学习 Intent Service 在通知栏上生成通知的使用方法。该示例还将解释onHandleIntent()方法的使用,该方法用于实现 Intent Service 的所有功能,包括发送广播和通知到通知栏。

此外,在本节末尾,你将了解它与 Thread 或其他之前提到的 Android 定义方法之间的区别。完成这段代码后,启动活动,你将看到以下屏幕:

从 Intent Service 生成假通知

启动 Activity 将显示“Hello World”屏幕

提示

注意:请记住,在这个示例中,我们将不会介绍项目中使用的完整文件集。由于这是本书的最后一章,我们假设您已经掌握了关于 XML 文件、资源和布局的 Android 开发基础知识。

从 Intent Service 生成假通知

显示进度通知的通知面板

代码概览

该示例指的是在需要向通知栏发送关于特定事件进展或信号的情景下使用 Intent Service。

package com.app.intentservice;

import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;

public class CustomIntentService extends IntentService {

  private static final int NOTIFICATION_ID=1;
  NotificationManager notificationManager;
  Notification notification;

  public static final String ACTION_CUSTOM_INTENT_SERVICE = "com.app.intentservice.RESPONSE";
  public static final String ACTION_MY_UPDATE ="com.app.intentservice.UPDATE";
  public static final String EXTRA_KEY_IN = "EXTRA_IN";
  public static final String EXTRA_KEY_OUT = "EXTRA_OUT";
  public static final String EXTRA_KEY_UPDATE = "EXTRA_UPDATE";
  String activityMessage;

  String extraOut;

  public CustomIntentService() {
    super("com.app.intentservice.CustomIntentService");
  }

  @Override
  protected void onHandleIntent(Intent intent) {

    //get input
    activityMessage = intent.getStringExtra(EXTRA_KEY_IN);
    extraOut = "Hello: " +  activityMessage;

    for(int i = 0; i <=10; i++){
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
    // TODO Auto-generated catch block
        e.printStackTrace();
      }

      //send update 
      Intent intentUpdate = new Intent();
      intentUpdate.setAction(ACTION_MY_UPDATE);
      intentUpdate.addCategory(Intent.CATEGORY_DEFAULT);
      intentUpdate.putExtra(EXTRA_KEY_UPDATE, i);
      sendBroadcast(intentUpdate);

      //generate notification
      String notificationText = String.valueOf((int)(100 * i / 10)) + " %";
      notification = new NotificationCompat.Builder(getApplicationContext())
      .setContentTitle("Progress")
      .setContentText(notificationText)
      .setTicker("Notification!")
      .setWhen(System.currentTimeMillis())
      .setDefaults(Notification.DEFAULT_SOUND)
      .setAutoCancel(true)
      .setSmallIcon(R.drawable.ic_launcher)
      .build();

      notificationManager.notify(NOTIFICATION_ID, notification);
    }

    //return result
    Intent intentResponse = new Intent();
    intentResponse.setAction(ACTION_CUSTOM_INTENT_SERVICE);
    intentResponse.addCategory(Intent.CATEGORY_DEFAULT);
    intentResponse.putExtra(EXTRA_KEY_OUT, extraOut);
    sendBroadcast(intentResponse);
  }

  @Override
  public void onCreate() {
    // TODO Auto-generated method stub
    super.onCreate();
    notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
  }
}

深入理解

创建一个新项目并打开src文件夹。创建一个名为CustomIntentService.java的新的类文件,它是IntentService的子类。扩展IntentService类并覆盖onHandleIntent(Intent intent)方法。

在这一点上,你已经准备好实现自己的 Intent Service,负责将消息发送到通知栏,并以进度百分比条的形式更新它。现在,让我们通过以下步骤开始理解代码:

  1. 第一步是在onHandleIntent()方法中使用变量notificationManagernotification进行声明。还有一些其他的静态最终变量将在本项目中使用。它们是NOTIFICATION_IDACTION_CustomIntentServiceACTION_MyUpdateEXTRA_KEY_INEXTRA_KEY_OUTEXTRA_KEY_UPDATE。还需要两个新的字符串变量以处理通知字符串,分别表示为activityMessageextraOut

  2. 这个IntentService的主要实现将在onHandleIntent()方法中进行,我们将在这里定义包括消息到通知栏和广播消息的工作。

  3. onHandleIntent()方法开始时,通过intent.getStringExtra()方法获取额外的数据,并将其保存在msgFromActivity变量中,稍后该变量将发送到广播。

  4. 我们的主要目标是发送一个通知,显示从 0 到 100%的进度(一个假的计数器),并在通知栏中更新。为此,我们初始化一个for循环,该循环将从 0 进行到 10。在开始时,我们将调用Thread.sleep(1000),这将使线程休眠,在 1000 毫秒内不工作。

  5. 一旦线程休眠了一定时间,我们的假进度更新的第一个计数器就完成了。下一步是发送一个广播,其主要目的是提供更新。为了看到这一点,我们使用以下代码行:

    //send update 
    Intent intentUpdate = new Intent();
    intentUpdate.setAction(ACTION_MyUpdate);
    intentUpdate.addCategory(Intent.CATEGORY_DEFAULT);
    intentUpdate.putExtra(EXTRA_KEY_UPDATE, i);
    sendBroadcast(intentUpdate);
    

    快速了解我们如何发送广播:创建一个新的意图对象,并为其提供一个名称和intentUpdate的动作;由于这是一个自定义动作,所以给它一个ACTION_MyUpdate的名称,你可以在代码中看到;定义它的类别,这也是一个自定义类别;放入计数器信息(显示循环当前计数器的变量)并发送此意图的广播。

  6. 下一步是将通知发送到通知栏。以下代码可以在上一个示例中看到:

    //generate notification
    String notificationText = String.valueOf((int)(100 * i / 10)) + " %";
    myNotification = new NotificationCompat.Builder(getApplicationContext())
    .setContentTitle("Progress")
    .setContentText(notificationText)
    .setTicker("Notification!")
    .setWhen(System.currentTimeMillis())
    .setDefaults(Notification.DEFAULT_SOUND)
    .setAutoCancel(true)
    .setSmallIcon(R.drawable.ic_launcher)
    .build();
    
    notificationManager.notify(MY_NOTIFICATION_ID, myNotification);
    

    这段代码将notificationText的值设置为循环的当前计数器,并将其转换为百分比;通过调用NotificationCompat.Builder()(这基本上是 Android SDK 中描述的构建器模式)创建一个新的通知,并为其提供应用程序上下文,设置其标题内容、文本、提醒、出现时间以及其他一些属性。最后,你需要调用notificationManager.notify()以在通知栏中显示它。

  7. 最后一步是发送另一个广播作为确认,它的流程与之前的广播相同,你可以在以下代码中看到:

    //return result
    Intent intentResponse = new Intent();
    intentResponse.setAction(ACTION_MyIntentService);
    intentResponse.addCategory(Intent.CATEGORY_DEFAULT);
    intentResponse.putExtra(EXTRA_KEY_OUT, extraOut);
    sendBroadcast(intentResponse);
    
  8. 代码中显示的最后一步是覆盖onCreate()方法。你可能已经注意到我们没有创建一个新的通知管理器对象,这肯定会出错。因此,为了创建新对象,我们将通过使用通知管理器getSystemService (Context.NOTIFICATION_SERVICE)获取 Android 的系统服务。

注意

这个示例还需要一个广播接收器。如果你还没有关于它的想法,可以参考前面的章节。

再举一个例子。

上一个示例主要处理了在 Android 通知栏中实现通知的问题。它涵盖了 Intent 服务的实现,但没有涉及广播接收器的制作及其注册。在这个例子中,我们将学习如何使用 Intent 服务并将所有输入数据转换为大写,并将其广播回广播接收器。广播接收器的实现也是这个示例的一部分。

从示例开始,在你的开发环境中使用以下代码来实现它:

public class IntentServiceExampleTwo extends IntentService {
  private static final String TAG =IntentServiceExampleTwo.class.getSimpleName();

  public static final String INPUT_TEXT_STRING="INPUT_TEXT_STRING";
  public static final String OUTPUT_TEXT="OUTPUT_TEXT";

  /**
   *  initiate service in background thread with service name
   */
  public IntentServiceExampleTwo() {
    super(IntentServiceExampleTwo.class.getSimpleName());
  }

  @Override
  protected void onHandleIntent(Intent intent) {
    Log.i(TAG,"onHandleIntent()");

    String data =intent.getStringExtra(INPUT_TEXT_STRING);
    Log.d(TAG,data);

    data=data.toUpperCase();

    SystemClock.sleep(4*1000);

    Intent stringBroadCastIntent =new Intent();

    stringBroadCastIntent.setAction(TextCapitalizeResultReceiver.ACTION_TEXT_CAPITALIZED);

    stringBroadCastIntent.addCategory(Intent.CATEGORY_DEFAULT);

    stringBroadCastIntent.putExtra(OUTPUT_TEXT, data);

    sendBroadcast(stringBroadCastIntent);
  }

}

这几乎与之前的第一个示例中的实现相同。在这个例子中,展示了onHandleIntent()方法的工作原理,其中进行以下步骤:

  1. onHandleIntent()方法中,你可以看到的第一步是从传入的意图中获取数据并将其保存到一个变量中。变量data包含我们将转换为大写的传入数据。

  2. 第二步是将数据记录到 LogCat 中,这是通过使用Log.d(String, String)方法获得的。第一个参数是TAG,通常是全局声明的类名,以便任何方法都可以使用它。这个类名对于将你的消息与其他消息区分开来很重要(使其易于阅读)。第二个参数是消息字符串,用于显示过程中的任何数据,以便开发者在运行时可以看到其值。

  3. 第三步是将这些数据转换成大写。这将有助于在广播的意图中反映更改。将其重新保存回数据变量中。

  4. 其余步骤与之前的示例相同,其中创建意图对象,定义类别和动作,将数据作为额外内容放入并发送给接收器进行广播。

下一步是设置将从sendBroadcast()方法接收的接收器。为此,请查看以下代码:

public class UpperCaseReceiver extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
    TextView textViewResult = (TextView)findViewById(R.id.receiving_text_view);
    String result =intent.getStringExtra(ExampleIntentService.OUTPUT_TEXT);
    textViewResult.setText(result);
  }

};

之前的代码是示例中关于如何创建广播接收器的部分,这将接收广播并设置 textView。你可以在代码中看到,覆盖了 onReceive() 方法,该类正在扩展广播接收器。在 onReceive() 方法内部,通过 intent.getStringExtra() 方法获取字符串并将其保存在结果字符串中。这个字符串将被用来设置 textView 的文本,以便你可以看到它们在 textView 中的反映。

接下来,下一步是使用 Intent Service 注册这个接收器。这将在与 Intent Filter 相关联的活动内完成,以便它可以产生效果。以下代码段展示了这一过程:

private void registerReceiver() {

  IntentFilter intentFilter =new IntentFilter(UpperCaseResultReceiver.ACTION_TEXT_CAPITALIZED);

  intentFilter.addCategory(Intent.CATEGORY_DEFAULT);

  capitalCaseReceiver=new UpperCaseResultReceiver();

  registerReceiver(capitalCaseReceiver, intentFilter);
}

registerReceiver() 方法在你的活动中声明,它将从 onCreate()onResume() 方法中被调用,以便在启动或恢复活动时注册广播接收器。

  • Intent Filter 被声明并使用名为 intentFilter 的对象初始化。

  • intentFilter 对象被设置为默认。

  • 广播接收器的对象被初始化并通过调用 registerReceiver(Receiver, IntentFilter) 方法与 Intent Filter 注册。

在使用 Intent Filter 注册接收器之后,下一步是在你的活动中使用它。为此,请查看以下代码。这段代码可以位于任何事件内:

Intent textUpperCaseIntent = new Intent(MainActivity.this, ExampleIntentService.class);

textUpperCaseIntent.putExtra(ExampleIntentService.INPUT_TEXT, inputText);

startService(textUpperCaseIntent);

以传统方式初始化一个意图,给出你刚刚创建的 IntentService 类,并放入你想要转换为大写的输入文本。通过 Intent.putExtra(String, String) 方法给这个意图提供额外的数据。最后一步是使用这个意图启动服务。我们将使用 startService() 方法,而不是典型的 startActivity() 方法,因为在 Android 中我们使用 startActivity 来通过意图启动活动。

Pending Intents

Pending Intent 是一种允许其他应用(或称为外部应用)访问你的意图权限以运行预定义代码段的意图。这样,许多其他应用(如闹钟管理和日历)可以利用你的应用来执行它们的任务。

Pending Intents 不会立即运行;而是在其他活动需要它运行时才执行。Pending Intent 是系统维护的一个引用,以便在稍后的阶段使用。这意味着即使包含 Pending Intent 的应用结束了,另一个应用仍然可以使用该上下文,直到对该意图调用 cancel()

注意

要通过 Pending Intent 执行广播,请使用 PendingIntent.getBroadcast() 获取 Pending Intent。

Pending Intents 可以通过三种方法启动,分别是 getActivity(Context context, int requestCode, Intent intent, int flags)getBroadcast(Context context, int requestCode, Intent intent, int flags)getService(Context context, int requestCode, Intent intent, int flags)。为了了解 Pending Intents 在 Android 应用中的制作和使用方式,您可以继续阅读下一节,其中将介绍实现过程。

如何让 Pending Intents 工作?

本节介绍 Pending Intents 的工作原理及解释。为了更好地理解这一点,我们建议您阅读之前提到的定义,以便您能更好地理解。

在这个例子中,我们将向您展示如何制作一个应用程序,用户可以在 editText 字段(以秒为单位)中输入时间,之后闹钟将会响起,Android 的闹钟管理器将根据相应地播放闹钟。

为了进一步了解,请查看以下代码:

EditText text = (EditText) findViewById(R.id.editText1);

int i = Integer.parseInt(text.getText().toString());

Intent intent = new Intent(MainActivity.this, MyBroadcastReceiver.class);

PendingIntent pendingIntent = PendingIntent.getBroadcast( MainActivity.this, 234324243, intent, 0);

AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);

alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (i * 1000), pendingIntent);

Toast.makeText(MainActivity.this, "Alarm set in " + i + " seconds",

Toast.LENGTH_SHORT).show();

之前编写的代码片段可以插入到任何事件中,比如一个按钮,用于获取 EditText 字段中的输入值,并通过 Pending Intents 处理它。

  1. 从布局文件中获取编辑文本,并创建一个名为 text 的对象,该对象保存该小部件的当前状态。

  2. 整数变量 i 将保存编辑文本中的输入值,通过 text.getText().toString() 获取。

  3. 创建一个显式意图,将 BroadcastReceiver 类作为意图的目标类(我们将在完成此部分后创建)。

  4. 为了启动 Pending Intent,我们使用 PendingIntent.getBroadcast(Context context, int requestCode, Intent intent, int flags, int, Intent, int) 方法。关于这个方法的更多描述可以在 developer.android.com/reference/android/app/PendingIntent.html 找到。

  5. 通过在 getSystemService() 方法中传入 ALARM_SERVICE 来获取闹钟的系统服务,并将其指向一个 AlarmManager 对象。

  6. 使用存储在 i 中的值设置闹钟管理器的值,并给它一个 Pending Intent,这将帮助它启动(因为闹钟管理器是 Android 的一个服务)。

  7. alarmManager.set() 方法包含参数 int 类型的类型、long triggerMilliSec(在其中您取当前系统时间并将变量 i 转换为毫秒后相加)以及 Pending Intent。

  8. 制作一个吐司通知,以显示闹钟管理的成功完成。

下一步是创建您选择的广播接收器并实现该接收器。为此,创建一个广播接收器并重写 onReceive() 方法。查看以下代码:

@Override
public void onReceive(Context context, Intent intent) {
  Toast.makeText(context, "Alarm is ringing...",
  Toast.LENGTH_LONG).show();

  Vibrator vibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
  vibrator.vibrate(2000);
}

这个接收器有一个吐司提示,它会指示其报警状态。下一步是声明一个振动器对象,可以通过调用 context.getSystemService(Context.VIBRATOR_SERVICE) 方法来初始化。这个方法负责返回直接影响手机物理振动器的对象。最后一步是调用 vibrator.vibrate(int) 方法来启动振动。

注意

要使用振动器,你需要在清单文件中添加权限。你可以使用以下代码片段来完成这个操作:

<uses-permission android:name="android.permission.VIBRATE" />

最后,我们必须在 AndroidManifest.xml 文件中声明这个接收器,可以通过使用以下代码简单地完成:

<receiver android:name="MyBroadcastReceiver" >
</receiver>

上一个示例描述了待定意图(pending Intent)和广播接收器(Broadcast Receivers)一起使用的情况。

概括

本书的最后一章的概要可以作为本书的结论。在本章中,我们学习了如何实现 intentServicePendingIntents 以及它们最佳的应用场景。IntentService 的特性及其与 Android 中最常使用的三种功能——线程(Thread)、服务(Services)和异步任务(Async Tasks)的比较。此外,在本章中,实现了待定意图的示例,并解释了每一步。可以将本章视为意图在 Android 中高级版本或高级用法的介绍。需要记住的是,这些功能的使用可能并不常见,但在某些情况下,你必须让它们工作,因为没有其他解决方案。

posted @ 2024-05-23 11:07  绝不原创的飞龙  阅读(17)  评论(0编辑  收藏  举报