原文:
zh.annas-archive.org/md5/0C85816AE60A0FB2F72BC5A43007FCC3
译者:飞龙
前言
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()
方法进行简单的数据传输,通过Parcelable
和Serializeable
类对象发送自定义数据对象,以及 Android 系统中数据传输的一些场景。
第六章,使用意图访问 Android 功能,涵盖了 Android 操作系统中最常见的软件功能,如布局、显示、连接性、通信、可访问性、触摸和硬件支持。本章讨论了两个重要的AndroidManifest
标签,<uses-feature>
和<uses-permission>
,它们的使用,以及将这些标签与移动硬件组件和 Android OS 功能进行比较。本章提供了在 Android 应用程序中使用意图的实际示例实现,如拨打电话、发送短信/MMS 消息、确认消息送达、接收消息以及使用自定义布局发送通知。
第七章,意图过滤器,详细介绍了意图和意图过滤器,以及它们如何向 Android 操作系统提供关于应用内部存在的活动信息。本章还涵盖了过滤器测试的细节,例如动作测试、数据测试、类别测试,以及在使用意图时这些测试的便利性。
第八章,广播意图,涵盖了 Android 中的广播以及广播意图。本章讨论了 Android 操作系统的系统广播意图,例如电量低、电源连接/断开、启动完成以及耳机插入/拔出等,并附有一些意图的实际示例实现。此外,本章还介绍了自定义广播意图及其在各种情况下的实际应用示例。
第九章,意图服务与待定意图,涵盖了意图的一些最先进的话题,例如与常见的Thread
、Service
或AsyncTask
方法相比,使用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 系列。
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)及其文档的使用。
安卓工作室(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 的版本是 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 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 应用程序中的应用程序资源,如媒体文件、图片、用户界面布局、菜单、动画、颜色和字符串(文本);换句话说,你可以认为这是处理媒体文件的最智能方式。它包括许多子文件夹,如drawable
、drawable-ldpi
、drawable-mdpi
、drawable-hdpi
、drawable-xhdpi
、drawable-xxhdpi
、raw
、layout
、anim
、menu
和values
。
Drawable与 Android 项目中使用的图片直接相关。这是一种在项目中保存图片的智能方式。我们知道市场上存在各种支持 Android OS 的设备。为了区分这些设备,低分辨率的图片被放在ldpi
文件夹中,供低分辨率设备使用。同样,mdpi
文件夹适用于中等屏幕密度的设备,hdpi
适用于高密度设备,xhdpi
适用于超高密度设备,依此类推。
提示
放在这些 drawable 文件夹中的图片应该有唯一的名称,以便从代码中通过单一引用来访问它们。
同样,为了放置音乐和声音内容,我们使用raw
文件夹以便从代码中访问它们。除了音乐和声音之外,任何其他文件也可以放在raw
文件夹中(例如,JSON 文件)。anim
、values
、menus
和layout
文件夹也是如此,分别用于放置动画、值、自定义菜单和不同类型的布局。
XML 组件
在 Android 中,开发者需要使用 XML 来创建用户界面。布局、菜单、子菜单以及许多其他内容都是以不同的 Android 标签形式基于 XML 定义的。除了布局,你还可以以 XML 文件的形式存储字符串、颜色代码等许多其他内容。该组件支持维护应用程序的层次结构,使所有开发者易于理解。
让我们看一下一些最重要的 XML 文件,它们是任何 Android 应用程序的支柱。
布局文件夹
在res
文件夹内,有一个名为layout
的文件夹,其中包含了所有活动的布局。需要注意的是,这个文件夹也有一些扩展,就像 drawable 文件夹一样。layout-land
和layout-port
方法分别用于在横屏和竖屏模式下保持布局的良好组织。
提示
XML 还可以用于创建自定义 drawable,这些 drawable 可以在不同场景下作为图片使用。例如,可以使用 XML 制作自定义按钮的图片,它会在点击和未点击状态下呈现不同的 UI 行为。
前面的截图是 Android Studio 的界面,你可以看到一个activity_main.xml
文件,该文件用于描述一个活动的布局。有一些 Android 定义的 XML 标签用于RelativeLayout
和TextView
(阅读下面的信息框)。同样,还有一些其他标签供开发者使用,以便在布局中包含不同类型的控件。
注意
RelativeLayout
是一个布局,其中子元素按照相对位置进行放置。这个布局经常被 Android 移动开发者使用。
TextView
是用于显示文本(包括数字、字符串和可编辑内容)的视图之一。
菜单文件夹
Android 提供了不同类型的菜单,以便在活动中快速访问常用的功能。可用的不同菜单如下:
-
上下文菜单
-
选项菜单(带操作栏)
-
弹出菜单
-
自定义菜单
由于本章的重点有限,我们无法完全展开讨论各种菜单的功能并给出示例。然而,所有类型的菜单都基于 XML 文件,在这些文件中,使用 Android 定义的标签如<menu>
、<item>
和<group>
来引入应用程序中的菜单。以下截图供参考:
Android ICS 选项菜单在左侧,自定义弹出菜单在右侧
values
文件夹
values
文件夹包含各种 XML 文件,开发者在许多场景下都可以使用它们。这个文件夹中最常见的文件是styles.xml
和strings.xml
。style
文件包含与任何 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
文件中更改版本代码和版本名称。
引用组件
安卓应用程序的另一个基本组件是引用组件。简单来说,这个组件帮助基于 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 的活动生命周期管理。这个生命周期被称为活动生命周期。生命周期管理堆栈中的活动,并通过回调方法通知活动状态的变化。由于状态变化,活动会接收到诸如活动创建、活动销毁等不同类型的状态。开发者重写这些回调方法以执行相应状态变化所需的步骤。例如,当活动开始时,应加载必要的资源;当活动被销毁时,应卸载这些资源以获得更好的应用性能。所有这些回调方法在管理活动生命周期方面都扮演着关键角色。开发者可以选择不重写、重写部分或全部方法。
活动的基本状态
基本上,活动存在三种状态:Resumed
(恢复),Paused
(暂停),和Stopped
(停止)。当活动恢复时,它显示在屏幕上并获得用户的焦点。这个活动保持在后台堆栈的前台部分。当另一个活动开始并在屏幕上可见时,这个活动就会被暂停。这个活动仍然在前台任务中,仍然存活,但它没有获得任何用户焦点。也有可能是新活动部分覆盖了屏幕。在这种情况下,暂停活动的部分仍然会在屏幕上可见。当活动完全从屏幕上消失并被前台另一个活动替换时,它进入停止状态。在这个停止状态下,活动仍然存活,但在后台堆栈的背景部分。暂停状态和停止状态之间的区别在于,在暂停状态下,活动附着在窗口管理器上,但在停止状态下,它没有附着在窗口管理器上。
注意
在极端低内存的情况下,Android 系统可能会通过要求其完成或直接杀死进程来杀死任何暂停或停止的活动。为了避免这个问题,开发者应该在暂停和停止的回调中保存所有必要的数据,并在恢复回调中检索这些数据。
活动生命周期的回调方法
当任何活动的状态发生改变时,会有各种回调方法被调用。开发者在这些方法中执行必要的任务和操作,以提高应用的性能。为了展示活动生命周期的实际应用,我们将在本节中创建一个小的 Android 应用程序。以下是逐步操作的步骤:
-
启动Android Studio。
-
创建一个空项目,详细信息如下截图所示:
Android Studio 中的新建项目对话框
-
在项目的
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(); } }
-
在模拟器中运行项目,你会看到以下顺序在屏幕上打印提示:
-
活动创建
-
活动启动
-
活动恢复
-
活动暂停
-
活动停止
-
活动销毁
-
让我们看看之前提到的代码是如何工作的。
当你运行项目时,模拟器将按照之前给出的顺序在屏幕上显示所有的提示。在项目开始时,会创建一个活动,然后启动该活动。活动启动后,它会在屏幕上显示,并且模拟器会打印恢复。现在,我们通过按后退键返回,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 活动、服务、数据传输以及许多其他因素。我们将在以下列表中看到这一点:
-
从一个活动过渡到另一个活动
-
从一个活动向另一个活动传输数据
-
与 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 用于放置内容,并在其中提供状态栏通知。
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,你需要执行以下步骤:
-
创建一个新的 Android 项目,或者选择任意一个现有的你想要在其中实现 Android Intents 的项目。
-
打开你想要实现意图的源活动。
注意
提醒一下,当活动中有事件调用发生时,会调用意图。例如,点击按钮时,下一个活动应该出现。所以按钮点击就是事件。
-
实现以下代码以实现此结果:
//------------------------------------------------------------------------- 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>
-
运行项目,会出现一个按钮。点击按钮,通过意图导航到下一个活动。
指示应用从第一个活动到第二个活动的流程
理解流程
之前的代码分为五个部分;我们将逐一描述它们。请记住,这些部分指的是 Android 项目中的五个不同文件。考虑到你将在这个预定义项目中使用这些代码,我们将从新创建项目的角度来描述,以便使其更加详尽和清晰。
第一部分 – MainActivity.java
MainActivity.java
是在创建项目时生成的第一个类。由于它是一个 Android 活动,因此它带有一个onCreate
方法,该方法在活动创建后立即被调用(如第一章,理解 Android,Android 活动生命周期所述)。与此活动关联的布局名为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 中一个非常方便的选项,用于从活动中带回结果。我们将通过示例代码详细讨论这个特性。
通过示例理解
在这个例子中,我们将查看有两个活动的情况。第一个活动将作为启动活动。第二个活动将作为目标活动,它还将向第一个活动返回一些结果。第一个活动将捕获结果,并根据该代码决定第二个活动中完成了或失败的任务类型。最后,根据返回的结果,在第一个活动中显示一些对话框消息。
深入示例
再次,以下代码分为五部分。这是上一个示例的修改,其中描述了意图的正常使用。为了实现这个示例,请执行以下步骤:
-
创建一个新项目或打开任何现有项目,以便进行更改。
-
打开源活动,在其中实现意图。
-
实现以下代码:
//---------------------------------------------------------------- //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>
-
运行项目,会出现一个按钮。点击按钮,通过意图导航到下一个活动。
-
当意图成功执行后,新的活动将会出现。点击模拟器或你正在测试的 Android 手机上的返回键,系统将会带你回到第一个活动。
-
返回第一个活动后,它会显示吐司通知,以便从目标活动中传达结果。
指示应用程序从第一个活动到第二个活动,并将结果返回到第一个活动的流程
解释代码
在这个例子中,如前所述,我们将了解如何获取结果,以验证(在第一个活动中)在第二个活动中执行的任务是否成功完成。
在继续之前,请记住这是之前定义例子的扩展;因此,我们之前讨论过的内容在这个例子中不会解释。如果你在这里发现任何难以理解的内容,请阅读之前的例子。
让我们一步一步开始。
第一部分 – 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_EDIT
或ACTION_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
类是项目的主活动。为了向活动提供布局文件以进行视觉展示,我们将在活动的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.java
和SecondActivity.java
几乎包含相同的代码,除了MainActivity
的按钮监听器使用显式意图启动新活动,以及SecondActivity
的按钮监听器只是通过自动按下安卓手机的返回按钮来将应用拉回。
到目前为止,我们已经了解到如何使用显式意图导航到另一个活动。但是,应该记住,这两个活动使用了不同的布局文件进行可视化表示,包含执行两项任务的操作按钮。需要注意的是,布局文件在显式意图的导航中不起任何作用。这些文件只是展示活动的视觉内容,以简化用户交互。现在,让我们关注这些布局文件。
activity_main.xml
文件
activity_main.xml
文件是MainActivity.java
的布局文件,采用 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
文件,其中有一个带有关于height
、width
和orientation
参数的<LinearLayout>
。正如你所见,在<LinearLayout>
标签内部,声明了通过活动中的findViewById()
方法引入 Java 代码的<Button>
标签。这个标签还带有如id
、width
、height
和text
等参数,这些参数将显示在布局中。
activity_main2.xml
文件
activity_main2.xml
文件布局是SecondActivity
类的视觉表示。以下代码展示了这个文件:
这个布局与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
异常。
关于我们的显式意图应用,两个活动MainActivity
和SecondActivity
都在AndroidManifest.xml
文件中注册。需要在此注意的是,MainActivity
有一个<intent-filter>
子标签,它将MainActivity
声明为整个应用程序的启动或入口活动。以下是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 类是我们应用的主活动,定义了应用程序的启动点。以下代码展示了类的实现:
这个类是从 Activity
类扩展而来的,我们覆盖了一些其方法。在 onCreate()
方法中,我们通过引用存储在应用 res/layout
文件夹中的布局来设置活动的 ContentView 布局。然后,我们在布局中提取按钮并为这些按钮设置 OnClickListener
。在早期的示例中,我们为按钮使用了 OnClickListener
接口的匿名对象。在这个示例中,我们通过在 setOnClickListener()
方法参数中传递 this
来提供 Activity
类作为我们的 OnClickListener
。传递给参数的对象必须实现 OnClickListener
接口并覆盖 onClick()
方法。因此,我们的活动类实现了这个接口以及扩展的活动。我们还在这个类中覆盖了 onClick()
方法。由于这次我们有两个按钮并且只有一个 OnClickListener
接口,我们必须首先找出哪个按钮被按下,然后相应地采取行动。
一种实现方式是获取按下视图的 ID,并与资源中的 ID 进行比较。因此,getId()
方法为我们返回视图的 ID;我们在 switch 代码块中传递 ID,并与我们按钮的 ID 进行比较。在这两种情况下,我们都在创建一个显式意图,传递活动的上下文和我们的服务类名作为要激活的目标组件,就像我们在活动示例中所做的那样。需要注意的是,服务是通过 startService()
方法启动并通过 stopService()
方法停止的。这些方法采用显式意图,包括有关需要启动或停止哪个服务的信息。这个类向我们展示了如何容易地使用显式意图从任何活动中启动或停止任何服务。像往常一样,这个主活动使用了两个按钮,Start
和 Stop
,这两个按钮是从位于 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
(宽度)、id
和text
(将出现在布局中的文本)的特定参数。最后但同样重要的是,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 文件是我们简单的行分享应用的视觉布局。以下代码片段展示了这个文件的实现:
我们有一个相对布局,在其中放置了两个视图:一个带有text
为Share
的按钮和一个<EditText>
标签,用于获取用户的输入行。在之前的例子中,我们使用了<LinearLayout>
来在屏幕上对齐视图。这次,我们使用了<RelativeLayout>
来添加视图。在相对布局中,我们根据其他视图来放置我们的视图。例如,android:layout_alignLeft
获取任何视图的 ID,并将此视图放在主视图的左侧。同样,在Share
按钮中,我们也使用了android:layout_below
属性,将其放置在文本字段下方。关于文本字段,这是屏幕上的第一个视图;因此,它可以相对于父视图放置。
注意
layout_ android:layout_alignParentLeft
和android: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。以下截图展示了应用屏幕:
在本例中,我们使用隐式意图与 Android 操作系统的其他应用进行通信。在下一个示例中,我们将看到其他应用如何与我们的应用通信。
为共享意图注册你的应用
在上一个示例中,我们从我们的应用触发了其他共享应用。在本例中,我们将为共享意图注册我们的应用,以便其他应用开发者和用户可以使用隐式意图与我们的应用通信。两个示例都使用了隐式意图,但使用方法在这两种情况下是不同的。
在上一个示例中,我们在 Java 文件中使用了隐式意图,并在OnClickListener
中的按钮点击时触发了意图。在此示例中,为了注册我们的应用以接收任何意图,例如此例中的分享意图,我们必须在AndroidManifest.xml
文件的 XML 文件中放置我们的代码。稍微复习一下,AndroidManifest.xml
文件管理我们应用的所有设置。现在让我们学习如何实现应用。
要开始此示例,请使用任何 Android IDE(如带有 ADT 插件的 Eclipse 或 Android Studio)创建一个空项目,或打开你想要接收发送意图的任何现有项目。在这个项目中,我们将探索在应用中获取其他应用共享数据时隐式意图的使用。我们首先将我们的应用注册为文本共享应用。然后,所有在 Android 中共享任何text/html
类型文件的应用都可以在用户选择时触发我们的应用。为了执行这个任务,我们修改了三个文件中的代码:一个布局文件、一个活动文件和清单文件。现在让我们逐一看看这些文件。
activity_main.xml
文件
activity_main.xml
文件是我们共享应用的视觉布局。以下代码展示了此文件的实现:
我们有一个包含文本视图组件的相对布局。我们将文本视图的初始文本值设置为“Hello World”;记住,我们从其他应用共享时得到的数据将打印在此文本视图内容中。在视觉表现之后,接下来总是应用的处理和编码逻辑。现在让我们看看下一个活动文件,它执行主要任务并实现使用隐式意图的逻辑。
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.SEND
,mimeType
为text/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 文件,它执行应用程序中的所有功能。以下代码段是此文件的实现:
我们从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,并将其传递给本活动的ContentResolver
的openInputStream()
。任何活动的ContentResolver
都可以通过调用Activity.getContentResolver()
方法获得。获取到 URI 的数据流后,我们通过调用BitmapFactory.decodeStream()
方法将其解码为位图,并将输出位图设置到我们的活动位图字段中。然后,我们在图像视图中设置位图。在try/catch
块的最后一部分,我们关闭了流。
现在,运行项目,您将看到如下截图所示的屏幕。用户点击按钮,将显示图库。然后,用户选择他喜欢的照片以显示,应用将在应用屏幕上展示它:
总结关于隐式意图的整个部分,我们实现了三个示例。在第一个示例中,我们学习了如何与其他应用共享数据。在第二个示例中,我们了解了其他应用如何与我们的应用共享数据。最后,在第三个也是最后一个示例中,我们学习了如何从图库获取图片并在我们的应用中使用它。在下一节也是最后一节中,我们将讨论 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 中引入。
注意
BluetoothAdapter
和 BluetoothDevice
类中的某些方法需要 BLUETOOTH
权限,有些还需要 BLUETOOTH_ADMIN
权限。因此,当在您的应用中使用这些类时,不要忘记在 Android 清单文件中添加这些权限。
到目前为止,我们已经讨论了 Android OS 中的一些蓝牙类以及这些类中的某些方法。在下一节中,我们将开发我们的第一个 Android 应用,它将要求用户打开蓝牙。
打开蓝牙应用
要执行任何蓝牙操作,必须先打开蓝牙。因此,在本节中,我们将开发一个 Android 应用,如果蓝牙设备尚未打开,它会要求用户打开蓝牙设备。用户可以接受并打开蓝牙,或者用户也可以拒绝。在后一种情况下,应用程序将继续运行,而蓝牙将保持关闭状态。可以说,使用意图可以非常容易地执行此操作。让我们通过查看代码来看看如何做到这一点。
首先,在您喜欢的 IDE 中创建一个空的 Android 项目。我们是在 Android Studio 中开发的。在撰写这本书的时候,项目处于预览模式,预计不久将进行测试版发布。现在,我们将修改项目中的几个文件,以创建我们的 Android 蓝牙应用。我们将修改两个文件。让我们在以下章节中看看这些文件。
MainActivity.java 文件
这个类代表我们 Android 应用的主活动。以下代码是在此类别中实现的:
在我们的活动中,我们声明了一个名为 BLUETOOTH_REQUEST_CODE
的常量值。这个常量作为请求代码或请求唯一标识符,在我们的应用与 Android 系统之间的通信中使用。当我们请求 Android 操作系统执行某些操作时,我们会传递任何请求代码。然后,Android 系统执行操作并将相同的请求代码返回给我们。通过比较我们的请求代码与 Android 的请求代码,我们就可以知道已执行的操作。如果代码不匹配,则意味着此操作是针对其他请求的,不是我们的请求。在 onCreate()
方法中,我们通过调用 setContentView()
方法设置活动的布局。然后,在接下来的几行中,我们执行实际的任务。
我们创建了一个字符串enableBT
,它获取与BluetoothAdapter
类相关的ACTION_REQUEST_ENABLE
方法的值。这个字符串在意图构造函数中传递,以告诉意图它的目的是启用蓝牙设备。与蓝牙启用请求字符串一样,安卓操作系统还包含许多其他请求,用于各种动作,如 Wi-Fi、传感器、相机等。在本章中,我们将了解一些请求字符串。在创建请求字符串之后,我们创建我们的意图,并将请求字符串传递给它。然后,我们通过在startActivityForResult()
方法中传递它来启动我们的意图。
需要注意的是,在之前的章节中,我们使用了startActivity()
方法,而没有使用startActivityForResult()
方法。基本上,startActivity()
方法仅通过意图启动任何活动,但startActivityForResult()
方法启动任何活动,在执行某些操作后,它会返回到原始活动并呈现操作结果。因此,在这个例子中,我们调用了请求安卓系统启用蓝牙设备的活动。安卓系统执行操作并询问用户是否应该启用设备。然后,安卓系统将结果返回给之前启动意图的原始活动。为了从其他活动获取任何结果到我们的活动,我们重写了onActivityResult()
方法。此方法在从其他活动返回后调用。该方法包含三个参数:requestCode
、resultCode
和dataIntent
。requestCode
参数是一个整数值,包含开发者提供的请求代码值。resultCode
参数是操作的结果。它告诉开发者操作是否已成功执行,是正面响应还是负面响应。dataIntent
对象包含原始调用意图数据,例如哪个活动启动了意图以及所有相关信息。现在,让我们详细看看我们重写的方法。我们首先检查requestCode
,我们的请求代码,是否为BLUETOOTH_REQUEST_CODE
。如果两者相同,我们比较结果代码以检查我们的结果是否正常。如果正常,这意味着蓝牙已被启用;因此,我们显示一个通知用户关于它的吐司,如果结果不正常,这意味着蓝牙尚未启用。这里我们也通过显示吐司来通知用户。
这就是执行我们蓝牙启用应用核心功能的活动类。现在,让我们在下一节中看看 Android 的清单文件。
AndroidManifest.xml 文件
AndroidManifest.xml 文件包含应用的所有必要设置和偏好。以下是此清单文件中包含的代码:
任何使用蓝牙设备的 Android 应用都必须有使用蓝牙的权限。因此,为了向用户提供权限,开发者会在 Android 清单文件中声明<uses-permission>
标签,并写入必要的权限。如代码所示,我们提供了两个权限:android.permission.BLUETOOTH
和android.permission.BLUETOOTH_ADMIN
。对于大多数启用蓝牙的应用,仅BLUETOOTH
权限就可以完成大部分工作。BLUETOOTH_ADMIN
权限仅适用于那些使用蓝牙管理设置的应用,例如使设备可被发现、搜索其他设备、配对等。当用户首次安装应用程序时,他会收到有关应用程序需要哪些权限的详细信息。如果用户接受并授予应用权限,应用将被安装;否则,用户无法安装应用。文件的其余部分与书中其他示例中的相同。
讨论了 Android 清单和活动文件之后,我们将通过编译和运行项目来测试我们的项目。当我们运行项目时,我们应该看到如下截图所示的屏幕:
启用蓝牙应用
当应用启动时,用户会看到一个对话框,以启用或禁用蓝牙设备。如果用户选择是,蓝牙将被打开,并且一个提示会通过显示蓝牙状态来更新状态。
跟踪蓝牙适配器状态
在上一个示例中,我们看到了如何仅通过将蓝牙请求的意图传递给 Android 系统,在几行代码内打开蓝牙设备。但是启用和禁用蓝牙是耗时的异步操作。因此,我们可以使用广播接收器来监听状态变化,而不是轮询蓝牙适配器的状态。在这个示例中,我们将看到如何使用广播接收器中的意图来跟踪蓝牙状态。
这个示例是之前示例的扩展,我们将使用相同的代码并添加新的代码。现在让我们看看代码。我们有三个文件,MainActivity.java
,BluetoothStateReceiver.java
和AndroidManifest.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 文件
这个类表示我们的自定义广播接收器,用于跟踪蓝牙设备的状态变化。以下代码显示了该文件的实现:
正如我们对活动和服务的操作一样,要创建自定义广播接收器,我们需要从BroadcastReceiver
类继承并重写方法以声明自定义行为。我们重写了onReceive()
方法,并在该方法中执行了跟踪蓝牙设备状态的主要功能。首先,我们将创建一个字符串变量来存储当前状态的字符串值。为了获取字符串值,我们使用了BluetoothAdapter.EXTRA_STATE
。现在,我们可以将这个值传递给意图的get()
方法来获取我们需要的数据。由于我们的状态是整数并且也是额外的信息,我们调用了Intent.getIntExtra()
并在其中传递了我们需要的字符串以及默认值-1
。现在,既然我们已经得到了当前状态码,我们可以将这些码与BluetoothAdapter
中预定义的码进行比较,以查看蓝牙设备的状态。有四个预定义的状态。
-
STATE_TURNING_ON
:此状态通知用户蓝牙正在打开的操作正在进行中。 -
STATE_ON
:此状态通知用户蓝牙已经打开。 -
STATE_TURNING_OFF
:此状态通知用户蓝牙设备正在关闭。 -
STATE_OFF
:此状态通知用户蓝牙已经关闭。
我们将当前状态与这些常量进行比较,并根据得到的结果显示提示信息。Android 的清单文件与之前的示例相同。
因此,简而言之,我们讨论了如何启用蓝牙设备,并通过意图让用户打开或关闭它。我们还看到了如何在广播接收器中使用意图跟踪蓝牙操作的状态并显示提示信息。以下屏幕截图显示了应用程序的演示:
启用蓝牙应用
设置为可发现
到目前为止,我们仅通过打开或关闭与蓝牙进行交互。但是,要通过蓝牙开始通信,一个设备必须设置为可发现以开始配对。我们不会为此意图的应用创建任何示例,但只会解释如何通过意图完成这一操作。要打开蓝牙,我们使用了BluetoothAdapter.ACTION_REQUEST_ENABLE
意图。我们将意图传递给startActivityForResult()
方法,并在onActivityResult()
方法中检查结果。现在,要使设备可发现,我们可以在意图中传递BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE
字符串。然后,我们将此意图传递给startActivityForResult()
方法,并在onActivityResult()
方法中跟踪结果以比较结果。
下面的代码片段展示了使设备可发现的意图创建过程:
在代码中,你可以看到没有什么新内容是没有在之前讨论过的。只是意图动作字符串类型发生了变化,其余部分保持不变。这就是意图的力量;你只需用几分钟和几行代码就可以完成几乎任何事情。
监控可发现性模式
正如我们追踪蓝牙状态变化一样,我们也可以使用本章前面介绍的确切方法来监控可发现性模式。我们必须通过扩展BroadcastReceiver
类来创建一个自定义的广播接收器。在onReceive()
方法中,我们将得到两个额外的字符串:BluetoothAdapter.EXTRA_PREVIOUS_SCAN_MODE
和BluetoothAdapter.EXTRA_SCAN_MODE
。然后,我们将这些字符串传递给Intent.getIntExtra()
方法以获取模式的整数值,然后我们将这些整数与预定义的模式进行比较以检测我们的模式。下面的代码片段展示了代码示例:
通过蓝牙进行通信
蓝牙通信 API 只是围绕标准的RFCOMM(标准蓝牙射频通信协议)的封装。为了与其他蓝牙设备通信,它们必须配对。我们可以使用BluetoothServerSocket
类通过蓝牙进行双向通信,该类用于建立监听套接字以启动设备间的链接,以及BluetoothSocket
用于创建一个新的客户端套接字来监听蓝牙服务器套接字。一旦建立连接,服务器套接字就会返回这个新的客户端套接字。我们将不讨论蓝牙在通信中的使用,因为这超出了本书的范围。
通过意图使用 Wi-Fi
在互联网时代,以及其在手机上的广泛应用,使得全球信息触手可及。几乎每一位安卓手机用户都期望所有应用能充分利用互联网。因此,为应用添加互联网接入成为了开发者的责任。例如,当用户使用你的应用时,他们可能会想要与朋友分享在应用中的活动,如完成一个游戏关卡或阅读新闻应用中的文章,通过各种社交网络分享,或者发送短信等等。所以,如果用户不能通过你的应用连接到互联网、社交平台或全球信息,那么这个应用就会变得非常局限,甚至可能令人感到无聊。
要执行任何使用互联网的活动,我们首先必须处理互联网连接本身,比如手机是否有任何活动的连接。在本节中,我们将了解如何通过我们的核心主题——意图——访问互联网连接。像蓝牙一样,我们可以通过意图完成许多与互联网连接相关的任务。我们将实现三个主要示例:检查手机的互联网状态、选择任何可用的 Wi-Fi 网络以及打开 Wi-Fi 设置。让我们开始第一个示例,使用意图检查手机的互联网连接状态。
检查互联网连接状态
在我们开始编写示例代码之前,我们需要了解一些重要的事情。任何连接到互联网的 Android 手机都可能拥有任何类型的连接。手机可以通过数据连接连接到互联网,也可以是任何开放的或安全的 Wi-Fi。数据连接被称为移动连接,通过 SIM 卡和服务提供商提供的移动网络连接。在这个例子中,我们将检测手机是否连接到任何网络,如果连接了,它连接的是哪种类型的网络。现在让我们实现代码。
有两个主要的文件负责应用程序的功能:NetworkStatusReceiver.java
和AndroidManifest.xml
。你可能对MainActivity.java
文件感到好奇。在以下示例中,由于应用程序的需求,这个文件没有被使用。我们在这个例子中要实现的功能是,无论手机的互联网连接状态如何改变,比如 Wi-Fi 开启或关闭,这个应用程序都会显示一个吐司来展示状态。该应用将在后台执行其工作,因此不需要活动和布局。现在,让我们逐一解释这些文件:
NetworkStatusReceiver.java 文件
这个类表示我们自定义的广播接收器,用于跟踪设备网络连接状态的变化。以下代码展示了该文件的实现:
正如我们对活动和服务的处理一样,要创建自定义广播接收器,我们需要从BroadcastReceiver
类继承并重写方法以声明自定义行为。我们重写了onReceive()
方法,并在该方法中执行了跟踪 Wi-Fi 设备状态的主要功能。我们已经在 Android 清单文件中将此接收器注册为网络状态更改,我们将在下一节讨论该文件。这个onReceive()
方法只在网络状态改变时被调用。因此,我们首先显示一个吐司,声明网络连接状态已改变。
注意
需要注意的是,任何广播接收器都不能在Toast
的上下文参数中使用this
来传递,就像我们在Activity
类中所做的那样,因为BroadcastReceiver
类没有像Activity
类那样扩展Context
类。
我们已经通知了用户关于网络状态的变化,但我们还没有通知用户具体发生了哪种变化。因此,在这个时候,我们的意图对象变得非常有用。它包含了网络的所有信息和数据,以extra
对象的形式存在。回顾前面的章节,extra
是Bundle
类的一个对象。我们创建一个本地的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
可以根据用户 ID(UID)而有所不同,应用程序应始终通过getActiveNetworkInfo()
或getAllNetworkInfo()
方法获取网络信息。
我们已经得到了我们感兴趣的所有值和数据;现在,我们将比较并检查数据以找到连接状态。我们检查这个NetworkInfo
数据是否为null
。如果不是null
,我们通过检查NetworkInfo
的getState()
方法的值来确认网络是否已连接。NetworkInfo.State
枚举表示粗粒度的网络状态。如果NetworkInfo.State
枚举等于NetworkInfo.State.CONNECTED
,意味着手机已连接到任何网络。请记住,我们仍然不知道我们连接的是哪种类型的网络。我们可以通过调用NetworkInfo.getTypeName()
方法来找到网络的类型。此方法将根据相应的情况返回Mobile
或Wi-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
过滤器动作来检测网络连接变化广播。除了这两点之外,这个文件中没有新的内容,其余代码与我们在前面章节中讨论的一致。以下是该文件的代码实现:
总结前面应用的细节,我们创建了一个自定义的广播接收器,并定义了网络变化的自定义行为,即显示提示信息,然后在清单文件中注册了我们的接收器,并声明了所需的权限。以下截图显示了在手机开启 Wi-Fi 时应用的一个简单演示:
网络状态变化应用
在前面的截图中,我们可以看到当开启 Wi-Fi 时,应用会显示一个提示框告知网络状态已改变。提示框之后,它会显示变化;在我们的案例中,Wi-Fi 已连接。你可能会好奇在这个应用中意图(intents)的作用。没有使用意图,这个应用是不可能实现的。首先在清单文件中使用意图来注册接收器,以便筛选网络状态变化。另一个意图的使用是在接收器中,当我们收到更新并想知道变化时。因此,我们使用了意图,并以extra
对象的形式从中提取数据,用于我们的目的。在这个例子中,我们没有创建自己的意图;而是仅使用了提供的意图。在下一个例子中,我们将创建自己的意图,并使用它们从应用中打开 Wi-Fi 设置。
打开 Wi-Fi 设置应用
到目前为止,我们仅将意图用于网络和 Wi-Fi 目的。在这个例子中,我们将创建意图对象并在我们的应用中使用它。在之前的示例应用中,我们检测了手机的 network change 状态并在屏幕上显示。在这个例子中,我们将在同一个应用中添加一个按钮。点击或轻触按钮,应用将打开 Wi-Fi 设置。用户可以从那里打开或关闭 Wi-Fi。当用户执行任何操作时,应用将在屏幕上显示网络状态变化。对于网络状态,我们使用了NetworkStatusReceiver.java
和AndroidManifest.xml
文件。现在,让我们打开同一个项目,并更改我们的MainActivity.java
和layout_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 设置的活动。这非常简单,完全没有火箭科学那么复杂。当我们运行这个应用时,我们将看到与以下截图类似的内容:
打开 Wi-Fi 设置应用
如从前面的截图中所见,当我们点击或轻触 Wi-Fi 设置按钮时,它将打开 Android 手机的 Wi-Fi 设置屏幕。在更改设置时,比如打开 Wi-Fi,它将显示吐司通知来展示更新的更改和网络状态。
我们已经讨论了使用意图进行通信的组件,在其中我们通过意图使用了蓝牙和 Wi-Fi,并看到了它们在各种示例和应用中的使用方法。现在,我们将讨论如何通过意图使用媒体组件,以及我们在以下各节中可以为媒体组件做些什么。
媒体组件
前一节我们讨论了通信组件的全部内容。但旧手机与新型智能手机的区别在于多媒体功能,比如高清音视频特性。手机的多媒体功能已成为许多消费者更为重视的考量因素。幸运的是,Android 系统为许多功能提供了多媒体 API,比如播放和记录各种图像、音频和视频格式,无论是本地还是流式传输。如果用简单的语言来描述媒体组件,这个主题只能在一个专门的章节中详细讲解,这超出了本书的范围。我们仅讨论那些可以通过意图触发、使用和访问的媒体组件。本节将要讨论的组件包括使用意图拍照、使用意图录制视频、使用意图的语音识别以及意图在文本到语音转换中的作用。前三个主题使用意图执行操作;但文本到语音转换的最后一个主题并不完全使用意图。我们还将开发一个示例应用程序来实际观察意图的使用。让我们逐一在以下小节中讨论这些主题。
使用意图拍照
现在,几乎每部手机都有数码相机组件。嵌入在手机中的数码相机的普及使得它们的价格和体积都下降了。Android 手机也包含从 320 万像素到 3200 万像素不等的数码相机。从开发的角度来看,可以通过许多不同的方法拍照。Android 系统也提供了相机控制和拍照的 API,但我们只关注一种使用意图的方法。这是 Android 开发中拍照最简单的方式,代码不超过几行。
我们将首先创建一个带有图像视图和按钮的布局。然后,在Activity
类中,我们从布局文件中获取我们视图的引用,并设置按钮的点击监听器。点击按钮时,我们将创建捕获图像的意图,并以子类的形式启动另一个活动。在得到结果后,我们将在我们的图像视图中显示那捕获的图像。
那么,准备好基本的空 Hello World 项目后,我们将修改三个文件并在其中添加我们的代码。这些文件是activity_main.xml
、MainActivity.java
和AndroidManifest.xml
。让我们逐一解释每个文件中的更改:
activity_main.xml 文件
这个文件代表了视觉布局。我们将添加一个ImageView
标签以显示捕获的图像,以及一个Button
标签以拍照并触发相机。该文件的代码实现如下:
如代码所示,我们在相对布局中放置了一个带有imageView1
ID 的ImageView
标签。这个 ID 将在主活动文件中使用,从布局中提取视图以在 Java 文件中使用。我们将视图通过在android:layout_centerHorizontal
标签中赋值为true
来放置在布局的水平中心。最初,我们将应用启动图标的默认图像设置到我们的图像视图中。在图像视图下方,我们放置了一个按钮视图。点击按钮时,将启动相机。按钮的 ID 通过图像视图布局下的android:layout_below
标签设置为btnTakePicture
。这种相对性是相对布局与线性布局相比的主要优势。现在,让我们看看执行主要功能并使用此布局作为视觉部分的应用的活动。
MainActivity.java 文件
此文件表示应用的主要启动活动。该文件使用layout_main.xml
文件作为视觉部分,并从Activity
类扩展而来。文件的代码实现如下:
我们通过重写活动的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
参数。由于我们使用这段代码来启动相机拍照意图,我们将我们的resultCode
与RESULT_OK
常量进行比较。两个条件都成功后,我们可以得出我们已经收到图像的结论。因此,我们通过调用getExtras().get()
方法使用意图获取我们的图像数据。这将给我们Object
类型的数据。我们进一步将其类型转换为Bitmap
,以便为ImageView
准备。
最后,我们调用setImageBitmap
方法,将新的位图设置到我们的图像视图中。如果你运行代码,你会看到一个图标图像和一个按钮。点击按钮后,将启动相机。当你拍照时,应用会崩溃并关闭。你可以在以下截图中看到:
拍照后应用崩溃了
你可能想知道为什么会发生崩溃。我们忘记提到一件事;每当任何应用使用相机时,我们必须在我们的清单文件中添加uses-feature
标签,告诉应用它将使用相机功能。让我们看看我们的 Android 清单文件,了解uses-feature
标签。
AndroidManifest.xml 文件
此文件定义了在我们的应用程序中要使用的所有设置和功能。只有一个我们没见过的新东西。文件的代码实现如下:
你可以看到我们添加了uses-feature
标签,并在android:name
属性中指定了android.hardware.camera
。这个标签告诉应用程序它将如何使用相机,Android 操作系统允许我们的应用程序使用外部相机。
在清单文件中添加这行代码并运行后,如果你手机里有多个相机应用,你会看到类似以下截图的内容:
通过意图应用拍照
在截图中,你可以看到用户被要求选择相机,拍照后,图像会在应用中显示。
当我们总结代码时,我们首先创建了一个带有图像视图和按钮的布局。然后在Activity
类中,我们从布局文件中获取我们视图的引用,并设置了按钮的点击监听器。点击按钮后,我们创建了捕获图像的意图,并启动了另一个作为子活动的活动。获取结果后,我们在图像视图中显示了捕获的图像。这就像在公园里散步一样简单。在下一节中,我们将了解如何使用意图录制视频。
使用意图录制视频
到目前为止,我们已经看到了如何使用意图拍照。在本节中,我们将看到如何使用意图录制视频。我们不会在本节中讨论整个项目。使用意图录制视频的流程与拍照几乎相同,只有一些小的变化。我们只在本节中讨论这些变化。现在,让我们看看应用是如何录制视频的。
我们做的第一个更改是在我们的布局文件中。我们移除了图像视图部分,并放置了VideoView
标签。以下代码实现展示了该标签:
你可以看到,这与ImageView
中的内容是一样的。现在,由于我们在布局中将图像视图更改为视频视图,因此我们也必须在活动中进行相应的更改。正如我们对ImageView
所做的那样,我们将创建一个VideoView
的字段对象,并在活动的onCreate()
方法中获取它的引用。以下代码示例展示了VideoView
字段对象的行:
一切都保持不变,我们已经讨论过这部分内容。现在,在我们的onClick()
方法中,我们将看到如何发送触发视频录制的意图。要放在onClick()
方法中发送意图的代码实现如下:
你可以看到我们创建了一个意图对象,并且在构造函数中,我们没有传递MediaStore.ACTION_IMAGE_CAPTURE
,而是传递了MediaStore.ACTION_VIDEO_CAPTURE
。此外,我们还通过调用putExtra()
方法在意图中放置了一个extra
对象。我们通过将MediaStore.EXTRA_VIDEO_QUALITY
值赋为1
来定义视频质量为high
的extra
对象。然后,我们再次将意图传递给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_AUDIO
和 android.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 文件
这个文件代表了应用的可视内容。我们将在该文件中添加文本字段和按钮视图。该文件的代码实现如下:
如你所见,我们放置了一个EditText
字段。我们将android:inputType
设置为textMultiLine
以在多行中输入文本。在文本字段下方,我们添加了一个带有 ID 为btnRecognize
的Button
视图。当点击此按钮时,它将用于启动语音识别活动。现在,让我们讨论一下主要活动文件。
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_PROMPT
的extra
,并在其中传递了一些字符串值。这将通知用户语音识别已开始。
接下来,我们添加了RecognizerIntent.EXTRA_MAX_RESULTS
的extra
,并将其值设置为1
。语音识别的准确性总是有所变化。因此,识别器将尝试更精确地识别。它会创建不同准确性和可能不同含义的不同结果。通过这个extra
,我们可以告诉识别器我们感兴趣的结果数量。在我们的应用中,我们将其设置为1
。这意味着识别器将只为我们提供一个结果。不能保证这个结果足够准确;这就是为什么建议传递大于1
的值。对于简单的情况,你可以传递一个值高达5
。记住,你传递的值越大,识别所需的时间就越长。
最后,我们添加了最后一个关于语言的可选extra
。我们将Locale.ENGLISH
作为RecognizerIntent.EXTRA_LANGUAGE
的值传递。这将告诉识别器语音的语言。因此,识别器无需检测语言,这使语音识别的准确性更高。
注意
语音识别引擎可能无法理解Locale
类中所有可用的语言。同样,也不是所有设备都支持语音识别。
添加所有extra
对象后,我们确保了意图对象已准备好。我们将其传递给startActivityForResult()
方法,并设置requestCode
为1
。当调用此方法时,会显示一个标准的语音识别对话框,并带有我们给出的提示信息。说话结束后,我们父活动的onActivityResult()
方法将被调用。我们首先检查requestCode
是否为1
,以确保这是我们语音识别的结果。之后,我们将检查resultCode
以确定结果是否正常。成功的结果后,我们会得到一个字符串数组列表,其中包含识别器识别的所有单词。我们可以通过调用getStringArrayListExtra()
方法并传递RecognizerIntent.EXTRA_RESULTS
来获取这些单词的列表。当resultCode
正常时,才会返回此列表;否则,我们会得到一个 null 值。在完成语音识别的相关操作后,我们现在可以将文本值设置为结果。为此,我们首先从布局中提取EditText
视图,并通过调用setText()
方法将我们的结果设置为文本字段的值。
注意
运行语音识别需要活跃的互联网连接。语音识别过程是在谷歌的服务器上执行的。安卓手机接收语音输入,将其发送到谷歌服务器,并在那里进行处理以识别。识别完成后,谷歌将结果发送回安卓手机,手机会通知用户关于结果,这样整个循环就完成了。
如果你运行这个项目,你会看到类似于以下截图的内容:
使用意图进行语音识别
在图片中,你可以看到点击识别按钮后,会显示一个标准的语音输入对话框。说话之后,我们会返回到父活动,并在识别语音后,它将在文本字段中打印所有文本。
意图在文本到语音转换中的作用
在上一节中,我们讨论了 Android 系统如何识别我们的语音并执行操作,例如通过语音命令控制手机。我们还使用意图开发了一个简单的语音到文本示例。这一节与上一节相反。在本节中,我们将讨论 Android 系统如何将我们的文本转换成优美的语音叙述。我们可以称之为文本到语音转换。Android 在版本 1.6(API 级别 4)中引入了文本到语音(TTS)转换引擎。我们可以使用这个 API 在应用程序内部产生语音,从而允许我们的应用与用户对话。如果我们添加语音识别,它将像是与应用程序对话一样。文本到语音转换需要预装语言包,由于手机存储空间有限,不能保证手机已经预装了任何语言包。因此,在使用文本到语音引擎创建任何应用时,检查语言包是否已安装是一个好习惯。
我们不能通过意图使用文本到语音转换。我们只能通过称为 TTS 的文本到语音引擎使用它。但在文本到语音转换中,意图有一个小作用。意图仅用于检查语言包是否预装。因此,任何使用文本到语音的应用在创建时,首先必须使用意图来检查语言包的安装状态。这就是意图在文本到语音转换中的作用。下面是检查语言包安装状态的示例代码:
在进行文本到语音转换时,我们首先要检查语言包。在代码中,我们可以看到我们正在创建一个意图对象。我们传递了Engine.ACTION_CHECK_TTS_DATA
常量,这会告诉系统该意图将检查文本到语音(TTS)数据和语言包。然后我们在startActivityForResult()
方法中传递该意图,并附上作为requestCode
使用的VAL_TTS_DATA
常量值。现在,如果安装了语言包且一切正常,我们将在onActivityResult()
方法中得到resultCode
为RESULT_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 设置的意图。我们还看到了如何通过意图拍照、录制视频、进行语音识别和文本到语音的转换。最后,我们看到了如何通过意图使用接近传感器。
在下一章中,我们将看到如何在不同活动、服务和其他移动组件之间通过意图传输数据。