安卓秘籍-问题解决方法-全-

安卓秘籍:问题解决方法(全)

原文:Android Recipes: A Problem-Solution Approach

协议:CC BY-NC-SA 4.0

零、前言

欢迎来到安卓秘籍!

如果你正在读这本书,你可能不需要被告知移动设备对软件开发者和用户来说代表着巨大的机会。近年来,Android 已经成为设备用户的顶级移动平台之一。这意味着,作为一名开发者,你必须知道如何利用 Android,这样你才能与这个市场和它提供的潜力保持联系。但是,任何新平台都会带来最佳实践或解决共同需求和问题的不确定性。

我们打算用 Android Recipes 给你工具,通过直接的例子,针对你试图解决的具体问题,为 Android 平台编写应用。这本书没有深入探究 Android SDK、NDK 或任何其他工具。我们不会用幕后的所有细节和理论来拖累你。这并不是说这些细节不有趣或不重要。你应该花时间去学习它们,因为它们可以避免你将来犯错误。然而,当你只是在寻找一个紧急问题的解决方案时,它们往往只是一种干扰。

这本书不是要教你 Java 编程,甚至是 Android 应用的构建模块。你在这本书里找不到很多基本的方法(比如如何用文本视图显示文本),因为我们觉得这些任务一旦学会就很容易记住。相反,我们开始着手解决那些曾经熟悉 Android 的开发人员需要经常做的任务,但是这些任务太复杂了,以至于无法用几行代码记住或完成。

Android 秘籍作为参考资料来查阅,这是一本资源丰富的秘籍,你可以随时打开,找到你需要的实用建议,以便又快又好地完成工作。

你会在书中发现什么?

虽然这本书不是 Android 的入门指南,但第一章概述了理解本书其余内容所必需的 Android 基础知识。第一章还向你展示了如何设置你的环境,以便你可以开发 Android 应用。具体来说,它向您展示了如何使用 ADT 插件安装 Android SDK 和 Eclipse。

当你成为一名经验丰富的 Android 应用开发人员时,你会希望通过不重新发明轮子来节省时间。相反,您会想要创建和使用自己的可重用代码库,或者使用其他人创建的库。第七章向你展示如何以基于 JAR 的库和 Android 库项目的形式创建和使用你自己的库代码。除了创建您自己的库,我们还将在 Android SDK 之外引入几个 Java 库,您的应用可以使用它们。

在中间的章节中,我们深入研究使用 Android SDK 来解决实际问题。您将学习有效创建跨设备边界运行的用户界面的技巧。你将成为整合硬件(收音机、传感器和照相机)的大师,这些硬件使移动设备成为如此独特的平台。我们甚至会讨论如何让这个系统为你服务,整合谷歌和各种设备制造商提供的服务和应用。在此过程中,您将了解到一些由社区开发的工具,这些工具有助于简化应用的开发和测试。

你对脚本语言(比如 Python 或者 Ruby)感兴趣吗?如果是这样的话,你会想看看附录 A ,它向你介绍了 Android 的脚本层。这个特殊的应用可以让你在设备上安装脚本语言解释器和脚本,然后运行这些脚本,这可以加快开发速度。

如果你想让你的应用成功,性能很重要。大多数时候,这不是问题,因为(从 2.2 版本开始)Android 的 Dalvik 虚拟机具有一个即时编译器,可以将 Dalvik 字节码编译成设备的本机代码。然而,如果这还不够,你还需要利用 Android NDK 来提高性能。附录 B 向您介绍了 NDK,并展示了它在 OpenGL 示例中的用途。

当创建应用时,你需要确保它们是高性能的、反应灵敏的和无缝的。运行良好的应用消耗的电池电量更少,响应性应用避免了可怕的应用不响应对话框,无缝应用可以与其他应用正常交互,从而不会打扰或混淆用户。此外,当您将应用发布到 Google 的 Android Market 时,您不希望该应用对不兼容的设备可见。相反,你希望 Android Market 过滤你的应用,让这些不兼容设备的用户无法下载(甚至无法看到)该应用。附录 C 为你提供了创建高性能、响应性和无缝应用的指导方针,从而使本书更加完整;利用过滤功能,只有那些设备与应用兼容的用户才能(从 Android Market)下载应用。

盯紧目标

在整本书中,你会看到我们已经用支持它所需的最低 API 级别标记了大多数秘籍。这本书里的大部分菜谱都标注了 API Level 1,也就是说使用的代码可以在针对 1.0 以后的任何版本 Android 的应用中运行。但是,在必要的地方,我们会使用在以后版本中引入的 API。密切注意每个秘籍的 API 级别标记,以确保您没有使用与您的应用支持的 Android 版本不匹配的代码。

一、Android 入门

安卓火热,很多人都在开发安卓应用(简称 app)。也许你也想开发应用,但不确定如何开始。尽管你可以研究谷歌的在线 Android 开发者指南 ( [developer.android.com/guide/index.html](http://developer.android.com/guide/index.html))来获得所需的知识,但你可能会被该指南提供的大量信息淹没。相比之下,本章提供了足够的理论来帮助你理解 Android 的基础知识。这个理论之后是几个秘籍,教你如何开发应用,并准备发布到谷歌的 Android 市场。

安卓是什么?

Android 开发者指南将 Android 定义为移动设备的软件栈——交付全功能解决方案所需的一组软件子系统。这个堆栈包括一个操作系统(Linux 内核的修改版本),中间件(将低级操作系统连接到高级应用的软件),部分基于 Java,以及关键应用(用 Java 编写),如网络浏览器(称为浏览器)和联系人管理器(称为联系人)。

Android 提供以下功能:

  • 支持重用和替换应用组件的应用框架(将在本章后面讨论)
  • 蓝牙、EDGE、3G 和 WiFi 支持(取决于硬件)
  • 摄像头、GPS、指南针和加速度计支持(取决于硬件)
  • 针对移动设备优化的 Dalvik 虚拟机(DVM)
  • GSM 电话支持(取决于硬件)
  • 基于开源 WebKit 引擎的集成浏览器
  • 对常见音频、视频和静止图像格式(MPEG4、H.264、MP3、AAC、AMR、JPG、PNG、GIF)的媒体支持
  • 由定制 2D 图形库支持的优化图形;基于 OpenGL ES 1.0 规范的 3D 图形(可选硬件加速)
  • 用于结构化数据存储的 SQLite

尽管不是 Android 设备软件栈的一部分,Android 丰富的开发环境(包括一个设备仿真器和一个 Eclipse IDE 插件)也可以被认为是 Android 的一个特性。

安卓的历史

与你想象的相反,Android 并不是谷歌发明的。相反,Android 最初是由 Android,Inc .开发的,这是一家位于加州帕洛阿尔托的小型创业公司。谷歌于 2005 年 7 月收购了这家公司,并于 2007 年 11 月发布了 Android SDK 的预览版。

2008 年 8 月中旬,谷歌发布了 Android 0.9 SDK 测试版,随后一个月后又发布了 Android 1.0 SDK。Table 1–1 概述了后续的 SDK 更新版本。(从 1.5 版本开始,每个主要版本都有一个基于甜点的代码名称。)

表 1–1。 安卓更新发布

| **SDK 更新** | **发布日期和变更** | | :-- | :-- | | One point one | 谷歌于 2009 年 2 月 9 日发布了 SDK 1.1。变化包括付费应用(通过 Android Market)和“语音搜索”支持。 | | 基于 Linux 内核 2.6.27 的 1.5(纸杯蛋糕) | 谷歌于 2009 年 4 月 30 日发布了 SDK 1.5。变化包括通过摄像机模式录制和观看视频的能力,将视频上传到 YouTube 和图片上传到 Picasa 的能力,用小工具填充主屏幕的能力,以及动画屏幕过渡。 | | 基于 Linux 内核 2.6.29 的 1.6(甜甜圈) | 谷歌于 2009 年 9 月 15 日发布了 SDK 1.6。变化包括改进的 Android 市场体验,集成的相机/摄像机/画廊界面,更新的“语音搜索”速度和其他改进,以及更新的搜索体验。 | | 基于 Linux 内核 2.6.29 的 2.0/2.1(艾克蕾尔) | 谷歌于 2009 年 10 月 26 日发布了 SDK 2.0。变化包括改进的用户界面、新的联系人列表、对 Microsoft Exchange 的支持、数字变焦、改进的谷歌地图(版本 3.1.2)、对浏览器应用的 HTML5 支持、动态壁纸和蓝牙 2.1 支持。

谷歌随后于 2009 年 12 月 3 日发布了 SDK 更新 2.0.1,2010 年 1 月 12 日发布了 SDK 更新 2.1。 |
| 基于 Linux 内核 2.6.32 的 2.2 (Froyo) | 谷歌于 2009 年 5 月 20 日发布了 SDK 2.2。变化包括将 Chrome 的 V8 JavaScript 引擎集成到浏览器应用中,通过蓝牙进行语音拨号和联系人共享,支持 Adobe Flash 10.1,通过 JIT 实现进一步提高应用速度,以及 USB 共享和 WiFi 热点功能。 |
| 2.3(姜饼)基于 Linux 内核 2.6.35.7 | 谷歌在 2010 年 12 月 6 日发布了 SDK 2.3。变化包括新的并发垃圾收集器,提高了应用的响应能力,支持陀螺仪感应,支持 WebM 视频播放和其他视频改进,支持近场通信,以及改进的社交网络功能。本书重点介绍 Android 2.3。

谷歌随后发布了 SDK 2.3.1 修复了一些 bug,以及 SDK 2.3.3,这是一个小功能发布,为 Android 2.3 平台增加了几项改进和 API。 |
| 基于 Linux 2.6.36 的 3.0(蜂巢) | 谷歌于 2011 年 2 月 22 日发布了 SDK 3.0。与之前的版本不同,3.0 版本只专注于平板电脑,如即将发布的第一款平板电脑摩托罗拉变焦(2011 年 2 月 24 日)。除了改进的用户界面,3.0 版本还改进了多任务处理,支持多核处理器,支持硬件加速,并提供了一个重新设计的小部件的 3D 桌面。 |

安卓架构

Android 软件栈由顶部的应用、中间的中间件(由应用框架、库和 Android 运行时组成)和底部的带有各种驱动程序的 Linux 内核组成。图 1–1 显示了这种分层架构。

images

图 1–1。 Android 的分层架构由几大部分组成。

用户关心应用,Android 附带了各种有用的核心应用,包括浏览器、联系人和电话。所有应用都是用 Java 编程语言编写的。应用构成了 Android 架构的顶层。

在应用层的正下方是应用框架,这是一组用于创建应用的高级构建模块。应用框架预装在 Android 设备上,由以下组件组成:

  • 活动管理器:这个组件提供了一个应用的生命周期,并维护一个共享的活动堆栈,用于在应用内部和应用之间导航。这两个主题都将在本章后面讨论。
  • 内容提供者:这些组件封装了可以在应用之间共享的数据(比如浏览器应用的书签)。
  • 位置管理器(Location Manager):这个组件使得 Android 设备能够知道自己的物理位置。
  • 通知管理器:这个组件让一个应用通知用户一个重要的事件(比如一条消息的到达),而不打断用户当前正在做的事情。
  • 包管理器:这个组件让一个应用了解当前安装在设备上的其他应用包。(本章稍后将讨论应用包。)
  • 资源管理器:这个组件让一个应用访问它的资源,这个话题在 Recipe 1–5 中有简要的讨论。
  • 电话管理器:这个组件让应用了解设备的电话服务。它还处理拨打和接听电话。
  • 视图系统:该组件管理用户界面元素和面向用户界面的事件生成。(秘籍 1–5 中简要讨论了这些主题。)
  • 窗口管理器(Window Manager):这个组件将屏幕空间组织到窗口中,分配绘图表面,并执行其他与窗口相关的任务。

应用框架的组件依赖一组 C/C++ 库来执行它们的工作。开发人员通过框架 API 与以下库进行交互:

  • FreeType :这个库支持位图和矢量字体渲染。
  • libc :这个库是标准 C 系统库的 BSD 派生实现,针对基于嵌入式 Linux 的设备进行了调整。
  • LibWebCore :这个库提供了一个现代化的快速网络浏览器引擎,支持 Android 浏览器和嵌入式网络视图。它基于 WebKit ( [en.wikipedia.org/wiki/WebKit](http://en.wikipedia.org/wiki/WebKit)),也用于谷歌 Chrome 和苹果 Safari 浏览器。
  • 媒体框架:这些基于 PacketVideo 的 OpenCORE 的库,支持许多流行的音频和视频格式的回放和录制,以及处理静态图像文件。支持的格式包括 MPEG4、H.264、MP3、AAC、AMR、JPEG 和 PNG。
  • OpenGL | ES :这些 3D 图形库提供了基于 OpenGL | ES 1.0 APIs 的 OpenGL 实现。他们使用硬件 3D 加速(如果可用)或内置的(高度优化的)3D 软件光栅化器。
  • SGL :这个库提供了底层的 2D 图形引擎。
  • SQLite :这个库提供了一个强大的轻量级关系数据库引擎,所有应用都可以使用,Mozilla Firefox 和苹果的 iPhone 也使用这个引擎进行持久存储。
  • SSL:这个库为网络通信提供了基于安全套接字层(基于 SSL)的安全性。
  • Surface Manager :这个库管理对显示子系统的访问,并无缝合成来自多个应用的 2D 和 3D 图形层。

Android 提供了一个运行时环境,它由核心库(实现 Apache Harmony Java 版本 5 实现的子集)和 Dalvik 虚拟机(DVM)组成,后者是一个基于处理器寄存器而不是基于堆栈的非 Java 虚拟机。

注意:谷歌的丹·博恩施泰因创造了达尔维克,并以他的一些祖先居住的冰岛渔村命名了这个虚拟机。

每个 Android 应用默认运行在自己的 Linux 进程中,该进程托管一个 Dalvik 实例。该虚拟机的设计使得设备可以高效地运行多个虚拟机。这种效率很大程度上是由于 Dalvik 执行基于 Dalvik 可执行文件(DEX)的文件——DEX 是一种针对最小内存占用进行优化的格式。

注意:当应用的任何部分需要执行时,Android 会启动一个进程,当不再需要该进程并且其他应用需要系统资源时,Android 会关闭该进程。

也许您想知道如何让非 Java 虚拟机运行 Java 代码。答案是 Dalvik 不运行 Java 代码。相反,Android 将编译后的 Java 类文件转换成 DEX 格式,Dalvik 执行的就是这些结果代码。

最后,库和 Android 运行时依赖于 Linux 内核(2.6 版)提供底层核心服务,如线程、低级内存管理、网络堆栈、进程管理和驱动程序模型。此外,内核充当硬件和软件堆栈其余部分之间的抽象层。

安卓安全模式

Android 的架构包括一个安全模型,可以防止应用执行被认为对其他应用、Linux 或用户有害的操作。这种安全模型主要基于通过标准 Linux 特性(如用户和组 id)的进程级实施,将进程放在安全沙箱中。

默认情况下,沙盒会阻止应用读取或写入用户的私人数据(如联系人或电子邮件),读取或写入另一个应用的文件,执行网络访问,保持设备唤醒,访问摄像头等。需要访问网络或执行其他敏感操作的应用必须首先获得许可。

Android 以各种方式处理权限请求,通常是根据证书自动允许或拒绝请求,或者提示用户授予或撤销权限。应用所需的权限在应用的清单文件中声明(将在本章后面讨论),以便在安装应用时 Android 知道它们。这些权限不会随后更改。

应用架构

Android 应用的架构不同于桌面应用架构。应用架构基于组件,这些组件通过使用清单描述的意图相互通信,这些意图存储在应用包中。

组件

应用是运行在 Linux 进程中并由 Android 管理的组件(活动、服务、内容提供者和广播接收器)的集合。这些组件共享一组资源,包括数据库、首选项、文件系统和 Linux 进程。

注意:并非所有这些组件都需要出现在一个应用中。例如,一个应用可能只包含活动,而另一个应用可能包含活动和服务。

这种面向组件的架构允许一个应用重用其他应用的组件,前提是这些其他应用允许重用它们的组件。组件重用减少了整体内存占用,这对于内存有限的设备非常重要。

为了使重用概念具体化,假设您正在创建一个绘图应用,让用户从调色板中选择一种颜色,并假设另一个应用已经开发了一个合适的颜色选择器,并允许该组件被重用。在这种情况下,绘图应用可以调用其他应用的颜色选择器,让用户选择一种颜色,而不是提供自己的颜色选择器。绘图应用不包含其他应用的颜色选择器,甚至也不链接到其他应用。相反,它会在需要时启动其他应用的颜色选择器组件。

当需要应用的任何部分(比如前面提到的颜色选择器)时,Android 会启动一个进程,并为该部分实例化 Java 对象。这就是为什么 Android 的应用没有单一的入口点(例如,没有 C 风格的main()功能)。相反,应用使用根据需要实例化和运行的组件。

活动

一个活动是一个呈现用户界面的组件,这样用户就可以与应用交互。例如,Android 的联系人应用包括输入新联系人的活动,其电话应用包括拨打电话号码的活动,其计算器应用包括执行基本计算的活动(参见图 1–2)。

images

图 1–2。Android 的计算器应用的主要活动让用户执行基本的计算。

虽然一个应用可以包含单个活动,但更常见的是应用包含多个活动。例如,Calculator 还包括一个“高级面板”活动,让用户计算平方根、执行三角学以及执行其他高级数学运算。

服务

一个服务是一个在后台无限期运行的组件,它不提供用户界面。与活动一样,服务在流程的主线程上运行;它必须产生另一个线程来执行耗时的操作。服务分为本地服务和远程服务。

  • 一个本地服务与应用的其余部分在相同的进程中运行。这样的服务使得实现后台任务变得容易。
  • 一个远程服务在一个单独的进程中运行。这种服务允许您执行进程间通信。

注意:服务不是一个单独的进程,尽管它可以被指定在一个单独的进程中运行。此外,服务不是线程。相反,一项服务让应用告诉 Android 它想在后台做的事情(即使用户没有直接与应用交互),并让应用向其他应用公开它的一些功能。

考虑一个服务,它通过一个活动播放音乐来响应用户的音乐选择。用户通过该活动选择要播放的歌曲,并且响应于该选择启动服务。该服务在另一个线程上播放音乐,以防止应用不响应对话框(在附录 C 中讨论)出现。

注意:使用服务来播放音乐的基本原理是,用户希望即使在启动音乐的活动离开屏幕之后,音乐也能继续播放。

广播接收器

广播接收器是接收广播并对其做出反应的组件。许多广播源自系统代码;例如,发出通知来指示时区已经改变或者电池电量低。

应用也可以发起广播。例如,一个应用可能希望让其他应用知道一些数据已经从网络下载到设备,现在可供他们使用。

内容提供商

内容提供商是一个组件,它使一个应用的特定数据集可供其他应用使用。数据可以存储在 Android 文件系统、SQLite 数据库或任何其他有意义的方式中。

内容提供者比直接访问原始数据更可取,因为它们将组件代码从原始数据格式中分离出来。这种分离防止了格式改变时的代码中断。

意图

意图是描述要执行的操作的消息(例如“发送电子邮件”或“选择照片”),或者在广播的情况下,提供已经发生的外部事件的描述(例如,设备的摄像头被激活)和正在被宣布的描述。

因为 Android 中几乎所有的东西都包含意图,所以有很多机会用你自己的组件替换现有的组件。例如,Android 提供发送电子邮件的意图。您的应用可以发送该意图来激活标准邮件应用,或者它可以注册一个响应“发送电子邮件”意图的活动,有效地用它自己的活动替换标准邮件应用。

这些消息被实现为android.content.Intent类的实例。一个Intent对象根据以下项目的某种组合来描述一条消息:

  • Action :命名要执行的动作的字符串,或者在广播意图的情况下,命名已经发生并正在报告的动作。动作由Intent常量描述,如ACTION_CALL(发起电话呼叫)、ACTION_EDIT(显示数据供用户编辑)和ACTION_MAIN(启动作为初始活动)。您还可以定义自己的操作字符串来激活应用中的组件。这些字符串应该包括应用包作为前缀(例如,"com.example.project.SELECT_COLOR")。
  • 类别:一个字符串,提供关于应该处理意图的组件种类的附加信息。例如,CATEGORY_LAUNCHER表示调用活动应该作为顶级应用出现在设备的应用启动器中。(在秘籍 1–4 中简要讨论了应用启动器。)
  • 组件名:一个字符串,指定用于 intent 的组件类的完全限定名(包加名称)。组件名称是可选的。如果置位,Intent对象被传递给指定类的一个实例。如果没有设置,Android 使用Intent对象中的其他信息来定位合适的目标。
  • Data :要操作的数据的统一资源标识符(比如联系人数据库中的人员记录)。
  • Extras :一组键值对,提供应该交付给处理意图的组件的附加信息。例如,给定一个发送电子邮件的动作,该信息可以包括消息的主题、正文等等。
  • 标志:位值,指示 Android 如何启动一个活动(例如,该活动应属于哪个任务——任务将在本章稍后讨论)以及如何在启动后处理该活动(例如,该活动是否可被视为最近的活动)。标志由Intent类中的常数表示;例如,FLAG_ACTIVITY_NEW_TASK指定该活动将成为该活动堆栈上新任务的开始。活动堆栈将在本章后面讨论。
  • Type :意图数据的 MIME 类型。通常情况下,Android 会从数据中推断出一种类型。通过指定类型,可以禁用该推断。

意图可以分为显性和隐性。一个显式意图通过名称指定目标组件(前面提到的组件名称项被赋值)。因为其他应用的开发人员通常不知道组件名称,所以显式意图通常用于应用内部消息(例如,一个活动启动位于同一应用内的另一个活动)。Android 向指定目标类的实例传递了一个明确的意图。只有Intent对象的组件名对确定哪个组件应该得到意图有影响。

一个隐含意图没有命名一个目标(组件名没有赋值)。隐式意图通常用于启动其他应用中的组件。Android 搜索最佳组件(执行请求动作的单个活动或服务)或组件(响应广播通知的一组广播接收器)来处理隐含的意图。在搜索过程中,Android 将Intent对象的内容与意图过滤器进行比较,意图过滤器是与可能接收意图的组件相关联的清单信息。

过滤器通告组件的能力,并且只识别组件可以处理的那些意图。它们向组件开放了接收广告类型的隐含意图的可能性。如果一个组件没有意图过滤器,它只能接收明确的意图。相比之下,带有过滤器的组件可以接收显式和隐式意图。Android 在将意图与意图过滤器进行比较时会参考Intent对象的动作、类别、数据和类型。它不考虑额外费用和旗帜。

清单

Android 通过检查应用的 XML 结构清单文件AndroidManifest.xml来了解应用的各种组件(以及更多)。例如,清单 1–1 展示了这个文件如何声明一个活动组件。

清单 1–1。 声明活动的清单文件

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"           package="com.example.project" android:versionCode="1"           android:versionName="1.0">    <application android:label="@string/app_name" android:icon="@drawable/icon">       <activity android:name=".MyActivity" android:label="@string/app_name">          <intent-filter>             <action android:name="android.intent.action.MAIN" />             <category android:name="android.intent.category.LAUNCHER" />          </intent-filter>       </activity>    </application> </manifest>

清单 1–1 以必要的<?xml version="1.0" encoding="utf-8"?> prolog 开始,它将该文件标识为 XML 1.0 版文件,其内容根据 UTF-8 编码标准进行编码。

清单 1–1 接下来显示一个<manifest>标签,它是这个 XML 文档的根元素;android标识 Android 命名空间,package标识 app 的 Java 包,versionCode / versionName标识版本信息。

嵌套在<manifest>中的是<application>,它是 app 组件标签的父标签。iconlabel属性指的是 Android 设备显示的代表应用的图标和标签资源。(秘籍 1–5 中简要讨论了资源。)

注意:资源由前缀@标识,后跟资源类别名称(如stringdrawable)、/和资源 ID(如app_nameicon)。

<application>标签的iconlabel属性指定缺省值,这些缺省值由标签没有指定这些属性的组件继承。

嵌套在<application>中的是<activity>,它描述了一个活动组件。这个标签的name属性标识了一个实现活动的类(MyActivity)。这个名字以句点字符开头,暗示它与com.example.project相关。

注意:在命令行创建AndroidManifest.xml时,句点不存在。然而,当这个文件在 Eclipse 中创建时,这个字符是存在的(在 Recipe 1–10 中讨论过)。无论如何,MyActivity是相对于<manifest>package值(com.example.project)。

嵌套在<activity>中的是<intent-filter>。这个标签声明了由封闭标签描述的组件的功能。例如,它通过嵌套的<action><category>标签来声明活动组件的功能。

  • <action>标识要执行的操作。这个标签的android:name属性被分配给"android.intent.action.MAIN",以将活动标识为应用的入口点。
  • <category>标识一个组件类别。这个标签的android:name属性被分配给"android.intent.category.LAUNCHER"来标识需要在应用启动器中显示的活动。

注:其他成分同样申报。例如,服务通过<service>标签声明,广播接收器通过<receiver>标签声明,内容提供商通过<provider>标签声明。除了可以在运行时创建的广播接收器之外,清单中没有声明的组件不是由 Android 创建的。

清单还可能包含<uses-permission>标签来标识应用需要的权限。例如,一个需要使用摄像头的应用会指定以下标签:<uses-permission android:name="android.permission.CAMERA" />

注意: <uses-permission>标签嵌套在<manifest>标签内。它们出现在与<application>标签相同的级别。

在应用安装时,应用请求的权限(通过<uses-permission>)由 Android 的包安装程序授予,基于对声明这些权限的应用的数字签名和/或与用户的交互的检查。

应用运行时,不会对用户进行任何检查。它在安装时被授予了特定的权限,可以根据需要使用该功能,或者该权限未被授予,任何使用该功能的尝试都将失败,而不会提示用户。

注: AndroidManifest.xml提供额外的信息,比如命名应用需要链接的任何库(除了默认的 Android 库),识别所有应用强制的对其他应用的权限(通过<permission>标签),比如控制谁可以启动应用的活动。

App 包

Android 应用是用 Java 编写的。为应用组件编译的 Java 代码被进一步转换成 Dalvik 的 DEX 格式。生成的代码文件以及任何其他所需的数据和资源随后被打包成一个应用包(APK) ,一个由.apk后缀标识的文件。

APK 不是应用,而是用于分发应用并将其安装在移动设备上。它不是一个应用,因为它的组件可能会重用另一个 APK 的组件,而且(在这种情况下)不是所有的应用都驻留在一个 APK 中。然而,通常称一个 APK 代表一个应用。

APK 必须使用证书(可识别应用的作者)进行签名,该证书的私钥由其开发者持有。证书不需要由证书颁发机构签名。相反,Android 允许用自签名证书对 apk 进行签名,这很典型。(APK 签名在秘籍 1–8 中讨论。)

APK 文件、用户 id 和安全性

安装在 Android 设备上的每个 APK 都有自己唯一的 Linux 用户 id,只要 APK 驻留在该设备上,这个用户 ID 就保持不变。

安全强制发生在进程级,所以包含在任何两个 APK 中的代码通常不能在同一个进程中运行,因为每个 APK 的代码需要作为不同的 Linux 用户运行。

然而,通过在每个 APK 的AndroidManifest.xml文件中给<manifest>标签的sharedUserId属性分配相同名称的用户 ID,可以让两个 apk 中的代码在同一个进程中运行。

当你进行这些分配时,你告诉 Android 这两个包将被视为相同的应用,具有相同的用户 id 和文件权限。

为了保持安全性,只有用相同签名签名的两个 apk(并且在其清单中请求相同的sharedUserId值)将被给予相同的用户 ID。

深度活动

活动由android.app.Activity类的子类描述,它是抽象android.content.Context类的间接子类。

注意: Context是一个抽象类,它的方法让应用访问关于它们环境的全局信息(比如它们的资源和文件系统),并允许应用执行上下文操作,比如启动活动和服务、广播意图和打开私有文件。

Activity子类覆盖 Android 在活动生命周期中调用的各种Activity 生命周期回调方法。例如,清单 1–2 中的SimpleActivity类扩展了Activity,也覆盖了void onCreate(Bundle bundle)void onDestroy()生命周期回调方法。

清单 1–2。 一种骨骼活动

`import android.app.Activity;
import android.os.Bundle;

public class SimpleActivity extends Activity
{
   @Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState); // Always call superclass method first.
      System.out.println("onCreate(Bundle) called");
   }
   @Override
   public void onDestroy()
   {
      super.onDestroy(); // Always call superclass method first.
      System.out.println("onDestroy() called");
   }
}`

在清单 1–2 中覆盖的onCreate(Bundle)onDestroy()方法首先调用它们的超类对应物,当覆盖void onStart()void onRestart()void onResume()void onPause()void onStop()生命周期回调方法时,必须遵循这种模式。

  • 首次创建活动时会调用onCreate(Bundle)。此方法用于创建活动的用户界面,根据需要创建后台线程,以及执行其他全局初始化。向onCreate()传递一个包含活动先前状态的android.os.Bundle对象,如果该状态被捕获的话;否则,传递空引用。Android 总是在调用onCreate(Bundle)之后调用onStart()方法。
  • 在活动对用户可见之前调用onStart()。Android 在活动来到前台时调用onStart()后调用onResume()方法,在活动变为隐藏时调用onStart()后调用onStop()方法。
  • onRestart()在活动停止之后,再次开始之前被调用。Android 总是在调用onRestart()之后调用onStart()
  • 在活动开始与用户交互之前调用onResume()。此时,活动获得焦点,用户输入指向该活动。Android 总是在调用onResume()之后调用onPause()方法,但只是在活动必须暂停的时候。
  • onPause()在 Android 即将恢复另一个活动时被调用。此方法通常用于保存未保存的更改、停止可能消耗处理器周期的动画等。它应该很快执行它的工作,因为下一个活动在它返回之前不会恢复。Android 在活动开始与用户交互时调用onPause()后调用onResume(),在活动变得对用户不可见时调用onStop()
  • 当活动对用户不再可见时,调用onStop()。这可能是因为该活动正在被销毁,或者因为另一个活动(现有活动或新活动)已经恢复并覆盖了该活动。Android 在活动即将回来与用户交互时调用onStop(),后调用onRestart(),在活动即将离开时调用onDestroy()方法。
  • onDestroy()在活动被销毁之前被调用,除非内存紧张,Android 强制杀死活动的进程。在这种情况下,onDestroy()永远不会被调用。如果onDestroy()被调用,这将是该活动收到的最后一个调用。

注意: Android 可以在onPause()onStop()onDestroy()返回后随时杀死托管活动的进程。从onPause()返回到onResume()被调用,活动处于可终止状态。在onPause()返回之前,该活动不会再次被取消。

这七种方法定义了活动的整个生命周期,并描述了以下三个嵌套循环:

  • 活动的整个生命周期被定义为从第一次调用onCreate(Bundle)到最后一次调用onDestroy()的所有内容。一个活动在onCreate(Bundle)执行其所有的初始设置“全局”状态,并在onDestroy()释放所有剩余的资源。例如,如果活动有一个线程在后台运行以从网络下载数据,它可能会在onCreate(Bundle)中创建该线程,并在onDestroy()中停止该线程。
  • 活动的可见生存期被定义为从调用onStart()到相应调用onStop()的所有内容。在此期间,用户可以在屏幕上看到活动,尽管它可能不在前台与用户交互。在这两种方法之间,活动可以维护向用户显示自身所需的资源。例如,它可以在onStart()中注册一个广播接收器,以监视影响其用户界面的变化,并在用户看不到活动显示的内容时在onStop()中注销该对象。当活动在对用户可见和隐藏之间交替时,onStart()onStop()方法可以被多次调用。
  • 活动的前台生存期被定义为从对onResume()的调用到对onPause()的相应调用的所有内容。在此期间,该活动位于屏幕上所有其他活动的前面,并与用户进行交互。活动可以频繁地在恢复和暂停状态之间转换;例如,onPause()在设备进入睡眠或新活动开始时被调用,而onResume()在活动结果或新意图被传递时被调用。这两种方法中的代码应该相当轻量级。

注意:每个生命周期回调方法都是一个钩子,活动可以覆盖它来执行适当的工作。当活动对象第一次被实例化时,所有活动都必须实现onCreate(Bundle)来执行初始设置。许多活动还实现了onPause()来提交数据更改,或者准备停止与用户的交互。

Figure 1–3 用这七种方法说明了活动的生命周期。

images

图 1–3。 一个活动的生命周期表明不能保证onDestroy()会被调用。

因为onDestroy()可能不会被调用,你不应该指望使用这个方法作为保存数据的地方。例如,如果一个活动正在编辑内容提供商的数据,这些编辑通常应该在onPause()中提交。

相比之下,onDestroy()通常被实现来释放与活动相关联的资源(比如线程),以便被销毁的活动不会在它的应用的其余部分仍在运行时留下这些东西。

图 1–3 显示一个活动是通过调用startActivity()开始的。更具体地说,活动是通过创建一个描述显式或隐式意图的Intent对象,并将该对象传递给Contextvoid startActivity(Intent intent)方法(启动一个新活动;完成时不返回任何结果)。

或者,可以通过调用Activityvoid startActivityForResult(Intent intent, int requestCode)方法来启动活动。指定的int结果作为参数返回给Activityvoid onActivityResult(int requestCode, int resultCode, Intent data)回调方法。

注意:响应活动可以通过调用ActivityIntent getIntent()方法来查看导致其启动的初始意图。Android 调用活动的void onNewIntent(Intent intent)方法(也位于Activity类中)将任何后续意图传递给活动。

假设你已经创建了一个名为SimpleActivity的应用,这个应用由SimpleActivity(在清单 1–2 中描述)和SimpleActivity2类组成。现在假设您想从SimpleActivityonCreate(Bundle)方法中启动SimpleActivity2。以下代码片段向您展示了如何启动SimpleActivity2:

Intent intent = new Intent(SimpleActivity.this, SimpleActivity2.class); SimpleActivity.this.startActivity(intent);

第一行创建一个描述明确意图的Intent对象。它通过将当前SimpleActivity实例的引用和SimpleActivity2Class实例传递给Intent(Context packageContext, Class<?> cls)构造函数来初始化这个对象。

第二行将这个Intent对象传递给startActivity(Intent),后者负责启动由SimpleActivity2.class描述的活动。如果startActivity(Intent)无法找到指定的活动(这不应该发生),它将抛出一个android.content.ActivityNotFoundException实例。

活动必须在应用的AndroidManifest.xml文件中声明,否则无法启动(因为它们对 Android 是不可见的)。例如,清单 1–3 中的AndroidManifest.xml文件声明了SimpleActivitySimpleActivity2——省略号表示与本次讨论无关的内容。

清单 1–3。 SimpleActivity的清单文件

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"           package="com.example.project" ...>    <application ...>       <activity android:name=".SimpleActivity" ...>          **<intent-filter ...>**             **<action android:name="android.intent.action.MAIN" />**             **<category android:name="android.intent.category.LAUNCHER" />**          **</intent-filter>**       </activity>       <activity android:name=".SimpleActivity2" ...>           **<intent-filter ...>**               **<action android:name="android.intent.action.VIEW" />**               **<data android:mimeType="image/jpeg" />**               **<category android:name="android.intent.category.DEFAULT" />**           **</intent-filter>**       </activity>       ...    </application> </manifest>

清单 1–3 揭示了SimpleActivitySimpleActivity2中的每一个都通过嵌套在<activity>中的<intent-filter>标签与一个意图过滤器相关联。SimpleActivity2<intent-filter>标签帮助 Android 确定当Intent对象的值匹配以下标签值时,该活动将被启动:

  • <action>android:name属性被赋予"android.intent.action.VIEW"
  • <data>android:mimeType属性被指定为"image/jpeg" MIME 类型——附加属性(如android:path)通常用于定位要查看的数据
  • <category>android:name属性被分配给"android.intent.category.DEFAULT",以允许在没有明确指定其组件的情况下启动活动。

以下代码片段向您展示了如何隐式启动SimpleActivity2:

Intent intent = new Intent(); intent.setAction("android.intent.action.VIEW"); intent.setType("image/jpeg"); intent.addCategory("android.intent.category.DEFAULT"); SimpleActivity.this.startActivity(intent);

前四行创建一个描述隐含意图的Intent对象。传递给IntentIntent setAction(String action)Intent setType(String type)Intent addCategory(String category)方法的值指定了意图的动作、MIME 类型和类别。它们帮助 Android 将SimpleActivity2识别为要启动的活动。

活动、任务和活动栈

Android 将一系列相关活动称为一个任务,并提供一个活动堆栈(也称为历史堆栈后台堆栈)来记住这个序列。启动任务的活动是推入堆栈的初始活动,称为根活动。该活动通常是用户通过设备的应用启动器选择的活动。当前正在运行的活动位于堆栈的顶部。

当当前活动启动另一个活动时,新活动被推送到堆栈上并获得焦点(成为正在运行的活动)。前一个活动保留在堆栈上,但已停止。当活动停止时,系统保留其用户界面的当前状态。

当用户按下设备的 BACK 键时,当前活动从堆栈中弹出(活动被销毁),之前的活动作为正在运行的活动恢复操作(其用户界面的先前状态被恢复)。

堆栈中的活动永远不会重新排列,只会从堆栈中推出和弹出。当当前活动启动时,活动被推到堆栈上,当用户使用 BACK 键离开时,活动弹出堆栈。因此,堆栈作为“后进先出”的对象结构运行。

每当用户按 BACK 时,堆栈中的一个活动就会弹出来显示前一个活动。这一直持续到用户返回到主屏幕或者任务开始时正在运行的任何活动。当所有活动都从堆栈中移除时,任务就不再存在。

查看 Google 在线 Android 文档中的“任务和后台堆栈”部分,了解更多关于活动和任务的信息:[developer.android.com/guide/topics/fundamentals/tasks-and-back-stack.html](http://developer.android.com/guide/topics/fundamentals/tasks-and-back-stack.html)

深度服务

服务由抽象的android.app.Service类的子类描述,它是Context的间接子类。

子类覆盖了 Android 在服务生命周期中调用的各种生命周期回调方法。例如,清单 1–4 中的SimpleService类扩展了Service,也覆盖了void onCreate()void onDestroy()生命周期回调方法。

清单 1–4。 一款骨骼服,版本 1

`import android.app.Service;

public class SimpleService extends Service
{
   @Override
   public void onCreate()
   {
      System.out.println("onCreate() called");
   }
   @Override
   public void onDestroy()
   {       System.out.println("onDestroy() called");
   }
   @Override
   public IBinder onBind(Intent intent)
   {
      System.out.println("onBind(Intent) never called");
      return null;
   }
}`

最初创建服务时调用onCreate(),删除服务时调用onDestroy()。因为它是抽象的,IBinder onBind(Intent intent)生命周期回调方法(将在本节稍后描述)必须总是被覆盖,即使只是为了返回null,这表明该方法被忽略。

注意: Service子类通常会覆盖onCreate()onDestroy()来执行初始化和清理。与ActivityonCreate(Bundle)onDestroy()方法不同,ServiceonCreate()方法不会被重复调用,它的onDestroy()方法总是被调用。

服务的生命周期发生在调用时间onCreate()和返回时间onDestroy()之间。与活动一样,服务在onCreate()中初始化,在onDestroy()中清理。例如,音乐回放服务可以在onCreate()中创建播放音乐的线程,并在onDestroy()中停止该线程。

本地服务通常通过ContextComponentName startService(Intent intent)方法启动,该方法返回一个android.content.ComponentName实例来标识启动的服务组件,如果服务不存在,则返回空引用。此外,startService(Intent)导致了如图图 1–4 所示的生命周期。

images

图 1–4。 startService(Intent)启动的服务的生命周期以调用onStartCommand(Intent, int, int)为特征。

startService(Intent)的调用导致对onCreate(),的调用,随后是对int onStartCommand(Intent intent, int flags, int startId)的调用。后一种生命周期回调方法取代了不推荐使用的void onStart(Intent intent, int startId)方法,使用以下参数进行调用:

  • intent是传递给startService(Intent)Intent对象。
  • flags可以提供关于开始请求的附加数据,但通常设置为 0。
  • startID是描述该启动请求的唯一整数。服务可以将这个值传递给Serviceboolean stopSelfResult(int startId)方法来停止自己。

onStartCommand(Intent, int, int)处理Intent对象,通常返回常量Service.START_STICKY来表示服务将继续运行,直到被显式停止。此时,服务正在运行并将继续运行,直到发生以下事件之一:

  • 另一个组件通过调用Contextboolean stopService(Intent intent)方法来停止服务。无论startService(Intent)被呼叫的频率如何,只需要一次stopService(Intent)呼叫。
  • 服务通过调用Service的重载stopSelf()方法之一,或者调用ServicestopSelfResult(int)方法来停止自身。

stopService(Intent)stopSelf()stopSelfResult(int)被调用后,Android 调用onDestroy()让服务执行清理任务。

注意:调用startService(Intent)启动服务时,onBind(Intent)不被调用。

清单 1–5 展示了一个可以在startService(Intent)方法的上下文中使用的框架服务类。

清单 1–5。 骨骼服,第二版

`import android.app.Service;

public class SimpleService extends Service
{
   @Override
   public void onCreate()
   {
      System.out.println("onCreate() called");
   }
   @Override
   public int onStartCommand(Intent intent, int flags, int startId)
   {
      System.out.println("onStartCommand(Intent, int, int) called");
      return START_STICKY;
   }
   @Override
   public void onDestroy()
   {
      System.out.println("onDestroy() called");
   }    @Override
   public IBinder onBind(Intent intent)
   {
      System.out.println("onBind(Intent) never called");
      return null;
   }
}`

下面的代码片段假定位于清单 1–2 的SimpleActivity类的onCreate()方法中,通过一个明确的意图,使用startService(Intent)来启动清单 1–5 的SimpleService类的一个实例:

Intent intent = new Intent(SimpleActivity.this, SimpleService.class); SimpleActivity.this.startService(intent);

远程服务通过Contextboolean bindService(Intent service, ServiceConnection conn, int flags)方法启动,该方法连接到一个正在运行的服务,在必要时创建服务,当成功连接时返回‘true’。bindService(Intent, ServiceConnection, int)导致了由图 1–5 所示的生命周期。

images

图 1–5。 bindService(Intent, ServiceConnection, int)启动的服务的生命周期不包括对onStartCommand(Intent, int, int)的调用。

bindService(Intent, ServiceConnection, int)的调用导致对onCreate()的调用,然后是对onBind(Intent)的调用,后者返回客户端用来与服务交互的通信通道(实现android.os.IBinder接口的类的实例)。

客户端与服务的交互如下:

  1. The client subclasses android.content.ServiceConnection and overrides this class's abstract void onServiceConnected(ComponentName className, IBinder service) and void onServiceDisconnected(ComponentName name) methods in order to receive information about the service as the service is started and stopped. When bindService(Intent, ServiceConnection, int) returns true, the former method is called when a connection to the service has been established; the IBinder argument passed to this method is the same value returned from onBind(Intent). The latter method is called when a connection to the service has been lost.

    当承载服务的进程崩溃或被终止时,通常会发生连接丢失。ServiceConnection实例本身并没有被删除——到服务的绑定将保持活动状态,当服务下次运行时,客户端将收到对onServiceConnected(ComponentName, IBinder)的调用。

  2. 客户端将ServiceConnection子类对象传递给bindService(Intent, ServiceConnection, int)

客户端通过调用Contextvoid unbindService(ServiceConnection conn)方法断开与服务的连接。服务重新启动后,此组件不再接收呼叫。如果没有其他组件绑定到该服务,则允许该服务随时停止。

在服务停止之前,Android 用传递给unbindService(ServiceConnection)Intent对象调用服务的boolean onUnbind(Intent intent)生命周期回调方法。假设onUnbind(Intent)没有返回‘true’,这告诉 Android 在每次客户端随后绑定到服务时调用服务的void onRebind(Intent intent)生命周期回调方法,Android 调用onDestroy()来销毁服务。

清单 1–6 展示了一个可以在bindService(Intent, ServiceConnection, int)方法的上下文中使用的框架服务类。

清单 1–6。 骨骼服,第三版

`import android.app.Service;

public class SimpleService extends Service
{
   public class SimpleBinder extends Binder
   {
      SimpleService getService()
      {
         return SimpleService.this;
      }
   }
   private final IBinder binder = new SimpleBinder();
   @Override    public IBinder onBind(Intent intent)
   {
      return binder;
   }
   @Override
   public void onCreate()
   {
      System.out.println("onCreate() called");
   }
   @Override
   public void onDestroy()
   {
      System.out.println("onDestroy() called");
   }
}`

清单 1–6 首先声明了一个扩展了android.os.Binder类的SimpleBinder内部类。SimpleBinder声明了一个返回SimpleService子类实例的SimpleService getService()方法。

注: BinderIBinder接口一起支持远程过程调用机制,用于进程间的通信。尽管这个例子假设服务和应用的其他部分在同一个进程中运行,但是仍然需要BinderIBinder

清单 1–6 接下来实例化SimpleBinder并将实例的引用分配给私有binder字段。该字段的值从随后覆盖的onBind(Intent)方法返回。

让我们假设清单 1–2 中的SimpleActivity类声明了一个名为ss ( private SimpleService ss;)的私有SimpleService字段。继续,让我们假设下面的代码片段包含在SimpleActivityonCreate(Bundle)方法中:

ServiceConnection sc = new ServiceConnection() {    public void onServiceConnected(ComponentName className, IBinder service)    {       ss = ((SimpleService.SimpleBinder) service).getService();       System.out.println("Service connected");    }    public void onServiceDisconnected(ComponentName className)    {       ss = null; System.out.println("Service disconnected");    } }; bindService(new Intent(SimpleActivity.this, SimpleService.class), sc,             Context.BIND_AUTO_CREATE);

这段代码首先实例化了一个ServiceConnection子类。覆盖的onServiceConnected(ComponentName, IBinder)方法使用service参数调用SimpleBindergetService()方法并保存结果。

虽然它必须存在,但是覆盖的onServiceDisconnected(ComponentName)方法永远不应该被调用,因为SimpleServiceSimpleActivity在同一个进程中运行。

接下来,代码片段将ServiceConnection子类对象,以及将SimpleService标识为意图目标的意图和Context.BIND_AUTO_CREATE(创建持久连接)传递给bindService(Intent, ServiceConnection, int)

注意:一个服务可以被启动(用startService(Intent))并绑定连接(用bindService(Intent, ServiceConnection, int))。在这种情况下,只要服务启动,Android 就会保持服务运行,或者一个或多个带有BIND_AUTO_CREATE标志的连接已经连接到服务。一旦这两种情况都不成立,服务的onDestroy()方法被调用,服务被终止。从onDestroy()返回后,所有清理工作,如停止线程或注销广播接收器,都应完成。

无论您如何启动服务,应用的AndroidManifest.xml文件都必须包含该组件的条目。下面的条目声明了SimpleService:

<service android:name=".SimpleService"> </service>

注意:虽然前面的例子使用了bindService(Intent, ServiceConnection, int)来启动一个本地服务,但是使用这个方法来启动一个远程服务更为典型。第五章向你介绍远程服务。

深度广播接收器

广播接收器由抽象android.content.BroadcastReceiver类的子类和覆盖BroadcastReceiver的抽象void onReceive(Context context, Intent intent)方法的类来描述。例如,清单 1–7 中的SimpleBroadcastReceiver类扩展了BroadcastReceiver并覆盖了这个方法。

清单 1–7。 一个骨骼广播接收器

public class **SimpleBroadcastReceiver extends BroadcastReceiver** {    @Override    **public void onReceive(Context context, Intent intent)**    {       System.out.println("onReceive(Context, Intent) called");    } }

通过创建一个Intent对象并将该对象传递给任意一个Context的广播方法(比如Context的重载sendBroadcast()方法)来启动一个广播接收器,这些方法将消息广播给所有感兴趣的广播接收器。

下面的代码片段,假定位于清单 1–2 的SimpleActivity类的onCreate()方法中,启动清单 1–7 的SimpleBroadcastReceiver类的一个实例:

Intent intent = new Intent(SimpleActivity.this, SimpleBroadcastReceiver.class); intent.putExtra("message", "Hello, broadcast receiver!"); SimpleActivity.this.sendBroadcast(intent);

调用IntentIntent putExtra(String name, String value)方法将消息存储为一个键/值对。和Intent的其他putExtra()方法一样,这个方法返回一个对Intent对象的引用,这样方法调用可以链接在一起。

除非您动态创建一个广播接收器,AndroidManifest.xml必须有一个此组件的条目。下面的条目声明了SimpleBroadcastReceiver:

<receiver android:name=".SimpleBroadcastReceiver"> </receiver>

深度内容提供商

内容提供者由抽象类android.content.ContentProvider的子类和覆盖抽象方法ContentProvider的类来描述(例如String getType(Uri uri))。例如,清单 1–8 中的SimpleContentProvider类扩展了ContentProvider并覆盖了这些方法。

清单 1–8。 一个骨架内容提供商

public class **SimpleContentProvider extends ContentProvider** {    @Override    **public int delete(Uri uri, String selection, String[] selectionArgs)**    {       System.out.println("delete(Uri, String, String[]) called");       return 0;    }    @Override    **public String getType(Uri uri)**    {       System.out.println("getType(Uri) called");       return null;    }    @Override    **public Uri insert(Uri uri, ContentValues values)**    {       System.out.println("insert(Uri, ContentValues) called");       return null;    }    @Override    **public boolean onCreate()**    {       System.out.println("onCreate() called");       return false;    }    @Override    **public Cursor query(Uri uri, String[] projection, String selection,**                        **String[] selectionArgs, String sortOrder)**    {       System.out.println("query(Uri, String[], String, String[], String) called");       return null;    }    @Override    **public int update(Uri uri, ContentValues values, String selection,**                      **String[] selectionArgs)**    {       System.out.println("update(Uri, ContentValues, String, String[]) called");       return 0;    } }

客户端不会实例化SimpleContentProvider并直接调用这些方法。相反,它们实例化抽象类android.content.ContentResolver的一个子类,并调用它的方法(比如public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder))。

注意:一个ContentResolver实例可以和任何内容提供者对话;它与提供者合作来管理任何涉及的进程间通信。

AndroidManifest.xml该组件必须有一个条目。下面的条目声明了SimpleContentProvider:

<provider android:name=".SimpleContentProvider"> </provider>

1–1。安装 Android SDK

问题

您已经阅读了之前的 Android 介绍,并渴望开发您的第一个 Android 应用。但是,你必须安装 Android SDK 2.3 才能开发应用。

解决方案

谷歌为 Windows、基于英特尔的 Mac OS X 和 Linux 操作系统提供了 Android SDK 2.3 发行版文件。下载并解压缩适用于您的平台的文件,并将其解压缩的主目录移动到一个方便的位置。您可能还想更新 PATH 环境变量,以便可以从文件系统中的任何地方访问 SDK 的命令行工具。

在下载和安装该文件之前,您必须了解 SDK 的要求。如果您的开发平台不满足这些要求,您就不能使用 SDK。

Android SDK 2.3 支持以下操作系统:

  • Windows XP (32 位)、Vista (32 位或 64 位)或 Windows 7 (32 位或 64 位)
  • Mac OS X 10.5.8 或更高版本(仅限 x86)
  • Linux(在 Ubuntu Linux,Lucid Lynx 上测试):需要 GNU C 库(glibc ) 2.11 或更高版本。64 位发行版必须能够运行 32 位应用。要了解如何添加对 32 位应用的支持,请参阅位于[developer.android.com/sdk/installing.html#troubleshooting](http://developer.android.com/sdk/installing.html#troubleshooting)的 Ubuntu Linux 安装说明。

你会很快发现 Android SDK 2.3 被组织成各种组件:SDK 工具、SDK 平台工具、不同版本的 Android 平台(也称为 Android 软件栈)、SDK 插件、Windows 的 USB 驱动程序、示例和离线文档。每个组件都需要最小数量的磁盘存储空间;所需空间总量取决于您选择安装的组件:

  • SDK 工具:SDK 的工具需要大约 35MB 的磁盘存储空间,必须安装。
  • SDK 平台工具:SDK 的平台工具需要大约 6MB 的磁盘存储空间,必须安装。
  • Android 平台:每个 Android 平台对应一个特定版本的 Android,需要大约 150MB 的磁盘存储空间。必须至少安装一个 Android 平台。
  • SDK 附加组件:每个可选的 SDK 附加组件(如 Google APIs 或第三方供应商的 API 库)需要大约 100MB 的磁盘存储空间。
  • 用于 Windows 的 USB 驱动程序:用于 Windows 平台的可选 USB 驱动程序需要大约 10MB 的磁盘存储空间。如果你在 Mac OS X 或 Linux 上开发,你不需要安装 USB 驱动。
  • 样例:每个 Android 平台的可选 app 样例都需要大约 10MB 的磁盘存储空间。
  • 离线文档:您可以选择下载文档,这样即使没有连接到互联网也可以查看,而不必在线访问 Android 文档。脱机文档需要大约 250MB 的磁盘存储空间。

最后,您应该确保安装了以下附加软件:

  • JDK 5 或 JDK 6:你需要安装其中一个 Java 开发工具包(JDK)来编译 Java 代码。仅仅安装 Java 运行时环境(JRE)是不够的。
  • Apache Ant:Linux 和 Mac 需要安装 Ant 1 . 6 . 5 或更高版本,Windows 需要安装 Ant 1.7 或更高版本,这样才能构建 Android 项目。

注意:如果您的开发平台上已经安装了一个 JDK,请花点时间确保它符合前面列出的版本要求(5 或 6)。一些 Linux 发行版可能包含 JDK 1.4,Android 开发不支持该版本。此外,Gnu 编译器 Java 版也不受支持。

它是如何工作的

将浏览器指向[developer.android.com/sdk/index.html](http://developer.android.com/sdk/index.html),下载android-sdk_r08-windows.zip (Windows)、android-sdk_r08-mac_86.zip (Mac OS X)和android-sdk_r08-linux_86.tgz (Linux)中的一个。

注意: Windows 开发者可以选择下载并运行installer_r08-windows.exe。这个工具可以自动完成安装过程中的所有工作。

比如你运行 Windows XP,下载android-sdk_r08-windows.zip。在解压缩这个文件之后,将解压缩的android-windows-sdk主目录移动到文件系统中一个方便的位置;例如,您可能会将未归档的C:\unzipped\android-sdk_r08-windows\android-sdk-windows主目录移动到 C:驱动器上的根目录,从而产生C:\android-sdk-windows

注意:要完成安装,请将tools子目录添加到 PATH 环境变量中,这样您就可以从文件系统中的任何地方访问 SDK 的命令行工具。

android-windows-sdk的后续检查显示,该主目录包含以下子目录和文件:

  • add-ons :这个最初为空的目录存储了来自谷歌和其他厂商的插件;例如,Google APIs 附加组件就存储在这里。
  • platforms :这个最初为空的目录将 Android 平台存储在单独的子目录中。例如,Android 2.3 将存储在一个platforms子目录中,而 Android 2.2 将存储在另一个platforms子目录中。
  • tools :这个目录包含一组平台无关的开发和剖析工具。该目录中的工具可能会随时更新,与 Android 平台版本无关。
  • SDK Manager.exe :一个启动 Android SDK 和 AVD 管理器工具的特殊工具,您可以使用它向您的 SDK 添加组件。
  • SDK Readme.txt :告诉您如何执行 SDK 的初始设置,包括如何在所有平台上启动 Android SDK 和 AVD 管理器工具。

tools目录包含各种有用的工具,包括:

  • android :创建和更新 Android 项目;用新的平台、插件和文档更新 Android SDK 以及创建、删除和查看 Android 虚拟设备(在秘籍 1–3 中讨论)。
  • emulator :运行一个完整的 Android 软件栈,下至内核级,包括一组您可以访问的预装应用(如浏览器)。
  • sqlite3 :管理 Android 应用创建的 SQLite 数据库。
  • zipalign :对 APK 文件进行归档对齐优化。

1–2。安装 Android 平台

问题

安装 Android SDK 不足以开发 Android 应用;您还必须安装至少一个 Android 平台。

解决方案

使用SDK Manager工具安装一个 Android 平台。

它是如何工作的

运行SDK Manager。该工具呈现了 Android SDK 和 AVD 管理器对话框,随后是刷新源选择安装包对话框。

Android SDK 和 AVD 管理器识别虚拟设备、已安装的软件包和可用的软件包。它还允许您配置代理服务器和其他设置。

出现此对话框时,对话框右侧列表中的已安装软件包条目会突出显示,列表右侧的窗格会标识所有已安装的软件包。如果你是第一次安装 Android,这个面板显示只安装了 Android SDK 工具(修订版 8)组件。

注意:您也可以使用android工具显示 Android SDK 和 AVD 管理器对话框。通过在命令行中单独指定android来完成这项任务。以这种方式显示时, Android SDK 和 AVD 管理器突出显示虚拟设备,而不是已安装的软件包。

显示该对话框后,SDK Manager扫描 Google 的服务器,寻找可安装的组件包。刷新源对话框显示其进度。

SDK Manager完成扫描后,它会出现选择要安装的软件包对话框(参见图 1–6)让你选择你想要安装的 SDK 组件。

images

图 1–6。 软件包列表标识了那些可以安装的软件包。

注意: Google 建议您在安装 SDK 组件之前禁用任何活动的防病毒软件。否则,您可能会遇到一个 SDK 管理器:未能安装对话框,告诉您无法重命名或移动文件夹,并告诉您暂时禁用防病毒软件,然后单击对话框的“是”按钮重试。

选择要安装的软件包对话框显示了一个软件包列表,其中列出了那些可以安装的软件包。它在已接受安装的软件包旁边显示复选标记,在拒绝安装的软件包旁边显示 x。

对于突出显示的软件包,“软件包说明和许可证”提供了软件包说明、依赖于正在安装的此软件包的其他软件包列表、有关包含该软件包的归档文件的信息以及其他信息。此外,您可以选择一个单选按钮来接受或拒绝该包。

注意:在某些情况下,一个 SDK 组件可能需要另一个组件或 SDK 工具的特定最低版本。除了记录这些依赖关系的包描述&许可证之外,如果有需要解决的依赖关系,开发工具会用调试警告通知您。

因为这本书关注的是 Android 2.3,所以你需要安装的包只有 Android SDK 平台-工具,修订版 1 和 SDK 平台 Android 2.3,API 9,修订版 1。通过单击各自窗格上的拒绝单选按钮,可以取消选中所有其他选中的包条目。

注意:如果你计划开发能在装有早期版本 Android 的设备上运行的应用,你可能想在那些版本旁边留下复选标记。然而,此时没有必要这样做;您可以随时回来通过SDK Manager添加这些版本。

确保只检查了这些条目后,单击 Install 按钮开始安装。图 1–7 显示了产生的安装档案对话框。

images

图 1–7。 安装归档文件对话框显示下载和安装每个选定的软件包归档文件的进度。

你可能会遇到 ADB 重启对话框,它告诉你一个依赖于 Android Debug Bridge (ADB)的包已经更新,并询问你是否要现在重启 ADB。点击是按钮,关闭亚行重启,然后在安装档案对话框中点击关闭。

现在,您应该看到 Android SDK 和 AVD 管理器的已安装软件包窗格,除了显示 Android SDK 工具修订版 8 之外,还显示 Android SDK 平台工具修订版 1 和 SDK 平台 Android 2.3、 API 9 修订版 1。您还应该观察以下新子目录:

  • platform-tools(在android-sdk-windows中)
  • android-9(在android-sdk-windows/platforms中)

platform-tools包含开发工具,可能会随着每个平台版本的发布而更新。其工具包括aapt (Android 素材打包工具——查看、创建、更新兼容 Zip 的档案(.zip.jar.apk);并将资源编译成二进制素材)、adb(Android Debug Bridge——管理仿真器实例或 Android 驱动的设备的状态)、dx (Dalvik 可执行文件——从 Java .class文件生成 Android 字节码)。android-9存储 Android 2.3 数据和面向用户界面的文件。

提示您可能希望将platform-tools添加到 PATH 环境变量中,这样您就可以从文件系统中的任何地方访问这些工具。

可用软件包和组件更新检测

与可用软件包对应的窗格显示可用于安装的软件包。它默认提供来自谷歌 Android 仓库和第三方插件(来自谷歌和三星)的软件包,但你可以添加其他托管自己的 Android SDK 插件的网站,然后从这些网站下载 SDK 插件。

例如,假设一家移动运营商或设备制造商提供了他们自己的 Android 设备支持的附加 API 库。为了使用其库来帮助开发应用,您必须安装运营商/设备制造商的 Android SDK 插件。

如果运营商或设备制造商在其网站上托管了 SDK 附加存储库文件,您必须按照以下步骤将网站添加到 SDK 管理器:

  1. 从列表框中选择可用的包。
  2. 单击结果窗格上的添加附加站点按钮,并将网站的repository.xml文件的 URL 输入到结果对话框的文本字段中。单击确定。

网站上提供的任何 SDK 组件都将出现在可用软件包下。

现有 SDK 组件的新版本偶尔会发布,并通过 SDK 存储库提供。在大多数情况下,假设您的环境中已经安装了这些组件,您会希望尽快下载新的修订版。

了解组件更新的最简单方法是访问“可用软件包”窗格。当您发现有新的版本可用时,使用SDK Manager将其下载并安装到您的环境中,使用与安装 Android 2.3 平台相同的方式。新组件将取代旧组件安装,但不会影响您的应用。

1–3。创建 Android 虚拟设备

问题

安装 Android SDK 和 Android 平台后,您就可以开始创建 Android 应用了。然而,你将无法通过emulator工具运行这些应用,直到你创建了一个 Android 虚拟设备(AVD) ,一个代表 Android 设备的设备配置。

解决方案

使用SDK Manager工具创建一个 AVD。

它是如何工作的

如有必要,运行SDK Manager。点击左侧列表中 Android SDK 和 AVD Manager 对话框的虚拟设备条目。您应该会看到图 1–8 中所示的窗格。

images

图 1–8。 初始没有安装 avd。

单击新建按钮。图 1–9 显示了产生的创建新的 Android 虚拟设备(AVD) 对话框。

images

图 1–9。 一个 AVD 由名称、目标平台、SD 卡、皮肤、硬件属性组成。

Figure 1–9 揭示了一个 AVD 有一个名字,目标是一个特定的 Android 平台,可以模拟 SD 卡,并提供一个具有一定屏幕分辨率的皮肤。输入 test_AVD 作为名称,选择Android 2.3 – API Level 9作为目标平台,输入 100 作为 SD 卡的大小字段。选择Android 2.3 – API Level 9会导致为皮肤选择Default (HVGA),其Abstracted LCD density属性设置为160每英寸点数(dpi)。

注意:如果你已经安装了 Android 2.3.1,选择Android 2.3.1 – API Level 9会导致Default (WVGA800)被选择为皮肤,并且Abstracted LCD density属性被设置为240 dpi。此外,还存在设置为24兆字节的Max VM application heap size属性。

输入之前的值并保持屏幕默认值后,通过单击创建 AVD 完成 AVD 创建。图 1–8 中的 AVD 窗格现在将包含一个test_AVD条目。

注意:在创建您计划用来测试编译后的应用的 AVD 时,请确保目标平台的 API 级别高于或等于您的应用所需的 API 级别。换句话说,如果您计划在 AVD 上测试您的应用,您的应用将无法访问比 AVD API 级别支持的 API 更新的平台 API。

虽然使用SDK Manager创建 AVD 更容易,但是您也可以通过指定android create avd -n *name* -t *targetID* [-option *value*]...使用android工具来完成这项任务。给定这个语法, name 标识设备配置(如target_AVD), targetID 是标识目标 Android 平台的整数 id(可以通过执行android list targets获得这个整数 ID),而[-option *value*]...标识一系列选项(如 SD 卡大小)。

如果您没有指定足够的选项,android会提示创建一个自定义硬件配置文件。如果您不想要自定义硬件配置文件,而希望使用默认的硬件仿真选项,请按 Enter 键。例如,android create avd -n test_AVD -t 1命令行会创建一个名为test_AVD的 AVD。这个命令行假设 1 对应于 Android 2.3 平台,并提示创建自定义硬件配置文件。

注意:每个 AVD 都是一个独立的设备,有自己的用户数据专用存储器、自己的 SD 卡等等。当您使用 AVD 启动emulator工具时,该工具会从 AVD 的目录中加载用户数据和 SD 卡数据。默认情况下,emulator将用户数据、SD 卡数据和缓存存储在分配给 AVD 的目录中。

1–4 岁。启动 AVD

问题

您必须启动 AVD,这可能需要几分钟时间,然后才能在其上安装和运行应用,并想知道如何完成这项任务。

解决方案

使用SDK Manager工具启动 AVD。或者,使用emulator工具启动 AVD。

它是如何工作的

参考图 1–8,您会注意到一个禁用的启动按钮。创建 AVD 条目后,此按钮不再被禁用。单击 Start 运行emulator工具,突出显示的 AVD 条目作为仿真器的设备配置。

出现一个启动选项对话框。此对话框标识 AVD 的外观和屏幕密度。它还提供了未选中的复选框,用于缩放模拟器显示的分辨率以匹配物理设备的屏幕大小,以及擦除用户数据。

注意:当你更新你的应用时,你会定期打包并安装在模拟器上,模拟器会在用户数据磁盘分区中保存应用及其状态数据。为了确保应用在更新时正常运行,您可能需要删除模拟器的用户数据分区,这可以通过选中擦除用户数据来完成。

单击 Launch 按钮启动带有 AVD 的仿真器。SDK Manager通过短暂显示一个启动 Android 模拟器对话框来响应,然后是命令窗口(在 Windows XP 上),最后显示模拟器窗口。

模拟器窗口分为左窗格和右窗格,左窗格在黑色背景上显示 Android 徽标,右窗格显示手机控件和键盘。图 1–10 显示了test_AVD装置的这些窗格。

images

图 1–10。 模拟器窗口左边是主屏幕,右边是手机控件和键盘。

如果你以前用过 Android 设备,你可能对主屏幕、手机控制和键盘很熟悉。如果没有,请记住以下几点:

  • 主屏幕是一个特殊的应用,可以作为使用 Android 设备的起点。
  • 主屏幕(以及每个应用屏幕)上方会出现一个状态栏。状态栏显示当前时间、电池剩余电量等信息;并且还提供对通知的访问。
  • 主屏幕显示壁纸背景。单击电话控制中的菜单按钮,然后在弹出菜单中单击壁纸以更改壁纸。
  • 主屏幕在顶部附近显示谷歌搜索小部件。一个小工具是一个微型应用视图,可以嵌入到主屏幕和其他应用中,并接收定期更新。
  • 主屏幕在底部附近显示了应用启动器。启动器显示图标,用于启动常用的电话和浏览器应用,并显示所有已安装应用的矩形网格,随后通过双击它们的图标来启动这些应用。
  • 主屏幕由多个窗格组成。单击应用启动器任一侧的点,将当前窗格替换为左侧或右侧的下一个窗格——点的数量表示左侧或右侧剩余待访问的窗格数量。或者,在应用启动器的中间图标上按住鼠标指针,以调出微型窗格图标列表;点按这些图标之一以显示相应的主屏幕面板。
  • 房子图标电话控制按钮带你从任何地方到主屏幕。
  • 菜单电话控制按钮为当前运行的应用提供特定于应用的选项菜单。
  • 弯曲的箭头图标电话控制键可让您返回到活动堆栈中的上一个活动。

当 AVD 运行时,您可以通过使用鼠标“触摸”触摸屏和键盘“按下”AVD 键来进行交互。Table 1–2 显示了 AVD 键和键盘键之间的映射。

表 1–2。AVD 键和键盘键之间的映射

| **AVD 密钥** | **键盘按键** | | :-- | :-- | | 主页 | 家 | | 菜单(左软键) | F2 或向上翻页 | | 星形(右软键) | Shift-F2 或向下翻页 | | 背部 | 经济社会委员会 | | 呼叫/拨号按钮 | 第三子代 | | 挂断/结束呼叫按钮 | 法乐四联症 | | 搜索 | F5 | | 电源按钮 | F7 | | 音量调高按钮 | 键盘+Ctrl-5 | | 音量调低按钮 | 键盘减号,Ctrl-F6 | | 相机按钮 | Ctrl-小键盘 _5,Ctrl-F3 | | 切换到以前的布局方向(纵向或横向) | 键盘 _7,Ctrl-F11 | | 切换到下一个布局方向 | 键盘 _9,Ctrl-F12 | | 打开/关闭手机网络 | F8 | | 切换代码分析 | F9(仅带`-trace`启动选项) | | 切换全屏模式 | Alt-Enter | | 切换轨迹球模式 | F6 | | 暂时进入轨迹球模式(按键时) | 删除 | | DPad 左/上/右/下 | 键盘 _4/8/6/2 | | DPad 中心点击 | 键盘 _5 | | 洋葱α增加/减少 | 小键盘 _ 乘法(*) /小键盘 _ 除法(/) |

提示:在使用键盘按键之前,您必须首先禁用开发计算机上的 NumLock。

表 1–2 指的是切换代码分析环境中的-trace启动选项。当通过emulator工具启动 AVD 时,该选项允许您将分析结果存储在一个文件中。

例如,emulator -avd test_AVD-trace results.txt启动设备配置仿真器test_AVD,当您按下 F9 时,还会将分析结果存储在results.txt中。再次按 F9 停止代码分析。

图 1–10 在标题栏显示 5554:test_AVD。5554 值标识了一个控制台端口,您可以使用它来动态查询和控制 AVD 的环境。

注意: Android 最多支持 16 个并发执行的 avd。每个 AVD 都分配有一个从 5554 开始的偶数控制台端口号。

您可以通过指定telnet localhost *console-port*连接到 AVD 的控制台。例如,指定telnet localhost 5554来连接 test_AVD 的控制台。Figure 1–11 向您展示了在 Windows XP 上生成的命令窗口。

images

图 1–11。 单独键入命令名以获得特定命令的帮助。

1–5。UC 简介

问题

现在,您已经安装了 Android SDK,安装了 Android 平台,并创建和启动了 AVD,您已经准备好创建一个应用,并在 AVD 上安装和运行这个应用。尽管你可以基于清单 1–2 的SimpleActivity类创建一个应用,你可能会发现这个菜谱的UC应用更有趣(也更有用)。

解决方案

UC(Units Converter 的首字母缩写)是一个让你在单位类型之间转换的应用。例如,您可以将特定的摄氏度数转换为其等效的华氏度数,将特定的磅数转换为其等效的千克数,等等。

它是如何工作的

UC由一个单独的活动(也称为UC)组成,该活动提供了一个用户界面(显示在秘籍 1–7 中),该界面包括一个输入/输出文本字段,用于输入要转换的单位数并显示转换结果,一个微调器,用于选择转换,以及用于清除文本字段、执行转换和关闭应用的按钮。

清单 1–9 展示了UC活动的源代码。

清单 1–9。 执行单位换算的活动

`// UC.java

package com.apress.uc;

import android.app.Activity;

import android.os.Bundle;

import android.text.Editable;
import android.text.TextWatcher;

import android.view.View;

import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;

public class UC extends Activity
{
   private int position = 0;

private double[] multipliers = {
      0.0015625,         // Acres to square miles
      101325.0,          // Atmospheres to Pascals
      100000.0,          // Bars to Pascals
      0,                 // Degrees Celsius to Degrees Fahrenheit (placeholder)
      0,                 // Degrees Fahrenheit to Degrees Celsius (placeholder)
      0.00001,           // Dynes to Newtons
      0.3048,            // Feet/Second to Metres/Second
      0.0284130625,      // Fluid Ounces (UK) to Litres
      0.0295735295625,   // Fluid Ounces (US) to Litres
      746.0,             // Horsepower (electric) to Watts
      735.499,           // Horsepower (metric) to Watts
      1/1016.0469088,    // Kilograms to Tons (UK or long)
      1/907.18474,       // Kilograms to Tons (US or short)
      1/0.0284130625,    // Litres to Fluid Ounces (UK)
      1/0.0295735295625, // Litres to Fluid Ounces (US)
      331.5,             // Mach Number to Metres/Second
      1/0.3048,          // Metres/Second to Feet/Second
      1/331.5,           // Metres/Second to Mach Number
      0.833,             // Miles/Gallon (UK) to Miles/Gallon (US)
      1/0.833,           // Miles/Gallon (US) to Miles/Gallon (UK)
      100000.0,          // Newtons to Dynes
      1/101325.0,        // Pascals to Atmospheres
      0.00001,           // Pascals to Bars
      640.0,             // Square Miles to Acres
      1016.0469088,      // Tons (UK or long) to Kilograms
      907.18474,         // Tons (US or short) to Kilograms
      1/746.0,           // Watts to Horsepower (electic)
      1/735.499          // Watts to Horsepower (metric)
   };

@Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);

final EditText etUnits = (EditText) findViewById(R.id.units);

final Spinner spnConversions = (Spinner) findViewById(R.id.conversions);
      ArrayAdapter aa;
      aa = ArrayAdapter.
             createFromResource(this, R.array.conversions,
                                android.R.layout.simple_spinner_item);
      aa.setDropDownViewResource(android.R.layout.simple_spinner_item);
      spnConversions.setAdapter(aa);

AdapterView.OnItemSelectedListener oisl;
      oisl = new AdapterView.OnItemSelectedListener()
      {
         @Override
         public void onItemSelected(AdapterView parent, View view,                                     int position, long id)          {             UC.this.position = position;          }` `@Override          public void onNothingSelected(AdapterView parent)
         {
            System.out.println("nothing");
         }
      };
      spnConversions.setOnItemSelectedListener(oisl);

final Button btnClear = (Button) findViewById(R.id.clear);
      AdapterView.OnClickListener ocl;
      ocl = new AdapterView.OnClickListener()
      {
         @Override
         public void onClick(View v)
         {
            etUnits.setText("");
         }
      };
      btnClear.setOnClickListener(ocl);
      btnClear.setEnabled(false);

final Button btnConvert = (Button) findViewById(R.id.convert);
      ocl = new AdapterView.OnClickListener()
      {
         @Override
         public void onClick(View v)
         {
            String text = etUnits.getText().toString();
            double input = Double.parseDouble(text);
            double result = 0;
            if (position == 3)
               result = input9.0/5.0+32; // Celsius to Fahrenheit
            else
            if (position == 4)
               result = (input-32)
5.0/9.0; // Fahrenheit to Celsius
            else
               result = input*multipliers[position];
            etUnits.setText(""+result);
         }
      };
      btnConvert.setOnClickListener(ocl);
      btnConvert.setEnabled(false);

Button btnClose = (Button) findViewById(R.id.close);
      ocl = new AdapterView.OnClickListener()
      {
         @Override
         public void onClick(View v)
         {
            finish();
         }
      };
      btnClose.setOnClickListener(ocl);

TextWatcher tw;
      tw = new TextWatcher()
      {          @Override
         public void afterTextChanged(Editable s)
         {
         }

@Override
         public void beforeTextChanged(CharSequence s, int start, int count,
                                       int after)
         {
         }

@Override
         public void onTextChanged(CharSequence s, int start, int before,
                                   int count)
         {
            if (etUnits.getText().length() == 0)
            {
               btnClear.setEnabled(false);
               btnConvert.setEnabled(false);
            }
            else
            {
               btnClear.setEnabled(true);
               btnConvert.setEnabled(true);
            }
         }
      };
      etUnits.addTextChangedListener(tw);
   }
}`

清单 1–9 以一个注释开始,这个注释可以方便地识别描述活动的源文件(UC.java)。这个清单接下来给出了一个包语句,它命名了包(com.apress.uc),其中存储了源文件的UC类,后面是一系列导入各种 Android API 类型的导入语句。

提示:你要熟悉 Android API 的包组织,以便在 Google 的 Android API reference ( [developer.android.com/reference/packages.html](http://developer.android.com/reference/packages.html))中快速找到 API 类型。随着您对 Android 应用开发的深入研究,您会希望快速找到关于这些类型的文档。

清单 1–9 接下来描述了UC类,它扩展了Activity。这个类首先声明了positionmultipliers字段:

  • position存储通过微调器选择的转换的从零开始的索引,默认为 0(微调器显示的第一个转换)。在该字段中存储微调器的位置简化了选择要执行的适当转换。
  • multipliers存储一个乘数值数组,每个条目对应一个微调器值。通过将输入值乘以multipliers[position]进行转换。然而,有两个例外:摄氏度到华氏度和华氏度到摄氏度。这些转换是单独处理的,因为它们也需要加法或减法运算。

应用的所有工作都发生在覆盖的onCreate(Bundle)方法中:不需要其他方法,这有助于保持这个应用的简单。

首先调用它的同名超类方法,这是一个所有覆盖活动方法都必须遵循的规则。

然后这个方法执行setContentView(R.layout.main)来建立应用的用户界面。

R.layout.main标识一个资源,一段应用代码所需的数据,你通过将它存储在一个单独的文件中来独立于代码维护它。

注意:资源简化了应用维护,使用户界面更容易适应不同的屏幕尺寸,并便于应用适应不同的语言。

您将此资源 ID 解释如下:

  • R是构建应用时生成的类的名称(由aapt工具生成)。这个类被命名为R,因为它的内容标识了各种资源(比如布局、图像、字符串和颜色)。
  • layout是嵌套在R中的类的名称。其 id 存储在该类中的所有资源描述特定的布局资源。每种资源都与一个以相似方式命名的嵌套类相关联。例如,string标识字符串资源。
  • main是在layout中声明的int常量的名称。该资源 ID 标识主布局资源。具体来说,main是指存储主屏幕布局信息的main.xml文件。mainUC唯一的布局资源。

R.layout.main被传递给Activityvoid setContentView(int layoutResID)方法,告诉 Android 使用main.xml中存储的布局信息创建一个用户界面屏幕。在幕后,Android 创建了在main.xml中描述的用户界面组件,并根据main.xml的布局数据将它们放置在屏幕上。

该用户界面基于视图(用户界面组件的抽象)和视图组(将相关用户界面组件分组的视图)。视图是子类化android.view.View类的实例,类似于 Java 组件。视图组是抽象类android.view.ViewGroup的子类,类似于 Java 容器。Android 将特定的视图(如按钮或微调器)称为小部件

注意:不要把这里的 widget 和 Android 主屏幕上显示的 widget 混淆了。虽然使用了相同的术语,但是用户界面部件和主屏幕部件是不同的。

继续,onCreate(Bundle)执行final EditText etUnits = (EditText) findViewById(R.id.units);。该语句首先调用ViewView findViewById(int id)方法,找到main.xml中声明的标识为unitsEditText视图,实例化android.widget.EditText并初始化为该视图的声明信息,然后将该对象的引用保存在局部变量etUnits中。这个变量是final,因为它是从一个匿名内部类中访问的。

以类似的方式,final Spinner spnConversions = (Spinner) findViewById(R.id.conversions);使用存储在main.xml中的声明性信息实例化android.widget.Spinner类,并保存结果对象引用以供后续访问。

注意:虽然从维护的角度来看,最好通过布局资源声明用户界面屏幕,并让 Android 代表您创建窗口小部件并将其添加到布局中,但 Android 允许您在需要时选择创建窗口小部件并以编程方式进行布局。

onCreate(Bundle)接下来通过首先调用android.widget.ArrayAdapter类的ArrayAdapter<CharSequence> createFromResource(Context context, int textArrayResId, int textViewResId)方法来处理没有文本显示的 spinner 对象,该方法返回一个向 spinner 提供文本消息的数组适配器:

  • context需要一个Context实例来标识当前的应用组件,它恰好是由关键字this指定的当前活动。
  • textArrayResId需要存储字符串的数组资源的 ID(比如"Degrees Celsius to Degrees Fahrenheit"),这些字符串恰好标识不同种类的转换。传递给该参数的R.array.conversions参数将conversions标识为一个包含转换字符串的array资源的名称,并在一个名为arrays.xml的文件中指定(稍后将在该秘籍中描述)。
  • textViewResId需要用于创建旋转器外观的layout资源的 ID。传递给该参数的android.R.layout.simple_spinner_item参数是存储在android包的R类的嵌套layout类中的预定义 ID。simple_spinner_item描述了一个类似 Java Swing combobox 的旋转器。

调用createFromResource(Context, int, int)后,onCreate(Bundle)调用ArrayAdaptervoid setDropDownViewResource(int resource)方法,参数为 android.R.layout.simple_spinner_item。该方法调用创建微调器的下拉视图部分。

现在已经创建了数组适配器,并用适当的单位转换字符串和布局信息进行了初始化,onCreate(Bundle)通过调用spnConversions.setAdapter(aa);将这些信息附加到微调器上。这个方法调用允许 spinner 小部件访问这些信息,并向用户显示一个转换列表。

注意: Spinner从它的抽象android.widget.AdapterView<T extends Adapter>祖先类继承了void setAdapter(T)方法。

UC需要跟踪当前选择的微调项目,以便它可以执行适当的转换。onCreate(Bundle)通过向微调器注册一个侦听器来实现这一点,微调器通过将微调器的位置分配给(前面提到的)position变量来响应项目选择事件。

onCreate(Bundle)首先实例化一个实现ArrayAdapter的嵌套OnItemSelectedListener接口的匿名类,然后通过调用AdapterViewvoid setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener)方法向 spinner 注册这个实例。

每当用户选择一个新项目时,OnItemSelectedListenervoid onItemSelected(AdapterView<?> parent, View view, int position, long id)方法就会被调用,这是保存位置的最佳位置。虽然不需要,但配套的void onNothingSelected(AdapterView<?> parent)方法也必须实现。

随着微调器的消失,onCreate(Bundle)将注意力转向创建清除、转换和关闭按钮。对于每个按钮,它调用findByViewId(int)main.xml获取按钮信息,然后实例化android.widget.Button类。

然后使用AdapterView的嵌套onClickListener接口来创建监听器对象,每当用户单击按钮时,就会调用这些对象的void onClick(View v)方法。每个监听器通过调用AdapterViewvoid setOnItemClickListener(AdapterView.OnItemClickListener listener)方法注册到它的Button对象。

Clear 按钮的 click listener 简单地执行etUnits.setText("")来清除用户输入或来自etUnits文本字段的转换结果。关闭按钮的点击监听器同样简单;它调用finish()终止当前活动和UC app。相比之下,Convert 按钮的 click listener 有更多的工作要完成:

  1. 获取etUnits文本字段的内容作为String对象:String text = etUnits.getText().toString();
  2. 将这个String对象解析为双精度浮点值:double input = Double.parseDouble(text);
  3. 根据position的值:result = input*9.0/5.0+32;result = (input-32)*5.0/9.0;result = input*multipliers[position];进行转换并保存结果。
  4. result : etUnits.setText(""+result);更新etUnits

还有一项任务需要onCreate(Bundle)执行:确保当etUnits为空时,清除和转换按钮被禁用。毕竟,清除一个空的文本字段是没有意义的,当试图解析一个空的文本字段时,parseDouble()会抛出一个异常。

onCreate(Bundle)通过android.widget.TextViewvoid addTextChangedListener(TextWatcher watcher)方法向etUnits文本字段注册一个文本观察器(其类实现android.text.TextWatcher接口的对象)来完成这项任务。TextViewEditText的超类。

TextWatcher声明了void afterTextChanged(Editable s)void beforeTextChanged(CharSequence s, int start, int count, int after)void onTextChanged(CharSequence s, int start, int before, int count)方法。只有后一种方法被覆盖以启用或禁用“清除”和“转换”按钮。

onTextChanged(s, int, int, int)首先计算etUnits.getText().length(),返回文本字段的长度。如果长度为 0(空文本字段),按钮通过btnClear.setEnabled(false);btnConvert.setEnabled(false);被禁用。否则,通过btnClear.setEnabled(true);btnConvert.setEnabled(true);使能。

UC的大部分资源都存储在 XML 文件中。例如,UC的小部件和布局信息存储在main.xml中,如清单 1–10 所示。

清单 1–10。??main.xml文件存储部件和布局信息

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:gravity="center_vertical"    android:background="@drawable/gradientbg"    android:padding="5dip">    <LinearLayout android:layout_width="fill_parent"       android:layout_height="wrap_content">       <TextView android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:layout_marginRight="10dip"          android:text="@string/units"          android:textColor="#000000"          android:textSize="15sp"          android:textStyle="bold"/>       <EditText android:id="@+id/units"          android:layout_width="fill_parent"          android:layout_height="wrap_content"          android:hint="type a number"          android:inputType="numberDecimal|numberSigned"          android:maxLines="1"/>    </LinearLayout>    <Spinner android:id="@+id/conversions"       android:layout_width="fill_parent"       android:layout_height="wrap_content"       android:prompt="@string/prompt"/>    <LinearLayout android:layout_width="fill_parent"       android:layout_height="wrap_content">       <Button android:id="@+id/clear"          android:layout_width="fill_parent"          android:layout_height="wrap_content"          android:layout_weight="1"          android:text="@string/clear"/>       <Button android:id="@+id/convert"          android:layout_width="fill_parent"          android:layout_height="wrap_content"          android:layout_weight="1"          android:text="@string/convert"/>       <Button android:id="@+id/close"          android:layout_width="fill_parent"          android:layout_height="wrap_content"          android:layout_weight="1"          android:text="@string/close"/>    </LinearLayout> </LinearLayout>

清单 1–10 从声明一个<LinearLayout>标签开始,该标签指定了一个布局(一个视图组,以某种方式在 Android 设备的屏幕上排列包含的视图),用于在屏幕上水平或垂直排列包含的小部件和嵌套布局。

标签指定了几个属性来控制这个线性布局。这些属性包括以下内容:

  • orientation将线性布局标识为水平或垂直。默认方向是水平的。"horizontal""vertical"是唯一可以分配给该属性的合法值。
  • layout_width标识布局的宽度。合法值包括"fill_parent"(占据整个宽度)和"wrap_content"(仅占据视图所需的宽度)。fill_parent在 Android 2.2 中更名为match_parent,但仍被支持和广泛使用。
  • layout_height标识布局的高度。合法值包括"fill_parent"(占据整个高度)和"wrap_content"(只占据视图要求的高度)。fill_parent在 Android 2.2 中更名为match_parent,但仍被支持和广泛使用。
  • gravity标识布局相对于屏幕的位置。例如,"center_vertical"指定布局应该在屏幕上垂直居中。
  • background通过资源引用识别背景图像或渐变(以@字符开始的特殊语法)。例如,"@drawable/gradientbg"引用了一个名为gradientbg可绘制资源(一个图像或图形)。
  • padding标识要添加到布局中的空间,以提供其自身和屏幕边缘之间的边界。"5dip"是指五个与密度无关的像素,虚拟像素单元,应用可以用它来以与屏幕密度无关的方式表达布局尺寸/位置。

注意:一个与密度无关的像素相当于 160 dpi 屏幕上的一个物理像素,这是 Android 假定的基线密度。在运行时,Android 透明地处理所需 dip 单位的任何缩放,基于使用中的屏幕的实际密度。倾角单位通过等式像素=倾角*(密度/ 160)转换为屏幕像素。例如,在 240 dpi 的屏幕上,1 个 dip 等于 1.5 个物理像素。Google 建议使用 dip 单位来定义应用的用户界面,以确保 UI 在不同屏幕上的正确显示。

第二个线性布局已嵌套在第一个线性布局内。因为没有指定orientation属性,这个布局水平地布置它的部件。与父布局一样,layout_width被分配给"fill_parent"。然而,layout_height被赋予了"wrap_content"以防止这种嵌套布局占据整个屏幕。

嵌套的线性布局封装了 textview 和 edittext 元素。textview 元素描述一个小部件,作为 edittext 元素描述的小部件的标签。除了layout_widthlayout_height之外,<textview>标签还提供以下属性:

  • layout_marginRight指定 textview 小工具右侧要保留的空间量;已经选择了 10 个与密度无关的像素作为空间量。
  • text标识此小部件显示的文本。文本通过@string/units来标识,它是对标准strings.xml资源文件中units条目的字符串资源引用(参见清单 1–12)。这个条目的值是文本。
  • textColor标识文本的颜色。颜色以#*RRGGBB*格式指定—#00000标识黑色。
  • textSize标识文本的大小。尺寸被指定为"15sp",它被解释为 15 个与缩放无关的像素(用户通过设备设置选择缩放)。Google 建议指定与缩放无关的像素(让用户缩放文本)或与设备无关的像素(防止用户缩放文本)。
  • textStyle标识文本样式,如粗体或斜体。样式设置为"bold"以强调文本,使其在屏幕上突出显示。

<edittext>标签提供了以下属性:

  • 标识这个小部件元素,以便可以从代码中引用它。通过使用以前缀@+id开始的特殊语法来指定资源标识符。例如,"@+id/units"将这个 edittext 小部件标识为units;通过指定R.id.units从代码中引用这个小部件资源。
  • hint标识在没有输入任何内容时出现在文本字段中的字符串。它为用户提供了一个提示,告诉用户应该在文本字段中输入什么样的数据。没有为该属性分配字符串资源引用,而是分配了"type a number"文字字符串来说明以下问题:虽然您可以在资源(甚至代码)中嵌入文字字符串值,但是您确实应该将它们存储在单独的strings.xml资源文件中,以便于将应用本地化为不同的语言,例如法语或德语。
  • inputType标识您希望用户输入的数据类型。默认情况下,可以输入任何字符。因为当需要一个数字时这是不可接受的,"numberDecimal|numberSigned"被分配给inputType。该字符串指定只能输入十进制数字。此外,这些数字可能是负数。
  • maxLines限制可输入文本字段的文本行数。"1"赋值表示只能输入一行文本。

在线性布局元素下面是一个名为conversions的微调元素。这个元素被声明为填充屏幕的宽度,而不是屏幕的高度。此外,其prompt属性被赋予"@string/prompt"以提示用户(在下拉视图上,如图图 1–15 所示)选择转换。

在 spinner 元素下面是另一个嵌套的线性布局,封装了清除、转换和关闭按钮。每个按钮都分配了一个唯一的 ID,因此可以从代码中引用它。它的layout_weight属性被赋予与其他按钮的layout_weight属性相同的值,这样每个按钮都有相同的宽度(看起来更好)。

Android 允许您将形状资源(如矩形或椭圆形)声明为 XML 文件。这些形状可以用直角或圆角、渐变背景和其他属性来声明。例如,清单 1–11 引入了一个带有渐变背景的矩形。

清单 1–11。gradientbg.xml文件存储一个渐变形状来给活动的背景着色

<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android">    <gradient android:startColor="#fccb06"       android:endColor="#fd6006"       android:angle="270"/>    <corners android:radius="10dp"/> </shape>

<shape>标签通过它的shape属性引入一个形状。如果该属性不存在,形状默认为矩形。

嵌套的<gradient>标签根据渐变定义形状的颜色,渐变是通过startColorendColorangle属性指定的。angle属性指定渐变扫过矩形的方向。如果angle不存在,角度默认为 0 度。

嵌套的<corners>标签决定了矩形是否有角。如果此标签存在,其属性标识每个或所有角的圆度。例如,清单 1–11 中的radius属性指定每个角的半径为 10 个与密度无关的像素—dpdip的同义词。

字符串应该分开存储,以便于文本的本地化。Android 要求字符串存储在名为strings.xml的文件中,如清单 1–12 所示。

清单 1–12。??strings.xml存储应用字符串的文件

<?xml version="1.0" encoding="utf-8"?> <resources>    <string name="app_name">Units Converter</string>    <string name="clear">Clear</string>    <string name="close">Close</string>    <string name="convert">Convert</string>    <string name="prompt">Select a conversion</string>    <string name="units">Units</string> </resources>

strings.xml文件将其字符串存储为嵌套在 resources 元素中的一系列字符串元素。每个<string>标签需要一个唯一的name属性,其内容标识字符串,并从代码或其他资源中引用。字符串文本放在<string></string>标签之间。

最后,转换字符串数组存储在arrays.xml中。清单 1–13 展示了这个标准文件的内容。

清单 1–13。arrays.xml文件存储了一组转换字符串

<?xml version="1.0" encoding="utf-8"?> <resources>    <string-array name="conversions">       <item>Acres to Square Miles</item>       <item>Atmospheres to Pascals</item>       <item>Bars to Pascals</item>       <item>Degrees Celsius to Degrees Fahrenheit</item>       <item>Degrees Fahrenheit to Degrees Celsius</item>       <item>Dynes to Newtons</item>       <item>Feet/Second to Metres/Second</item>       <item>Fluid Ounces (UK) to Litres</item>       <item>Fluid Ounces (US) to Litres</item>       <item>Horsepower (electric) to Watts</item>       <item>Horsepower (metric) to Watts</item>       <item>Kilograms to Tons (UK or long)</item>       <item>Kilograms to Tons (US or short)</item>       <item>Litres to Fluid ounces (UK)</item>       <item>Litres to Fluid ounces (US)</item>       <item>Mach Number to Metres/Second</item>       <item>Metres/Second to Feet/Second</item>       <item>Metres/Second to Mach Number</item>       <item>Miles/Gallon (UK) to Miles/Gallon (US)</item>       <item>Miles/Gallon (US) to Miles/Gallon (UK)</item>       <item>Newtons to Dynes</item>       <item>Pascals to Atmospheres</item>       <item>Pascals to Bars</item>       <item>Square Miles to Acres</item>       <item>Tons (UK or long) to Kilograms</item>       <item>Tons (US or short) to Kilograms</item>       <item>Watts to Horsepower (electric)</item>       <item>Watts to Horsepower (metric)</item>    </string-array> </resources>

Android 允许你在arrays.xml中存储不同类型数据的数组。例如,<string-array>表示数组包含字符串。这个标签需要一个name属性,其值唯一地标识这个数组。每个数组项都是通过将其内容放在<item></item>标签之间来指定的。

1–6 岁。创建统一通信

问题

您想学习如何使用 Android SDK 的命令行工具创建UC,但不确定如何完成这项任务。

解决方案

使用android工具创建UC,然后使用ant构建这个项目。

它是如何工作的

创建UC的第一步是使用android工具创建一个项目。以这种方式使用时,android要求您遵守以下语法(为了可读性,该语法分布在多行中):

android create project --target *target_ID*                        --name *your_project_name*                        --path /*path*/*to*/*your*/*project*/*project_name*                        --activity *your_activity_name*                        --package *your_package_namespace*

除了指定项目名称的--name(或–n)(如果提供,此名称将在您构建应用时用于生成的.apk文件名),以下所有选项都是必需的:

  • --target(或-t)选项指定了应用的构建目标。 target_ID 值是标识 Android 平台的整数值。您可以通过调用android list targets来获得这个值。如果您只安装了 Android 2.3 平台,这个命令应该输出一个标识为 integer ID 1 的 Android 2.3 平台目标。
  • --path(或-p)选项指定项目目录的位置。如果目录不存在,则创建该目录。
  • --activity(或-a)选项指定默认活动类的名称。生成的 classfile 在/*path*/*to*/*your*/*project*/*project_name*/*src*/*your_package_namespace*/中创建,如果没有指定--name(或-n)的话,它将被用作.apk文件名。
  • --package(或-k)选项指定项目的包名称空间,它必须遵循 Java 语言中指定的包规则。

假设一个 Windows XP 平台,并假设一个C:\prj\dev层次结构,其中UC项目将存储在C:\prj\dev\UC中,从文件系统中的任何地方调用以下命令来创建UC:

android create project -t 1 -p C:\prj\dev\UC -a UC -k com.apress.uc

该命令创建各种目录,并向其中一些目录添加文件。它在C:\prj\dev\UC中专门创建了以下文件和目录结构:

  • AndroidManifest.xml是正在构建的应用的清单文件。该文件与之前通过--activity-a选项指定的Activity子类同步。
  • bin是 Apache Ant 构建脚本的输出目录。
  • build.properties是构建系统的可定制属性文件。您可以编辑此文件以覆盖 Apache Ant 使用的默认构建设置,并提供一个指向您的密钥库和密钥别名的指针,以便构建工具可以在以发布模式构建您的应用时对其进行签名(在方法 1–8 中讨论)。
  • build.xml是该项目的 Apache Ant 构建脚本。
  • default.properties是构建系统的默认属性文件。不要修改这个文件。
  • libs必要时包含私有库。
  • local.properties包含 Android SDK 主目录的位置。
  • proguard.cfg包含了 ProGuard 的配置数据,这是一个 SDK 工具,允许开发人员将他们的代码混淆(使代码很难逆向工程)作为发布版本的一个集成部分。
  • res包含项目资源。
  • src包含项目的源代码。

res包含以下目录:

  • drawable-hdpi包含用于高密度屏幕的可绘制资源(如图标)。
  • drawable-ldpi包含低密度屏幕的可提取资源。
  • drawable-mdpi包含中密度屏幕的可抽取资源。清单 1–11 中的gradientbg.xml文件存储在这个目录中。
  • layout包含布局文件。清单 1–10 中的main.xml文件存储在这个目录中。
  • values包含值文件。清单 1–12 的strings.xml和清单 1–13 的arrays.xml文件都存储在这个目录下。

另外,src包含了com\apress\uc目录结构,最后的uc子目录包含了一个骨架UC.java源文件。这个框架文件的内容被替换为清单 1–9。

假设C:\prj\dev\UC是最新的,在 Apache 的ant工具的帮助下构建这个应用,默认处理这个目录的build.xml文件。在命令行中,指定ant,后跟debugrelease,以指示构建模式:

  • 调试模式:搭建 app 进行测试调试。构建工具用调试密钥对结果 APK 进行签名,并用zipalign优化 APK。指定ant debug
  • 发布方式:构建 app 发布给用户。您必须用您的私钥签署结果 APK,然后用zipalign优化 APK。(我将在本章后面讨论这些任务。)指定ant release

通过从C:\prj\dev\UC目录调用ant debug在调试模式下构建UC。该命令创建一个包含ant生成的R.java文件的gen子目录(在com\apress\uc目录层次中),并将创建的UC-debug.apk文件存储在bin子目录中。

1–7 岁。安装和运行统一通信

问题

您希望在之前启动的 AVD 上安装刚刚创建的UC-debug.apk包文件,并运行此应用。

使用adb工具安装UC。导航到应用启动屏幕运行UC

它是如何工作的

假设 AVD 仍在运行,执行adb install C:\prj\dev\UC\bin\UC-debug.apk在 AVD 上安装UC-debug.apk。几分钟后,您应该会看到类似以下内容的几条消息:

411 KB/s (19770 bytes in 0.046s)         pkg: /data/local/tmp/UC-debug.apk Success

在主屏幕上,单击应用启动器图标(主屏幕底部居中的矩形网格图标)并向下滚动结果屏幕上的应用图标列表。图 1–12 显示了单位转换器应用入口。

images

图 1–12。 高亮显示的单位转换器应用条目显示一个自定义图标(在一个icon.png文件中,该文件包含在本书的代码中),该图标也存储在drawable-mdpi中。

点击单位转换器图标,您应该会看到如图图 1–13 所示的屏幕。

images

图 1–13。??【单位】文本框提示用户输入一个数字。

在单位文本框中输入 37 ,你会看到如图图 1–14 所示的屏幕。

images

图 1–14。 清除和转换按钮不再被禁用。

点击微调按钮,您将看到如图图 1–15 所示的屏幕。

images

图 1–15。 微调器在其转换名称下拉列表的顶部显示提示。

选择“摄氏度至华氏度”,您将看到类似于 Figure 1–16 的屏幕。

images

图 1–16。 点击转换后,单位文本框显示转换结果。

点击关闭以终止应用并返回到如图 Figure 1–12 所示的启动器屏幕。

注意:虽然UC看起来运行正常,但是在发布应用之前,应该对它(以及任何其他应用的)代码进行单元测试,以验证代码是正确的。谷歌在线 Android 开发者指南在[developer.android.com/guide/topics/testing/index.html](http://developer.android.com/guide/topics/testing/index.html)的“测试”部分深入探讨了这个话题。

1–8。准备 UC 进行发布

问题

您对UC工作正常感到满意,现在您想准备将其发布到 Google 的 Android Market 或另一个发布服务。

解决方案

在发布诸如UC之类的应用之前,您应该对该应用进行版本化。然后,在发布模式下构建应用,并对其应用包进行签名和对齐。

它是如何工作的

谷歌的在线安卓开发者指南 ( [developer.android.com/guide/index.html](http://developer.android.com/guide/index.html))提供了关于发布应用的大量信息。这份秘籍没有重复指南的信息,而是介绍了准备UC出版的必要步骤。

版本 UC

Android 允许你通过其versionCodeversionName属性在AndroidManifest.xml<manifest>标签中指定版本信息来给你的应用添加版本信息。

versionCode被赋予一个表示应用代码版本的整数值。该值是一个整数,以便其他应用可以通过编程对其进行评估,例如检查升级或降级关系。尽管您可以将该值设置为任何所需的整数,但您应该确保应用的每个后续版本都使用更大的值。Android 不强制执行这种行为,但是在后续版本中增加值是规范的。

versionName被赋予一个字符串值,表示应用代码的发布版本,应该显示给用户(由应用显示)。这个值是一个字符串,因此您可以将应用版本描述为一个<*major*>.<*minor*>.<*point*>字符串,或者任何其他类型的绝对或相对版本标识符。和android:versionCode一样,Android 不把这个值用于任何内部目的。发布服务可以提取versionName值以显示给用户。

UCAndroidManifest.xml文件中的<manifest>标签包括一个初始化为"1"versionCode属性和一个初始化为"1.0"versionName属性。

在发布模式下构建 UC

假设 Windows XP 是以前的C:\prj\dev\UC目录,并且该目录是当前目录,则执行以下命令行:

ant release

该命令行生成UC-unsigned.apk并将该文件存储在bin目录中。它还输出一条消息,说明这个 APK 必须签名并与zipalign对齐。

签署 UC 的 App 包

Android 要求所有安装的应用都要用证书进行数字签名,证书的私钥由应用的开发者持有。Android 使用证书作为识别应用作者和在应用之间建立信任关系的手段;它不使用证书来控制用户可以安装哪些应用。证书不需要由证书颁发机构签名:Android 应用使用自签名证书是完全允许的,也是典型的。

注意: Android 只在安装时测试签名者证书的到期日期。如果应用的签名者证书在应用安装后过期,应用将继续正常运行。

在您可以签署UC-unsigned.apk之前,您必须获得一个合适的私钥。如果满足以下条件,则私钥是合适的:

  • 该键表示应用要标识的个人、公司或组织实体。
  • 密钥的有效期超过了应用的预期寿命。谷歌建议有效期超过 25 年。如果你计划在 Android Market 上发布应用,请记住,有效期必须在 2033 年 10 月 22 日之后结束。如果应用是用有效期在该日期之前过期的密钥签名的,则不能上传该应用。
  • 密钥不是由 Android SDK 工具生成的调试密钥。

JDK 的keytool工具用于创建合适的私钥。下面的命令行(为了可读性分成两行)使用keytool来生成这个密钥:

keytool -genkey -v -keystore uc-release-key.keystore -alias uc_key -keyalg RSA         -keysize 2048 -validity 10000

指定了以下命令行参数:

  • -genkey使keytool生成一个公钥和一个私钥(密钥对)。
  • -v启用详细输出。
  • -keystore标识存储私钥的密钥库(一个文件);在命令行中,密钥库被命名为uc-release-key.keystore
  • -alias标识密钥的别名(在实际签名操作期间指定别名时,仅使用前八个字符);在命令行中,别名被命名为uc_key
  • -keyalg指定生成密钥时使用的加密算法;虽然支持DSARSA,但是RSA是在命令行中指定的。
  • -keysize指定每个生成密钥的大小(以比特为单位);因为 Google 建议使用 2048 位或更高的密钥大小(默认大小为 1024 位),所以在命令行中指定了2048
  • -validity指定密钥保持有效的期限(以天为单位)(Google 推荐值为 10000 或更大);10000是在命令行指定的。

keytool提示您输入密码(以保护对密钥库的访问),并再次输入相同的密码。然后,它会提示您输入姓名、您的组织单位名称、您的组织名称、您的城市或地区名称、您的州或省的名称,以及您的组织单位的两个字母的国家代码。

keytool随后提示您指出该信息是否正确(通过键入yes并按 Enter,或按 Enter 表示否)。假设您输入了yeskeytool允许您为密钥选择不同的密码,或者使用与密钥库相同的密码。

注意:保管好你的私人密钥。否则,您的应用创作身份和用户信任可能会受到影响。以下是保护您的私钥安全的一些提示:

*为密钥库和密钥选择强密码。

*用keytool生成密钥时,不要在命令行提供-storepass-keypass 选项。如果这样做,您的密码将出现在您的 shell 历史记录中,您计算机上的任何用户都可以访问。

*当用jarsigner给你的应用签名时,不要在命令行提供-storepass-keypass选项(原因和上一篇技巧中提到的一样)。

*不要把你的私人密码匙交给或借给任何人,也不要让未经授权的人知道你的密钥库和密码匙。

keytool在当前目录下创建uc-release-key.keystore。您可以通过执行以下命令行来查看该密钥库的信息:

keytool -list -v -keystore uc-release-key.keystore

请求密钥库密码后,keytool输出密钥库中的条目数(应该是 1)和证书信息。

JDK 的jarsigner工具用于签署UC-unsigned.apk。假设C:\prj\dev\UC是当前目录,该目录包含keytool创建的uc-release-key.keystore文件,该目录包含一个包含UC-unsigned.apkbin子目录,执行以下命令行对该文件进行签名:

jarsigner -verbose -keystore uc-release-key.keystore bin/UC-unsigned.apk uc_key

指定了以下命令行参数:

  • -verbose启用详细输出。
  • -keystore标识存储私钥的密钥库;uc-release-key.keystore是在命令行指定的。
  • bin/UC-unsigned.apk标识正在签名的 APK 的位置和名称。
  • uc-key标识之前为私钥创建的别名。

jarsigner提示您输入之前通过keytool指定的密钥库密码。然后,该工具输出类似于以下内容的消息:

   adding: META-INF/MANIFEST.MF    adding: META-INF/UC_KEY.SF    adding: META-INF/UC_KEY.RSA   signing: res/layout/main.xml   signing: AndroidManifest.xml   signing: resources.arsc   signing: res/drawable-hdpi/icon.png   signing: res/drawable-ldpi/icon.png   signing: res/drawable-mdpi/gradientbg.xml   signing: res/drawable-mdpi/icon.png   signing: classes.dex

执行jarsigner -verify bin/UC-unsigned.apk以验证UC-unsigned.apk已被签署。

假设成功,您应该会注意到一条“jar verified.”消息。假设失败,您应该会注意到以下消息:

no manifest. jar is unsigned. (signatures missing or not parsable)

对齐 UC 的 App 包

作为性能优化,Android 要求签名的 APK 的未压缩内容相对于文件的开头对齐,并为此提供了zipalign SDK 工具。根据 Google 的文档,APK 中所有未压缩的数据,如图像或原始文件,都是以 4 字节为边界对齐的。

zipalign需要以下语法将输入 APK 与输出 APK 对齐:

zipalign [-f] [-v] <*alignment*>*infile.apkoutfile.apk*

指定了以下命令行参数:

  • 如果存在的话,-f强制覆盖outfile.apk
  • -v启用详细输出。
  • alignment指定 APK 内容在此字节数边界上对齐;似乎zipalign忽略了除了4.之外的任何值
  • infile.apk标识要对齐的已签名的 APK 文件。
  • outfile.apk标识生成的已签名和校准的 APK 文件。

假设C:\prj\dev\UC\bin是当前目录,执行下面的命令行将UC-unsigned.apkUC.apk对齐:

zipalign –f –v 4 UC-unsigned.apk UC.apk

zipalign需要以下语法来验证现有 APK 是否对齐:

zipalign -c -v <*alignment*>*existing.apk*

指定了以下命令行参数:

  • -c确认existing.apk.的对准
  • -v启用详细输出。
  • alignment指定 APK 内容在此字节数边界上对齐;似乎zipalign忽略了除了4.之外的任何值
  • infile.apk标识要对齐的已签名的 APK 文件。

执行以下命令行,验证UC.apk是否对齐:

zipalign –c –v 4 UC.apk

zipalign显示 APK 条目列表,指出哪些是压缩的,哪些不是,随后是验证成功或验证失败消息。

1–9。迁移到 Eclipse

问题

您更喜欢使用 Eclipse IDE 开发应用。

解决方案

要使用 Eclipse 开发应用,您需要安装一个 IDE,比如 Eclipse Classic 3.6.1。此外,您需要安装 ADT 插件。

它是如何工作的

在使用 Eclipse 开发 Android 应用之前,您必须至少完成以下三项任务中的前两项:

  1. 安装 Android SDK 和至少一个 Android 平台(参见方法 1–1 和 1–2)。还必须安装 JDK 5 或 JDK 6。
  2. 为 Eclipse IDE 安装一个与 Android SDK 和 Android 开发工具(ADT)插件兼容的 Eclipse 版本。
  3. 安装 ADT 插件。

您应该按照显示的顺序完成这些任务。在安装 Eclipse 之前不能安装 ADT 插件,在安装 Android SDK 和至少一个 Android 平台之前不能配置或使用 ADT 插件。

有益的 ADT 插件

虽然不使用 ADT 插件也可以在 Eclipse 中开发 Android 应用,但是使用这个插件创建、调试和开发这些应用要快得多,也容易得多。

ADT 插件提供以下功能:

  • 它让您可以从 Eclipse IDE 内部访问其他 Android 开发工具。例如,ADT 允许您访问 Dalvik Debug Monitor Server (DDMS)工具的许多功能,允许您截取屏幕截图、管理端口转发、设置断点以及直接从 Eclipse 查看线程和进程信息。
  • 它提供了一个新的项目向导,帮助您快速创建和设置新 Android 应用所需的所有基本文件。
  • 它自动化并简化了构建 Android 应用的过程。
  • 它提供了一个 Android 代码编辑器,帮助您为 Android 清单和资源文件编写有效的 XML。
  • 它允许您将项目导出到签名的 APK 中,然后分发给用户。

在学习如何安装 Eclipse 之后,您将学习如何安装 ADT 插件。

Eclipse.org 网站提供了几个满足不同要求的 IDE 包供下载。Google 对您应该下载和安装哪个 IDE 包提出了一些规定和建议:

  • 安装 Eclipse 3.4 (Ganymede)或更高版本的 IDE 包。
  • 确保正在下载的 Eclipse 包包含 Eclipse JDT (Java 开发工具)插件。大多数软件包都包含这个插件。
  • 您应该安装 Eclipse Classic(版本 3.5.1 和更高版本)、Eclipse IDE for Java Developers 或 Eclipse IDE for Java EE Developers 包中的一个。

完成以下步骤来安装 Eclipse Classic 3.6.1:

  1. 将浏览器指向位于[www.eclipse.org/downloads/packages/eclipse-classic-361/heliossr1](http://www.eclipse.org/downloads/packages/eclipse-classic-361/heliossr1)的 Eclipse Classic 3.6.1 页面。
  2. 通过单击本页右侧下载链接框中的链接之一,选择适当的分发文件。例如,您可以单击 Windows 32 位平台。
  3. 单击下载链接,将分发文件保存到硬盘上。例如,你可以将eclipse-SDK-3.6.1–win32.zip保存到你的硬盘上。
  4. 解压缩分发文件并将eclipse主目录移动到一个方便的位置。例如,您可以将eclipse移动到您的C:\Program Files目录中。
  5. 您可能还想为位于eclipse主目录中的eclipse应用创建一个桌面快捷方式。

完成以下步骤以安装 ADT 插件的最新版本:

  1. 启动 Eclipse。
  2. 第一次启动 Eclipse 时,您会在闪屏后发现一个工作区启动器对话框。您可以使用此对话框选择存储项目的工作空间文件夹。您还可以告诉 Eclipse 在以后的启动中不要显示这个对话框。更改或保留默认文件夹设置,然后单击确定。
  3. 一旦 Eclipse 显示了它的主窗口,从 Help 菜单中选择 Install New Software。
  4. 在出现的安装对话框的可用软件窗格中点击添加按钮。
  5. 在随后出现的添加存储库对话框中,在名称字段中输入远程站点的名称(例如 Android Plugin ),并在位置字段中输入 [dl-ssl.google.com/android/eclipse/](https://dl-ssl.google.com/android/eclipse/) 。单击确定。
  6. 现在,您应该会在出现在安装对话框中间的列表中看到开发者工具。
  7. 选中开发者工具旁边的复选框,这将自动选中嵌套的 Android DDMS、Android 开发工具和 Android 层次结构查看器复选框。单击下一步。
  8. 出现的安装细节窗格列出了 Android DDMS、Android 开发工具和 Android Hierarchy Viewer。单击“下一步”阅读并接受许可协议,安装所有依赖项,然后单击“完成”。
  9. 出现一个安装软件对话框,并负责安装。如果遇到安全警告对话框,点击确定。
  10. 最后,Eclipse 会显示一个软件更新对话框,提示您重启这个 IDE。单击“立即重新启动”按钮重新启动。

提示:如果在第 5 步中获取插件有困难,请尝试在位置字段中指定http而不是https(出于安全原因,首选https)。

要完成 ADT 插件的安装,您必须通过修改 Eclipse 中的 ADT 首选项来配置这个插件,以指向 Android SDK 主目录。通过完成以下步骤来完成此任务:

  1. 从“窗口”菜单中选择“首选项”,打开“首选项”面板。对于 Mac OS X,从 Eclipse 菜单中选择 Preferences。
  2. 从左侧面板中选择 Android。
  3. 单击 SDK 位置文本字段旁边的浏览按钮,找到您下载的 SDK 的主目录(例如C:\android-sdk-windows)。
  4. 单击应用,然后单击确定。

注意:关于安装 ADT 插件的更多信息,以及遇到困难时的帮助信息,请查看谷歌在线 Android 开发者指南中的 Eclipse 页面的 ADT 插件([developer.android.com/sdk/eclipse-adt.html](http://developer.android.com/sdk/eclipse-adt.html))。

1–10。用 Eclipse 开发统一通信

问题

现在您已经安装了 Eclipse Classic 3.6.1 和 ADT 插件,您想学习如何使用这个 IDE/插件来开发UC

您首先需要创建一个名为UC的 Android Eclipse 项目。然后引入不同的源文件,并将资源拖到不同的目录中。最后,通过从菜单栏中选择 Run 来执行UC

它是如何工作的

用 Eclipse 开发UC的第一个任务是创建一个新的 Android 项目。完成以下步骤来创建该项目:

  1. 如果没有运行,启动 Eclipse。
  2. 从“文件”菜单中选择“新建”,然后从弹出菜单中选择“项目”。
  3. New Project 对话框中,展开向导树中的 Android 节点,选择该节点下的 Android 项目分支,点击 Next 按钮。
  4. 在出现的新 Android 项目对话框中,将UC输入到项目名称文本字段中。输入的名称标识了存储UC项目的文件夹。
  5. 如果未选中,请选择“在工作区中创建新项目”单选按钮。
  6. 在构建目标下,选择适当的 Android 目标作为UC的构建目标。这个目标指定了您希望您的应用在哪个 Android 平台上构建。假设您只安装了 Android 2.3 平台,那么只有这个构建目标应该出现,并且应该已经被选中。
  7. 在属性下,在应用名称文本字段中输入 Units Converter 。这个人类可读的标题将出现在 Android 设备上。继续,在包名文本字段中输入 com.apress.uc 。该值是包名称空间(遵循与 Java 编程语言中的包相同的规则),所有源代码都将驻留在该名称空间中。选中创建活动复选框(如果未选中),并在此复选框旁边的文本字段中输入UC作为应用启动活动的名称。未选中此复选框时,文本字段被禁用。最后,在 Min SDK 版本文本字段中输入整数9 ,以确定在 Android 2.3 平台上正确运行UC所需的最低 API 级别。
  8. 单击完成。

Eclipse 通过在 Eclipse 工作区目录中创建一个包含以下子目录和文件的UC目录来做出响应:

  • .settings :该目录包含一个org.eclipse.jdt.core.prefs文件,记录项目特定的设置。
  • assets :该目录用于存储非结构化层次的文件。存储在该目录中的任何内容都可以通过原始字节流由应用检索。
  • bin :您的 APK 文件存储在这里。
  • gen :生成的R.java文件存储在反映包层次结构的子目录结构中(如com\apress\uc)。
  • res : App 资源存放在各个子目录中。
  • src : App 源代码按照包层次存储。
  • .classpath :这个文件存储了项目的类路径信息,以便可以定位到项目所依赖的外部库。
  • .project :这个文件包含了重要的项目信息,比如项目的种类,包含了哪些构建者,项目附加了哪些链接资源。
  • AndroidManifest.xml :该文件包含UC的舱单信息。
  • default.properties :该文件包含项目设置。
  • Proguard。cfg :该文件包含 ProGuard 配置数据。

关闭欢迎选项卡。Eclipse 提供了如图图 1–17 所示的用户界面。

images

图 1–17。 Eclipse 的用户界面围绕着一个菜单栏、一个工具栏、几个窗口(比如 Package Explorer 和 Outline)、一个状态栏和一个为编辑器窗口保留的空白区域。

这个用户界面被称为工作台。“包资源管理器”窗口出现在左侧,并提供一个可展开的节点列表,这些节点标识当前工作区中的各个项目及其组件。图 1–17 显示UC是工作区中唯一的项目。

要了解 Eclipse 如何组织UC 项目,请单击该节点左侧的+图标。图 1–18 展示了一个扩展的项目层次结构。

images

图 1–18。 点击额外的+图标显示更多UC的文件组织。

双击UC.java node。Eclipse 通过显示图 1–19 中显示的UC.java窗口做出响应。

images

图 1–19。【UC.java 揭示骨骼内容。

用清单 1–9 中的替换UC.java的框架内容,忽略 Eclipse 报告的错误。稍后您将更正这些错误。

完成以下步骤,将必要的资源引入此项目:

  1. 双击main.xml节点。Eclipse 以图形布局模式显示 main.xml 编辑器窗口。
  2. 点击窗口下方的main.xml选项卡,切换到文本模式。用清单 1–10 替换窗口内容。
  3. 双击strings.xml节点。Eclipse 在资源模式下显示一个strings.xml编辑器窗口。
  4. 单击窗口下方的 strings.xml 选项卡切换到文本模式。用清单 1–12 替换窗口内容。
  5. 右键单击“值”节点,从弹出菜单中选择“新建”,然后选择“其他”。出现一个对话框。
  6. 展开向导列表中的 XML 节点,选择 XML 文件,然后单击下一步。在下一个窗格中,将文件名字段中的NewFile.xml替换为arrays.xml;单击完成。
  7. Eclipse 在设计模式下显示一个 arrays.xml 编辑器窗口。点按窗口下方的源标签以切换到文本模式。用清单 1–13 替换窗口内容。
  8. 右键单击drawable-mdpi节点,从弹出菜单中选择新建,然后选择其他。出现一个新的对话框。
  9. 展开向导列表中的 XML 节点,选择 XML 文件,然后单击下一步。在下一个窗格中,将文件名字段中的NewFile.xml替换为gradientbg.xml;单击完成。
  10. Eclipse 在设计模式下显示一个gradientbg.xml编辑器窗口。点按窗口下方的源标签以切换到文本模式。用清单 1–11 替换窗口内容。
  11. 右键单击drawable-mdpi下的icon.png节点。从弹出菜单中选择删除,并删除该节点。
  12. 将本章代码档案中的icon.png文件复制到剪贴板。右键单击drawable-mdpi并从弹出菜单中选择粘贴。

从菜单栏中选择运行,并从出现的下拉菜单中选择运行。在出现的运行方式对话框中,选择 Android 应用并点击确定。

如果一切顺利,Eclipse 将使用test_AVD设备启动emulator工具,安装UC应用,并使该应用开始运行(参见图 1–13)。

注意: Eclipse 为 Android 应用开发提供的支持远远超出了本秘籍的范围。例如,如果您需要调试一个失败的 Android 应用,您可以通过从窗口菜单中选择打开透视图,然后从弹出菜单中选择其他,然后从打开透视图对话框中选择 DDMS 来启动 Dalvik 调试监视器服务。要了解 DDMS,请查看 J Beer 的“如何使用谷歌 Android 的 Dalvik 调试监控服务(DDMS)工具”教程([www.brighthub.com/mobile/google-android/articles/25023.aspx](http://www.brighthub.com/mobile/google-android/articles/25023.aspx))和 James Sugrue 的“调试 Android:使用 DDMS 查看引擎盖下”教程([java.dzone.com/articles/debugging-android-using-ddms](http://java.dzone.com/articles/debugging-android-using-ddms))。

关于通过 Eclipse/ADT 插件开发 Android 应用的更多见解,请查看 Lars Vogel 的“Android 开发教程-姜饼”教程([www.vogella.de/articles/Android/article.html](http://www.vogella.de/articles/Android/article.html))。

总结

Android 让许多为这个平台开发(甚至销售)应用的人兴奋不已。现在加入进来还为时不晚,本章将带您快速浏览 Android 的关键概念和开发工具。

您首先了解到 Android 是一个用于移动设备的软件堆栈,这个堆栈由应用、中间件和 Linux 操作系统组成。然后,您了解了 Android 的历史,包括已经发布的各种 SDK 更新。

你接下来遇到了 Android 的分层架构,它包括顶部的应用;应用框架、C/C++ 库和作为中间件的 Dalvik 虚拟机;底部是 Linux 内核的修改版本。

接下来,您会看到应用架构,它基于组件(活动、服务、广播接收器和内容提供者),这些组件通过使用意图相互通信,由清单描述,并存储在应用包中。

然后,您学习了如何通过子类化android.app.Activity类来实现活动,通过子类化抽象android.app.Service类来实现服务,通过子类化抽象android.content.BroadcastReceiver类来实现广播接收器,以及通过子类化抽象android.content.ContentProvider类来实现内容提供者。

在这一点上,第一章脱离了这个基本理论,通过一系列的秘籍关注实际问题。最初的方法集中在安装 Android SDK 和 Android 平台,创建 AVD,并用这个 AVD 启动仿真器。

下一批秘籍向您介绍了一个样本单位转换器应用。他们还向您展示了如何创建这个应用,如何将它安装在模拟器上,如何从模拟器上运行它,以及如何准备发布版本以发布到 Google 的 Android Market。

在命令行环境中使用命令行工具可能会很乏味。出于这个原因,最后的两个方法关注于迁移到 Eclipse IDE,并向您展示了如何在这个图形环境的上下文中开发 Units Converter。

在探索 Units Converter 应用时,向您介绍了一些用户界面概念。第二章通过介绍关注各种 Android 用户界面技术的方法,构建了这些概念。

二、用户界面秘籍

Android 平台设计用于在各种不同的设备类型、屏幕尺寸和屏幕分辨率上运行。为了帮助开发人员应对这一挑战,Android 提供了丰富的用户界面组件工具包,可以根据他们特定应用的需求进行利用和定制。Android 还非常依赖可扩展的 XML 框架和资源限定符集来创建能够适应这些环境变化的动态布局。在这一章中,我们来看看一些实用的方法来构建这个框架,以满足您特定的开发需求。

2–1。自定义窗口

问题

默认的窗口元素不适合您的应用。

解决方案

(API 一级)

使用主题和WindowManager自定义窗口属性和功能。无需任何定制,Android 应用中的活动将加载默认的系统主题,看起来有点像 Figure 2–1。

窗口颜色是黑色的,在活动的顶部有一个标题栏(通常是灰色的)。状态栏显示在所有东西的上方,下方有轻微的阴影效果。这些都是由窗口控制的应用的可定制方面,可以为整个应用或特定活动进行设置。

images

图 2–1。 一场裸奔活动

它是如何工作的
用主题定制窗口属性

Android 中的主题是一种适用于整个应用或活动的外观样式。应用主题时有两种选择:使用系统主题或创建自定义主题。无论哪种情况,都会在 AndroidManifest.xml 文件中应用一个主题,如清单 2–1 所示。

清单 2–1。 AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"     …>     <!—Apply to the application tag for a global theme -->     <application android:theme="THEME_NAME"         …>         <!—Apply to the activity tag for an individual theme -->         <activity android:name=".Activity" android:theme="THEME_NAME"             …>             <intent-filter>                   …             </intent-filter>         </activity>     </application> </manifest>

系统主题

Android 框架打包的 styles.xml 包含一些主题选项,并设置了一些有用的自定义属性。在 SDK 文档中引用 R.style 将提供完整的列表,但这里有一些有用的示例:

  • 主题。not title bar:从应用了此主题的组件中移除标题栏。
  • 主题。移除标题栏和状态栏,填满整个屏幕。
  • 主题。对话:让活动看起来像对话的有用主题。
  • 主题。壁纸 (API Level 5): 应用用户选择的壁纸作为窗口背景。

清单 2–2 是通过设置AndroidManifest.xml文件中的android:theme属性应用于整个应用的系统主题示例:

清单 2–2。 应用上设置了主题的清单

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"     …>     <!—Apply to the application tag for a global theme -->     <application android:theme="Theme.NoTitleBar"         …>         …     </application> </manifest>

自定义主题

有时提供的系统选择是不够的。毕竟,窗口中的一些可定制元素甚至没有在系统选项中提及。定义一个自定义主题来完成这项工作很简单。

如果还没有,在项目的 res/values 路径中创建一个styles.xml文件。记住,主题只是在更大范围内应用的样式,所以它们是在同一个地方定义的。与窗口定制相关的主题方面可以在 SDK 的 R.attr 参考中找到,但这里是最常见的项目:

  • android:windowNoTitle
    • 控制是否移除默认标题栏。
    • 设置为true移除标题栏。
  • android:windowFullscreen
    • 控制是否删除系统状态栏。
    • 设置为true以移除状态栏并填充整个屏幕。
  • android:windowBackground
    • 作为背景应用的颜色或可绘制资源
    • 设置为颜色或可绘制值或资源
  • android:windowContentOverlay
    • 放置在窗口内容前景上的 Drawable。默认情况下,这是状态栏下方的阴影。
    • 设置为任何资源来代替默认的状态栏阴影,或者设置为 null(XML 中的@null)来移除它。
  • android:windowTitleBackgroundStyle
    • 应用于窗口标题视图的样式
    • 设置为任何样式资源。
  • android:windowTitleSize
    • 窗口标题视图的高度
    • 设置为任何维度或维度资源
  • android:windowTitleStyle
    • 应用于窗口标题文本的样式
    • 设置为任何样式资源

清单 2–3 是一个创建两个定制主题的 styles.xml 文件的例子:

  • MyTheme.One: No title bar and the default status bar shadow removed
  • MyTheme.Two: Fullscreen with a custom background image

清单 2–3。 带有两个自定义主题的 RES/values/styles . XML

<?xml version="1.0" encoding="utf-8"?> <resources>     <style name="MyTheme.One" parent="@android:style/Theme">         <item name="android:windowNoTitle">true</item>         <item name="android:windowContentOverlay">@null</item>     </style>     <style name="MyTheme.Two" parent="@android:style/Theme">         <item name="android:windowBackground">@drawable/window_bg</item>         <item name="android:windowFullscreen">true</item>     </style> </resources>

请注意,主题(或样式)也可能指示从其继承属性的父主题,因此不需要从头开始创建整个主题。在这个例子中,我们选择继承 Android 的默认系统主题,只定制我们需要区分的属性。所有平台主题都在 Android 包的res/values/themes.xml中定义。有关更多细节,请参考 SDK 关于样式和主题的文档。

清单 2–4 展示了如何将这些主题应用到 AndroidManifest.xml 中的单个活动实例:

清单 2–4。 以每个活动设定的主题显现

`
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    …>
    <!—Apply to the application tag for a global theme -->
    <application
        …>
        <!—Apply to the activity tag for an individual theme -->
        <activity android:name=".ActivityOne" android:theme="MyTheme.One"
            …>
            
                  …
            

        
        <activity android:name=".ActivityTwo" android:theme="MyTheme.Two"
            …>
            
                  …
            

        


`

在代码中定制窗口特性

除了使用样式 XML 之外,窗口属性也可以从活动中的 Java 代码定制。这种方法向开发人员开放了一个稍有不同的特性集供定制,尽管与 XML 样式有一些重叠。

通过代码定制窗口包括在为活动设置内容视图之前,使用Activity.requestWindowFeature()方法为每个特性变化向系统发出请求。

注意:所有使用Activity.requestWindowFeature()的扩展窗口功能的请求必须在调用Activity.setContentView()之前提出。此后所做的任何更改都不会发生。

您可以从该窗口请求的功能及其含义定义如下:

  • FEATURE_CUSTOM_TITLE:设置自定义布局资源为活动标题视图。
  • FEATURE_NO_TITLE:从活动中删除标题视图。
  • 在标题中使用确定的(0-100%)进度条。
  • FEATURE_INDETERMINATE_PROGRESS:在标题视图中使用一个小的不确定(圆形)进度指示器。
  • FEATURE_LEFT_ICON:在标题视图的左侧包含一个小标题图标。
  • FEATURE_RIGHT_ICON:在标题视图的右侧包含一个小标题图标。

FEATURE_CUSTOM_TITLE

使用这个窗口特性将标准标题替换为完全自定义的布局资源(参见清单 2–5)。

清单 2–5。 活动设置自定义标题布局

`protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Request window features before setContentView
    requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
    setContentView(R.layout.main);

//Set the layout resource to use for the custom title
    getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.custom_title);

}`

注意:因为此功能完全取代了默认标题视图,所以它不能与任何其他窗口功能标志结合使用。

特征编号标题

使用此窗口功能移除标准标题视图(参见清单 2–6)。

清单 2–6。 活动移除标准标题视图

`protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Request window features before setContentView
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.main);

}`

注意:因为此功能完全删除了默认标题视图,所以它不能与任何其他窗口功能标志结合使用。

功能 _ 进度

使用此窗口功能访问窗口标题中确定的进度条。进度可以设置为从 0 (0%)到 10000 (100%)之间的任何值(参见清单 2–7)。)

清单 2–7。 使用窗口进度条的活动

`protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Request window features before setContentView
    requestWindowFeature(Window.FEATURE_PROGRESS);
    setContentView(R.layout.main);

//Set the progress bar visibility
    setProgressBarVisibility(true);
    //Control progress value with setProgress
    setProgress(0);
    //Setting progress to 100% will cause it to disappear
    setProgress(10000);

}`

特征 _ 不确定 _ 进度

使用此窗口功能访问不确定的进度指示器,以显示后台活动。由于这个指示器是不确定的,它只能被显示或隐藏(见清单 2–8)。

清单 2–8。 使用窗口不定进度条的活动

`protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Request window features before setContentView
    requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
    setContentView(R.layout.main);

//Show the progress indicator
    setProgressBarIndeterminateVisibility(true);

//Hide the progress indicator
setProgressBarIndeterminateVisibility(false);
}`

特征 _ 左侧 _ 图标

使用这个窗口特性在标题视图的左侧放置一个小的可绘制图标(参见清单 2–9)。

清单 2–9。 活动使用功能图标

`protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Request window features before setContentView
    requestWindowFeature(Window.FEATURE_LEFT_ICON);
    setContentView(R.layout.main);

//Set the layout resource to use for the custom title
    setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, R.drawable.icon);
}`

特征 _ 右键 _ 图标

使用这个窗口特性来放置一个右对齐的小可绘制图标(见清单 2–10)。

清单 2–10。 活动使用功能图标

`protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Request window features before setContentView
    requestWindowFeature(Window.FEATURE_RIGHT_ICON);
    setContentView(R.layout.main);

//Set the layout resource to use for the custom title
    setFeatureDrawableResource(Window.FEATURE_RIGHT_ICON, R.drawable.icon);
}`

注意: FEATURE_RIGHT_ICON 不一定表示图标会放在标题文字的右侧。

图 2–2 显示了同时启用所有图标和进度功能的活动。请注意此视图中所有元素的相对位置。

images

图 2–2。 在 Froyo 之前的活动(左)和 Froyo 之后的活动(右)中启用的窗口功能

注意,在 8 (Froyo)之前的 API 级别中,RIGHT功能图标的布局仍然在标题文本的左侧。API Levels 8 和更高版本纠正了这个问题,现在在视图的右侧显示图标,尽管仍然在不确定进度指示器的左侧,如果它是可见的话。

2–2。创建和显示视图

问题

应用需要视图元素来显示信息并与用户交互。

解决方案

(API 一级)

无论是使用 Android SDK 中众多可用视图和小部件之一,还是创建自定义显示,所有应用都需要视图来与用户交互。在 Android 中创建用户界面的首选方法是用 XML 定义它们,并在运行时扩展它们。

Android 中的视图结构是一棵树,根通常是活动或窗口的内容视图。视图组是管理一个或多个子视图显示的特殊视图,这些子视图可以是另一个视图组,并且树会继续增长。所有标准布局类都源自 ViewGroup,是 XML 布局文件根节点的最常见选择。

它是如何工作的

让我们用两个按钮实例和一个接受用户输入的 EditText 来定义一个布局。我们可以在 res/layout/中定义一个名为 main.xml 的文件,内容如下(见清单 2–11)。

清单 2–11。 res/layout/main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   android:orientation="vertical">   <EditText     android:id="@+id/editText"     android:layout_width="fill_parent"     android:layout_height="wrap_content"   />   <LinearLayout     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:orientation="horizontal">     <Button       android:id="@+id/save"       android:layout_width="wrap_content"       android:layout_height="wrap_content"       android:text="Save"     />     <Button       android:id="@+id/cancel"       android:layout_width="wrap_content"       android:layout_height="wrap_content"       android:text="Cancel"     />   </LinearLayout> </LinearLayout>

LinearLayout是一个ViewGroup,它以水平或垂直的方式一个接一个地布置元素。在main.xml中,EditText和内LinearLayout依次垂直排列。内部LinearLayout(按钮)的内容水平排列。带有android:id值的视图元素是需要在 Java 代码中引用的元素,以便进一步定制或显示。

要使这个布局显示活动的内容,它必须在运行时膨胀。用一个方便的方法重载了Activity.setContentView()方法,只需要布局 ID 值。在这种情况下,在活动中设置布局就像这样简单:

public void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);     setContentView(R.layout.main);     //Continue Activity initialization }

除了提供 ID 值(main.xml 自动具有 R.layout.main 的 ID)之外,不需要任何东西。如果布局在附加到窗口之前需要更多的定制,您可以手动放大它,并在将其添加为内容视图之前做一些工作。清单 2–12 放大了相同的布局,并在显示之前添加了第三个按钮。

清单 2–12。 显示前的布局修改

`public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Inflate the layout file
    LinearLayout layout = (LinearLayout)getLayoutInflater().inflate(R.layout.main, null);
    //Add a new button
    Button reset = new Button(this);
    reset.setText("Reset Form");
    layout.addView(reset,
        new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));

//Attach the view to the window
    setContentView(layout);
}`

在这个实例中,XML 布局在活动代码中使用一个LayoutInflater展开,它的inflate()方法返回展开视图的句柄。由于 LayoutInflater.inflate()返回一个视图,我们必须将它转换成 XML 中的特定子类,以便不仅仅是将它附加到窗口。

注意:XML 布局文件中的根元素是从LayoutInflater.inflate()返回的视图元素。

2–3 岁。监控点击动作

问题

当用户点击视图时,应用需要做一些工作。

解决方案

(API 一级)

确保视图对象是可点击的,并附加一个视图。OnClickListener 来处理事件。默认情况下,SDK 中的许多小部件已经是可点击的,例如按钮、图像按钮和复选框。然而,通过在 XML 中设置android:clickable="true"或从代码中调用View.setClickable(true),任何视图都可以接收点击事件。

它是如何工作的

要接收和处理 click 事件,创建一个 OnClickListener 并将其附加到 view 对象。在本例中,视图是在根布局中定义的按钮,如下所示:

<Button   android:id="@+id/myButton"   android:layout_width="wrap_content"   android:layout_height="wrap_content"   android:text="My Button" />

在活动代码中,通过按钮的android:id值和附加的监听器来检索按钮(参见清单 2–13)。

清单 2–13。 在按钮上设置监听器

`public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Retrieve the button object
    Button myButton = (Button)findViewById(R.id.myButton);
    //Attach the listener
    myButton.setOnClickListener(clickListener);
}

//Listener object to handle the click events
View.OnClickListener clickListener = new View.OnClickListener() {
    public void onClick(View v) {
        //Code to handle the click event
    {
};`

(API 4 级)

从 API Level 4 开始,有一种更有效的方法来附加基本的点击监听器以查看小部件。视图小部件可以在 XML 中设置android:onClick属性,运行时会使用 Java 反射在事件发生时调用所需的方法。如果我们修改前面的示例以使用此方法,按钮的 XML 将变成如下所示:

<Button   android:layout_width="wrap_content"   android:layout_height="wrap_content"   android:text="My Button"   android:onClick="onMyButtonClick" />

在这个例子中不再需要android:id属性,因为我们在代码中引用它的唯一原因是为了添加监听器。这也简化了 Java 代码,看起来像清单 2–14。

清单 2–14。XML 中附加的监听器

`public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //No code required here to attach the listener
}

public void onMyButtonClick(View v) {
    //Code to handle the click event
}`

2–4 岁。独立于解决方案的素材

问题

您的应用使用的图形素材不能很好地使用 Android 的传统机制在更高分辨率的屏幕上放大图像。

解决方案

(API 4 级)

使用资源限定符,并为每个素材提供多种大小。Android SDK 定义了四种类型的屏幕分辨率或密度,如下所示:

  • 低(ldpi): 120dpi
  • 中等(mdpi): 160dpi
  • 高(hdpi): 240dpi
  • 超高(xhdpi): 320dpi(在 API Level 8 中添加)

默认情况下,一个 Android 项目可能只有一个存储所有图形资源的res/drawable/目录。在这种情况下,Android 将在中等分辨率的屏幕上以 1:1 的比例显示这些图像。当应用在更高分辨率的屏幕上运行时,Android 会将图像放大到 150%(xhdpi 为 200%),这可能会导致质量下降。

它是如何工作的

为避免此问题,建议您以不同的分辨率提供每个图像资源的多个副本,并将它们放入资源限定的目录路径中。

  • res/drawable-ldpi/
    • mdpi 时大小的 75%
  • res/drawable-mdpi/
    • 记为原始图像大小
  • res/drawable-hdpi/
    • mdpi 时大小的 150%
  • res/drawable-xhdpi/
    • mdpi 时大小的 200%
    • 仅当应用支持 API 级别 8 作为最低目标时

图像在所有目录中必须具有相同的文件名。例如,如果您在 AndroidManifest.xml 中保留了默认的图标值(即android:icon="@drawable/icon"),那么您将在项目中放置以下资源文件。

res/drawable-ldpi/icon.png(36×36 像素)

res/drawable-mdpi/icon.png (48x48 像素)

res/drawable-hdpi/icon.png(72×72 像素)

res/drawable-xhdpi/icon.png (96x96 像素,如果支持的话)

Android 将选择适合设备分辨率的素材,并将其作为应用图标显示在启动器屏幕上,从而不会缩放,也不会损失图像质量。

作为另一个例子,一个徽标图像要在整个应用中的几个地方显示,在中等分辨率的设备上是 200x200 像素。应该使用资源限定符以所有支持的大小提供该图像。

res/drawable-ldpi/logo.png (150x150 像素)

res/drawable-mdpi/logo.png(200×200 像素)

res/drawable-hdpi/logo.png(300×300 像素)

这个应用不支持超高分辨率显示,所以我们只提供三个图像。当需要引用这个资源时,只需使用@drawable/logo(来自 XML)或R.drawable.logo(来自 Java 代码),Android 就会显示相应的资源。

2–5 岁。锁定活动方向

问题

您的应用中的某个活动不应该被允许旋转,或者旋转需要来自应用代码的更直接的干预。

解决方案

(API 一级)

使用 AndroidManifest.xml 文件中的静态声明,可以修改每个单独的活动以锁定纵向或横向。这只能应用于<activity>标签,所以不能对整个应用范围执行一次。只需将android:screenOrientation="portrait"android:screenOrientation="landscape"添加到<activity>元素,它们将始终显示在指定的方向,而不管设备如何定位。

还有一个选项可以在名为“behind”的 XML 中传递。如果一个 Activity 元素设置了android:screenOrientation="behind",它将从堆栈中的前一个 Activity 中获取它的设置。这对于活动匹配其发起者的锁定方向来说是一种非常有用的方法,可以实现一些稍微更动态的行为。

它是如何工作的

在清单 2–15 中描述的示例 AndroidManifest.xml 有三个活动。其中两个被锁定为纵向(MainActivity 和 ResultActivity),而 UserEntryActivity 被允许旋转,大概是因为用户可能希望旋转并使用物理键盘。

清单 2–15。

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"       package="com.examples.rotation"       android:versionCode="1"       android:versionName="1.0">     <application android:icon="@drawable/icon" android:label="@string/app_name">         <activity android:name=".MainActivity"             android:label="@string/app_name"             android:screenOrientation="portrait">             <intent-filter>                 <action android:name="android.intent.action.MAIN" />                 <category android:name="android.intent.category.LAUNCHER" />             </intent-filter>         </activity>         <activity android:name=".ResultActivity"             android:screenOrientation="portrait" />         <activity android:name=".UserEntryActivity" />     </application> </manifest>

2–6 岁。动态定向锁定

问题

存在屏幕不应该旋转的情况,但是这种情况是暂时的,或者取决于用户的愿望。

解决方案

(API 一级)

使用 Android 中的请求方向机制,应用可以调整用于显示活动的屏幕方向,将其固定到特定的方向或将其释放给设备来决定。这是通过使用Activity.setRequestedOrientation()方法完成的,该方法从ActivityInfo.screenOrientation属性分组中获取一个整数常量。

默认情况下,请求的方向设置为SCREEN_ORIENTATION_UNSPECIFIED,这允许设备自己决定应该使用哪个方向。这通常是基于设备的物理方向的决定。使用Activity.getRequestedOrientation()也可以随时检索当前请求的方向。

它是如何工作的
用户旋转锁定按钮

作为一个例子,让我们创建一个 ToggleButton 实例,它控制是否锁定当前方向,允许用户在任何时候控制活动是否应该改变方向。

在 main.xml 布局中的某处,定义了一个 ToggleButton 实例:

<ToggleButton     android:id="@+id/toggleButton"     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:textOff="Lock"     android:textOn="LOCKED" />

在活动代码中,我们将创建一个按钮状态监听器,该监听器根据按钮的当前值锁定和释放屏幕方向(参见清单 2–16)。

清单 2–16。 动态锁定/解锁屏幕方向的活动

`public class LockActivity extends Activity {

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

//Get handle to the button resource
        ToggleButton toggle = (ToggleButton)findViewById(R.id.toggleButton);
        //Set the default state before adding the listener
        if( getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED ) {
            toggle.setChecked(true);
        } else {
            toggle.setChecked(false);
        }
        //Attach the listener to the button
        toggle.setOnCheckedChangeListener(listener);
    }

OnCheckedChangeListener listener = new OnCheckedChangeListener() {
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            int current = getResources().getConfiguration().orientation;
            if(isChecked) {
                switch(current) {
                case Configuration.ORIENTATION_LANDSCAPE:
                    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
                    break;
                case Configuration.ORIENTATION_PORTRAIT:
                    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
                    break;
                default:
                    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
                }
            } else {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
            }
        }
    }

}`

监听器中的代码是这个秘籍的关键成分。如果用户按下按钮并切换到 on 状态,则通过存储来自Resources.getConfiguration()orientation参数读取当前方位。配置对象和请求的方向使用不同的常量来映射状态,所以我们打开当前的方向,用适当的常量调用setRequestedOrientation()

注意:如果请求一个不同于当前状态的方向,并且您的活动在前台,则该活动将立即改变以适应该请求。

如果用户按下按钮,它切换到关闭状态,我们不再想锁定方向,所以再次用SCREEN_ORIENTATION_UNSPECIFIED常量调用setRequestedOrientation()将控制返回给设备。如果设备方向指示活动与应用锁定的位置不同,这也可能导致立即发生变化。

注意:设置一个请求方向并不能而不是阻止默认活动生命周期的发生。如果设备配置发生变化(键盘滑出或设备方向改变),活动仍然会被销毁和重新创建,因此所有关于保持活动状态的规则仍然适用。

2–7 岁。手动处理旋转

问题

在轮换期间销毁和重新创建活动的默认行为会导致应用出现不可接受的性能损失。

如果没有定制,Android 将通过完成当前的 Activity 实例并在其位置上创建一个新的、适合新配置的 Activity 实例来响应配置更改。这可能会导致不适当的性能损失,因为必须保存 UI 状态,并完全重新构建 UI。

解决方案

(API 一级)

利用android:configChanges manifest 参数来指示 Android 某个活动将在没有运行时帮助的情况下处理旋转事件。这不仅减少了 Android 销毁和重新创建活动实例的工作量,还减少了应用的工作量。在活动实例保持不变的情况下,应用不必花费时间来保存和恢复当前状态,以保持对用户的一致性。

注册一个或多个配置变更的活动将通过Activity.onConfigurationChanged()回调方法得到通知,在这里它可以执行与变更相关的任何必要的手动处理。

为了完全处理旋转,活动应该注册两个配置更改参数:orientationkeyboardHiddenorientation参数记录器件方向改变时的任何事件的活动。keyboardHidden参数记录用户滑入或滑出物理键盘时事件的活动。虽然后者可能不是直接感兴趣的,但如果你没有注册这些事件,Android 将在事件发生时重新创建你的活动,这可能会破坏你在处理旋转方面的努力。

它是如何工作的

这些参数被添加到 AndroidManifest.xml 中的任何<activity>元素,如下所示:

<activity android:name=".MyActivity" android:configChanges="orientation|keyboardHidden" />

可以在同一个赋值语句中注册多个更改,在它们之间使用管道“|”字符。因为这些参数不能应用于<application>元素,所以每个单独的活动都必须在 AndroidManifest.xml 中注册。

注册活动后,配置更改会导致调用活动的onConfigurationChanged()方法。清单 2–17 是一个简单的活动定义,可以用来处理变更发生时收到的回调。

清单 2–17。 手动管理轮换的活动

`public class MyActivity extends Activity {

@Override
    protected void onCreate(Bundle savedInstanceState) {
        //Calling super is required
        super.onCreate(savedInstanceState);
        //Load view resources
        loadView();
    }

@Override
    public void onConfigurationChanged(Configuration newConfig) {
        //Calling super is required
        super.onConfigurationChanged(newConfig);
        //Store important UI state
        saveState();
        //Reload the view resources
        loadView();
    }

private void saveState() {
        //Implement any code to persist the UI state
    }

private void loadView() {
        setContentView(R.layout.main);

//Handle any other required UI changes upon a new configuration
        //Including restoring and stored state
    }
}`

注意: Google 不建议以这种方式处理旋转,除非这是应用性能所必需的。所有特定于配置的资源都必须手动加载,以响应每个更改事件。

值得注意的是,Google 建议在活动轮换时允许默认的重新创建行为,除非您的应用的性能需要绕过它。首先,这是因为如果你将替代资源存储在资源限定的目录中(比如用于横向布局的res/layout-land/),你将失去 Android 为加载替代资源提供的所有帮助。

在示例活动中,所有处理视图布局的代码都被抽象为私有方法loadView(),从onCreate()onConfigurationChanged()中调用。在这种方法中,像setContentView()这样的代码被放置以确保适当的布局被加载以匹配配置。

调用setContentView()将完全重新加载视图,因此任何重要的 UI 状态仍然需要保存,并且不需要像onSaveInstanceState()onRestoreInstanceState()这样的生命周期回调的帮助。为此,该示例实现了一个名为saveState()的方法。

2–8 岁。创建弹出式菜单操作

问题

您希望在用户选择用户界面的某个部分时,为用户提供多种操作。

解决方案

(API 一级)

显示一个ContextMenuAlertDialog以响应用户操作。

它是如何工作的
上下文菜单

使用一个ContextMenu是一个有用的解决方案,特别是当你想在一个ListView或其他AdapterView中基于一个项目点击提供一个动作列表的时候。这是因为ContextMenu.ContextMenuInfo对象提供了关于所选择的特定项目的有用信息,比如 id 和位置,这可能有助于构建菜单。

首先在 res/menu/中创建一个 XML 文件来定义菜单本身;我们称之为contextmenu.xml(见清单 2–18)。

清单 2–18。RES/menu/context menu . XML

`

         `

然后,利用Activity中的onCreateContextMenu()onContextItemSelected()展开菜单并处理用户选择(参见清单 2–19)。

清单 2–19。 利用自定义菜单的活动

`@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
    super.onCreateContextMenu(menu, v, menuInfo);
    getMenuInflater().inflate(R.menu.contextmenu, menu);
    menu.setHeaderTitle("Choose an Option");
}

@Override
public boolean onContextItemSelected(MenuItem item) {
    //Switch on the item’s ID to find the action the user selected
    switch(item.getItemId()) {
    case R.id.menu_delete:
        //Perform delete actions
        return true;
    case R.id.menu_copy:
        //Perform copy actions
        return true;
    case R.id.menu_edit:
        //Perform edit actions
        return true;
    }
    return super.onContextItemSelected(item);
}`

为了激发这些回调方法,您必须注册将触发菜单的视图。实际上,这将视图的View.OnCreateContextMenuListener设置为当前的Activity:

`@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Register a button for context events
    Button button = new Button(this);
    registerForContextMenu(button);

setContentView(button);
}`

这个秘籍的关键要素是调用Activity.openContextMenu()方法来随时手动触发菜单。Android 中的默认行为是当长按发生时,许多视图显示一个ContextMenu作为主点击动作的替代。然而,在这种情况下,我们希望菜单成为主要动作,所以我们从动作监听器方法中调用openContextMenu():

public void onClick(View v) {     openContextMenu(v); }

将所有的部分绑在一起,我们有一个简单的Activity来注册一个按钮,当点击时显示我们的菜单(见清单 2–20)。

清单 2–20。 利用上下文动作菜单的活动

`public class MyActivity extends Activity {

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //Register a button for context events
        Button button = new Button(this);
        button.setText("Click for Options");
        button.setOnClickListener(listener);
        registerForContextMenu(button);

setContentView(button);
    }

View.OnClickListener listener = new View.OnClickListener() {
        public void onClick(View v) {
            openContextMenu(v);
        }
    };

@Override
    public void onCreateContextMenu(ContextMenu menu, View v,
                ContextMenu.ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        getMenuInflater().inflate(R.menu.contextmenu, menu);
        menu.setHeaderTitle("Choose an Option");
    }

@Override
    public boolean onContextItemSelected(MenuItem item) {
        //Switch on the item’s ID to find the action the user selected
        switch(item.getItemId()) {
        case R.id.menu_delete:
            //Perform delete actions
            return true;
        case R.id.menu_copy:
            //Perform copy actions
            return true;
        case R.id.menu_edit:
            //Perform edit actions
            return true;
        }
        return super.onContextItemSelected(item);
    }

}`

结果应用如图 2–3 中的所示。

images

图 2–3。 上下文动作菜单

警报对话框

使用AlertDialog.Builder可以构建一个类似的 AlertDialog,但是带有一些额外的选项。AlertDialog 是一个非常通用的类,用于创建简单的弹出窗口以获得用户的反馈。用 AlertDialog。构建器、单选或多选列表、按钮和消息字符串都可以轻松地添加到一个紧凑的小部件中。

为了说明这一点,让我们创建与使用 AlertDialog 之前相同的弹出选择。这一次,我们将在选项列表的底部添加一个取消按钮(参见清单 2–21)。

清单 2–21。 使用报警对话框的动作菜单

`public class MyActivity extends Activity {

AlertDialog actions;

@Override
    protectedvoid onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setTitle("Activity");
        Button button = new Button(this);
        button.setText("Click for Options");
        button.setOnClickListener(buttonListener);

AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Choose an Option");
        String[] options = {"Delete Item","Copy Item","Edit Item"};
        builder.setItems(options, actionListener);
        builder.setNegativeButton("Cancel", null);
        actions = builder.create();

setContentView(button);
    }

//List selection action handled here
    DialogInterface.OnClickListener actionListener =
            new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            switch(which) {
            case 0: //Delete
                break;
            case 1: //Copy
                break;
            case 2: //Edit
                break;
            default:
                break;
            }
        }
    };

//Button action handled here (pop up the dialog)
    View.OnClickListener buttonListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            actions.show();
        }
    };
}`

在这个例子中,我们创建了一个新的AlertDialog.Builder实例,并使用它的便利方法来添加:

  • 一个标题,使用setTitle()
  • 可选择的选项列表,使用带有字符串数组的setItems()(也适用于数组资源)
  • 一个取消按钮,使用setNegativeButton()

我们附加到列表项的侦听器返回哪个列表项被选为我们提供的数组中的从零开始的索引,因此 switch 语句检查适用的三种情况中的每一种。我们为 cancel 按钮的侦听器传入 null,因为在这个实例中,我们只是希望 Cancel 关闭对话框。如果在 cancel 上有一些重要的工作要做,可以将另一个侦听器传递给setNegativeButton()方法。

当按下按钮时,生成的应用现在看起来像 Figure 2–4。

images

图 2–4。 警报对话框动作菜单

2–9。自定义选项菜单

问题

当用户按下硬件菜单按钮时,您的应用需要做的不仅仅是显示标准菜单。

解决方案

(API 一级)

截取菜单按钮的KeyEvent,代之以一个自定义视图。

它是如何工作的

通过重写onKeyDown()onKeyUp()方法,可以在ActivityView内部拦截该事件:

@Override public boolean onKeyUp(int keyCode, KeyEvent event) {     if(keyCode == KeyEvent.KEYCODE_MENU) {         //Create and display a custom menu view         //Return true to consume the event         return true;     }     //Pass other events along their way up the chain     return super.onKeyUp(keyCode, event); }

注意: Activity.onKeyDown()Activity.onKeyUp()只有在其子视图首先处理事件时才被调用。在使用这些事件时,返回一个真值是很重要的,这样它们就不会被错误地传递到链上。

下一个例子展示了一个Activity,当用户按下菜单键时,它显示一组包装在一个简单的AlertDialog中的定制按钮,代替传统的选项菜单。在清单 2–22 中,我们将在 res/layout/中为按钮创建一个布局,并将其命名为 custommenu.xml

清单 2–22。RES/layout/custom menu . XML

<?xml version="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="wrap_content"   android:orientation="horizontal">   <ImageButton     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:layout_weight="1"     android:src="@android:drawable/ic_menu_send"   />   <ImageButton     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:layout_weight="1"     android:src="@android:drawable/ic_menu_save"   />   <ImageButton     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:layout_weight="1"     android:src="@android:drawable/ic_menu_search"   />   <ImageButton     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:layout_weight="1"     android:src="@android:drawable/ic_menu_preferences"   /> </LinearLayout>

这是一个有四个重量相等的按钮的布局(所以空间在屏幕上是均匀的),显示了 Android 中的一些默认菜单图像。在清单 2–23 中,我们可以放大这个布局,并将其作为视图应用于AlertDialog

清单 2–23。 活动覆盖菜单动作

`public class MyActivity extends Activity {

MenuDialog menuDialog;
privateclass MenuDialog extends AlertDialog {

public MenuDialog(Context context) {
        super(context);
        setTitle("Menu");
        View menu = getLayoutInflater().inflate(R.layout.custommenu, null);
        setView(menu);
    }

@Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if(keyCode == KeyEvent.KEYCODE_MENU) {
            dismiss();
            returntrue;
        }
        returnsuper.onKeyUp(keyCode, event);
    }
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
    if(keyCode == KeyEvent.KEYCODE_MENU) {
        if(menuDialog == null) {
            menuDialog = new MenuDialog(this);
        }
        menuDialog.show();
        return true;
    }
    return super.onKeyUp(keyCode, event);
}

}`

在这里,我们选择监视Activity.onKeyUp()方法,如果是菜单按压,则通过创建和显示AlertDialog的自定义子类来处理事件。

这个例子为对话框创建了一个自定义类,这样我们就可以扩展AlertDialog.onKeyUp()方法,在用户再次按下菜单按钮时关闭自定义菜单。我们不能在Activity中处理这个事件,因为AlertDialog在前台时会消耗所有的关键事件。我们这样做是为了匹配 Android 标准菜单的现有功能,因此不会破坏用户对应用行为的预期。

当加载前一个活动,并按下菜单按钮时,我们得到类似于 Figure 2–5 的结果。

images

图 2–5。 自定义选项菜单

2–10。自定义后退按钮

问题

您的应用需要以自定义方式处理用户按下硬件后退按钮的情况。

解决方案

(API 一级)

类似于覆盖菜单按钮的功能,硬件后退按钮向您的Activity发送一个KeyEvent,它可以在您的应用代码中被拦截和处理。

它是如何工作的

与秘籍 2–9 的方式相同,覆盖onKeyDown()将赋予您控制权:

@Override public boolean onKeyDown(int keyCode, KeyEvent event) {     if(keyCode == KeyEvent.KEYCODE_BACK) {         //Implement a custom back function         //Return true to consume the event         return true;     }     //Pass other events along their way up the chain     return super.onKeyDown(keyCode, event); }

注意:覆盖硬件按钮事件时应小心。所有的硬件按钮在整个 Android 系统中都有一致的功能,在这些界限之外调整功能会让用户感到困惑和不安。

与前面的例子不同,您不能可靠地使用onKeyUp(),因为默认行为(比如完成当前活动)发生在按键被按下时,而不是在按键被释放时。由于这个原因,onKeyUp()通常永远不会被调用返回键。

(API 等级 5)

从艾克蕾尔开始,SDK 包含了Activity.onBackPressed()回调方法。如果您的应用面向 SDK 级别 5 或更高,则可以重写此方法以执行自定义处理。

@Override public void onBackPressed() {     //Custom back button processing     //Must manually finish when complete     finish(); }

这个方法的默认实现只是简单地为您调用finish(),所以如果您希望活动在您的处理完成后关闭,这个实现将需要直接调用finish()

2–11。模拟主屏幕按钮

问题

您的应用需要采取与用户按下硬件 HOME 按钮相同的动作。

解决方案

(API 一级)

用户点击 Home 按钮的动作向系统发送一个Intent,告诉它加载 HOME 活动。这与在应用中启动任何其他活动没有什么不同;你只需要构建适当的意图来获得效果。

它是如何工作的

在活动中您希望此操作发生的任何位置添加以下行:

Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); startActivity(intent);

此函数的一个常见用途是覆盖 back 按钮以返回主页,而不是返回到上一个活动。这在前台活动下的所有内容都可能受到保护的情况下(例如,登录屏幕)很有用,并且让默认的后退按钮行为发生可能会允许对系统进行不安全的访问。下面是一个使用这两者来使某个活动在按下 back 时调出主屏幕的示例:

@Override public boolean onKeyDown(int keyCode, KeyEvent event) {     if(keyCode == KeyEvent.KEYCODE_BACK) {         Intent intent = new Intent(Intent.ACTION_MAIN);         intent.addCategory(Intent.CATEGORY_HOME);         startActivity(intent);         returntrue;     }     returnsuper.onKeyDown(keyCode, event); }

2–12 岁。_monitoring_textview_changes

问题

您的应用需要持续监控 TextView 小部件(如 EditText)中的文本变化。

解决方案

(API 一级)

实现android.text.TextWatcher接口。TextWatcher在更新文本的过程中提供了三种回调方法:

public void beforeTextChanged(CharSequence s, int start, int count, int after); public void onTextChanged(CharSequence s, int start, int before, int count); public void afterTextChanged(Editable s);

beforeTextChanged()onTextChanged()方法主要是作为通知提供的,因为您实际上不能在这两个方法中对 CharSequence 进行更改。如果你试图截取输入到视图中的文本,当afterTextChanged()被调用时,可能会发生变化。

它是如何工作的

要用一个TextView注册一个TextWatcher实例,调用TextView.addTextChangedListener()方法。从语法上注意到一个TextView可以注册多个TextWatcher

字符计数器示例

TextWatcher 的一个简单用法是创建一个实时字符计数器,它在用户键入或删除信息时跟随 EditText。清单 2–24 是一个示例活动,它为此实现了 TextWatcher,向 EditText 小部件注册,并在活动标题中打印字符数。

清单 2–24。 人物计数器活动

`public class MyActivity extends Activity implements TextWatcher {

EditText text;
int textCount;

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //Create an EditText widget and add the watcher
        text = new EditText(this);
        text.addTextChangedListener(this);

setContentView(text);
    }

/* TextWatcher Implemention Methods */
    public void beforeTextChanged(CharSequence s, int start, int count, int after) { }

public void onTextChanged(CharSequence s, int start, int before, int end) {
        textCount = text.getText().length();
        setTitle(String.valueOf(textCount));
    }

public void afterTextChanged(Editable s) { }

}`

因为我们的需求不包括修改插入的文本,所以我们可以从onTextChanged()开始读取计数,一旦文本发生变化,就会这样做。其他方法未被使用,并保留为空。

货币格式化程序示例

SDK 有一些预定义的TextWatcher实例来格式化文本输入;PhoneNumberFormattingTextWatcher就是其中之一。他们的工作是在用户输入时应用标准格式,减少输入清晰数据所需的击键次数。

在清单 2–25 中,我们创建了一个 CurrencyTextWatcher 来将货币符号和分隔符插入到文本视图中。

清单 2–25。 货币格式器

`public class CurrencyTextWatcher implements TextWatcher {

boolean mEditing;

public CurrencyTextWatcher() {
        mEditing = false;
}

public synchronizedvoid afterTextChanged(Editable s) {
        if(!mEditing) {
            mEditing = true;

//Strip symbols
            String digits = s.toString().replaceAll("\D", "");
            NumberFormat nf = NumberFormat.getCurrencyInstance();
            try{
                String formatted = nf.format(Double.parseDouble(digits)/100);
                s.replace(0, s.length(), formatted);
            } catch (NumberFormatException nfe) {
                    s.clear();
            }

mEditing = false;
        }
    }

public void beforeTextChanged(CharSequence s, int start, int count, int after) { }

public void onTextChanged(CharSequence s, int start, int before, int count) { }

}`

注意:afterTextChanged()中的Editable值进行更改将导致TextWatcher方法被再次调用(毕竟,您只是更改了文本)。因此,编辑的自定义 TextWatcher 实现应该使用布尔值或其他某种跟踪机制来跟踪编辑来自何处,否则您可能会创建一个无限循环。

我们可以将这个自定义文本格式化程序应用于活动中的 EditText(参见清单 2–26)。

清单 2–26。 使用货币格式化程序的活动

`public class MyActivity extends Activity {

EditText text;

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        text = new EditText(this);
        text.addTextChangedListener(new CurrencyTextWatcher());

setContentView(text);
    }

}`

如果您使用这个格式化程序格式化用户输入,用 XML 定义 EditText,那么这将非常方便,这样您就可以应用android:inputTypeandroid:digits约束来轻松地保护字段,防止输入错误。特别是,在 EditText 中添加android:digits="0123456789."(注意小数点后面的句号)将保护这个格式化程序和用户。

2–13 岁。滚动文本视图滚动条

问题

您希望创建一个“ticker”视图,在屏幕上不断滚动其内容。

解决方案

(API 一级)

使用TextView的内置选框功能。当一个TextView的内容太大而不适合它的边界时,默认情况下文本会被截断。这种截断可以使用android:ellipsize属性进行配置,该属性可以设置为以下选项之一:

  • 没有人
    • 默认。
    • 截断文本的结尾,不带可视指示器。
  • 开始
    • 在视图的开头用省略号截断文本的开头。
  • 中间
    • 用视图中间的省略号截断文本的中间。
  • 目标
    • 用视图末尾的省略号截断文本的结尾。
  • 选取框
    • 不要省略;选中时动画显示和滚动文本。

注意:TextView被选中时,选框功能仅用于动画和滚动文本。将android:ellipsize属性单独设置为 marquee 不会激活视图。

它是如何工作的

为了创建无限重复的自动跑马灯,我们向 XML 布局添加了一个 TextView,如下所示:

<TextView   android:id="@+id/ticker"   android:layout_width="fill_parent"   android:layout_height="wrap_content"   android:singleLine="true"   android:scrollHorizontally="true"   android:ellipsize="marquee"   android:marqueeRepeatLimit="marquee_forever" />

配置该视图的关键属性是最后四个。如果没有android:singleLineandroid:scrollHorizontally,文本视图将不能正确地布局,以允许文本比视图长(这是滚动条滚动的一个关键要求)。设置android:ellipsizeandroid:marqueeRepeatLimit允许滚动发生,并且持续不确定的时间量。重复限制也可以设置为任何整数值,这将重复滚动动画很多次,然后停止。

在 XML 中正确设置 TextView 属性后,Java 代码必须将 selected 状态设置为 true,这将启用滚动动画:

TextView ticker = (TextView)findViewById(R.id.ticker); ticker.setSelected(true);

如果您需要让动画基于用户界面中的某些事件开始和停止,只需每次分别用 true 或 false 调用setSelected()

2–14 岁。动画视图

问题

您的应用需要将一个视图对象制作成动画,或者作为一种过渡,或者作为一种效果。

解决方案

(API 一级)

一个Animation对象可以应用于任何视图,并使用View.startAnimation()方法运行;这将立即运行动画。你也可以使用View.setAnimation()来安排一个动画,并将对象附加到一个视图中,但不要立即运行它。在这种情况下,Animation必须设置其开始时间参数。

它是如何工作的
系统动画

为了方便起见,Android SDK 提供了一些可以应用于视图的过渡动画,可以在运行时使用AnimationUtils类加载这些动画:

  • 滑动并淡入
    • AnimationUtils.makeInAnimation()
    • 使用布尔参数确定幻灯片是向左还是向右。
  • 向上滑动并淡入
    • AnimationUtils.makeInChildBottomAnimation()
    • 视图总是从底部向上滑动。
  • 滑动并淡出
    • AnimationUtils.makeOutAnimation()
    • 使用布尔参数确定幻灯片是向左还是向右。
  • 淡出
    • AnimationUtils.loadAnimation()
    • 将 int 参数设置为android.R.anim.fade_out.
  • 渐显
    • AnimationUtils.loadAnimation()
    • 将 int 参数设置为android.R.anim.fade_in.

注意:这些过渡动画只是暂时改变视图的绘制方式。如果要永久添加或删除对象,还必须设置视图的可见性参数。

清单 2–27 显示了每个按钮点击事件中视图的出现和消失。

清单 2–27。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <Button     android:id="@+id/toggleButton"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Click to Toggle"   />   <View     android:id="@+id/theView"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:background="#AAA"   /> </LinearLayout>

在清单 2–28 中,按钮上的每一个用户动作都会以动画形式切换其下方灰色视图的可见性。

清单 2–28。 活动动画视图转场

`public class AnimateActivity extends Activity implements View.OnClickListener {

View viewToAnimate;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

Button button = (Button)findViewById(R.id.toggleButton);
        button.setOnClickListener(this);

viewToAnimate = findViewById(R.id.theView);
    }

@Override
    public void onClick(View v) {
        if(viewToAnimate.getVisibility() == View.VISIBLE) {
            //If the view is visible already, slide it out to the right
            Animation out = AnimationUtils.makeOutAnimation(this, true);
            viewToAnimate.startAnimation(out);
            viewToAnimate.setVisibility(View.INVISIBLE);
        } else {
            //If the view is hidden, do a fade_in in-place
            Animation in = AnimationUtils.loadAnimation(this, android.R.anim.fade_in);
            viewToAnimate.startAnimation(in);
            viewToAnimate.setVisibility(View.VISIBLE);
        }
    }
}`

视图通过向右滑动并同时淡出来隐藏,而视图简单地在显示时淡入到位。我们在这里选择了一个简单的View作为目标来演示任何 UI 元素(因为它们都是View的子类)都可以用这种方式制作动画。

自定义动画

创建自定义动画,通过缩放、旋转和变换视图来添加效果,也可以为用户界面提供宝贵的附加内容。在 Android 中,我们可以创建以下动画元素:

  • 阿尔法动画
    • 动画显示视图透明度的变化。
  • 旋转模拟
    • 动画显示视图旋转的变化。
    • 旋转发生的点是可配置的。默认情况下,左上角被选中。
  • 缩放动画
    • 动画显示视图比例(大小)的变化。
    • 刻度变化的中心点是可配置的。默认情况下,左上角被选中。
  • 翻译形象化
    • 动画显示视图位置的变化。

让我们通过创建一个在图像上创建“硬币翻转”效果的示例应用来说明如何构建和添加一个自定义动画对象(参见清单 2–30)。

清单 2–29。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <ImageView     android:id="@+id/flip_image"     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:layout_centerInParent="true"   /> </RelativeLayout>

清单 2–30。 带自定义动画的活动

`public class Flipper extends Activity {

boolean isHeads;
    ScaleAnimation shrink, grow;
    ImageView flipImage;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

flipImage = (ImageView)findViewById(R.id.flip_image);
        flipImage.setImageResource(R.drawable.heads);
        isHeads = true;

shrink = new ScaleAnimation(1.0f, 0.0f, 1.0f, 1.0f,
                           ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
                           ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
        shrink.setDuration(150);
        shrink.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {}

@Override
            public void onAnimationRepeat(Animation animation) {}

@Override
            public void onAnimationEnd(Animation animation) {
                if(isHeads) {
                    isHeads = false;
                    flipImage.setImageResource(R.drawable.tails);
                } else {
                    isHeads = true;
                    flipImage.setImageResource(R.drawable.heads);
                }
                flipImage.startAnimation(grow);
            }
        });
        grow = new ScaleAnimation(0.0f, 1.0f, 1.0f, 1.0f,
                         ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
                         ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
        grow.setDuration(150);
    }

@Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_DOWN) {
            flipImage.startAnimation(shrink);
            returntrue;
        }
        returnsuper.onTouchEvent(event);
    }
}`

该示例包括以下相关组件:

  • 硬币头部和尾部的两个图像资源(我们将其命名为 heads.png 和尾巴)。
    • 这些图像可以是放置在 res/drawable 中的任何双图像资源。ImageView 默认显示头像。
  • 两个 ScaleAnimation 对象
    • 缩小:将图像中心的宽度从全宽缩小到零。
    • 增长:将图像宽度从零增加到围绕中心的全部。
  • 匿名 AnimationListener 按顺序链接两个动画

自定义动画对象可以用 XML 或代码来定义。在下一节中,我们将看看如何将动画制作成 XML 资源。这里我们使用下面的构造函数创建了两个ScaleAnimation对象:

ScaleAnimation(   float fromX,   float toX,   float fromY,   float toY,   int pivotXType,   float pivotXValue,   int pivotYType,   float pibotYValue )

前四个参数是要应用的水平和垂直比例因子。请注意,在示例中,X 从 100–0%收缩到 0–100%增长,而 Y 始终保持 100%。

其余参数定义了动画播放时视图的锚点。在这种情况下,我们告诉应用锚定视图的中点,并在视图收缩时将两边都拉向中间。对于扩展图像来说,情况正好相反:中心保持不变,图像向其原始边缘扩展。

Android 本身并没有办法将多个动画对象按顺序链接在一起,所以我们使用了一个Animation.AnimationListener来实现这个目的。监听器有方法通知动画何时开始、重复和完成。在这种情况下,我们只对后者感兴趣,这样当收缩动画完成时,我们可以在它之后自动启动增长动画。

示例中使用的最后一个方法是用setDuration()方法来设置动画持续时间。这里提供的值是以毫秒为单位的,所以我们的整个抛硬币过程需要 300 毫秒,每个ScaleAnimation需要 150 毫秒。

动画集

很多时候,您正在搜索创建的自定义动画需要前面描述的基本类型的组合;这就是AnimationSet变得有用的地方。AnimationSet定义一组应该同时运行的动画。默认情况下,所有动画将一起开始,并在各自的持续时间内完成。

在本节中,我们还将展示如何使用 Android 首选的 XML 资源方法来定义自定义动画。XML 动画应该在项目的 res/anim/文件夹中定义。支持以下标签,它们都可以是动画的根节点或子节点:

  • <alpha>:alpha animation 对象
  • <rotate>:旋转动画对象
  • <scale>:一个 ScaleAnimation 对象
  • <translate>:translate animation 对象
  • <set>:动画 t

但是,只有<set>标签可以是父标签并包含其他动画标签。

在这个例子中,让我们看看硬币投掷动画,并添加另一个维度。我们将每个 ScaleAnimation 与 TranslateAnimation 作为一个集合进行配对。理想的效果是图像在“翻转”时在屏幕上上下滑动为此,在清单 2–31 和清单 2–32 中,我们将在两个 XML 文件中定义我们的动画,并将它们放在 res/anim/中。第一个是 grow.xml。

清单 2–31。RES/anim/grow . XML

<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android">   <scale     android:duration="150"     android:fromXScale="0.0"     android:toXScale="1.0"     android:fromYScale="1.0"     android:toYScale="1.0"     android:pivotX="50%"     android:pivotY="50%"   /> <translate     android:duration="150"     android:fromXDelta="0%"     android:toXDelta="0%"     android:fromYDelta="50%"     android:toYDelta="0%"   /> </set>

后面是 shrink.xml:

清单 2–32。 res/anim/shrink.xml

<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <scale     android:duration="150"     android:fromXScale="1.0"     android:toXScale="0.0"     android:fromYScale="1.0"     android:toYScale="1.0"     android:pivotX="50%"     android:pivotY="50%"   />   <translate     android:duration="150"     android:fromXDelta="0%"     android:toXDelta="0%"     android:fromYDelta="0%"     android:toYDelta="50%"   /> </set>

定义比例值与以前在代码中使用构造函数时没有任何不同。但是,有一点需要注意,那就是 pivot 参数的单位定义样式。所有可以定义为ABSOULUTERELATIVE_TO_SELFRELATIVE_TO_PARENT的动画尺寸都使用以下 XML 语法:

  • ABSOLUTE:用浮点值表示实际像素值(如“5.0”)。
  • RELATIVE_TO_SELF:使用 0-100 之间的百分比值(如“50%”)。
  • RELATIVE_TO_PARENT:使用带后缀“p”的百分比值(例如,“25%p”)。

定义了这些动画文件后,我们可以修改前面的例子来加载这些集合(参见清单 2–33 和 2–34)。

清单 2–33。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <ImageView     android:id="@+id/flip_image"     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:layout_centerInParent="true"   /> </RelativeLayout>

清单 2–34。 活动使用动画集

`public class Flipper extends Activity {

boolean isHeads;
    Animation shrink, grow;
    ImageView flipImage;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

flipImage = (ImageView)findViewById(R.id.flip_image);
        flipImage.setImageResource(R.drawable.heads);
        isHeads = true;

shrink = AnimationUtils.loadAnimation(this, R.anim.shrink);
        shrink.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {}

@Override
            public void onAnimationRepeat(Animation animation) {}

@Override
            public void onAnimationEnd(Animation animation) {
                if(isHeads) {
                    isHeads = false;
                    flipImage.setImageResource(R.drawable.tails);
                } else {
                    isHeads = true;
                    flipImage.setImageResource(R.drawable.heads);
                }
                flipImage.startAnimation(grow);
            }
        });
        grow = AnimationUtils.loadAnimation(this, R.anim.grow);
    }

@Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_DOWN) {
            flipImage.startAnimation(shrink);
            returntrue;
        }
        returnsuper.onTouchEvent(event);
    }
}`

结果是硬币会翻转,而且每次翻转都会在屏幕的 y 轴上轻微地上下滑动。

2–15 岁。创建可绘制的背景

问题

您的应用需要创建带有渐变和圆角的自定义背景,并且您不想浪费时间缩放大量图像文件。

解决方案

(API 一级)

使用 Android 最强大的 XML 资源系统实现:创建可绘制的形状。当您能够这样做时,将这些视图创建为 XML 资源是有意义的,因为它们本身是可伸缩的,并且当设置为背景时,它们将适合视图的边界。

当使用<shape>标签在 XML 中定义 drawable 时,实际结果是一个GradientDrawable对象。您可以将对象定义为矩形、椭圆形、直线形或环形;虽然矩形是最常用的背景。特别是,当使用矩形时,可以为形状定义以下参数:

  • 拐角半径
    • 定义用于倒圆所有四个角的半径,或定义单独的半径以不同的方式倒圆每个角
  • 梯度
    • 线性、径向或扫描
    • 两个或三个颜色值
    • 方向为 45 度的任意倍数(0 表示从左到右,90 表示从下到上,依此类推。)
  • 纯色
    • 填充形状的单色
    • 也定义了渐变,不太好
  • 中风
    • 形状周围的边框
    • 定义宽度和颜色
  • 尺寸和填充
它是如何工作的

为视图创建静态背景图像可能很棘手,因为图像通常必须以多种尺寸创建,才能在所有设备上正确显示。如果预期视图的大小会根据其内容动态变化,那么这个问题就更复杂了。

为了避免这个问题,我们在 res/drawable 中创建了一个 XML 文件来描述一个形状,我们可以将它作为任何视图的android:background属性来应用。

渐变列表视图行

这种技术的第一个例子是创建一个渐变矩形,它适合作为ListView中单独行的背景。这个形状的 XML 在清单 2–35 中定义。

清单 2–35。RES/drawable/back gradient . XML

<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"   android:shape="rectangle">   <gradient     android:startColor="#EFEFEF"     android:endColor="#989898"     android:type="linear"     android:angle="270"   /> </shape>

这里我们选择了两种灰度之间的线性渐变,从上到下。如果我们想给渐变添加第三种颜色,我们可以给<gradient>标签添加一个android:middleColor属性。

现在,这个 drawable 可以被任何用来创建你的 ListView 的自定义项目的视图或布局引用(我们将在 Recipe 2–23 中讨论更多关于创建这些视图的内容)。通过将属性android:background="@drawable/backgradient"添加到视图的 XML 中,或者在 Java 代码中调用View.setBackgroundResource(R.drawable.backgradient),drawable 将被添加为背景。

高级提示:XML 中对颜色的限制是三个,但是GradientDrawable的构造函数接受一个用于颜色的int[]参数,您可以传递任意多的颜色。

当我们将这个 drawable 作为背景应用到ListView中的行时,结果将类似于图 2–6。

images

图 2–6。 渐变可绘制为行背景

圆形视图组

XML drawables 的另一个常见用途是为一个布局创建一个背景,该布局将一些小部件可视化地组合在一起。为了美观,圆角和细边框也经常使用。XML 中定义的这个形状看起来像清单 2–36 中的。

清单 2–36。RES/drawable/round back . XML

<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"   android:shape="rectangle">   <solid     android:color="#FFF"   />   <corners     android:radius="10dip"   />   <stroke     android:width="5dip"     android:color="#555"   /> </shape>

在这种情况下,我们选择白色作为填充颜色,灰色作为边框线条。正如前面的例子中提到的,通过将属性android:background="@drawable/roundback"包含到视图的 XML 中,或者在 Java 代码中调用View.setBackgroundResource(R.drawable.roundback),这个 drawable 可以被任何视图或布局作为背景引用。

当作为背景应用于视图时,结果如图 2–7 所示。

images

图 2–7。 带边框的圆角矩形作为视图背景

2–16。创建自定义状态绘图

问题

您想要定制一个元素,比如具有多种状态(默认、按下、选中等等)的ButtonCheckBox

解决方案

(API 一级)

创建一个可应用于元素的状态列表。无论您是用 XML 定义自己的可绘制图形,还是使用图像,Android 都通过另一个 XML 元素<selector>提供了创建多个图像的单个引用以及它们应该可见的条件的方法。

它是如何工作的

让我们来看一个示例状态列表 drawable,并讨论它的各个部分:

<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android">   <item android:state_enabled="false" android:drawable="@drawable/disabled" />   <itemandroid:state_pressed="true"android:drawable="@drawable/selected" />   <item android:state_focused="true"android:drawable="@drawable/selected" />   <item android:drawable="@drawable/default" /> </selector>

注意:<selector>是订单特定的。Android 将在遍历列表时返回它完全匹配的第一个状态的 drawable。在确定将哪些状态属性应用于每个项目时,请记住这一点。

列表中的每一项都标识了为使被引用的可提取项成为被选中项而必须生效的状态。如果需要匹配多个状态值,可以为一个项目添加多个状态参数。Android 将遍历列表,选择第一个符合当前视图所有标准的状态。因此,将您的正常或默认状态放在列表的底部,不附加任何标准,这被认为是一个好的做法。

下面是最常用的状态属性列表。所有这些都是布尔值:

  • state_enabled
    • 视图将从isEnabled().返回的值
  • state_pressed
    • 用户在触摸屏上按下视图。
  • state_focused
    • 视图有焦点。
  • state_selected
    • 用户使用按键或数字键盘选择视图。
  • state_checked
    • 可检查视图将从isChecked().返回的值

现在,让我们看看如何将这些状态列表 drawables 应用到不同的视图中。

按钮和可点击的部件

像 Button 这样的小部件被设计成当视图在上述状态中移动时,它们的背景可以改变。因此,XML 中的android:background属性或View.setBackgroundDrawable()方法是附加状态列表的合适方法。清单 2–37 是一个在 RES/drawable/called button _ States . XML 中定义的文件示例:

清单 2–37。RES/drawable/button _ States . XML

<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android">   <item android:state_enabled="false" android:drawable="@drawable/disabled" />   <itemandroid:state_pressed="true"android:drawable="@drawable/selected" />   <item android:drawable="@drawable/default" /> </selector>

这里列出的三个@drawable资源是选择器要在其中切换的项目中的图像。正如我们在上一节中提到的,如果没有其他条目包含与当前视图匹配的状态,那么最后一个条目将作为默认值返回,因此我们不需要在那个条目上包含一个匹配的状态。将它附加到 XML 中定义的视图,如下所示:

<Button   android:layout_width="wrap_content"   android:layout_height="wrap_content"   android:text="My Button"   android:background="@drawable/button_states" />

复选框和可检查的小部件

许多实现 Checkable 接口的小部件,如 CheckBox 和 CompoundButton 的其他子类,具有稍微不同的改变状态的机制。在这些情况下,背景与状态没有关联,自定义 drawable 来表示“选中”状态是通过另一个名为 button 的属性来完成的。在 XML 中,这是android:button属性,而在代码中,CompoundButton.setButtonDrawable()方法应该可以做到这一点。

清单 2–38 是一个在 RES/drawable/called check _ States . XML 中定义的文件的例子。同样,列出的@drawable资源意味着引用要切换的项目中的图像。

清单 2–38。RES/drawable/check _ States . XML

<?xml version="1.0" encoding="utf-8"?>   <selector xmlns:android="http://schemas.android.com/apk/res/android">   <item android:state_enabled="false" android:drawable="@drawable/disabled" />   <itemandroid:state_checked="true"android:drawable="@drawable/checked" />   <item android:drawable="@drawable/unchecked" /> </selector>

并附加到 XML 中的复选框:

<CheckBox   android:layout_width="wrap_content"   android:layout_height="wrap_content"   android:button="@drawable/check_states" />

2–17 岁。将遮罩应用到图像

问题

您需要应用一个图像或形状作为剪辑蒙版来定义应用中第二个图像的可见边界。

解决方案

(API 一级)

使用 2D 图形和一个PorterDuffXferMode,你可以将任意蒙版(以另一个位图的形式)应用于位图图像。该秘籍的基本步骤如下:

  1. 创建一个可变位图(空白),并在其中绘制一个画布。
  2. 首先在画布上绘制蒙版图案。
  3. PorterDuffXferMode涂在油漆上。
  4. 使用传输模式在画布上绘制源图像。

其中的关键成分是PorterDuffXferMode,它考虑了绘制操作过程中源对象和目标对象的当前状态。目标是现有的画布数据,源是当前操作中应用的图形数据。

有许多模式参数可以与此相关联,它们对结果产生不同的影响,但是对于掩蔽,我们感兴趣的是使用PorterDuff.Mode.SRC_IN模式。该模式将只在源和目的重叠的位置绘制,绘制的像素将来自源;换句话说,源被目标的边界截断。

它是如何工作的
圆角位图

这种技术的一个非常常见的用途是在将位图图像显示在ImageView中之前对其应用圆角。对于这个例子,Figure 2–8 是我们将要遮罩的原始图像。

images

图 2–8。 原始源图像

我们将首先在画布上创建一个圆角半径为所需的圆角矩形,这将作为图像的“蒙版”。然后,当我们将源图像绘制到同一个画布上时,应用PorterDuff.Mode.SRC_IN变换,结果将是带有圆角的源图像。

这是因为 SRC_IN 传输模式告诉 paint 对象只在源和目标(我们已经绘制的圆角矩形)重叠的画布位置上绘制像素,并且绘制的像素来自源。清单 2–39 是活动内部的代码。

清单 2–39。 将圆角矩形蒙版应用于位图的活动

`public class MaskActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ImageView iv = new ImageView(this);

//Create and load images (immutable, typically)
        Bitmap source = BitmapFactory.decodeResource(getResources(), R.drawable.dog);

//Create a mutable location, and a canvas to draw into it
        Bitmap result = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

//Create and draw the rounded rectangle "mask" first
        RectF rect = new RectF(0,0,source.getWidth(),source.getHeight());
        float radius = 25.0f;
        paint.setColor(Color.BLACK);
        canvas.drawRoundRect(rect, radius, radius, paint);
        //Switch over and paint the source using the transfer mode
        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
        canvas.drawBitmap(source, 0, 0, paint);
        paint.setXfermode(null);

iv.setImageBitmap(result);
        setContentView(iv);
    }
}`

您努力的结果显示在图 2–9 中。

images

图 2–9。 应用了圆角矩形蒙版的图像

任意遮罩图像

让我们看一个更有趣的例子。在这里,我们拍摄了两幅图像,一幅是源图像,另一幅是代表我们想要应用的蒙版的图像——在这种情况下,是一个倒置的三角形(见图 2–10)。

images

图 2–10。 【原始源图像】(左)和任意遮罩图像(右)

选取的蒙版图像不必符合此处选取的样式,蒙版为黑色像素,其他地方为透明像素。但是,这是保证系统完全按照您的预期绘制遮罩的最佳选择。清单 2–40 是简单的活动代码,用于屏蔽图像并将其显示在视图中。

清单 2–40。 将任意蒙版应用于位图的活动

`public class MaskActivity extends Activity {

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ImageView iv = new ImageView(this);

//Create and load images (immutable, typically)
        Bitmap source = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
        Bitmap mask = BitmapFactory.decodeResource(getResources(), R.drawable.triangle);

//Create a mutable location, and a canvas to draw into it
        Bitmap result = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

//Draw the mask image first, then paint the source using the transfer mode
        canvas.drawBitmap(mask, 0, 0, paint);
        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
        canvas.drawBitmap(source, 0, 0, paint);
        paint.setXfermode(null);

iv.setImageBitmap(result);
        setContentView(iv);
    }
}`

和以前一样,我们首先在画布上绘制蒙版,然后使用PorterDuff.Mode.SRC_IN模式绘制源图像,只绘制与现有蒙版像素重叠的源像素。结果看起来有点像图 2–11。

images

图 2–11。 应用了蒙版的图像

请在家里试试这个

以这种方式应用PorterDuffXferMode来混合两个图像可以创建许多有趣的结果。尝试使用相同的示例代码,但是将PorterDuff.Mode参数改为许多其他选项中的一个。每种模式都会以稍微不同的方式混合两个位图。玩得开心!

2–18 岁。创建持久的对话

问题

您希望创建一个用户对话框,其中包含多个输入字段或一些其他信息集,如果设备旋转,这些信息需要持久化。

解决方案

(API 一级)

根本不要使用对话框;创建一个以对话为主题的活动。对话框是托管对象,当设备在可见的情况下旋转时必须正确处理,否则会导致窗口管理器中的引用泄漏。您可以通过让您的活动使用像 Activity.showDialog()Activity.dismissDialog()这样的方法来呈现对话框来减轻这个问题,但是这只能解决一个问题。

该对话框没有任何自己的机制来通过循环保持状态,并且该作业(根据设计)退回到呈现它的活动。这导致了额外的工作,以确保对话框在被关闭之前可以传回或保存输入其中的任何值。

如果您有一个界面要呈现给用户,它需要持久化状态并通过旋转保持面向前方,一个更好的解决方案是让它成为一个活动。这允许该对象访问用于保存/恢复状态的全套生命周期回调方法。此外,作为一个活动,它不需要在轮换期间被管理为解除和再次呈现,这消除了泄漏引用的担心。使用Theme.Dialog系统主题,您仍然可以从用户的角度使活动表现得像一个对话框。

它是如何工作的

清单 2–41 是一个简单活动的例子,在文本视图中有一个标题和一些文本。

清单 2–41。 主题为对话的活动

public class DialogActivity extends Activity {     @Override     public void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setTitle("Activity");         TextView tv = new TextView(this);         tv.setText("I'm Really An Activity!");         //Add some padding to keep the dialog borders away         tv.setPadding(15, 15, 15, 15);         setContentView(tv);     } }

我们可以在应用的 AndroidManifest.xml 文件中将对话框主题应用于该活动(参见 Figure 2–42)。

清单 2–42。 清单以对话主题设置上述活动

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"       package="com.examples.dialogs"       android:versionCode="1"       android:versionName="1.0">     <application android:icon="@drawable/icon" android:label="@string/app_name">         <activity android:name=".DialogActivity"                   android:label="@string/app_name"                   android:theme="@android:style/Theme.Dialog">             <intent-filter>                 <action android:name="android.intent.action.MAIN" />                 <category android:name="android.intent.category.LAUNCHER" />             </intent-filter>         </activity>     </application> </manifest>

注意android:theme="@android:style/Theme.Dialog"参数,它创建了一个对话框的外观和感觉,具有成熟活动的所有优点。当您运行该应用时,您将看到如图图 2–12 所示的屏幕。

images

图 2–12。 将对话主题应用到活动中

请注意,尽管这是一个用于所有意图和目的的活动,但它可以作为用户界面内部的一个对话框,部分覆盖其下的活动(在本例中是主屏幕)。

2–19 岁。实施特定情况的布局

问题

您的应用必须是通用的,可以在不同的屏幕尺寸和方向上运行。您需要为每个实例提供不同的布局资源。

解决方案

(API 4 级)

构建多个布局文件,并使用资源限定符让 Android 选择合适的布局。我们将看看如何使用资源来创建针对不同屏幕方向和大小的资源。

它是如何工作的
特定方向

要为活动创建不同的资源,以便在纵向和横向中使用,请使用以下限定词:

  • 资源-土地
  • 资源-端口

这适用于所有资源类型,但在这种情况下最常见的是用布局来实现。因此,项目中没有 res/layout/目录,而是有一个 res/layout-port/和一个 res/layout-land/目录。

注意:包含默认资源目录而不使用限定符是一个很好的做法。如果 Android 运行在不符合你列出的任何特定标准的设备上,这给了它一些依靠。

尺寸特定

还有屏幕尺寸限定符(物理尺寸,不要与像素密度混淆),我们可以用它来定位平板电脑等大屏幕设备。在大多数情况下,一个单一的布局将足以满足所有物理屏幕尺寸的手机。但是,您可能希望向平板电脑布局添加更多功能,以帮助填充用户必须操作的明显更多的屏幕空间。对于物理屏幕大小,以下资源限定符是可接受的:

  • 资源-小型
  • 资源-中等
  • 资源-大

因此,要在一个通用应用中包含一个平板电脑专用的布局,我们还可以添加一个 RES/layout-large/目录。

举例

让我们来看一个将此付诸实践的快速示例。我们将定义一个活动,在代码中加载一个布局资源。但是,这种布局将在资源中定义三次,以在纵向、横向和平板电脑上产生不同的结果。首先是活动,如清单 2–43 所示。

清单 2–43。 简单活动加载一个布局

`public class UniversalActivity extends Activity {

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}`

我们现在将在 res/layout/main.xml 中定义一个默认/纵向布局(参见清单 2–44)。

清单 2–44。 res/layout/main.xml

`


  
  

在 API Level 9 (Android 2.3)中,增加了一个资源限定符来支持“超大”屏幕:

  • 资源 -xlarge

根据 SDK 文档,传统的“大”屏幕大约在 5 到 7 英寸的范围内。“超大”的新定义涵盖了大约 7 到 10 英寸以上的屏幕。

如果您的应用是针对 API Level 9 构建的,那么您也应该将您的平板电脑布局包含在 res/layout-xlarge/目录中。请记住,运行 Android 2.2 或更早版本的表只会将 res/layout-large/识别为有效的限定符。

2–20。自定义键盘操作

问题

您想要自定义软键盘的 enter 键的外观、用户点击它时发生的操作或两者。

(API 三级)

为使用键盘输入数据的 widget 自定输入法(IME)选项。

它是如何工作的
自定义回车键

当键盘在屏幕上可见时,return 键上的文本通常具有基于视图中可聚焦项目顺序的动作。当未指定时,如果视图中有更多可移动的焦点,键盘将显示“下一个”动作,或者如果当前关注最后一个项目,键盘将显示“完成”动作。然而,通过在视图的 XML 中设置android:imeOptions值,可以为每个输入视图定制这个值。下面列出了您可以设置来自定义 return 键的值:

  • actionUnspecified:默认显示设备选择的动作
    • 行动事件将是 IME_NULL
  • actionGo:显示“Go”作为回车键
    • 行动事件将是 IME _ 行动 _ 去
  • actionSearch:显示一个搜索窗口作为 return 键
    • 活动事件将是 IME _ 活动 _ 搜索
  • actionSend:显示“Send”作为回车键
    • 活动事件将是 IME _ 活动 _ 发送
  • actionNext:显示“Next”作为返回键
    • 行动事件将是 IME _ 行动 _ 下一个
  • actionDone:显示“Done”作为返回键
    • 活动事件将是 IME _ 活动 _ 完成

让我们看一个带有两个可编辑文本字段的示例布局,如清单 2–47 所示。第一个将在 return 键上显示搜索玻璃,第二个将显示“Go”

清单 2–47。EditText 小工具上带有自定义输入选项的布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   android:orientation="vertical">   <EditText     android:id="@+id/text1"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:imeOptions="actionSearch"   />   <EditText     android:id="@+id/text2"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:imeOptions="actionGo" /> </LinearLayout>

键盘的最终显示会有所不同,因为一些制造商特定的 UI 套件包括不同的键盘,但在纯 Google UI 上的结果将显示为如图图 2–15。

images

图 2–15。 回车键自定义输入选项的结果

注意:自定义编辑器选项仅适用于软输入法。更改该值不会影响用户在物理硬件键盘上按 return 键时生成的事件。

自定义动作

定制当用户按下回车键时发生的事情和调整它的显示一样重要。覆盖任何动作的默认行为只需要将一个TextView.OnEditorActionListener附加到感兴趣的视图。让我们继续上面的示例布局,这次为两个视图添加一个自定义动作(参见清单 2–48)。

清单 2–48。 活动实现自定义键盘动作

`public class MyActivity extends Activity implements OnEditorActionListener {

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

//Add the listener to the views
        EditText text1 = (EditText)findViewById(R.id.text1);
        text1.setOnEditorActionListener(this);
        EditText text2 = (EditText)findViewById(R.id.text2);
        text2.setOnEditorActionListener(this);
    }

@Override
    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
        if(actionId == IME_ACTION_SEARCH) {
            //Handle search key click
            return true;
        }
        if(actionId == IME_ACTION_GO) {
            //Handle go key click
            return true;
        }
        return false;
    }
}`

布尔返回值onEditorAction()告诉系统您的实现是否已经使用了该事件,或者是否应该将它传递给下一个可能的响应者(如果有的话)。当您的实现处理事件时,返回 true 非常重要,这样就不会发生其他处理。但是,当您不处理事件时返回 false 也同样重要,这样您的应用就不会从系统的其余部分窃取关键事件。

2–21。解除软键盘

问题

您需要用户界面上的一个事件来隐藏或消除屏幕上的软键盘。

(API 三级)

使用InputMethodManager.hideSoftInputFromWindow()方法明确告诉输入法管理器隐藏任何可见的输入法。

它是如何工作的

下面是一个如何在View.OnClickListener中调用这个方法的例子:

public void onClick(View view) {     InputMethodManager imm = (InputMethodManager)getSystemService(             Context.INPUT_METHOD_SERVICE);     imm.hideSoftInputFromWindow(view.getWindowToken(), 0); }

注意hideSoftInputFromWindow()将 IBinder 窗口令牌作为参数。这可以通过View.getWindowToken()从当前附加到窗口的任何视图对象中检索。在大多数情况下,特定事件的回调方法要么引用正在进行编辑的 TextView,要么引用被点击以生成事件的视图(如按钮)。这些视图是获取窗口令牌并将其传递给InputMethodManager的最方便的对象。

2 至 22 日。自定义适配器视图空视图

问题

当 AdapterView (ListView、GridView 等)有空数据集时,您希望显示自定义视图。

解决方案

(API 一级)

将您希望显示的视图布置在 AdapterView 所在的树中,并调用AdapterView.setEmptyView()让 AdapterView 管理它。AdapterView 将根据附加的 ListAdapter 的isEmpty()方法的结果在它自己和它的空视图之间切换可见性参数。

重要提示:确保在布局中包含 AdapterView 和空视图。AdapterView 仅更改两个对象的可见性参数;它不会在布局树中插入或删除它们。

它是如何工作的

下面是用一个简单的文本视图作为空的。首先,一个包括两个视图的布局,如清单 2–49 所示。

清单 2–49。 包含 AdapterView 和一个空视图的布局

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <TextView     android:id="@+id/myempty"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="No Items to Display"   />   <ListView     android:id="@+id/mylist"     android:layout_width="fill_parent"     android:layout_height="fill_parent"   /> </FrameLayout>

然后,在活动中,给 ListView 一个对空视图的引用,这样它就可以被管理了(参见清单 2–50)。

清单 2–50。 连接空视图和列表的活动

`public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ListView list = (ListView)findViewById(R.id.mylist);
    TextView empty = (TextView)findViewById(R.id.myempty);
    //Attach the reference
    list.setEmptyView(empty);

//Continue adding adapters and data to the list

}`

让空虚变得有趣

空视图不必像单一的文本视图那样简单和乏味。让我们试着让事情对用户更有用一点,并在列表为空时添加一个刷新按钮(参见清单 2–51)。

清单 2–51。 交互空布局

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <LinearLayout     android:id="@+id/myempty"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:orientation="vertical">     <TextView       android:layout_width="fill_parent"       android:layout_height="wrap_content"       android:text="No Items to Display"     />     <Button       android:layout_width="fill_parent"       android:layout_height="wrap_content"       android:text="Tap Here to Refresh"     />   </LinearLayout>   <ListView     android:id="@+id/mylist"     android:layout_width="fill_parent"     android:layout_height="fill_parent"   /> </FrameLayout>

现在,使用与前面相同的活动代码,我们已经将整个布局设置为空视图,并为用户添加了处理数据缺失的功能。

2–23。自定义 ListView 行

问题

您的应用需要为 ListView 中的每一行使用更加定制的外观。

解决方案

(API 一级)

创建一个定制的 XML 布局,并将其传递给一个公共适配器,或者扩展您自己的适配器。然后,您可以应用自定义状态绘图来覆盖每一行的背景和选定状态。

它是如何工作的
单纯的习俗

如果您的需求很简单,请创建一个可以连接到现有 ListAdapter 以进行填充的布局。我们将以 ArrayAdapter 为例。ArrayAdapter 可以接受要扩展的自定义布局资源的参数,以及该布局中要用数据填充的一个 TextView 的 ID。让我们为背景和满足这些要求的布局创建一些自定义的 drawables(参见清单 2–52 到 2–54)。

清单 2–52。RES/drawable/row _ background _ default . XML

<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"   android:shape="rectangle">   <gradient     android:startColor="#EFEFEF"     android:endColor="#989898"     android:type="linear"     android:angle="270"   /> </shape>

清单 2–53。RES/drawable/row _ background _ pressed . XML

<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"   android:shape="rectangle">   <gradient     android:startColor="#0B8CF2"     android:endColor="#0661E5"     android:type="linear"     android:angle="270"   /> </shape>

清单 2–54。RES/drawable/row _ background . XML

<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android">   <item android:state_pressed="true" android:drawable="@drawable/row_background_pressed"/>   <item android:drawable="@drawable/row_background_default"/> </selector>

清单 2–55 显示了一个自定义布局,其中文本完全居中,而不是向左对齐。

清单 2–55。RES/layout/custom _ row . XML

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="wrap_content"   android:padding="10dip"   android:background="@drawable/row_background">   <TextView     android:id="@+id/line1"     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:layout_gravity="center"     /> </LinearLayout>

此布局将自定义渐变状态列表集作为其背景;为列表中的每个项目设置默认状态和按下状态。现在,既然我们已经定义了一个与 ArrayAdapter 所期望的相匹配的布局,我们可以创建一个并在我们的列表中设置它,而不需要任何进一步的定制(参见清单 2–56)。

清单 2–56。 活动使用自定义行布局

`public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ListView list = new ListView(this);
    ArrayAdapter adapter = new ArrayAdapter(this,
                R.layout.custom_row,
                R.id.line1,
                new String[] {"Bill","Tom","Sally","Jenny"});
    list.setAdapter(adapter);

setContentView(list);
}`

适应更复杂的选择

有时定制列表行也意味着扩展 ListAdapter。如果在一行中有多条数据,或者其中任何一条都不是文本,通常会出现这种情况。在这个例子中,让我们再次使用自定义 drawables 作为背景,但是让布局更有趣一些(参见清单 2–57)。

清单 2–57。RES/layout/custom _ row . XML 修改后

`

  
  


`

这种布局包含相同的居中 TextView,但每边都以 ImageView 为边界。为了将这种布局应用到 ListView,我们需要在 SDK 中扩展一个 ListAdapters。扩展哪一个取决于列表中显示的数据源。如果数据仍然只是一个简单的字符串数组,并且扩展 ArrayAdapter 就足够了。如果数据更复杂,可能需要对抽象 BaseAdapter 进行全面扩展。唯一需要扩展的方法是getView(),它控制列表中每一行的显示方式。

在我们的例子中,数据是一个简单的字符串数组,所以我们将创建一个 ArrayAdapter 的简单扩展(参见清单 2–58)。

清单 2–58。 活动和自定义 ListAdapter 来显示新的布局

`public class MyActivity extends Activity {

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ListView list = new ListView(this);
        setContentView(list);

CustomAdapter adapter = new CustomAdapter(this,
                    R.layout.custom_row,
                    R.id.line1,
                    new String[] {"Bill","Tom","Sally","Jenny"});
        list.setAdapter(adapter);

}

privateclass CustomAdapter extends ArrayAdapter {

public CustomAdapter(Context context, int layout, int resId, String[] items) {
            //Call through to ArrayAdapter implementation
            super(context, layout, resId, items);
        }

@Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View row = convertView;
            //Inflate a new row if one isn’t recycled
            if(row == null) {
                row = getLayoutInflater().inflate(R.layout.custom_row, parent, false);
            }

String item = getItem(position);
            ImageView left = (ImageView)row.findViewById(R.id.leftimage);
            ImageView right = (ImageView)row.findViewById(R.id.rightimage);
            TextView text = (TextView)row.findViewById(R.id.line1);

left.setImageResource(R.drawable.icon);
            right.setImageResource(R.drawable.icon);
            text.setText(item);

return row;
        }
    }
}`

注意,我们使用相同的构造函数来创建适配器的实例,因为它是从ArrayAdapter继承的。因为我们覆盖了适配器的视图显示机制,所以现在将R.layout.custom_rowR.id.line1传递给构造函数的唯一原因是它们是构造函数的必需参数;在这个例子中,它们不再有用了。

现在,当 ListView 想要显示一行时,它将调用其适配器上的getView(),这是我们定制的,因此我们可以控制每行如何返回。向getView() 方法传递一个名为 convertView 的参数,这对性能非常重要。XML 的布局膨胀是一个昂贵的过程,为了最小化它对系统的影响,ListView在列表滚动时回收视图。如果一个回收的视图可供重用,它将作为 convertView 传递到getView()中。尽可能重用这些视图,而不是增加新的视图,以保持列表的滚动性能快速响应。

在本例中,调用getItem()获取列表中该位置的当前值(我们的字符串数组),然后在TextView上为该行设置该值。我们还可以将每行中的图像设置为对数据有意义的内容,尽管为了简单起见,这里将它们设置为应用图标。

2–24。制作 ListView 节标题

问题

您希望创建一个包含多个节的列表,每个节的顶部都有一个标题。

解决方案

(API 一级)

使用这里定义的 SimplerExpandableListAdapter 代码和一个ExpandableListView。Android 没有正式的可扩展方法来创建列表中的部分,但是它提供了ExpandableListView小部件和相关的适配器,用于处理分段列表中的二维数据结构。缺点是 SDK 提供的处理这些数据的适配器对于简单的数据结构来说很麻烦。

它是如何工作的

输入 SimplerExpandableListAdapter(参见清单 2–59),它是 BaseExpandableListAdapter 的一个扩展,作为一个例子,它处理一个Array字符串数组,其中一个单独的字符串数组用于部分标题。

清单 2–59。??【SimplerExpandableListAdapter】

`public class SimplerExpandableListAdapter extends BaseExpandableListAdapter {
    private Context mContext;
    private String[][] mContents;
    private String[] mTitles;

public SimplerExpandableListAdapter(Context context, String[] titles, String[][] contents) {
        super();
        //Check arguments
        if(titles.length != contents.length) {
            thrownew IllegalArgumentException("Titles and Contents must be the same size.");
        }

mContext = context;
        mContents = contents;
        mTitles = titles;
    }

//Return a child item
    @Override
    public String getChild(int groupPosition, int childPosition) {
        return mContents[groupPosition][childPosition];
    }

//Return a item's id
    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return 0;
    }

//Return view for each item row
    @Override
    public View getChildView(int groupPosition, int childPosition,
            boolean isLastChild, View convertView, ViewGroup parent) {
        TextView row = (TextView)convertView;
        if(row == null) {
            row = new TextView(mContext);
        }
        row.setText(mContents[groupPosition][childPosition]);
        return row;
    }

//Return number of items in each section
    @Override
    public int getChildrenCount(int groupPosition) {
        return mContents[groupPosition].length;
    }

//Return sections
    @Override
    public String[] getGroup(int groupPosition) {
        return mContents[groupPosition];
    }

//Return the number of sections
    @Override
    public int getGroupCount() {
        return mContents.length;
    }

//Return a section's id
    @Override
    public long getGroupId(int groupPosition) {
        return 0;
    }

//Return a view for each section header
    @Override
    public View getGroupView(int groupPosition, boolean isExpanded,
            View convertView, ViewGroup parent) {
        TextView row = (TextView)convertView;
        if(row == null) {
            row = new TextView(mContext);
        }
        row.setTypeface(Typeface.DEFAULT_BOLD);
        row.setText(mTitles[groupPosition]);
        return row;
    }

@Override
    public boolean hasStableIds() {
        returnfalse;
    }

@Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        returntrue;
    }

}`

现在我们可以创建一个简单的数据结构,并用它来填充一个示例活动中的ExpandableListView(参见清单 2–60)。

清单 2–60。 使用 SImplerExpandableListAdapter 的活动

`public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Set up an expandable list
    ExpandableListView list = new ExpandableListView(this);
    list.setGroupIndicator(null);
    list.setChildIndicator(null);
    //Set up simple data and the new adapter
    String[] titles = {"Fruits","Vegetables","Meats"};
    String[] fruits = {"Apples","Oranges"};
    String[] veggies = {"Carrots","Peas","Broccoli"};
    String[] meats = {"Pork","Chicken"};
    String[][] contents = {fruits,veggies,meats};
    SimplerExpandableListAdapter adapter = new SimplerExpandableListAdapter(this, titles, contents);

list.setAdapter(adapter);
    setContentView(list);
}`

那该死的扩张

以这种方式使用 ExpandableListView 有一个问题:它会扩展。ExpandableListView 设计用于在点击标题时展开和折叠组标题下的子数据。此外,默认情况下,所有组都是折叠的,因此您只能看到标题项。

在某些情况下,这可能是理想的行为,但如果您只想添加节标题,通常就不是这样了。在这种情况下,需要采取两个额外的步骤:

  1. 在活动代码中,展开所有组。类似于for(int i=0; i < adapter.getGroupCount(); i++) {     list.expandGroup(i); }
  2. 在适配器中,重写 onGroupCollapsed()以强制重新扩展。这将需要向适配器添加对列表小部件的引用。@Override public void onGroupCollapsed(int groupPosition) {     list.expandGroup(groupPosition); }

2–25 岁。创建复合控件

问题

您需要创建一个自定义小部件,它是现有元素的集合。

解决方案

(API 一级)

通过扩展通用视图组和添加功能来创建自定义小部件。创建自定义或可重用用户界面元素的最简单、最强大的方法之一是利用 Android SDK 提供的现有小部件创建复合控件。

它是如何工作的

ViewGroup及其子类LinearLayoutRelativeLayout等等,通过帮助您放置组件,为您提供了简化这一过程的工具,因此您可以更加关注添加的功能。

文字影像按钮

让我们通过制作一个 Android SDK 本身没有的小部件来创建一个例子:一个包含图像或文本作为其内容的按钮。为此,我们将创建 TextImageButton 类,它是FrameLayout的扩展。它将包含一个用于处理文本内容的TextView,以及一个用于图像内容的ImageView(参见清单 2–61)。

清单 2–61。 自定义 TextImageButton 小工具

`public class TextImageButton extends FrameLayout {

private ImageView imageView;
    private TextView textView;

/* Constructors */
    public TextImageButton(Context context) {
        this(context, null);
    }

public TextImageButton(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

public TextImageButton(Context context, AttributeSet attrs, int defaultStyle) {
        super(context, attrs, defaultStyle);
        imageView = new ImageView(context, attrs, defaultStyle);
        textView = new TextView(context, attrs, defaultStyle);
        //create layout parameters
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
                    LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
        //Add the views
        this.addView(imageView, params);
        this.addView(textView, params);

//Make this view interactive
        setClickable(true);
        setFocusable(true);
        //Set the default system button background
        setBackgroundResource(android.R.drawable.btn_default);

//If image is present, switch to image mode
        if(imageView.getDrawable() != null) {
            textView.setVisibility(View.GONE);
            imageView.setVisibility(View.VISIBLE);
        } else {
            textView.setVisibility(View.VISIBLE);
            imageView.setVisibility(View.GONE);
        }
    }

/* Accessors */
    public void setText(CharSequence text) {
        //Switch to text
        textView.setVisibility(View.VISIBLE);
        imageView.setVisibility(View.GONE);
        //Apply text
        textView.setText(text);
    }

public void setImageResource(int resId) {
        //Switch to image
        textView.setVisibility(View.GONE);
        imageView.setVisibility(View.VISIBLE);
        //Apply image
        imageView.setImageResource(resId);
    }

public void setImageDrawable(Drawable drawable) {
        //Switch to image
        textView.setVisibility(View.GONE);
        imageView.setVisibility(View.VISIBLE);
        //Apply image
        imageView.setImageDrawable(drawable);
    }
}`

SDK 中的所有小部件都有三个构造函数。第一个构造函数只将上下文作为参数,通常用于在代码中创建新视图。剩下的两个在从 XML 展开视图时使用,其中 XML 文件中定义的属性作为 AttributeSet 参数传入。在这里,我们使用 Java 的this()符号将前两个构造函数深化到真正完成所有工作的那个。以这种方式构建自定义控件可以确保我们仍然可以在 XML 布局中定义该视图。如果不实现属性化的构造函数,这是不可能的。

构造函数创建一个TextViewImageView,并将它们放入布局中。默认情况下,FrameLayout 不是一个交互式视图,因此构造函数使控件可点击和可聚焦,以便它可以处理用户交互事件;我们还在视图上设置了系统的默认按钮背景,以提示用户这个小部件是交互式的。剩下的代码根据作为属性传入的数据设置默认显示模式(文本或图像)。

添加访问器函数是为了方便以后切换按钮内容。如果内容发生变化,这些函数还负责在文本和图像模式之间进行切换。

因为这个自定义控件不在android.viewandroid.widget包中,所以当它在 XML 布局中使用时,我们必须使用完全限定名。清单 2–62 和 2–63 展示了一个显示定制小部件的示例活动。

清单 2–62。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   android:orientation="vertical">   <com.examples.customwidgets.TextImageButton     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:textColor="#000"     android:text="Click Me!"   />   <com.examples.customwidgets.TextImageButton     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:src="@drawable/icon"   /> </LinearLayout>

清单 2–63。 活动使用新的自定义小部件

`public class MyActivity extends Activity {

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}`

请注意,我们仍然可以使用传统的属性来定义要显示的文本或图像等属性。这是因为我们用属性化的构造函数来构造每个项目(FrameLayout、TextView 和 ImageView ),所以每个视图设置它感兴趣的参数,而忽略其余的。

如果我们定义一个活动来使用这个布局,结果看起来像 Figure 2–16。

images

图 2–16。 以文本和图像两种模式显示的 TextImageButton】

需要了解的有用工具:DroidDraw

第一章介绍了一款名为UC的单位换算安卓应用。除了探索UC的源代码,本章还探索了这个应用的资源,从描述应用主屏幕布局的main.xml布局文件开始。

即使对于高级开发人员来说,手工编写布局和其他资源文件也是一件乏味的事情。为此,Brendan Burns 教授创造了一个名为 DroidDraw 的工具。

DroidDraw 是一个基于 Java 的工具,它有助于构建 Android 应用的用户界面。此工具不生成应用逻辑。相反,它生成 XML 布局和其他资源信息,这些信息可以合并到另一个开发工具的应用项目中。

获取并启动 DroidDraw

DroidDraw 由droiddraw.org网站托管。从这个网站的主页上,您可以试用 DroidDraw 作为一个 Java 小程序,也可以下载适用于 Mac OS X、Windows 和 Linux 平台的 DroidDraw 应用。

例如,点击主页面的 Windows 链接并下载droiddraw-r1b18.zip以获得 DroidDraw for Windows。(发行版 1,Build 18 是撰写本文时最新的 DroidDraw 版本。)

解压缩droiddraw-r1b18.zip,你会发现用于启动 DroidDraw 的droiddraw.exedroiddraw.jar(一个可执行的 JAR 文件)。从 Windows 资源管理器中,双击任一文件名启动该工具。

提示:指定java -jar droiddraw.jar通过 JAR 文件在命令行启动 DroidDraw。

图 2–17 展示了 DroidDraw 的用户界面。

images

图 2–17。 DroidDraw 的用户界面展示了一个 Android 设备屏幕的模型。

探索 DroidDraw 的用户界面

图 2–17 展示了一个简单的用户界面,包括一个菜单栏、一个屏幕区域、一个选项卡区域和一个输出区域。您可以少量拖动每个区域的边框来放大或缩小该区域。

菜单栏由文件、编辑、属性和帮助菜单组成。文件显示以下菜单项:

  • 打开:打开一个安卓布局文件(如main.xml)
  • 保存:将当前布局信息保存到上次打开的布局文件中。如果没有打开布局文件,将显示一个对话框。
  • 另存为:显示一个对话框,提示用户输入布局文件名,并将当前布局信息保存到该文件中。
  • 退出:退出 DroidDraw。未保存的更改将会丢失。

“编辑”菜单提供以下菜单项:

  • 剪切:从输出区域中删除所选文本及其右侧的字符。
  • 复制:将选择的文本从输出区复制到剪贴板。
  • 粘贴:将剪贴板的内容粘贴到当前选择的内容上,或者粘贴到输出区域的当前插入符号位置。
  • 全选:选择输出区的全部内容。
  • 清除屏幕:从屏幕区域显示的用户界面中移除所有的小工具和布局信息。
  • 从标签设置 id:不是将文本"@+id/widget29"分配给小部件的android:id XML 属性,而是将小部件的值(如按钮的 OK 文本)分配给android:id;例如"@+id/Ok"。下次生成 XML 布局信息时,该文本将显示在输出区域中。

与文件和编辑菜单不同,项目菜单的菜单项似乎没有完全实现。

帮助菜单提供以下菜单项:

  • 教程:将默认浏览器指向http://www.droiddraw.org/tutorial.html,探索一些有趣的 DroidDraw 教程。
  • 关于:呈现一个简单的关于对话框,没有任何版本信息。
  • 捐赠:将默认浏览器指向 PayPal 网站进行捐赠,以支持 DroidDraw 的持续开发。

屏幕区域呈现正在构建的 Android 屏幕的视觉反馈。它还提供了根布局和屏幕尺寸下拉列表框,用于选择哪个布局作为最终的父布局(选项包括 AbsoluteLayout、LinearLayout、RelativeLayout、ScrollView 和 TableLayout),以及选择目标屏幕尺寸,以便您知道用户界面在该屏幕上显示时的外观(选项包括 QVGA 横向、QVGA 纵向、HVGA 横向和 HVGA 纵向)。

选项卡式区域提供了一个小部件选项卡,其小部件可以被拖动到屏幕上;一个布局选项卡,其布局可以被拖动到屏幕上;一个属性选项卡,用于输入所选小部件/布局的属性值;一个字符串/颜色/数组选项卡,用于输入这些资源;以及一个支持选项卡,用于进行捐赠。

最后,输出区域提供了一个 textarea,当您单击它的 Generate 按钮时,它会显示所显示屏幕的 XML 等价物。Load 按钮似乎没有完成任何有用的事情(尽管它似乎可以撤销一个清除屏幕的操作)。

创建简单的屏幕

假设您正在构建一个应用,它显示(通过 textview 组件)一个随机选择的著名语录来响应按钮点击。你决定使用 DroidDraw 来构建应用的单一屏幕。

启动 DroidDraw,将 HVGA 肖像作为屏幕大小,并将 AbsoluteLayout 替换为 LinearLayout 作为根布局,以便在垂直列中显示 textview 和 button 组件。

注:与 Android 选择水平作为LinearLayout的默认方向不同,DroidDraw 选择垂直作为默认方向。

在 Widgets 选项卡上,选择 TextView 并将其拖到屏幕上。选择属性选项卡,在宽度文本字段中输入fill_parent,在高度文本字段中输入100px,在文本文本字段中输入Quotation。单击应用;图 2–18 显示了结果屏幕。

images

图 2–18。 文本视图组件出现在屏幕顶部。

在 Widgets 选项卡上,选择按钮并将其拖动到屏幕上。选择属性选项卡,在宽度文本字段中输入fill_parent,在文本文本字段中输入Get Quote。单击应用;图 2–19 显示了结果屏幕。

images

图 2–19。 按钮组件出现在 textview 组件下面。

从文件菜单中选择另存为,将该屏幕的 XML 保存到名为main.xml的资源文件中。正如你在第一章的中了解到的,这个文件最终被放在一个 Android 项目的res目录的layout子目录中。

或者,您可以单击 Generate 按钮(在输出区域的底部)来生成屏幕的 XML(参见清单 2–64),选择该文本(通过 Edit 的 Select All 菜单项),并将其复制到剪贴板(通过 Edit 的 copy 菜单项)以备后用。

清单 2–64。 main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout   android:id="@+id/widget27"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   > <TextView   android:id="@+id/widget29"   android:layout_width="fill_parent"   android:layout_height="100px"   android:text="Quotation"   > </TextView> <Button   android:id="@+id/widget30"   android:layout_width="fill_parent"   android:layout_height="wrap_content"   android:text="Get Quote"   > </Button> </LinearLayout>

DroidDraw 将文本分配给 XML 属性,而不是使用资源引用。例如,清单 2–64 将“Quotation”而不是@string/quotation分配给TextView元素的android:text属性。

尽管从维护的角度来看嵌入字符串是不方便的,但是您可以使用 strings 选项卡输入字符串资源名称/值对,然后单击 Save 按钮将这些资源保存到一个strings.xml文件中,并在以后手动输入引用。

总结

正如你所看到的,Android 在提供的 SDK 中提供了一些非常灵活和可扩展的用户界面工具。正确利用这些工具意味着你可以不用担心你的应用在今天运行 Android 的各种设备上看起来和感觉上是否一样。

在这一章中,我们探讨了如何使用 Android 的资源框架为多个设备提供资源。您看到了处理静态图像以及创建自己的可绘制图像的技术。我们研究了如何覆盖窗口装饰的默认行为以及系统输入方法。我们研究了通过动画视图增加用户价值的方法。最后,我们通过创建新的定制控件和定制用于显示数据集的 AdapterViews 来扩展默认工具包。

在下一章中,我们将看看如何使用 SDK 与外界交流;访问网络资源并与其他设备通话。

三、通信和网络

许多成功的移动应用的关键是它们与远程数据源连接和交互的能力。Web 服务和 API 在当今世界非常丰富,允许应用与任何服务进行交互,从天气预报到个人财务信息。将这些数据放在用户手中,并使其可以从任何地方访问,这是移动平台的最大优势之一。Android 建立在谷歌所熟知的网络基础之上,为与外界交流提供了丰富的工具集。

3–1。显示 Web 信息

问题

来自 Web 的 HTML 或图像数据需要在应用中呈现,无需任何修改或处理。

解决方案

(API 一级)

WebView中显示信息。WebView是一个视图小部件,可以嵌入到任何布局中,在您的应用中显示本地和远程的 Web 内容。WebView基于与 Android 浏览器应用相同的开源 WebKit 技术;为应用提供相同级别的功能和能力。

它是如何工作的

在显示从网上下载的资源时,WebView有一些非常令人满意的特性,尤其是二维滚动(同时水平和垂直于)和缩放控件。一个WebView可以是存放大图像的完美地方,比如一个体育场地图,用户可能想要平移和缩放。在这里,我们将讨论如何使用本地和远程素材来实现这一点。

显示网址

最简单的情况是通过向WebView提供资源的 URL 来显示 HTML 页面或图像。以下是这种技术在您的应用中的一些实际用途:

  • 无需离开应用即可访问您的公司网站
  • 显示 web 服务器上的实时内容页面,如 FAQ 部分,无需升级应用即可更改。
  • 显示用户希望使用平移/缩放进行交互的大图像资源。

让我们来看一个简单的例子,它加载了一个非常流行的网页,但是在一个活动的内容视图中,而不是打开浏览器(参见清单 3–1 和 3–2)。

清单 3–1。 包含 WebView 的活动

`public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

WebView webview = new WebView(this);
        //Enable JavaScript support
        webview.getSettings().setJavaScriptEnabled(true);
        webview.loadUrl("http://www.google.com/");

setContentView(webview);
    }
}`

注意:默认情况下,WebView禁用 JavaScript 支持。如果您正在显示的内容需要 JavaScript,请确保在WebView.WebSettings对象中启用它。

清单 3–2。 AndroidManifest.xml 设置所需权限

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"       package="com.examples.webview"       android:versionCode="1"       android:versionName="1.0">     <application android:icon="@drawable/icon" android:label="@string/app_name">         <activity android:name=".MyActivity">             <intent-filter>                 <action android:name="android.intent.action.MAIN" />                 <category android:name="android.intent.category.LAUNCHER" />             </intent-filter>
        </activity>     </application>     <uses-permission android:name="android.permission.INTERNET" /> </manifest>

重要提示:如果你加载到WebView的内容是远程的,AndroidManifest.xml 必须声明它使用了android.permission.INTERNET权限。

结果显示您的活动中的 HTML 页面(参见 Figure 3–1)。

images

图 3–1。 网页视图中的 HTML 页面

本地素材

WebView在显示本地内容时也非常有用,可以利用 HTML/CSS 格式或它为内容提供的平移/缩放行为。您可以使用 Android 项目的assets目录来存储您希望在WebView中显示的资源,比如大图像或 HTML 文件。为了更好地组织素材,您还可以在素材下创建目录来存储文件。

WebView.loadUrl()可以显示使用 file:///android_asset/ <资源路径> URL schema 下存储的素材。例如,如果文件android.jpg被放在素材目录中,那么可以使用

file:///android_asset/android.jpg

如果同样的文件放在 assets 下名为images的目录中,WebView可以用 URL 加载它

file:///android_assimg/android.jpg

另外,WebView.loadData()会将存储在字符串资源或变量中的原始 HTML 加载到视图中。使用这种技术,预先格式化的 HTML 文本可以存储在res/values/strings.xml中,或者从远程 API 下载并显示在应用中。

清单 3–3 和 3–4 展示了一个示例活动,其中两个WebView小部件相互垂直堆叠。上面的视图显示了存储在素材目录中的一个大图像文件,下面的视图显示了存储在应用字符串资源中的一个 HTML 字符串。

清单 3–3。 res/layout/main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   android:orientation="vertical">   <WebView     android:id="@+id/upperview"     android:layout_width="fill_parent"     android:layout_height="fill_parent"     android:layout_weight="1"   />   <WebView     android:id="@+id/lowerview"     android:layout_width="fill_parent"     android:layout_height="fill_parent"     android:layout_weight="1"   /> </LinearLayout>

清单 3–4。 显示本地网页内容的活动

`public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

WebView upperView = (WebView)findViewById(R.id.upperview);
        //Zoom feature must be enabled
        upperView.getSettings().setBuiltInZoomControls(true);
        upperView.loadUrl("file:///android_asset/android.jpg");

WebView lowerView = (WebView)findViewById(R.id.lowerview);
        String htmlString =
            "

Header

This is HTML text
Formatted in italics

";
        lowerView.loadData(htmlString, "text/html", "utf-8");
    }
}`

显示活动时,每个 WebView 占据屏幕垂直空间的一半。HTML 字符串按预期格式化,而大图像可以水平和垂直滚动;用户甚至可以放大或缩小(参见图 3–2)。

images

图 3–2。 显示本地资源的两个网页视图

3–2。拦截 WebView 事件

问题

您的应用使用 WebView 来显示内容,但也需要监听和响应用户在页面上单击的链接。

解决方案

(API 一级)

安装一个WebViewClient并将其连接到WebView上。WebViewClientWebChromeClient是两个 WebKit 类,允许应用获得事件回调并定制WebView的行为。默认情况下,如果没有WebViewClient出现,WebView会将一个 URL 传递给要处理的ActivityManager,这通常会导致在浏览器应用中加载任何点击的链接,而不是当前的WebView

工作原理

在清单 3–5 中,我们创建了一个带有WebView的活动,它将处理自己的 URL 加载。

清单 3–5。 使用 WebView 处理 URL 的活动

`public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

WebView webview = new WebView(this);
        webview.getSettings().setJavaScriptEnabled(true);
        //Add a client to the view
        webview.setWebViewClient(new WebViewClient());
        webview.loadUrl("http://www.google.com");
        setContentView(webview);
    }
}`

在这个例子中,简单地提供一个普通的WebViewClientWebView允许它自己处理任何 URL 请求,而不是把它们传递给ActivityManager,所以点击一个链接将在同一个视图中加载所请求的页面。这是因为默认实现只是为 shouldOverrideUrlLoading()返回 false,这告诉客户端将 URL 传递给 WebView,而不是应用。

在下一个案例中,我们将利用WebViewClient.shouldOverrideUrlLoading()回调来拦截和监控用户活动(参见清单 3–6)。

清单 3–6。 拦截 WebView URLs 的活动

`public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

WebView webview = new WebView(this);
        webview.getSettings().setJavaScriptEnabled(true);
        //Add a client to the view
        webview.setWebViewClient(mClient);
        webview.loadUrl("http://www.google.com");
        setContentView(webview);
    }

private WebViewClient mClient = new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            Uri request = Uri.parse(url);

if(TextUtils.equals(request.getAuthority(), "www.google.com")) {
                //Allow the load
                return false;
            }

Toast.makeText(MyActivity.this, "Sorry, buddy", Toast.LENGTH_SHORT).show();
            returntrue;
        }     };
}`

在这个例子中,shouldOverrideUrlLoading()根据传递的 url 决定是否将内容加载回这个WebView中,防止用户离开 Google 的站点。返回 URL 的主机名部分,我们用它来检查用户点击的链接是否在谷歌的域名上(www.google.com)。如果我们可以验证该链接是指向另一个 Google 页面的,那么返回 false 将允许WebView加载内容。如果没有,我们通知用户并返回 true 告诉WebViewClient应用已经处理了这个 URL,并且不允许WebView加载它。

这种技术可以更复杂,应用实际上通过做一些有趣的事情来处理 URL。甚至可以开发一个定制的模式来创建应用和WebView内容之间的完整接口。

3–3 岁。使用 JavaScript 访问 WebView

问题

您的应用需要访问显示在WebView中的当前内容的原始 HTML,以读取或修改特定的值。

解决方案

(API 一级)

创建一个 JavaScript 接口来连接WebView和应用代码。

它是如何工作的

WebView.addJavascriptInterface()将一个 Java 对象绑定到 JavaScript,这样就可以在WebView中调用它的方法。使用这个接口,JavaScript 可以用来在您的应用代码和WebView的 HTML 之间编组数据。

注意:允许 JavaScript 控制您的应用本身就存在安全威胁,允许远程执行应用代码。应该考虑到这种可能性来使用这个接口。

让我们来看一个实际例子。清单 3–7 展示了一个简单的 HTML 表单,它将从本地素材加载到 WebView 中。清单 3–8 是一个使用两个 JavaScript 函数在 WebView 中的活动首选项和内容之间交换数据的活动。

清单 3–7。 assets/form.html

`

Enter Email:
`

清单 3–8。 活动与 JavaScript 桥接口

`public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

WebView webview = new WebView(this);
        webview.getSettings().setJavaScriptEnabled(true);
        webview.setWebViewClient(mClient);
        //Attach the custom interface to the view
        webview.addJavascriptInterface(new MyJavaScriptInterface(), "BRIDGE");

setContentView(webview);
        //Load the form
        webview.loadUrl("file:///android_asset/form.html");
    }

private static final String JS_SETELEMENT =         "javascript:document.getElementById('%s').value='%s'";
    private static final String JS_GETELEMENT =
        "javascript:window.BRIDGE.storeElement('%s',document.getElementById('%s').value)";
    private static final String ELEMENTID = "emailAddress";

private WebViewClient mClient = new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            //Before leaving the page, attempt to retrieve the email using JavaScript
            view.loadUrl(String.format(JS_GETELEMENT, ELEMENTID, ELEMENTID));
            return false;
        }

@Override
        public void onPageFinished(WebView view, String url) {
            //When page loads, inject address into page using JavaScript
            SharedPreferences prefs = getPreferences(Activity.MODE_PRIVATE);
            view.loadUrl(String.format(JS_SETELEMENT, ELEMENTID,
                prefs.getString(ELEMENTID, "")));
        }
    };

privateclass MyJavaScriptInterface {
        //Store an element in preferences
        @SuppressWarnings("unused")
        public void storeElement(String id, String element) {
            SharedPreferences.Editor edit =
                getPreferences(Activity.MODE_PRIVATE).edit();
            edit.putString(id, element);             edit.commit();
            //If element is valid, raise a Toast
            if(!TextUtils.isEmpty(element)) {
                Toast.makeText(MyActivity.this, element, Toast.LENGTH_SHORT).show();
            }
        }
    }
}`

在这个有点做作的例子中,单个元素表单是用 HTML 创建的,并显示在 WebView 中。在活动代码中,我们在 id 为“emailAddress”的WebView中查找一个表单值,并在每次通过shouldOverrideUrlLoading()回调点击页面上的链接(在本例中,是表单的提交按钮)时,将其值保存到SharedPreferences。每当页面加载完成时(即onPageFinished()被调用),我们试图将当前值从SharedPreferences注入回web form

创建了一个名为MyJavaScriptInterface的 Java 类,它定义了方法storeElement()。当创建视图时,我们调用WebView.addJavascriptInterface()方法将这个对象附加到视图上,并将其命名为桥。调用该方法时,String 参数是一个名称,用于引用 JavaScript 代码内部的接口。

我们在这里定义了两个 JavaScript 方法作为常量字符串,JS_GETELEMENTJS_SETELEMENT。这些方法通过传递给在 WebView 上执行。loadUrl()注意,JS_GETELEMENT是对调用我们的自定义接口函数(引用为BRIDGE.storeElement)的引用,该函数将调用MyJavaScripInterface上的方法,并将表单元素的值存储在 preferences 中。如果从表单中检索到的值不为空,也会引发一个Toast

任何 JavaScript 都可以以这种方式在 WebView 上执行,并且它不需要作为自定义界面的一部分包含在方法中。例如,JS_SETELEMENT使用纯 JavaScript 来设置页面上表单元素的值。

这种技术的一个流行应用是记住用户可能需要在应用中输入的表单数据,但是表单必须是基于 Web 的,例如 Web 应用的预订表单或付款表单,它没有较低级别的 API 可以访问。

3–4 岁。下载图像文件

问题

您的应用需要从 Web 或其他远程服务器下载并显示图像。

(API 三级)

使用AsyncTask在后台线程中下载数据。AsyncTask是一个包装器类,让线程化长时间运行的操作进入后台变得无痛而简单;以及用内部线程池管理并发性。除了处理后台线程之外,还在操作执行之前、期间和之后提供了回调方法,允许您在主 UI 线程上进行任何所需的更新。

它是如何工作的

在下载图像的上下文中,让我们创建一个名为 WebImageView 的 ImageView 的子类,它将从远程源缓慢加载图像,并在图像可用时立即显示。下载将在AsyncTask操作中执行(参见清单 3–9)。

清单 3–9。 WebImageView

`public class WebImageView extends ImageView {

private Drawable mPlaceholder, mImage;

public WebImageView(Context context) {
        this(context, null);
    }

public WebImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

public WebImageView(Context context, AttributeSet attrs, int defaultStyle) {
        super(context, attrs, defaultStyle);
    }

public void setPlaceholderImage(Drawable drawable) {
        mPlaceholder = drawable;
        if(mImage == null) {
            setImageDrawable(mPlaceholder);
        }
    }

public void setPlaceholderImage(int resid) {
        mPlaceholder = getResources().getDrawable(resid);
        if(mImage == null) {
            setImageDrawable(mPlaceholder);
        }
    }

public void setImageUrl(String url) {
        DownloadTask task = new DownloadTask();
        task.execute(url);     }

private class DownloadTask extends AsyncTask<String, Void, Bitmap> {
        @Override
        protected Bitmap doInBackground(String... params) {
            String url = params[0];
            try {
                URLConnection connection = (new URL(url)).openConnection();
                InputStream is = connection.getInputStream();
                BufferedInputStream bis = new BufferedInputStream(is);

ByteArrayBuffer baf = new ByteArrayBuffer(50);
                int current = 0;
                while ((current = bis.read()) != -1) {
                    baf.append((byte)current);
                }
                byte[] imageData = baf.toByteArray();
                return BitmapFactory.decodeByteArray(imageData, 0, imageData.length);
            } catch (Exception exc) {
                return null;
            }
        }

@Override
        protectedvoid onPostExecute(Bitmap result) {
            mImage = new BitmapDrawable(result);
            if(mImage != null) {
                setImageDrawable(mImage);
            }
        }
    };
}`

如您所见,WebImageView是 Android ImageView小部件的简单扩展。setPlaceholderImage()方法允许一个本地的 drawable 被设置为显示图像,直到远程内容下载完成。一旦使用setImageUrl()给视图一个远程 URL,大部分有趣的工作就开始了,此时定制的 AsyncTask 开始工作。

注意,AsyncTask是强类型的,有三个输入参数值、进度值和结果值。在这种情况下,一个字符串被传递给任务的 execute 方法,后台操作应该返回一个位图。中间值,即进度,我们在这个例子中没有使用,所以它被设置为 Void。当扩展AsyncTask时,唯一需要实现的方法是doInBackground(),它定义了要在后台线程上运行的工作块。在前面的示例中,这是连接到所提供的远程 URL 并下载图像数据的地方。完成后,我们试图从下载的数据中创建一个Bitmap。如果在任何一点发生错误,操作将中止并返回 null。

AsyncTask中定义的其他回调方法,如onPreExecute()onPostExecute()onProgressUpdate(),在主线程上被调用,目的是更新用户界面。在前面的例子中,onPostExecute()用于用结果数据更新视图的图像。

重要提示: Android UI 类不是线程安全的。确保使用发生在主线程上的回调方法之一来更新 UI。不要从doInBackground()内更新视图。

清单 3–10 和 3–11 展示了一个在活动中使用这个类的简单例子。因为这个类不是android.widgetandroid.view包的一部分,所以当在 XML 中使用它时,我们必须使用完全限定的包名。

清单 3–10。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   android:orientation="vertical">   <com.examples.WebImageView     android:id="@+id/webImage"     android:layout_width="wrap_content"     android:layout_height="wrap_content"   /> </LinearLayout>

清单 3–11。 范例活动

`public class WebImageActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

WebImageView imageView = (WebImageView)findViewById(R.id.webImage);
        imageView.setPlaceholderImage(R.drawable.icon);
        imageView.setImageUrl("http://apress.com/resource/weblogo/Apress_120x90.gif");
    }
}`

在这个例子中,我们首先设置一个本地图像(应用图标)作为WebImageView占位符。该图像立即显示给用户。然后,我们告诉视图从 Web 上获取一个 press 徽标的图像。如前所述,这将在后台下载图像,并在完成后替换视图中的占位符图像。正是这种创建后台操作的简单性使得 Android 团队将AsyncTask称为“无痛线程”。

3–5 岁。完全在后台下载

问题

应用必须下载大量资源到设备上,例如电影文件,而不需要用户保持应用活动。

(API 9 级)

使用DownloadManager API。DownloadManager是添加到 SDK 中的一项服务,API 级别为 9,允许长时间运行的下载完全由系统进行移交和管理。使用这项服务的主要优点是,DownloadManager将继续尝试下载资源,通过失败,连接改变,甚至设备重启。

它是如何工作的

清单 3–12 是一个使用 DownloadManager 处理大型图像文件下载的示例活动。完成后,图像将显示在 ImageView 中。每当您使用 DownloadManager 从 Web 访问内容时,一定要在应用的清单中声明您正在使用android.permission.INTERNET

清单 3–12。 下载管理器示例活动

`public class DownloadActivity extends Activity {

private staticfinal String DL_ID = "downloadId";
    private SharedPreferences prefs;

private DownloadManager dm;
    private ImageView imageView;

@Override
    publicvoid onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        imageView = new ImageView(this);
        setContentView(imageView);

prefs = PreferenceManager.getDefaultSharedPreferences(this);
        dm = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
    }

@Override
    publicvoid onResume() {
        super.onResume();

if(!prefs.contains(DL_ID)) {
            //Start the download
            Uri resource = Uri.parse("http://www.bigfoto.com/dog-animal.jpg");
            DownloadManager.Request request = new DownloadManager.Request(resource);
            request.setAllowedNetworkTypes(Request.NETWORK_MOBILE |
                Request.NETWORK_WIFI);
            request.setAllowedOverRoaming(false);
            //Display in the notification bar
            request.setTitle("Download Sample");
            long id = dm.enqueue(request);
            //Save the unique id
            prefs.edit().putLong(DL_ID, id).commit();
        } else {
            //Download already started, check status             queryDownloadStatus();
        }

registerReceiver(receiver,
            newIntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    }

@Override
    publicvoid onPause() {
        super.onPause();
        unregisterReceiver(receiver);
    }

private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        publicvoid onReceive(Context context, Intent intent) {
            queryDownloadStatus();
        }
    };

privatevoid queryDownloadStatus() {
        DownloadManager.Query query = new DownloadManager.Query();
        query.setFilterById(prefs.getLong(DL_ID, 0));
        Cursor c = dm.query(query);
        if(c.moveToFirst()) {
                int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
                switch(status) {
                case DownloadManager.STATUS_PAUSED:
                case DownloadManager.STATUS_PENDING:
                case DownloadManager.STATUS_RUNNING:
                    //Do nothing, still in progress
                    break;
                case DownloadManager.STATUS_SUCCESSFUL:
                    //Done, display the image
                    try {
                        ParcelFileDescriptor file =
                            dm.openDownloadedFile(prefs.getLong(DL_ID, 0));
                        FileInputStream fis =
                            new ParcelFileDescriptor.AutoCloseInputStream(file);
                        imageView.setImageBitmap(BitmapFactory.decodeStream(fis));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                case DownloadManager.STATUS_FAILED:
                    //Clear the download and try again later
                    dm.remove(prefs.getLong(DL_ID, 0));
                    prefs.edit().clear().commit();
                    break;
                }
            }
        }

}`

重要:截至本书出版之日,SDK 中有一个 bug 抛出异常,声称android.permission.ACCESS_ALL_DOWNLOADS需要使用DownloadManager。这个异常实际上是在android.permission.INTERNET不在你的清单中时抛出的。

这个例子在Activity.onResume()方法中完成了所有有用的工作,因此应用可以在用户每次返回活动时确定下载的状态。管理器中的下载可以通过调用DownloadManager.enqueue()时返回的长 ID 值来引用。在本例中,我们将该值保存在应用的首选项中,以便随时监控和检索下载的内容。

在第一次启动示例应用时,会创建一个DownloadManager.Request对象来表示要下载的内容。至少,这个请求需要远程资源的Uri。然而,在请求上设置许多有用的属性来控制它的行为。一些有用的属性包括:

  • Request.setAllowedNetworkTypes()
  • 设置可以检索下载的特定网络类型。
  • Request.setAllowedOverRoaming()
  • 设置设备处于漫游连接时是否允许下载。
  • Request.setTitle()
  • 为下载设置要在系统通知中显示的标题。
  • Request.setDescription()
  • 为下载设置要在系统通知中显示的描述。

一旦获得了 ID,应用就使用该值来检查下载的状态。通过注册一个BroadcastReceiver来监听ACTION_DOWNLOAD_COMPLETE广播,应用将通过在活动的 ImageView 上设置图像文件来对下载完成做出反应。如果下载完成时活动暂停,在下次恢复时将检查状态并设置ImageView内容。

值得注意的是,ACTION_DOWNLOAD_COMPLETE是由DownloadManager为它可能管理的每个下载发送的广播。因此,我们仍然需要检查我们感兴趣的下载 ID 是否真的准备好了。

目的地

在清单 3–12 的例子中,我们从未告诉 DownloadManager将文件放在哪里。相反,当我们想要访问文件时,我们使用保存在首选项中的 ID 值的DownloadManager.openDownloadedFile()方法来获得一个ParcelFileDescriptor,它可以被转换成应用可以读取的流。这是获取下载内容的简单直接的方法,但是需要注意一些注意事项。

如果没有特定的目的地,文件将被下载到共享的下载缓存中,系统保留随时删除文件以回收空间的权利。因此,以这种方式下载是一种快速获取数据的便捷方式,但如果您需要更长期的下载,则应使用DownloadManager.Request方法之一在外部存储器上指定一个永久目的地:

  • Request.setDestinationUri()
    • 将目标设置为位于外部存储器上的文件 Uri。
  • Request.setDestinationInExternalFilesDir()
    • 将目标设置为外部存储器上的隐藏目录。
  • Request.setDestinationInExternalPublicDir()
    • 将目标设置为外部存储器上的公共目录。

注意:所有写入外部存储器的目标方法都需要你的应用在清单中声明使用android.permission.WRITE_EXTERNAL_STORAGE

当调用DownloadManager.remove()从管理器列表中清除条目或者用户清除下载列表时,没有明确目的地的文件也经常被删除;在这些情况下,系统不会删除下载到外部存储器的文件。

3–6 岁。访问 REST API

问题

您的应用需要通过 HTTP 访问 RESTful API,以便与远程主机的 web 服务进行交互。

解决方案

(API 三级)

在 AsyncTask 中使用 Apache HTTP 类。Android 包括 Apache HTTP 组件库,它提供了一种创建到远程 API 的连接的健壮方法。Apache 库包含一些类,可以轻松地创建 GET、POST、PUT 和 DELETE 请求,并提供对 SSL、cookie 存储、身份验证以及特定 API 在其 HttpClient 中可能具有的其他 HTTP 需求的支持。

REST 代表具象状态转移,是当今 web 服务的一种常见架构风格。RESTful APIs 通常使用标准 HTTP 动词来创建对远程资源的请求,响应通常以结构化文档格式返回,如 XML、JSON 或逗号分隔值(CSV)。

它是如何工作的

清单 3–13 是一个 AsyncTask,它可以处理任何 HttpUriRequest 并返回字符串响应。

清单 3–13。 AsyncTask 处理 HttpRequest

`public class RestTask extends AsyncTask<HttpUriRequest, Void, String> {

public static final String HTTP_RESPONSE = "httpResponse";

private Context mContext;
    private HttpClient mClient;
    private String mAction;

public RestTask(Context context, String action) {
        mContext = context;
        mAction = action;
        mClient = new DefaultHttpClient();
    }

public RestTask(Context context, String action, HttpClient client) {
        mContext = context;
        mAction = action;
        mClient = client;
    }

@Override
    protected String doInBackground(HttpUriRequest... params) {
        try{
            HttpUriRequest request = params[0];
            HttpResponse serverResponse = mClient.execute(request);

BasicResponseHandler handler = new BasicResponseHandler();
            String response = handler.handleResponse(serverResponse);
            return response;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

@Override
    protectedvoid onPostExecute(String result) {
        Intent intent = new Intent(mAction);
        intent.putExtra(HTTP_RESPONSE, result);
        //Broadcast the completion
        mContext.sendBroadcast(intent);
    }

}`

RestTask可以使用或不使用 HttpClient 参数来构造。允许这样做的原因是多个请求可以使用同一个客户机对象。如果您的 API 需要 cookies 来维护一个会话,或者如果有一组特定的必需参数很容易设置一次(如 SSL 存储),这将非常有用。任务接受一个HttpUriRequest参数进行处理(其中HttpGetHttpPostHttpPutHttpDelete都是子类)并执行它。

一个BasicResponseHandler处理响应,这是一个方便的类,它将我们的任务从需要检查响应错误中抽象出来。如果响应代码是 1XX 或 2XX,将返回字符串形式的 HTTP 响应,但是如果响应代码是 300 或更大,将抛出 HttpResponseException。

在与 API 的交互完成之后,这个类的最后一个重要部分存在于onPostExecute()中。在构造时,RestTask 将一个字符串参数作为一个Intent的动作,这个动作被广播回所有监听器,API 响应被封装为一个额外的。这种广播是通知 API 调用者数据已准备好进行处理的机制。

现在让我们使用这个强大的新工具来创建一些基本的 API 请求。在下面的例子中,我们使用 Yahoo!搜索 REST API。这个 API 对于每个请求只有两个必需的参数:

  • 阿皮德
  • 用于标识发出请求应用的唯一值
  • 询问
  • 表示要执行的搜索查询的字符串

访问developer.yahoo.com/search了解更多关于这个 API 的信息。

获取示例

GET 请求是许多公共 API 中最简单也是最常见的请求。必须随请求一起发送的参数被编码到 URL 字符串本身中,因此不需要提供额外的数据。让我们创建一个 GET 请求来搜索“Android”(参见清单 3–14)。

清单 3–14。 执行 API 获取请求的活动

`public class SearchActivity extends Activity {

private static final String SEARCH_ACTION = "com.examples.rest.SEARCH";
    private static final String SEARCH_URI =
           "http://search.yahooapis.com/WebSearchService/V1/webSearch?appid=%s&query=%s";

private TextView result;
    private ProgressDialog progress;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);         result = new TextView(this);
        setContentView(result);

//Create the search request
        try{
            String url = String.format(SEARCH_URI, "YahooDemo","Android");
            HttpGet searchRequest = new HttpGet( new URI(url) );

RestTask task = new RestTask(this,SEARCH_ACTION);
            task.execute(searchRequest);
            //Display progress to the user
            progress = ProgressDialog.show(this, "Searching", "Waiting For Results...",
                true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

@Override
    public void onResume() {
        super.onResume();
        registerReceiver(receiver, new IntentFilter(SEARCH_ACTION));
    }

@Override
    public void onPause() {
        super.onPause();
        unregisterReceiver(receiver);
    }

private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            //Clear progress indicator
            if(progress != null) {
                progress.dismiss();
            }
            String response = intent.getStringExtra(RestTask.HTTP_RESPONSE);
            //Process the response data (here we just display it)
            result.setText(response);
        }
    };
}`

在这个例子中,我们用我们想要连接的 URL 创建了我们需要的 HTTP 请求类型(在这个例子中,是对 search.yahooapis.com 的 GET 请求)。URL 存储为一个常量格式的字符串,Yahoo!API (appid 和 query)是在运行时创建请求之前添加的。

创建一个RestTask,它带有一个独特的动作字符串,在完成时将被广播,然后任务被执行。该示例还定义了一个BroadcastReceiver,并为发送给RestTask的同一个动作注册了它。当任务完成时,这个接收器将捕获广播,API 响应可以被解包和处理。我们将在菜谱 3–7 和 3–8 中讨论如何解析结构化的 XML 和 JSON 响应,所以现在这个例子只是向用户界面显示原始响应。

帖子示例

很多时候,API 要求您提供一些数据作为请求的一部分,可能是认证令牌或搜索查询的内容。API 将要求您通过 HTTP POST 发送请求,因此这些值可能会被编码到请求正文中,而不是 URL 中。让我们再次运行对“Android”的搜索,但是这次使用一个帖子(参见清单 3–15)。

清单 3–15。 执行 API POST 请求的活动

`public class SearchActivity extends Activity {

private static final String SEARCH_ACTION = "com.examples.rest.SEARCH";
    private static final String SEARCH_URI =
            "http://search.yahooapis.com/WebSearchService/V1/webSearch";
    private static final String SEARCH_QUERY = "Android";

private TextView result;
    private ProgressDialog progress;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setTitle("Activity");
        result = new TextView(this);
        setContentView(result);

//Create the search request
        try{
            HttpPost searchRequest = new HttpPost( new URI(SEARCH_URI) );
            List parameters = new ArrayList();
            parameters.add(new BasicNameValuePair("appid","YahooDemo"));
            parameters.add(new BasicNameValuePair("query",SEARCH_QUERY));
            searchRequest.setEntity(new UrlEncodedFormEntity(parameters));

RestTask task = new RestTask(this,SEARCH_ACTION);
            task.execute(searchRequest);
            //Display progress to the user
            progress = ProgressDialog.show(this, "Searching", "Waiting For Results...",
true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

@Override
    public void onResume() {
        super.onResume();
        registerReceiver(receiver, new IntentFilter(SEARCH_ACTION));
    }

@Override
    public void onPause() {
        super.onPause();
        unregisterReceiver(receiver);
    }`

    private BroadcastReceiver receiver = new BroadcastReceiver() {         @Override         public void onReceive(Context context, Intent intent) {             //Clear progress indicator             if(progress != null) {                 progress.dismiss();             }             String response = intent.getStringExtra(RestTask.HTTP_RESPONSE);             //Process the response data (here we just display it)             result.setText(response);         }     }; }

注意,在这个例子中,传递给 API 执行搜索所需的参数被编码到一个HttpEntity中,而不是直接在请求 URL 中传递。在这种情况下创建的请求是一个HttpPost实例,它仍然是HttpUriRequest的子类(像HttpGet),所以我们可以使用同一个RestTask来运行操作。与 GET 示例一样,我们将讨论解析结构化的 XML 和 JSON 响应,就像 Recipes 3–7 和 3–8 中的这个一样,所以现在这个示例只是向用户界面显示原始响应。

注意:Android SDK 捆绑的 Apache 库不支持多部分 HTTP POSTs。但是,来自公共可用的org.apache.http.mime库的MultipartEntity是兼容的,可以作为外部资源引入到您的项目中。

基本认证

使用 API 的另一个常见需求是某种形式的身份验证。针对 REST API 认证的标准正在出现,比如 OAuth 2.0,但是最常见的认证方法仍然是基于 HTTP 的基本用户名和密码认证。在清单 3–16 中,我们修改了RestTask以支持每个请求的 HTTP 报头中的认证。

清单 3–16。 带基本认证的 rest task

`public class RestAuthTask extends AsyncTask<HttpUriRequest, Void, String> {

publicstaticfinal String HTTP_RESPONSE = "httpResponse";

private static final String AUTH_USER = "user@mydomain.com";
    private static final String AUTH_PASS = "password";

private Context mContext;
    private AbstractHttpClient mClient;
    private String mAction;

public RestAuthTask(Context context, String action, boolean authenticate) {
        mContext = context;
        mAction = action;         mClient = new DefaultHttpClient();
        if(authenticate) {
            UsernamePasswordCredentials creds =
                    new UsernamePasswordCredentials(AUTH_USER, AUTH_PASS);
            mClient.getCredentialsProvider().setCredentials(AuthScope.ANY, creds);
        }
    }

public RestAuthTask(Context context, String action, AbstractHttpClient client) {
        mContext = context;
        mAction = action;
        mClient = client;
    }

@Override
    protected String doInBackground(HttpUriRequest... params) {
        try{
            HttpUriRequest request = params[0];
            HttpResponse serverResponse = mClient.execute(request);

BasicResponseHandler handler = new BasicResponseHandler();
            String response = handler.handleResponse(serverResponse);
            return response;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

@Override
    protectedvoid onPostExecute(String result) {
        Intent intent = new Intent(mAction);
        intent.putExtra(HTTP_RESPONSE, result);
        //Broadcast the completion
        mContext.sendBroadcast(intent);
    }

}`

Apache 范例中的HttpClient增加了基本认证。由于我们的示例任务允许传入一个特定的客户机对象以供使用,该对象可能已经具有必要的身份验证凭证,因此我们只修改了创建默认客户机的情况。在这种情况下,使用用户名和密码字符串创建一个UsernamePasswordCredentials实例,然后在客户端的CredentialsProvider上进行设置。

3–7 岁。解析 JSON

问题

您的应用需要解析来自 API 或其他源的响应,这些响应是用 JavaScript 对象符号(JSON)格式化的。

(API 一级)

使用 Android 中内置的 org.json 解析器类。SDK 附带了一组非常有效的类,用于解析 org.json 包中的 JSON 格式的字符串。只需从格式化的字符串数据中创建一个新的JSONObjectJSONArray,您将拥有一组访问器方法来从其中获取原始数据或嵌套的JSONObjectJSONArray

它是如何工作的

默认情况下,这个 JSON 解析器是严格的,这意味着当遇到无效的 JSON 数据或无效的键时,它会异常中止。如果没有找到请求的值,以“get”为前缀的访问器方法将抛出一个JSONException。在某些情况下,这种行为并不理想,对于,有一组附带的方法以“opt”为前缀。当找不到所请求的键值时,这些方法将返回 null,而不是抛出异常。此外,它们中的许多都有一个重载版本,该版本也接受一个 fallback 参数来返回,而不是 null。

让我们看一个如何将 JSON 字符串解析成有用片段的例子。考虑清单 3–17 中的 JSON。

清单 3–17。 例子 JSON

{     "person": {         "name": "John",         "age": 30,         "children": [             {                 "name": "Billy"                 "age": 5             },             {                 "name": "Sarah"                 "age": 7             },             {                 "name": "Tommy"                 "age": 9             }         ]     } }

这用三个值定义了一个对象:name(字符串)、age(整数)和 children。名为“children”的参数是另外三个对象的数组,每个对象都有自己的名字和年龄。如果我们使用 org.json 来解析这些数据,并在 TextViews 中显示一些元素,它看起来就像清单 3–18 和清单 3–19 中的例子。

清单 3–18。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   android:orientation="vertical">   <TextView     android:id="@+id/line1"     android:layout_width="fill_parent"     android:layout_height="wrap_content"   />   <TextView     android:id="@+id/line2"     android:layout_width="fill_parent"     android:layout_height="wrap_content"   />   <TextView     android:id="@+id/line3"     android:layout_width="fill_parent"     android:layout_height="wrap_content"T   /> </LinearLayout>

清单 3–19。 示例 JSON 解析活动

`public class MyActivity extends Activity {
    private static final String JSON_STRING =
        "{"person":{"name":"John","age":30,"children":         [{"name":"Billy","age":5}," + ""name":"Sarah","age":7},         {"name":"Tommy","age":9}]}}";

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

TextView line1 = (TextView)findViewById(R.id.line1);
        TextView line2 = (TextView)findViewById(R.id.line2);
        TextView line3 = (TextView)findViewById(R.id.line3);
        try {
            JSONObject person = (new JSONObject(JSON_STRING)).getJSONObject("person");
            String name = person.getString("name");
            line1.setText("This person's name is " + name);
            line2.setText(name + " is " + person.getInt("age") + " years old.");
            line3.setText(name + " has " + person.getJSONArray("children").length()
                + " children.");
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}`

对于这个例子,JSON 字符串被硬编码为一个常量。创建活动时,字符串被转换成 JSONObject,此时它的所有数据都可以作为键-值对来访问,就像存储在地图或字典中一样。所有的业务逻辑都包装在一个 try/catch 语句中,因为我们使用严格的方法来访问数据。

函数JSONObject.getString()JSONObject.getInt()用于读取原始数据并放入TextViewgetJSONArray()方法取出嵌套的“子”数组。JSONArray使用与JSONObject相同的访问器方法来读取数据,但是它们将数组中的索引作为参数,而不是键的名称。此外,JSONArray可以返回它的长度,我们在示例中使用它来显示这个人有几个孩子。

示例应用的结果如图 3–3 所示。

images

图 3–3。 显示活动中解析的 JSON 数据

调试窍门

JSON 是一种非常有效的符号;然而,对于人类来说,读取原始的 JSON 字符串可能很困难,这使得调试解析问题变得很困难。您正在解析的 JSON 通常来自远程数据源,或者您并不完全熟悉,出于调试目的,您需要显示它。JSONObject 和 JSONArray 都有一个重载的toString()方法,该方法接受一个整数参数,以返回和缩进的方式漂亮地打印数据,使其更容易破译。经常在一个比较麻烦的部分加上myJsonObject.toString(2)这样的东西,可以节省时间,也不会头疼。

3–8。解析 XML

问题

您的应用需要解析来自 API 或其他源的 XML 格式的响应。

解决方案

(API 一级)

实现org.xml.sax.helpers.DefaultHandler的子类,使用基于事件的 SAX 解析数据。Android 有三种主要方法可以用来解析 XML 数据:DOM、SAX 和 Pull。其中实现最简单、最节省内存的是 SAX 解析器。SAX 解析通过遍历 XML 数据并在每个元素的开头和结尾生成回调事件来工作。

它是如何工作的

为了进一步描述这一点,让我们看看请求 RSS/ATOM 新闻提要时返回的 XML 格式(参见清单 3–20)。

清单 3–20。 RSS 基本结构

<rss version="2.0">   <channel>     <item>       <title></title>       <link></link>       <description></description>     </item>     <item>       <title></title>       <link></link>       <description></description>     </item>     <item>       <title></title>       <link></link>       <description></description>     </item>     …   </channel> </rss>

在每个<title><link><description>标签之间是与每个项目相关的值。使用 SAX,我们可以将这些数据解析成一个项目数组,然后应用可以在列表中向用户显示这些项目(参见清单 3–21)。

清单 3–21。??【自定义处理程序】解析 RSS

`public class RSSHandlerextends DefaultHandler {

public class NewsItem {
        public String title;
        public String link;
        public String description;

@Override
        public String toString() {
            return title;
        }
    }

private StringBuffer buf;
    private ArrayList feedItems;
    private NewsItem item;

privateboolean inItem = false;

public ArrayList getParsedItems() {
        return feedItems;
    }

//Called at the head of each new element
    @Override
    public void startElement(String uri, String name, String qName, Attributes atts) {
        if("channel".equals(name)) {
            feedItems = new ArrayList();
        } elseif("item".equals(name)) {
            item = new NewsItem();
            inItem = true;
        } elseif("title".equals(name) && inItem) {
            buf = new StringBuffer();
        } elseif("link".equals(name) && inItem) {
            buf = new StringBuffer();
        } elseif("description".equals(name) && inItem) {
            buf = new StringBuffer();
        }
    }

//Called at the tail of each element end
    @Override
    public void endElement(String uri, String name, String qName) {
        if("item".equals(name)) {
            feedItems.add(item);
            inItem = false;
        } elseif("title".equals(name) && inItem) {
            item.title = buf.toString();
        } elseif("link".equals(name) && inItem) {
            item.link = buf.toString();
        } elseif("description".equals(name) && inItem) {
            item.description = buf.toString();
        }

buf = null;
    }`

    //Called with character data inside elements     @Override     public void characters(char ch[], int start, int length) {         //Don't bother if buffer isn't initialized         if(buf != null) {             for (int i=start; i<start+length; i++) {                 buf.append(ch[i]);             }         }     } }

通过startElement()endElement()在每个元素的开头和结尾通知RSSHandler。在这两者之间,组成元素值的字符被传递到characters()回调中。

  1. 当解析器遇到第一个元素时,条目列表被初始化。
  2. 当遇到每个 item 元素时,会初始化一个新的 NewsItem 模型。
  3. 在每个 item 元素内部,数据元素被捕获到 StringBuffer 中,并被插入到 NewsItem 的成员中。
  4. 当到达每个项目的末尾时,NewsItem 将被添加到列表中。
  5. 解析完成后,feedItems 是提要中所有项目的完整列表。

让我们通过使用 Recipe 3–6 中的 API 示例中的一些技巧来下载 RSS 格式的最新谷歌新闻(参见清单 3–22)来看看这一点。

清单 3–22。 解析 XML 并显示项目的活动

`public class FeedActivity extends Activity {
    private static final String FEED_ACTION = "com.examples.rest.FEED";
    private static final String FEED_URI = "http://news.google.com/?output=rss";

private ListView list;
    private ArrayAdapter adapter;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

list = new ListView(this);
        adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1,
            android.R.id.text1);
        list.setAdapter(adapter);
        list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View v, int position,
            long id) {                 NewsItem item = adapter.getItem(position);
                //Launch the link in the browser
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setData(Uri.parse(item.link));
                startActivity(intent);
            }
        });

setContentView(list);
    }

@Override
    public void onResume() {
        super.onResume();
        registerReceiver(receiver, new IntentFilter(FEED_ACTION));
        //Retrieve the RSS feed
        try{
            HttpGet feedRequest = new HttpGet( new URI(FEED_URI) );
            RestTask task = new RestTask(this,FEED_ACTION);
            task.execute(feedRequest);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

@Override
    public void onPause() {
        super.onPause();
        unregisterReceiver(receiver);
    }

private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String response = intent.getStringExtra(RestTask.HTTP_RESPONSE);

try {
                //Parse the response data using SAX
                SAXParserFactory factory = SAXParserFactory.newInstance();
                SAXParser p = factory.newSAXParser();
                RSSHandler parser = new RSSHandler();
                //Run the parsing operation
                p.parse(new InputSource(new StringReader(response)), parser);
                //Clear all current items from the list
                adapter.clear();
                //Add all items from the parsed XML
                for(NewsItem item : parser.getParsedItems()) {
                    adapter.add(item);
                }
                //Tell adapter to update the view
                adapter.notifyDataSetChanged();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };
}`

该示例被修改为显示一个ListView,它将由来自 RSS 提要的解析后的条目填充。在这个例子中,我们向列表中添加了一个OnItemClickListener,它将在浏览器中启动新闻条目的链接。

一旦数据从BroadcastReceiver中的 API 返回,Android 内置的 SAXParser 就会处理遍历 XML 字符串的工作。SAXParser.parse()使用我们的RSSHandler的一个实例来处理 XML,这导致处理程序的 feedItems 列表被填充。接收者然后遍历所有解析过的条目,并将它们添加到一个ArrayAdapter中,以便在ListView中显示。

3–8 岁。接收短信

问题

您的应用必须对传入的 SMS 消息(通常称为文本消息)做出反应。

解决方案

(API 一级)

注册一个BroadcastReceiver来监听传入的消息,并在onReceive()中处理它们。每当有短信传入时,操作系统就会用android.provider.Telephony.SMS_RECEIVED动作触发一个广播意图。您的应用可以注册一个 BroadcastReceiver 来过滤这个意图并处理传入的数据。

注意:接收该广播并不妨碍系统的其他应用也接收它。默认的消息应用将仍然接收和显示任何传入的短信。

它是如何工作的

在之前的秘籍中,我们将BroadcastReceiver定义为活动的私有内部成员。在这种情况下,最好单独定义接收者,并使用<receiver>标签在 AndroidManifest.xml 中注册它。这将允许您的接收器处理传入的事件,即使您的应用是不活跃的。清单 3–23 和 3–24 显示了一个示例接收器,它监控所有收到的短信,并在一个有趣的聚会到来时举杯庆祝。

清单 3–23。 传入短信广播接收器

public class SmsReceiver extends BroadcastReceiver {     private static final String SHORTCODE = "55443";

`    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle bundle = intent.getExtras();

Object[] messages = (Object[])bundle.get("pdus");
        SmsMessage[] sms = new SmsMessage[messages.length];
        //Create messages for each incoming PDU
        for(int n=0; n < messages.length; n++) {
            sms[n] = SmsMessage.createFromPdu((byte[]) messages[n]);
        }
        for(SmsMessage msg : sms) {
            //Verify if the message came from our known sender
            if(TextUtils.equals(msg.getOriginatingAddress(), SHORTCODE)) {
                Toast.makeText(context,
                        "Received message from the mothership: "+msg.getMessageBody(),
                        Toast.LENGTH_SHORT).show();
            }
        }
    }
}`

清单 3–24。Partial Android manifest . XML

<?xml version="1.0" encoding="utf-8"?> <manifest …>     <application …>       <receiver android:name=".SmsReceiver">         <intent-filter>           <action android:name="android.provider.Telephony.SMS_RECEIVED" />         </intent-filter>       </receiver>     </application>     <uses-permission android:name="android.permission.RECEIVE_SMS" /> </manifest>

重要提示:接收短信需要在清单中声明android.permission.RECEIVE_SMS权限!

传入的 SMS 消息通过广播意图的附加内容作为字节数组的对象数组来传递,每个字节数组代表一个 SMS 分组数据单元(PDU)。SmsMessage.createFromPdu()是一种方便的方法,允许我们从原始 PDU 数据创建SmsMessage对象。设置工作完成后,我们可以检查每条消息,以确定是否有一些有趣的东西需要处理。在示例中,我们将每条消息的源地址与一个已知的短代码进行比较,并在短代码到达时通知用户。

在示例中启动 Toast 的地方,您可能希望向用户提供一些更有用的东西。也许 SMS 消息包含您的应用的 offer 代码,您可以启动适当的活动在应用中向用户显示该信息。

3–9。发送短信

问题

您的应用必须发出传出的 SMS 消息。

解决方案

(API 4 级)

使用SMSManager发送文本和数据短信。SMSManager是一个系统服务,处理发送 SMS 并向应用提供关于操作状态的反馈。SMSManager提供使用SmsManager.sendTextMessage()SmsManager.sendMultipartTextMessage()发送文本信息,或使用SmsManager.sendDataMessage()发送数据信息的方法。这些方法中的每一个都采用 PendingIntent 参数来将发送操作的状态和消息传递传递回请求的目的地。

它是如何工作的

让我们来看一个简单的示例活动,它发送 SMS 消息并监控其状态(参见清单 3–25)。

清单 3–25。 活动发送短信

`public class SmsActivity extends Activity {
    private static final String SHORTCODE = "55443";
    private static final String ACTION_SENT = "com.examples.sms.SENT";
    private static final String ACTION_DELIVERED = "com.examples.sms.DELIVERED";

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

Button sendButton = new Button(this);
        sendButton.setText("Hail the Mothership");
        sendButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                sendSMS("Beam us up!");
            }
        });

setContentView(sendButton);
    }

privatevoid sendSMS(String message) {
        PendingIntent sIntent = PendingIntent.getBroadcast(this, 0,
            new Intent(ACTION_SENT), 0);
        PendingIntent dIntent = PendingIntent.getBroadcast(this, 0,
            new Intent(ACTION_DELIVERED), 0);
        //Monitor status of the operation         registerReceiver(sent, new IntentFilter(ACTION_SENT));
        registerReceiver(delivered, new IntentFilter(ACTION_DELIVERED));
        //Send the message
        SmsManager manager = SmsManager.getDefault();
        manager.sendTextMessage(SHORTCODE, null, message, sIntent, dIntent);
    }

private BroadcastReceiver sent = new BroadcastReceiver(){
        @Override
        public void onReceive(Context context, Intent intent) {
            switch (getResultCode()){
            case Activity.RESULT_OK:
                //Handle sent success
                break;
            case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
            case SmsManager.RESULT_ERROR_NO_SERVICE:
            case SmsManager.RESULT_ERROR_NULL_PDU:
            case SmsManager.RESULT_ERROR_RADIO_OFF:
                //Handle sent error
                break;
            }

unregisterReceiver(this);
        }
    };

private BroadcastReceiver delivered = new BroadcastReceiver(){
        @Override
        public void onReceive(Context context, Intent intent) {
            switch (getResultCode()){
            case Activity.RESULT_OK:
                //Handle delivery success
                break;
            case Activity.RESULT_CANCELED:
                //Handle delivery failure
                break;
            }

unregisterReceiver(this);
        }
    };
}`

重要提示:发送短信需要在清单中声明android.permission.SEND_SMS权限!

在本例中,每当用户点击按钮时,就会通过SMSManager发送一条 SMS 消息。因为SMSManager是一个系统服务,所以必须调用静态的SMSManager.getDefault()方法来获得对它的引用。sendTextMessage()以目的地址(号码)、服务中心地址、消息为参数。服务中心地址应该为空,以允许SMSManager使用系统默认值。

注册了两个BroadcastReceiver来接收将要发送的回调意图:一个用于发送操作的状态,另一个用于交付的状态。只有当操作挂起时,才会注册接收器,一旦处理了意图,它们就会注销自己。

3–10。通过蓝牙通信

问题

您希望利用蓝牙通信在应用中的设备之间传输数据。

解决方案

(API 等级 5)

使用 API Level 5 中引入的蓝牙 API 来创建对等连接。蓝牙是一种非常流行的无线技术,如今几乎所有的移动设备都采用了这种技术。许多用户认为蓝牙是他们的移动设备与无线耳机连接或与汽车立体声系统集成的一种方式。然而,蓝牙也可以是开发者在他们的应用中创建对等连接的一种简单而有效的方式。

它是如何工作的

重要提示:Android 模拟器目前不支持蓝牙。为了执行本例中的代码,它必须在 Android 设备上运行。此外,为了适当地测试功能,需要两个设备同时运行应用。

蓝牙点对点

清单 3–26 到 3–28 展示了一个使用蓝牙找到附近其他用户并快速交换联系信息的例子(在本例中,只是一个电子邮件地址)。通过发现可用的“服务”并通过引用其唯一的 128 位 UUID 值连接到这些服务,从而通过蓝牙建立连接。这意味着您想要使用的服务的 UUID 必须提前被发现或知道。

在本例中,同一应用在连接两端的两台设备上运行,因此我们可以自由地在代码中将 UUID 定义为常数,因为两台设备都将引用它。

注意:为了确保您选择的 UUID 是独一无二的,请使用网上众多免费 UUID 生成器中的一个。

清单 3–26。Android manifest . XML

`

    <application android:icon="@drawable/icon" android:label="@string/app_name"
        
            
                
                
            

        

    
    


    
`

重要提示:记住android.permission.BLUETOOTH必须在清单中声明才能使用这些 API。此外,必须声明android.permission.BLUETOOTH_ADMIN,以便对首选项(如可发现性)进行更改,并启用/禁用适配器。

清单 3–27。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <TextView     android:id="@+id/label"     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:textAppearance="?android:attr/textAppearanceLarge"     android:text="Enter Your Email:"   />   <EditText     android:id="@+id/emailField"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:layout_below="@id/label"     android:singleLine="true"     android:inputType="textEmailAddress"   />   <Button     android:id="@+id/scanButton"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:layout_alignParentBottom="true"     android:text="Connect and Share"   />   <Button
    android:id="@+id/listenButton"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:layout_above="@id/scanButton"     android:text="Listen for Sharers"   /> </RelativeLayout>

本例的用户界面由一个供用户输入电子邮件地址的EditText和两个启动通信的按钮组成。标题为“监听共享者”的按钮将设备置于监听模式。在这种模式下,设备将接受任何试图与之连接的设备并与之通信。标题为“连接和共享”的按钮将设备置于搜索模式。在这种模式下,设备搜索当前正在监听的任何设备并建立连接(参见清单 3–28)。

清单 3–28。 蓝牙交流活动

`public classExchangeActivity extends Activity {

// Unique UUID for this application (generated from the web)
    private static final UUID MY_UUID =
        UUID.fromString("321cb8fa-9066-4f58-935e-ef55d1ae06ec");
    //Friendly name to match while discovering
    private static final String SEARCH_NAME = "bluetooth.recipe";

BluetoothAdapter mBtAdapter;
    BluetoothSocket mBtSocket;
    Button listenButton, scanButton;
    EditText emailField;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        setContentView(R.layout.main);

//Check the system status
        mBtAdapter = BluetoothAdapter.getDefaultAdapter();
        if(mBtAdapter == null) {
            Toast.makeText(this, "Bluetooth is not supported.",                 Toast.LENGTH_SHORT).show();
            finish();
            return;
        }
        if (!mBtAdapter.isEnabled()) {
            Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableIntent, REQUEST_ENABLE);
        }

emailField = (EditText)findViewById(R.id.emailField);
        listenButton = (Button)findViewById(R.id.listenButton);
        listenButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Make sure the device is discoverable first
                if (mBtAdapter.getScanMode() !=                             BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
                    Intent discoverableIntent = new
                            Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
                    discoverableIntent.putExtra(BluetoothAdapter.                             EXTRA_DISCOVERABLE_DURATION, 300);
                    startActivityForResult(discoverableIntent, REQUEST_DISCOVERABLE);
                    return;
                }
                startListening();
            }
        });
        scanButton = (Button)findViewById(R.id.scanButton);
        scanButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mBtAdapter.startDiscovery();
                setProgressBarIndeterminateVisibility(true);
            }
        });
    }

@Override
    public void onResume() {
        super.onResume();
        //Register the activity for broadcast intents
        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
        registerReceiver(mReceiver, filter);
        filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
        registerReceiver(mReceiver, filter);
    }

@Override
    public void onPause() {
        super.onPause();
        unregisterReceiver(mReceiver);
    }

@Override
    public void onDestroy() {
        super.onDestroy();
        try {
            if(mBtSocket != null) {
                mBtSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

private static final int REQUEST_ENABLE = 1;
    private static final int REQUEST_DISCOVERABLE = 2;

@Override
    protectedvoid onActivityResult(int requestCode, int resultCode, Intent data) {
        switch(requestCode) {
        case REQUEST_ENABLE:
            if(resultCode != Activity.RESULT_OK) {                 Toast.makeText(this, "Bluetooth Not Enabled.",                     Toast.LENGTH_SHORT).show();
                finish();
            }
            break;
        case REQUEST_DISCOVERABLE:
            if(resultCode == Activity.RESULT_CANCELED) {
                Toast.makeText(this, "Must be discoverable.",
                    Toast.LENGTH_SHORT).show();
            } else {
                startListening();
            }
            break;
        default:
            break;
        }
    }

//Start a server socket and listen
    privatevoid startListening() {
        AcceptTask task = new AcceptTask();
        task.execute(MY_UUID);
        setProgressBarIndeterminateVisibility(true);
    }

//AsyncTask to accept incoming connections
    privateclass AcceptTask extends AsyncTask<UUID,Void,BluetoothSocket> {

@Override
        protected BluetoothSocket doInBackground(UUID... params) {
            String name = mBtAdapter.getName();
            try {
                //While listening, set the discovery name to a specific value
                mBtAdapter.setName(SEARCH_NAME);
                BluetoothServerSocket socket =
                    mBtAdapter.listenUsingRfcommWithServiceRecord("BluetoothRecipe", params[0]);
                BluetoothSocket connected = socket.accept();
                //Reset the BT adapter name
                mBtAdapter.setName(name);
                return connected;
            } catch (IOException e) {
                e.printStackTrace();
                mBtAdapter.setName(name);
                return null;
            }
        }

@Override
        protectedvoid onPostExecute(BluetoothSocket socket) {
            if(socket == null) {
                return;
            }
            mBtSocket = socket;
            ConnectedTask task = new ConnectedTask();
            task.execute(mBtSocket);
        }`

`}

//AsyncTask to receive a single line of data and post
    privateclass ConnectedTask extends AsyncTask<BluetoothSocket,Void,String> {

@Override
        protected String doInBackground(BluetoothSocket... params) {
            InputStream in = null;
            OutputStream out = null;
            try {
                //Send your data
                out = params[0].getOutputStream();
                out.write(emailField.getText().toString().getBytes());
                //Receive the other's data
                in = params[0].getInputStream();
                byte[] buffer = newbyte[1024];
                in.read(buffer);
                //Create a clean string from results
                String result = new String(buffer);
                //Close the connection
                mBtSocket.close();
                return result.trim();
            } catch (Exception exc) {
                return null;
            }
        }

@Override
        protectedvoid onPostExecute(String result) {
            Toast.makeText(ExchangeActivity.this, result, Toast.LENGTH_SHORT).show();
            setProgressBarIndeterminateVisibility(false);
        }
    }

// The BroadcastReceiver that listens for discovered devices
    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

// When discovery finds a device
            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                // Get the BluetoothDevice object from the Intent
                BluetoothDevice device =
                    intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                if(TextUtils.equals(device.getName(), SEARCH_NAME)) {
                    //Matching device found, connect
                    mBtAdapter.cancelDiscovery();
                    try {
                        mBtSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
                        mBtSocket.connect();
                        ConnectedTask task = new ConnectedTask();
                        task.execute(mBtSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }                 }
            //When discovery is complete
            } elseif (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                setProgressBarIndeterminateVisibility(false);
            }

}
    };
}`

当应用首次启动时,它会对设备的蓝牙状态进行一些基本的检查。如果BluetoothAdapter.getDefaultAdapter()返回 null,则表明设备不支持蓝牙,应用将不再运行。即使设备上有蓝牙,应用也必须启用蓝牙才能使用它。如果蓝牙被禁用,启用适配器的首选方法是向系统发送一个意图,并以BluetoothAdapter.ACTION_REQUEST_ENABLE作为操作。这将通知用户问题,并允许他们启用蓝牙。可以使用 enable()方法手动启用BluetoothAdapter,但是我们强烈建议您不要这样做,除非您已经通过其他方式请求了用户的许可。

蓝牙验证后,应用等待用户输入。如前所述,该示例可以在每个设备上设置为两种模式之一,即监听模式或搜索模式。让我们看看每种模式的路径。

Listen Mode

点击“监听共享者”按钮,应用开始监听传入的连接。为了让设备接受来自它可能不知道的设备的传入连接,它必须被设定为可被发现。应用通过检查适配器的扫描模式是否等于SCAN_MODE_CONNECTABLE_DISCOVERABLE来验证这一点。如果适配器不满足此要求,则向系统发送另一个意图,通知用户他们应该允许设备可被发现,类似于用于请求启用蓝牙的方法。如果用户接受这个请求,活动将返回一个结果,该结果等于他们允许设备被发现的时间长度;如果他们取消请求,活动将返回Activity.RESULT_CANCELED。我们的例子监视用户在onActivityResult()取消,并在这些条件下结束。

如果用户允许发现,或者如果设备已经被发现,则创建并执行AcceptTask。该任务为我们定义的服务的指定 UUID 创建一个侦听器套接字,并在等待传入的连接请求时阻塞。一旦收到有效的请求,它就会被接受,应用进入连接模式。

在设备侦听期间,其蓝牙名称被设置为一个已知的唯一值(SEARCH_NAME),以加快发现过程(我们将在“搜索模式”部分了解更多原因)。一旦建立了连接,就恢复了适配器的默认名称。

搜索模式

点击“连接和共享”按钮,让应用开始搜索要连接的另一台设备。它通过启动蓝牙发现过程并在广播接收器中处理结果来实现这一点。当通过BluetoothAdapter.startDiscovery()开始发现时,Android 将在两种情况下通过广播异步回调:当发现另一个设备时,以及当该过程完成时。

当活动对用户可见时,私有接收器mReceiver一直被注册,并将通过每个新发现的设备接收广播。回想一下关于监听模式的讨论,监听设备的设备名称被设置为唯一值。在每次发现时,接收器检查设备名称是否与我们已知的值匹配,并在找到一个时尝试连接。这对发现过程的速度很重要,因为否则验证每个设备的唯一方法是尝试连接到特定的服务 UUID,并查看操作是否成功。蓝牙连接过程是重量级的,而且很慢,只有在必要的时候才应该这样做,以保持事情运行良好。

这种匹配设备的方法还使用户无需手动选择要连接的设备。该应用足够智能以找到运行相同应用并处于监听模式的另一设备来完成传输。删除用户还意味着该值应该是唯一的和模糊的,以避免找到其他可能意外具有相同名称的设备。

找到匹配的设备后,我们取消发现过程(因为它也是重量级的,会降低连接速度),并连接到服务的 UUID。成功连接后,应用进入连接模式。

Connected Mode

一旦连接,两个设备上的应用将创建一个ConnectedTask来发送和接收用户联系信息。连接的BluetoothSocket有一个InputStream和一个OutputStream可用于进行数据传输。首先,电子邮件文本字段的当前值被打包并写入OutputStream。然后,读取InputStream以接收远程设备的信息。最后,每个设备获取它接收到的原始数据,并将其打包成一个干净的字符串显示给用户。

ConnectedTask.onPostExecute()方法的任务是向用户显示交换的结果;目前,这是通过用接收到的内容举杯庆祝来完成的。交易完成后,连接关闭,两台设备处于相同的模式,并准备执行另一次交换。

有关这个主题的更多信息,请查看 Android SDK 提供的 BluetoothChat 示例应用。这个应用很好地演示了如何为用户在设备之间发送聊天消息建立一个长期连接。

超越安卓的蓝牙

正如我们在本节开始时提到的,除了手机和平板电脑之外,蓝牙还存在于许多无线设备中。RFCOMM 接口也存在于像蓝牙调制解调器和串行适配器这样的设备中。用于在 Android 设备之间创建对等连接的相同 API 也可以用于连接到其他嵌入式蓝牙设备,以实现监控和控制的目的。

与这些嵌入式设备建立连接的关键是获得它们支持的 RFCOMM 服务的 UUID。和前面的例子一样,通过适当的 UUID,我们可以创建一个蓝牙套接字并传输数据。然而,由于 UUID 不像上一个例子中那样为人所知,我们必须有一个发现和获得它的方法。

SDK 中有这种功能,尽管没有记录下来,并且在将来的版本中可能会有变化。

Discover a UUID

快速浏览一下 BluetoothDevice 的源代码(由于 Android 的开源根),可以发现有几个隐藏的方法可以返回远程设备的 UUID 信息。最简单的使用方法是名为getUuids()的同步(阻塞)方法,它返回引用每个服务的ParcelUuid对象的数组。但是,由于该方法当前是隐藏的,所以必须使用 Java 反射来调用它。下面是一个使用反射从远程设备读取服务记录的 UUIDs 的示例方法:

public ParcelUuid servicesFromDevice(BluetoothDevice device) {     try {         Class cl = Class.forName("android.bluetooth.BluetoothDevice");         Class[] par = {};         Method method = cl.getMethod("getUuids", par);         Object[] args = {};         ParcelUuid[] retval = (ParcelUuid[])method.invoke(device, args);         return retval;     } catch (Exception e) {         e.printStackTrace();         return null;     } }

该流程还有一个名为fetchUuidsWithSdp()的异步版本,可以以同样的方式调用。因为它是异步的,所以结果通过广播意图返回。为android.bleutooth.device.action.UUID注册一个BroadcastReceiver(注意 Bluetooth 的拼写错误)以获得一个带有为该设备发现的 UUIDs 的回调。获得的ParcelUuid数组是一个额外的传递,其意图由android.bluetooth.device.extra.UUID引用,它与同步示例的结果相同。

3–11。查询网络可达性

问题

您的应用需要知道网络连接的变化。

解决方案

(API 一级)

ConnectivityManager监控设备的连接性。移动应用设计中要考虑的最重要的问题之一是网络并不总是可用的。随着人们的移动,网络的速度和能力会发生变化。因此,使用网络资源的应用应该始终能够检测到这些资源是否可达,并在不可达时通知用户。

除了可达性之外,ConnectivityManager 还可以为应用提供有关连接类型的信息。这使得你可以决定是否下载一个大文件,因为用户目前正在漫游,这可能会花费他们一大笔钱。

它是如何工作的

清单 3–26 创建了一个包装器方法,您可以将它放在代码中以检查网络连接。

清单 3–29。 ConnectivityManager 包装器

public boolean isNetworkReachable() {     ConnectivityManager mManager =             (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);     NetworkInfo current = mManager.getActiveNetworkInfo();     if(current == null) {         return false;     }     return (current.getState() == NetworkInfo.State.CONNECTED); }

检查网络状态的大部分工作都是由这个包装器完成的,这个包装器方法是为了简化每次检查所有可能的网络路径。注意,如果没有可用的活动数据连接,ConnectivityManager.getActiveNetworkInfo()将返回 null,因此我们必须首先检查这种情况。如果存在活动网络,我们可以检查其状态,这将返回以下内容之一:

  1. 不连贯的
  2. 连接
  3. 连接的
  4. 分离

当状态恢复为已连接时,网络被认为是稳定的,我们可以利用它来访问远程资源。

每当网络请求失败时,调用可达性检查,并通知用户他们的请求由于缺乏连通性而失败,这被认为是一种良好的做法。清单 3–30 是网络访问失败时这样做的一个例子。

清单 3–30。 通知用户连接失败

try {     //Attempt to access network resource     //May throw HttpResponseException or some other IOException on failure } catch (Exception e) {     if( !isNetworkReachable() ) {         AlertDialog.Builder builder = new AlertDialog.Builder(context);         builder.setTitle("No Network Connection");         builder.setMessage("The Network is unavailable. Please try your request again later.");         builder.setPositiveButton("OK",null);         builder.create().show();     } }

确定连接类型

如果知道用户是否连接到一个对带宽收费的网络也很重要,我们可以在活动的网络连接上调用NetworkInfo.getType()(参见清单 3–31)。

清单 3–31。 连接管理器带宽检查

public boolean isWifiReachable() {     ConnectivityManager mManager =             (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);     NetworkInfo current = mManager.getActiveNetworkInfo();     if(current == null) {         return false;     }     return (current.getType() == ConnectivityManager.TYPE_WIFI); }

这种可达性检查的修改版本确定用户是否连接到 WiFi 连接,通常指示他们在带宽不收费的情况下具有更快的连接。

总结

在当今的互联世界中,将 Android 应用连接到网络和网络服务是增加用户价值的绝佳方式。Android 用于连接网络和其他远程主机的框架使得添加这一功能变得简单明了。我们已经探索了如何将 Web 标准引入到您的应用中,使用 HTML 和 JavaScript 与用户交互,但是是在本地上下文中。您还看到了如何使用 Android 从远程服务器下载内容,并在您的应用中使用这些内容。我们还揭示了 web 服务器并不是唯一值得连接的主机,它使用蓝牙和 SMS 从一个设备直接与另一个设备通信。在下一章,我们将看看如何使用 Android 提供的工具与设备的硬件资源进行交互。

四、与设备硬件和介质交互

将应用软件与设备硬件集成为创造只有移动平台才能提供的独特用户体验提供了机会。使用麦克风和摄像头捕捉媒体允许应用通过照片或录制的问候融入个人风格。传感器和位置数据的集成可以帮助您开发应用来回答相关问题,如“我在哪里?”“我在看什么?”

在这一章中,我们将探讨如何使用 Android 提供的位置、媒体和传感器 API 来为您的应用增加移动设备带来的独特价值。

4–1。集成设备位置

问题

您希望利用设备的功能来报告其在应用中的当前物理位置。

解决方案

(API 一级)

利用 Android LocationManager提供的后台服务。移动应用通常可以为用户提供的最强大的好处之一是能够通过包含基于用户当前位置的信息来添加上下文。应用可能会要求LocationManager定期提供设备位置的更新,或者只是在检测到设备移动了很远的距离时才提供。

使用 Android 定位服务时,应注意尊重设备电池和用户的意愿。使用设备的 GPS 获得精细的位置定位是一个电力密集型过程,如果持续开着,会很快耗尽用户设备的电池。出于这个原因,以及其他原因,Android 允许用户禁用某些位置数据来源,如设备的 GPS。当您的应用决定如何获取位置时,必须遵守这些设置。

每个位置源也伴随着准确度的折衷。GPS 将返回更精确的位置(几米以内),但需要更长的时间来定位,并消耗更多的能量;而网络位置通常会精确到几公里,但是返回得更快并且使用更少的功率。在决定访问哪些源时,考虑应用的要求;如果您的应用只希望显示当地城市的信息,也许 GPS 定位是不必要的。

重要提示:在应用中使用定位服务时,请记住android.permission.ACCESS_COARSE_LOCATIONandroid.permission.ACCESS_FINE_LOCATION必须在应用清单中声明。如果你声明了android.permission.ACCESS_FINE_LOCATION,你不需要两者,因为它也包含了粗略的权限。

它是如何工作的

在为活动或服务中的用户位置创建简单的监视器时,我们需要考虑一些操作:

  1. 确定我们要使用的源是否已启用。如果不是,决定是否要求用户启用它或尝试其他来源。
  2. 使用合理的最小距离和更新间隔值注册更新。
  3. 不再需要更新时注销更新以节省设备电量。

在清单 4–1 中,我们注册了一个活动来监听用户可见的位置更新,并在屏幕上显示该位置。

清单 4–1。 活动监控位置更新

`publicclass MyActivity extends Activity {

LocationManager manager;
    Location currentLocation;

TextView locationView;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); locationView = new TextView(this);
        setContentView(locationView);

manager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
    }

@Override
    public void onResume() {
        super.onResume();
        if(!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            //Ask the user to enable GPS
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("Location Manager");
            builder.setMessage("We want to use your location, but GPS is currently disabled.\n"
                    +"Would you like to change these settings now?");
            builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    //Launch settings, allowing user to make a change
                    Intent i = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
                    startActivity(i);
                }
            });
            builder.setNegativeButton("No", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    //No location service, no Activity
                    finish();
                }
            });
            builder.create().show();
        }

//Get a cached location, if it exists
        currentLocation = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        updateDisplay();
        //Register for updates
        int minTime = 5000;
        float minDistance = 0;
        manager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
                minTime, minDistance, listener);
    }

@Override
    public void onPause() {
        super.onPause();
        manager.removeUpdates(listener);
    }

//Update text view
    privatevoid updateDisplay() {
        if(currentLocation == null) {
            locationView.setText("Determining Your Location...");
        } else {
            locationView.setText(String.format("Your Location:\n%.2f, %.2f",
                    currentLocation.getLatitude(),                     currentLocation.getLongitude()));
        }
    }

//Handle location callback events
    private LocationListener listener = new LocationListener() {

@Override
        public void onLocationChanged(Location location) {
            currentLocation = location;
            updateDisplay();
        }

@Override
        public void onProviderDisabled(String provider) { }

@Override
        public void onProviderEnabled(String provider) { }

@Override
        public void onStatusChanged(String provider, int status, Bundle extras) { }

};
}`

本例选择严格使用设备的 GPS 来获取位置更新。因为它是此活动功能的关键要素,所以每次恢复后承担的第一个主要任务是检查LocationManager.GPS_PROVIDER是否仍然启用。如果出于某种原因,用户禁用了此功能,我们会询问他们是否愿意启用 GPS,让他们有机会纠正这种情况。应用没有能力为用户做到这一点,所以如果他们同意,我们使用意图动作Settings.ACTION_LOCATION_SOURCE_SETTINGS启动一个活动,这将调出设备设置,以便用户可以启用 GPS。

一旦 GPS 处于活动状态并且可用,该活动就会注册一个LocationListener来通知位置更新。除了提供者类型和目的地侦听器之外,LocationManager.requestLocationUpdates()方法还接受两个重要参数:

  • minTime
    • 更新之间的最小时间间隔,以毫秒为单位。
    • 将此项设置为非零值允许位置提供者在再次更新之前休息大约指定的时间。
    • 这是一个保存功率的参数,并且不应该被设置为任何低于最小可接受更新速率的值。
  • minDistance
    • 发送下一次更新之前设备必须移动的距离,单位为米。
    • 将此项设置为非零将阻止更新,直到确定设备至少移动了这么多。

在本例中,我们要求发送更新的频率不超过每五秒钟一次,而不考虑位置是否发生了显著变化。当这些更新到达时,注册的监听器的onLocationChanged()方法被调用。请注意,当不同提供者的状态发生变化时,LocationListener 也会得到通知,尽管我们在这里没有利用这些回调。

注意:如果是在某个服务或其他后台操作中接收更新,Google 建议最小时间间隔不低于 60000(60 秒)。

该示例保存了对它接收到的最新位置的运行引用。最初,通过调用getLastKnownLocation()将该值设置为提供者缓存的最后一个已知位置,如果提供者没有缓存的位置值,则可能返回 null。对于每个传入的更新,位置值被重置,并且用户界面显示被更新以反映新的变化。

4–2。映射位置

问题

您希望在地图上为用户显示一个或多个位置。

解决方案

(API 一级)

向用户展示地图的最简单方法是用位置数据创建一个意图,并将其传递给 Android 系统,以便在地图应用中启动。在后面的章节中,我们将更深入地研究这种方法来完成许多不同的任务。此外,可以使用 Google Maps API SDK 插件提供的MapViewMapActivity将地图嵌入到您的应用中。

Maps API 是核心 SDK 的附加模块,尽管它们仍然捆绑在一起。如果您还没有 Google APIs SDK,请打开 SDK 管理器,您会发现在“第三方插件”下列出了每个 API 级别的包。

为了在您的应用中使用 Maps API,必须首先从 Google 获得 API 密钥。此密钥是使用签名应用的私钥生成的。如果没有 API 键,可以使用映射类,但不会向应用返回地图切片。

注:欲了解关于 SDK 的更多信息,并获取 API 密钥,请访问code . Google . com/Android/add-ons/Google-APIs/mapkey . html。还要注意,Android 对所有在调试模式下运行的应用使用相同的签名密钥(比如当它们从 IDE 中运行时),因此一个密钥可以为您在测试阶段开发的所有应用服务。

如果您在仿真器中运行代码进行测试,那么该仿真器必须使用 SDK 目标构建,该目标包括 Google APIs for mapping 以正确运行。如果从命令行创建模拟器,这些目标被命名为“Google Inc.:GoogleAPIs:X”,其中“X”是 API 版本指示器。如果您从 ide(比如 Eclipse)内部创建模拟器,那么目标具有类似的命名约定“Google API(Google Inc .)–X”,其中“X”是 API 版本指示符。

有了 API 密匙和合适的测试平台,就可以开始了。

它是如何工作的

要显示地图,只需在一个MapActivity中创建一个MapView的实例。在 XML 布局中,必须传递给MapView的一个必需属性是从 Google 获得的 API 键。参见清单 4–2。

清单 4–2。 布局中的典型 MapView

<com.google.android.maps.MapView   android:layout_width="fill_parent"   android:layout_height="fill_parent"   android:enabled="true"   android:clickable="true"   android:apiKey="API_KEY_STRING_HERE" />

注意:当将MapView添加到 XML 布局中时,必须包括完全限定的包名,因为该类不存在于android.viewandroid.widget中。

尽管 MapView 也可以从代码中实例化,但 API 键仍然需要作为构造函数参数:

MapView map = new MapView(this, "API_KEY_STRING_HERE");

此外,应用清单必须声明它对 Maps 库的使用,Maps 库双重地充当 Android Market 过滤器,将应用从没有此功能的设备上删除。

现在,让我们来看一个例子,它将最后一个已知的用户位置放在地图上并显示出来。参见清单 4–3。

清单 4–3。Android manifest . XML

`

    
    
    


        
            
                
                
            

        


`

请注意为 INTERNET 和 ACCESS_FINE_LOCATION 声明的权限。后者是必需的,因为这个例子是挂钩回LocationManager来获取缓存的位置值。清单中必须存在的另一个关键要素是引用 Google Maps API 的<uses-library>标签。Android 需要这个项目来正确地将外部库链接到您的应用构建中,但它还有另一个目的。Android Market 使用库声明来过滤应用,因此它不能安装在没有配备正确映射库的设备上。参见清单 4–4。

清单 4–4。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <TextView       android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:gravity="center_horizontal"     android:text="Map Of Your Location"   />   <com.google.android.maps.MapView     android:id="@+id/map"     android:layout_width="fill_parent"     android:layout_height="fill_parent"     android:enabled="true"     android:clickable="true"     android:apiKey="YOUR_API_KEY_HERE"   /> </LinearLayout>

记下您必须输入的必需 API 密钥的位置。另外,请注意,MapView不必是活动布局中唯一的东西,尽管事实上它必须在MapActivity中膨胀。参见清单 4–5。

清单 4–5。 显示缓存位置的地图活动

`publicclass MyActivity extends MapActivity {

MapView map;
    MapController controller;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

map = (MapView)findViewById(R.id.map);
        controller = map.getController();

LocationManager manager =
            (LocationManager)getSystemService(Context.LOCATION_SERVICE);
        Location location = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        int lat, lng;
        if(location != null) {
            //Convert to microdegrees
            lat = (int)(location.getLatitude() * 1000000);
            lng = (int)(location.getLongitude() * 1000000);
        } else {
            //Default to Google HQ
            lat = 37427222;
            lng = -122099167;
        }
        GeoPoint mapCenter = new GeoPoint(lat,lng);
        controller.setCenter(mapCenter);
        controller.setZoom(15);
    }

//Required abstract method, return false
    @Override
    protectedboolean isRouteDisplayed() {
        return false;
    }
}`

此活动获取最新的用户位置,并将地图居中于该点。所有对地图的控制都是通过一个MapController实例来完成的,我们通过调用MapView.getController()来获得这个实例;控制器可用于平移、缩放和调整屏幕上的地图。在这个例子中,我们使用控制器的setCenter()setZoom()方法来调整地图显示。

MapController.setCenter()将一个GeoPoint作为它的参数,这个参数与我们从 Android 服务接收到的Location略有不同。主要区别在于GeoPoint用微度(或度数* 1E6)来表示纬度和经度,而不是用表示整度的十进制值。因此,我们必须在将Location值应用到地图之前对其进行转换。

MapController.setZoom()允许地图以编程方式缩放到指定级别,介于 1 和 21 之间。默认情况下,地图将缩放到级别 1,SDK 文档将其定义为全局视图,每增加一个级别,地图将放大两倍。参见图 4–1。

images

图 4–1。 用户位置地图

您可能会注意到的第一件事是,地图没有在位置点上显示任何指示器(如大头针)。在 Recipe 4–3 中,我们将创建这些注释,并描述如何定制它们。

4–3 岁。注释地图

问题

除了显示以特定位置为中心的地图之外,您的应用还需要添加注释,以便更明显地标记该位置。

解决方案

(API 一级)

为地图创建一个自定义的ItemizedOverlay,包括所有要标记的点。ItemizedOverlay是一个抽象基类,它处理MapView上各个项目的所有绘图。项目本身是OverlayItem的实例,它是一个模型类,定义名称、副标题和可绘制标记来描述地图上的点。

它是如何工作的

让我们创建一个实现,它将获取一个 GeoPoints 数组,并使用相同的可绘制标记在地图上绘制它们。参见清单 4–6。

清单 4–6。 基本明细实现

`public class LocationOverlay extends ItemizedOverlay {
    private List mItems;

public LocationOverlay(Drawable marker) {
        super( boundCenterBottom(marker) );
    }

public void setItems(ArrayList items) {
        mItems = items;
        populate();
    }

@Override
    protected OverlayItem createItem(int i) {
        returnnew OverlayItem(mItems.get(i), null, null);
    }

@Override
    publicint size() {
        return mItems.size();
    }

@Override
    protected boolean onTap(int i) {
        //Handle a tap event here
        return true;
    }
}`

在这个实现中,构造函数使用一个Drawable来表示放置在地图上每个位置的标记。覆盖图中使用的Drawable必须有适当的界限,而boundCenterBottom()是一个方便的方法来处理这个问题。具体来说,它应用了边界,使得Drawable上接触地图位置的点将位于底部像素行的中心。

ItemizedOverlay 有两个必须被覆盖的抽象方法:createItem(),它必须返回声明类型的对象,以及size(),它返回被管理的项目的数量。这个例子获取了一个GeoPoint的列表,并将它们全部包装到OverlayItem中。一旦所有数据都出现并准备好显示,就应该在覆盖图中调用populate()方法,在这个例子中是在setItems()的末尾。

让我们将这个覆盖图应用到地图上,使用默认的应用图标作为标记,在 Google HQ 周围绘制三个自定义位置。参见清单 4–7。

清单 4–7。 活动使用自定义地图叠加

`public class MyActivity extends MapActivity {

MapView map;
    MapController controller;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

map = (MapView)findViewById(R.id.map);
        controller = map.getController();

ArrayList locations = new ArrayList();
        //Google HQ @ 37.427,-122.099
        locations.add(new GeoPoint(37427222,-122099167));
        //Subtract 0.01 degrees
        locations.add(new GeoPoint(37426222,-122089167));
        //Add 0.01 degrees
        locations.add(new GeoPoint(37428222,-122109167));

LocationOverlay myOverlay =
            new LocationOverlay(getResources().getDrawable(R.drawable.icon));
        myOverlay.setItems(locations);
        map.getOverlays().add(myOverlay);
        controller.setCenter(locations.get(0));
        controller.setZoom(15);

}
    //Required abstract method, return false
    @Override
    protected boolean isRouteDisplayed() {
        return false;
    }

}`

运行时,该活动产生如图图 4–2 所示的显示。

images

图 4–2。 地图与详解

请注意MapViewItemizedOverlay是如何在标记上绘制阴影的。

但是,如果我们想要定制每个项目,使其显示不同的标记图像,该怎么办呢?我们该怎么做?通过显式设置项目的标记,可以为每个项目返回一个自定义的Drawable。在这种情况下,提供给ItemizedOverlay构造函数的 Drawable 只是一个缺省值,如果不存在自定义覆盖的话。考虑对实现进行修改,如清单 4–8 所示。

清单 4–8。 用自定义标记逐项覆盖

`public class LocationOverlay extends ItemizedOverlay {
    private List mItems;
    private List mMarkers;

public LocationOverlay(Drawable marker) {
        super( boundCenterBottom(marker) );
    }

public void setItems(ArrayList items, ArrayList drawables) {
        mItems = items;
        mMarkers = drawables;
        populate();
    }

@Override
    protected OverlayItem createItem(int i) {
        OverlayItem item = new OverlayItem(mItems.get(i), null, null);         item.setMarker( boundCenterBottom(mMarkers.get(i)) );
        return item;
    }

@Override
    publicint size() {
        return mItems.size();
    }

@Override
    protected boolean onTap(int i) {
        //Handle a tap event here
        return true;
    }
}`

通过这一修改,创建的 OverlayItems 现在可以接收一个定制的标记图像,其形式为与图像列表中的项目索引相匹配的有界的Drawable。如果您设置的Drawable有状态,当选择或触摸该项目时,将显示按下和聚焦状态。我们修改后使用新实现的例子看起来像清单 4–9。

清单 4–9。 提供自定义标记的示例活动

`public class MyActivity extends MapActivity {

MapView map;
    MapController controller;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

map = (MapView)findViewById(R.id.map);
        controller = map.getController();

ArrayList locations = new ArrayList();
        ArrayList images = new ArrayList();

//Google HQ 37.427,-122.099
        locations.add(new GeoPoint(37427222,-122099167));
        images.add(getResources().getDrawable(R.drawable.logo));
        //Subtract 0.01 degrees
        locations.add(new GeoPoint(37426222,-122089167));
        images.add(getResources().getDrawable(R.drawable.icon));
        //Add 0.01 degrees
        locations.add(new GeoPoint(37428222,-122109167));
        images.add(getResources().getDrawable(R.drawable.icon));

LocationOverlay myOverlay =
            new LocationOverlay(getResources().getDrawable(R.drawable.icon));
        myOverlay.setItems(locations, images);
        map.getOverlays().add(myOverlay);
        controller.setCenter(locations.get(0));
        controller.setZoom(15);     }

//Required abstract method, return false
    @Override
    protected boolean isRouteDisplayed() {
        return false;
    }
}`

现在,我们的示例为它希望在地图上显示的每个项目提供了一个离散的图像。具体来说,我们已经决定用一个版本的 Google 徽标来代表实际的 Google HQ 位置,同时用相同的标记保留其他两个点。参见图 4–3。

images

图 4–3。 用自定义标记覆盖地图

让他们交互

也许您注意到了 LocationOverlay 中定义的onTap()方法,但从未提及。ItemizedOverlay基本实现的另一个很好的特性是,它处理点击测试,并且当它点击一个特定的项目时,有一个方便的方法来引用该项目的索引。通过这个方法,您可以敬酒、显示对话框、开始一个新的活动,或者任何其他适合用户点击注释获取更多信息的操作。

我呢?

Android 的地图 API 还包括一个特殊的覆盖图来绘制用户位置,即MyLocationOverlay。这个覆盖图使用起来非常简单,但是只有当它所在的活动可见时才应该启用它。否则,不必要的资源使用将导致设备性能下降和电池寿命延长。参见清单 4–10。

清单 4–10。 添加 MyLocationOverlay

`public class MyActivity extends MapActivity {

MapView map;
    MyLocationOverlay myOverlay;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

map = (MapView)findViewById(R.id.map);
        myOverlay = new MyLocationOverlay(this, map);
        map.getOverlays().add(myOverlay);
    }

@Override
    public void onResume() {
        super.onResume();
        myOverlay.enableMyLocation();
    }

@Override
    public void onPause() {
        super.onResume();
        myOverlay.disableMyLocation();
    }

//Required abstract method, return false
    @Override
    protected boolean isRouteDisplayed() {
        return false;
    }
}`

这将在用户的最新位置上显示一个标准的点或箭头标记(取决于指南针是否在使用),并且只要启用覆盖,就会随着用户的移动进行跟踪。

使用MyLocationOverlay的关键是在不使用时禁用其功能(当活动不可见时),并在需要时重新启用它们。就像使用LocationManager一样,这确保了这些服务不会消耗不必要的能量。

4–4。捕捉图像和视频

问题

您的应用需要利用设备的摄像头来捕捉媒体,无论是静态图像还是短视频剪辑。

解决方案

(API 三级)

向 Android 发送一个意向,将控制权转移给相机应用,并返回用户捕获的图像。Android 确实包含用于直接访问相机硬件、预览和拍摄快照或视频的 API。但是,如果您的唯一目标是使用用户熟悉界面的摄像头简单地获取媒体内容,那么没有比移交更好的解决方案了。

它是如何工作的

让我们来看看如何使用相机应用拍摄静态图像和视频剪辑。

图像捕捉

让我们来看一个示例活动,当按下“拍照”按钮时,该活动将激活相机应用,并以位图的形式接收该操作的结果。参见清单 4–11 和清单 4–12。

清单 4–11。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <Button     android:id="@+id/capture"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Take a Picture"   />   <ImageView     android:id="@+id/image"     android:layout_width="fill_parent"     android:layout_height="fill_parent"     android:scaleType="centerInside"   /> </LinearLayout>

清单 4–12。 活动捕捉图像

`public class MyActivity extends Activity {

privatestaticfinalintREQUEST_IMAGE = 100;

Button captureButton;
    ImageView imageView;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

captureButton = (Button)findViewById(R.id.capture);
        captureButton.setOnClickListener(listener);

imageView = (ImageView)findViewById(R.id.image);
    }

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(requestCode == REQUEST_IMAGE&& resultCode == Activity.RESULT_OK) {
            //Process and display the image
            Bitmap userImage = (Bitmap)data.getExtras().get("data");
            imageView.setImageBitmap(userImage);
        }
    }

private View.OnClickListener listener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            startActivityForResult(intent, REQUEST_IMAGE);
        }
    };
}`

该方法捕获图像并返回一个缩小的位图作为“数据”字段中的额外内容。如果您需要捕获图像并需要将全尺寸图像保存在某处,在开始捕获之前,将图像目的地的Uri插入意图的MediaStore.EXTRA_OUTPUT字段。参见清单 4–13。

清单 4–13。 全尺寸图像捕捉到文件

`public class MyActivity extends Activity {

private static final int REQUEST_IMAGE = 100;

Button captureButton;
    ImageView imageView;
    File destination;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);         captureButton = (Button)findViewById(R.id.capture);
        captureButton.setOnClickListener(listener);

imageView = (ImageView)findViewById(R.id.image);

destination = new File(Environment.getExternalStorageDirectory(),"image.jpg");
    }

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(requestCode == REQUEST_IMAGE&& resultCode == Activity.RESULT_OK) {
            try {
                FileInputStream in = new FileInputStream(destination);
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inSampleSize = 10; //Downsample by 10x

Bitmap userImage = BitmapFactory.decodeStream(in, null, options);
                imageView.setImageBitmap(userImage);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

private View.OnClickListener listener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            //Add extra to save full-image somewhere
            intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(destination));
            startActivityForResult(intent, REQUEST_IMAGE);
        }
    };
}`

此方法将指示相机应用将图像存储在其他地方(在本例中,在设备的 SD 卡上存储为“image.jpg”),并且结果不会按比例缩小。当操作返回后要检索图像时,我们现在直接进入我们告诉相机存储的文件位置。

然而,使用BitmapFactory.Options,我们仍然在显示到屏幕之前缩小图像,以避免一次将全尺寸位图加载到内存中。还要注意,这个例子选择了一个位于设备外部存储器上的文件位置,这需要在 API 级别 4 及以上声明android.permission.WRITE_EXTERNAL_STORAGE权限。如果您的最终解决方案将文件写在其他地方,这可能是不必要的。

视频拍摄

使用这种方法捕捉视频剪辑同样简单,尽管产生的结果略有不同。在任何情况下,实际的视频剪辑数据都不会直接在 Intent extras 中返回,并且总是保存到目标文件位置。以下两个参数可以作为额外参数传递:

  1. MediaStore.EXTRA_VIDEO_QUALITY
    1. 描述用于捕获视频的质量级别的整数值。
    2. 低质量的允许值为 0,高质量的允许值为 1。
  2. MediaStore.EXTRA_OUTPUT
    1. 保存视频内容的 Uri 目标位置。
    2. 如果不存在,视频将保存在设备的标准位置。

当视频记录完成时,数据保存的实际位置作为结果意图的数据字段中的Uri返回。让我们看一个类似的例子,它允许用户记录并保存他们的视频,然后将保存的位置显示回屏幕。参见清单 4–14 和清单 4–15。

清单 4–14。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <Button     android:id="@+id/capture"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Take a Video"   />   <TextView     android:id="@+id/file"     android:layout_width="fill_parent"     android:layout_height="fill_parent"   /> </LinearLayout>

清单 4–15。 活动捕捉一个视频片段

`public class MyActivity extends Activity {

private static final int REQUEST_VIDEO = 100;

Button captureButton;
    TextView text;
    File destination;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

captureButton = (Button)findViewById(R.id.capture);
        captureButton.setOnClickListener(listener);

text = (TextView)findViewById(R.id.file);         destination = new File(Environment.getExternalStorageDirectory(),"myVideo");
    }

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(requestCode == REQUEST_VIDEO&& resultCode == Activity.RESULT_OK) {
            String location = data.getData().toString();
            text.setText(location);
        }
    }

private View.OnClickListener listener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
            //Add (optional) extra to save video to our file
            intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(destination));
            //Optional extra to set video quality
            intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
            startActivityForResult(intent, REQUEST_VIDEO);
        }
    };
}`

这个例子和前面保存图像的例子一样,将录制的视频放在设备的 SD 卡上(对于 API 级别 4+需要android.permission.WRITE_EXTERNAL_STORAGE权限)。为了启动这个过程,我们向媒体商店发送一个意向。ACTION_VIDEO_CAPTURE 动作字符串给系统。Android 将启动默认的相机应用来处理视频录制,并在录制完成时返回一个 OK 结果。我们通过调用onActivityResult()回调方法中的Intent.getData()来检索数据作为 Uri 存储的位置,然后向用户显示该位置。

此示例明确要求使用低质量设置拍摄视频,但此参数是可选的。如果MediaStore.EXTRA_VIDEO_QUALITY不在请求意图中,设备通常会选择使用高质量拍摄。

在提供了MediaStore.EXTRA_OUTPUT的情况下,返回的Uri应该与您请求的位置相匹配,除非出现错误,导致应用无法写入该位置。如果不提供该参数,返回值将是一个content://Uri,用于从系统的 MediaStore 内容提供商检索媒体。

稍后,在方法 4–8 中,我们将研究在您的应用中播放该媒体的实用方法。

4–5。制作自定相机覆盖图

问题

许多应用需要更直接地访问摄像头,或者是为了覆盖控件的自定义用户界面,或者是为了显示关于通过基于位置和方向传感器的信息可见的内容的元数据(增强现实)。

解决方案

(API 等级 5)

在自定义活动中直接连接到摄像机硬件。Android 提供 API 来直接访问设备的摄像头,以获取预览提要和拍摄照片。当应用的需求增长到不仅仅是简单地抓拍并返回一张照片以供显示时,我们可以访问这些。

注意:因为我们在这里对摄像机采取了更直接的方法,所以需要在清单中声明android.permission.CAMERA权限。

它是如何工作的

我们从创建一个SurfaceView开始,这是一个用于实时绘图的专用视图,我们将在其中附加相机的预览流。这为我们提供了一个视图中的实时预览,我们可以在活动中以我们选择的任何方式进行布局。从那以后,只需添加适合应用上下文的其他视图和控件。让我们来看看代码(参见清单 4–16 和清单 4–17)。

注:这里使用的Camera级是android.hardware.Camera,不要和android.graphics.Camera混淆。确保在应用中导入了正确的引用。

清单 4–16。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <SurfaceView     android:id="@+id/preview"     android:layout_width="fill_parent"     android:layout_height="fill_parent"   /> </RelativeLayout>

清单 4–17。 活动展示现场摄像预览

`import android.hardware.Camera;

publicclass PreviewActivity extends Activity implements SurfaceHolder.Callback {

Camera mCamera;
    SurfaceView mPreview;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

mPreview = (SurfaceView)findViewById(R.id.preview);
        mPreview.getHolder().addCallback(this);
        mPreview.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

mCamera = Camera.open();
    }

@Override
    public void onPause() {
        super.onPause();
        mCamera.stopPreview();
    }

@Override
    public void onDestroy() {
        super.onDestroy();
        mCamera.release();
    }

//Surface Callback Methods
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
    {
        Camera.Parameters params = mCamera.getParameters();
        //Get all the devices's supported sizes and pick the first (largest)
        List<Camera.Size> sizes = params.getSupportedPreviewSizes();
        Camera.Size selected = sizes.get(0);
        params.setPreviewSize(selected.width,selected.height);
        mCamera.setParameters(params);

mCamera.startPreview();
    }

@Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            mCamera.setPreviewDisplay(mPreview.getHolder());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

@Override
    public void surfaceDestroyed(SurfaceHolder holder) { }
}`

注意:如果你在模拟器上测试,没有摄像头可以预览。模拟器显示什么来模拟预览取决于您运行的版本。要验证此代码是否正常工作,请在您的特定模拟器上打开 Camera 应用,并注意预览效果。这个示例中应该会出现相同的显示。

在这个例子中,我们创建了一个填充窗口的SurfaceView,并告诉它我们的活动将被通知所有的SurfaceHolder回调。摄像机在完全初始化之前不能在表面上显示预览信息,所以我们一直等到调用surfaceCreated()来将视图的SurfaceHolder附加到Camera实例。类似地,我们等待调整预览的大小并开始绘制,直到表面被赋予其大小,这发生在调用surfaceChanged()时。

调用Parameters.getSupportedPreviewSizes()会返回设备可以接受的所有尺寸的列表,它们通常按从大到小的顺序排列。在本例中,我们选择第一个(因此也是最大的)预览分辨率,并用它来设置大小。

注意:在 2.0 (API Level 5)之前的版本中,对于Parameters.setPreviewSize(),直接从该方法中传递高度和宽度参数是可以接受的;但在 2.0 和更高版本中,相机只会将其预览设置为设备支持的分辨率之一。否则尝试将导致异常。

Camera.startPreview()开始在表面上实时绘制摄像机数据。请注意,预览始终以横向显示。在 Android 2.2 (API Level 8)之前,官方没有办法调整预览显示的旋转。因此,建议使用摄像机预览的活动将其方向固定为清单中的android:screenOrientation=“landscape”以匹配。

相机服务一次只能由一个应用访问。因此,一旦不再需要摄像机,请立即致电Camera.release(),这一点很重要。在示例中,当活动结束时,我们不再需要摄像机,因此这个调用发生在onDestroy()中。

后来的补充

如果您的应用以它们为目标,那么在 API 的较高版本中有两个附加功能也是有用的:

  • Camera.setDisplayOrientation(int degrees)
    • API 等级 8 可用(安卓 2.2)。
    • 将实时预览设置为 0 度、90 度、180 度或 270 度。0 映射到默认的横向方向。
  • Camera.open(int which)
    • API 级(安卓 2.3)可用。
    • 支持多个摄像头(主要是正面和背面摄像头)。
    • 取 0 到getNumberOfCameras() -1 的参数。
照片覆盖

现在,我们可以在前面的示例中添加任何适合在相机预览顶部显示的控件或视图。让我们修改预览,以包括一个取消和快照照片按钮。参见清单 4–18 和清单 4–19。

清单 4–18。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <SurfaceView     android:id="@+id/preview"     android:layout_width="fill_parent"     android:layout_height="fill_parent"   />   <RelativeLayout     android:layout_width="fill_parent"     android:layout_height="100dip"     android:layout_alignParentBottom="true"     android:gravity="center_vertical"     android:background="#A000">     <Button       android:layout_width="100dip"       android:layout_height="wrap_content"       android:text="Cancel"       android:onClick="onCancelClick"     />     <Button       android:layout_width="100dip"       android:layout_height="wrap_content"       android:layout_alignParentRight="true"       android:text="Snap Photo"       android:onClick="onSnapClick"     />   </RelativeLayout> </RelativeLayout>

清单 4–19。 添加了照片控件的活动

`public class PreviewActivity extends Activity implements
                SurfaceHolder.Callback, Camera.ShutterCallback, Camera.PictureCallback {

Camera mCamera;
    SurfaceView mPreview;

@Override public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

mPreview = (SurfaceView)findViewById(R.id.preview);
        mPreview.getHolder().addCallback(this);
        mPreview.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

mCamera = Camera.open();
    }

@Override
    public void onPause() {
        super.onPause();
        mCamera.stopPreview();
    }

@Override
    public void onDestroy() {
        super.onDestroy();
        mCamera.release();
        Log.d("CAMERA","Destroy");
    }

public void onCancelClick(View v) {
        finish();
    }

public void onSnapClick(View v) {
        //Snap a photo
        mCamera.takePicture(this, null, null, this);
    }

//Camera Callback Methods
    @Override
    public void onShutter() {
        Toast.makeText(this, "Click!", Toast.LENGTH_SHORT).show();
    }

@Override
    public void onPictureTaken(byte[] data, Camera camera) {

//Store the picture off somewhere
        //Here, we chose to save to internal storage
        try {
            FileOutputStream out = openFileOutput("picture.jpg", Activity.MODE_PRIVATE);
            out.write(data);
            out.flush();
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

//Must restart preview
        camera.startPreview();     }

//Surface Callback Methods
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        Camera.Parameters params = mCamera.getParameters();
        List<Camera.Size> sizes = params.getSupportedPreviewSizes();
        Camera.Size selected = sizes.get(0);
        params.setPreviewSize(selected.width,selected.height);
        mCamera.setParameters(params);

mCamera.setDisplayOrientation(90);
        mCamera.startPreview();
    }

@Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            mCamera.setPreviewDisplay(mPreview.getHolder());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

@Override
    public void surfaceDestroyed(SurfaceHolder holder) { }
}`

在这里,我们添加了一个简单的,部分透明的覆盖,包括一对相机操作的控制。取消所采取的行动是微不足道的;我们简单地完成活动。然而,在手动拍摄照片并将照片返回到应用时,Snap Photo 引入了更多的相机 API。一个用户动作将启动Camera.takePicture()方法,该方法接受一系列回调指针。

注意,本例中的活动实现了另外两个接口:Camera.ShutterCallbackCamera.PictureCallback。前者在尽可能接近图像被捕获的时刻被调用(当“快门”关闭时),而后者可以在图像的不同形式可用的多个实例中被调用。

takePicture()的参数是单个ShutterCallback,最多三个PictureCallback实例。将在以下时间调用PictureCallback(按照它们作为参数出现的顺序):

  1. 在用原始图像数据捕获图像之后
    1. 这可能会在内存有限的设备上返回 null。
  2. 在用缩放的图像数据(称为后视图像)处理图像之后
    1. 这可能会在内存有限的设备上返回 null。
  3. 在用 JPEG 图像数据压缩图像之后

这个例子只关心当 JPEG 准备好的时候被通知。因此,这也是最后一次回调,也是预览必须再次启动的时间点。如果在拍照后没有再次调用startPreview(),那么表面上的预览将保持冻结在捕获的图像上。

4–6 岁。录制音频

问题

您有一个应用需要利用设备麦克风来记录音频输入。

解决方案

(API 一级)

使用MediaRecorder捕捉音频并将其保存到文件中。

它是如何工作的

MediaRecorder 使用起来非常简单。您只需要提供一些关于用于编码的文件格式和数据存储位置的基本信息。清单 4–20 和 4–21 提供了一个将音频文件录制到设备的 SD 卡上的示例,用于监控用户操作的开始和停止时间。

重要提示:为了使用MediaRecorder记录音频输入,您还必须在应用清单中声明android.permission.RECORD_AUDIO权限。

清单 4–20。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <Button     android:id="@+id/startButton"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Start Recording"   />   <Button     android:id="@+id/stopButton"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Stop Recording"     android:enabled="false"   /> </LinearLayout>

清单 4–21。 活动录音

`public class RecordActivity extends Activity {

private MediaRecorder recorder;
    private Button start, stop;
    File path;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

start = (Button)findViewById(R.id.startButton);
        start.setOnClickListener(startListener);
        stop = (Button)findViewById(R.id.stopButton);
        stop.setOnClickListener(stopListener);

recorder = new MediaRecorder();
        path = new File(Environment.getExternalStorageDirectory(),"myRecording.3gp");

resetRecorder();
    }

@Override
    public void onDestroy() {
        super.onDestroy();
        recorder.release();
    }

private void resetRecorder() {
        recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
        recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
        recorder.setOutputFile(path.getAbsolutePath());
        try {
            recorder.prepare();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

private View.OnClickListener startListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            try {
                recorder.start();

start.setEnabled(false);
                stop.setEnabled(true);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };

private View.OnClickListener stopListener = new View.OnClickListener() {
        @Override         public void onClick(View v) {
            recorder.stop();
            resetRecorder();

start.setEnabled(true);
            stop.setEnabled(false);
        }
    };
}`

这个例子的用户界面非常简单。有两个按钮,用户可以根据录制状态交替使用。当用户按下 start 时,我们启用 stop 按钮并开始记录。当用户按下 stop 时,我们重新启用 start 按钮,并将记录器重置为再次运行。

MediaRecorder 的设置非常简单。我们在 SD 卡上创建一个名为“myRecording.3gp”的文件,并在setOutputFile()中传递路径。其余的设置方法告诉录像机使用设备麦克风作为输入(音频源。MIC),并使用默认编码器为输出创建 3GP 文件格式。

现在,你可以使用任何设备的文件浏览器或媒体播放器应用来播放这个音频文件。稍后,在方法 4–8 中,我们将指出如何通过应用播放音频。

4–7 岁。添加语音识别

问题

您的应用需要语音识别技术来解释语音输入。

解决方案

(API 三级)

使用android.speech包的类来利用每个 Android 设备的内置语音识别技术。每一个配备语音搜索的 Android 设备(从 Android 1.5 开始提供)都为应用提供了使用内置SpeechRecognizer处理语音输入的能力。

要激活这个过程,应用只需向系统发送一个RecognizerIntent,识别服务将记录语音输入并对其进行处理;返回一个字符串列表,表明识别器认为它听到了什么。

它是如何工作的

让我们来看看这项技术的实际应用。参见清单 4–22。

清单 4–22。 活动发起并处理语音识别

`public class RecognizeActivity extends Activity {

private static final int REQUEST_RECOGNIZE = 100;

TextView tv;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        tv = new TextView(this);
        setContentView(tv);

Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
                        RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
        intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Tell Me Your Name");
        try {
            startActivityForResult(intent, REQUEST_RECOGNIZE);
        } catch (ActivityNotFoundException e) {
            //If no recognizer exists, download one from Android Market
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("Not Available");
            builder.setMessage("There is currently no recognition application installed."
                +"  Would you like to download one?");
            builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    //Download, for example, Google Voice Search
                    Intent marketIntent = new Intent(Intent.ACTION_VIEW);
                    marketIntent.setData
                        (Uri.parse("market://details?id=com.google.android.voicesearch"));
                }
            });
            builder.setNegativeButton("No", null);
            builder.create().show();
        }
    }

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(requestCode == REQUEST_RECOGNIZE&& resultCode == Activity.RESULT_OK) {
            ArrayList matches =                 data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
            StringBuilder sb = new StringBuilder();
            for(String piece : matches) {
                sb.append(piece);
                sb.append('\n');
            }
            tv.setText(sb.toString());
        } else {
            Toast.makeText(this, "Operation Canceled", Toast.LENGTH_SHORT).show();
        }
    }
}`

注意:如果你在模拟器中测试你的应用,要注意 Android Market 和任何语音识别器都不太可能安装。最好在设备上测试这个例子的操作。

这个例子在应用启动时自动启动语音识别活动,并要求用户“告诉我你的名字”。收到用户的语音并处理结果后,Activity 返回用户可能说过的内容列表。这个列表是按照概率排序的,所以在很多情况下,简单地称matches.get(0)为最佳选择并继续前进是明智的。但是,该活动获取所有返回值,并出于娱乐目的将它们显示在屏幕上。

当启动SpeechRecognizer时,有许多额外的东西可以传递,目的是定制行为。本例使用了两种最常见的方法:

  • 额外 _ 语言 _ 模型
    • 帮助微调来自语音处理器的结果的值。
    • 典型的语音到文本查询应该使用 LANGUAGE_MODEL_FREE_FORM 选项。
    • 如果进行较短的请求类型查询,LANGUAGE_MODEL_WEB_SEARCH 可能会产生更好的结果。
  • 额外提示
    • 显示为用户语音提示的字符串值。

除此之外,传递一些其他参数也是有用的:

  • 额外 _ 最大 _ 结果
    • 设置返回结果的最大数量的整数。
  • 额外语言
    • 请求以不同于当前系统默认语言的语言返回结果。
    • 有效 IETF 标签的字符串值,如“en-US”或“es”

4–8 岁。播放音频/视频

问题

应用需要在设备上播放本地或远程的音频或视频内容。

(API 一级)

使用MediaPlayer播放本地或流媒体。无论内容是音频还是视频,本地还是远程,MediaPlayer都将高效地连接、准备和播放相关媒体。在这个菜谱中,我们还将探索使用MediaControllerVideoView作为简单的方法来将交互和视频播放包含到活动布局中。

它是如何工作的

注意:在期望播放特定的媒体剪辑或流之前,请阅读开发者文档的“Android 支持的媒体格式”部分以验证支持。

音频播放

让我们看一个简单的例子,只用MediaPlayer来播放声音。参见清单 4–23。

清单 4–23。 活动播放本地声音

`public class PlayActivity extends Activity implements MediaPlayer.OnCompletionListener {

Button mPlay;
    MediaPlayer mPlayer;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

mPlay = new Button(this);
        mPlay.setText("Play Sound");
        mPlay.setOnClickListener(playListener);

setContentView(mPlay);
    }

@Override
    public void onDestroy() {
        super.onDestroy();
        if(mPlayer != null) {
            mPlayer.release();
        }
    }

private View.OnClickListener playListener = new View.OnClickListener() {

@Override
        public void onClick(View v) {
            if(mPlayer == null) {
                try {
                    mPlayer = MediaPlayer.create(PlayActivity.this, R.raw.sound);                     mPlayer.start();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                mPlayer.stop();
                mPlayer.release();
                mPlayer = null;
            }
        }
    };

//OnCompletionListener Methods
    @Override
    public void onCompletion(MediaPlayer mp) {
        mPlayer.release();
        mPlayer = null;
    }

}`

此示例使用一个按钮来开始和停止本地声音文件的回放,该文件存储在项目的 res/raw 目录中。MediaPlayer.create()是一种具有多种形式的便利方法,旨在一步完成玩家对象的构建和准备。本例中使用的表单引用了一个本地资源 ID,但是也可以使用create()来访问和播放远程资源

MediaPlayer.create(Context context, Uri uri);

创建后,该示例立即开始播放声音。声音播放时,用户可以再次按下按钮停止播放。该活动还实现了MediaPlayer.OnCompletionListener接口,因此当播放操作正常完成时,它会收到一个回调。

在这两种情况下,一旦停止播放,MediaPlayer 实例就会被释放。这种方法允许资源仅在被使用时才被保留,并且声音可以被播放多次。为了确保资源不会被不必要地保留,当活动被销毁时,如果它仍然存在,玩家也会被释放。

如果您的应用需要播放许多不同的声音,您可以考虑在播放结束时调用reset()而不是release()。但是记住,当玩家不再被需要的时候(或者活动结束了),还是要给release()打电话。

音频播放器

除了简单的回放之外,如果应用需要为用户创建一种交互式体验,以便能够播放、暂停和搜索媒体,该怎么办?MediaPlayer 上有一些方法可以用自定义 UI 元素来实现所有这些功能,但是 Android 也提供了 MediaController 视图,所以您不必这么做。参见列表 4–24 和 4–25。

清单 4–24。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:id="@+id/root"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <TextView     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:layout_gravity="center_horizontal"     android:text="Now Playing..."   />   <ImageView     android:id="@+id/coverImage"     android:layout_width="fill_parent"     android:layout_height="fill_parent"     android:scaleType="centerInside"   /> </LinearLayout>

清单 4–25。 用媒体控制器播放音频的活动

`public class PlayerActivity extends Activity implements
            MediaController.MediaPlayerControl, MediaPlayer.OnBufferingUpdateListener {

MediaController mController;
    MediaPlayer mPlayer;
    ImageView coverImage;

int bufferPercent = 0;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

coverImage = (ImageView)findViewById(R.id.coverImage);

mController = new MediaController(this);
        mController.setAnchorView(findViewById(R.id.root));
    }

@Override
    public void onResume() {
        super.onResume();
        mPlayer = new MediaPlayer();
        //Set the audio data source
        try {
            mPlayer.setDataSource(this, Uri.parse("URI_TO_REMOTE_AUDIO"));
            mPlayer.prepare();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //Set an image for the album cover
        coverImage.setImageResource(R.drawable.icon); mController.setMediaPlayer(this);
        mController.setEnabled(true);
    }

@Override
    public void onPause() {
        super.onPause();
        mPlayer.release();
        mPlayer = null;
    }

@Override
    public boolean onTouchEvent(MotionEvent event) {
        mController.show();
        return super.onTouchEvent(event);
    }

//MediaPlayerControl Methods
    @Override
    public int getBufferPercentage() {
        return bufferPercent;
    }

@Override
    public int getCurrentPosition() {
        return mPlayer.getCurrentPosition();
    }

@Override
    public int getDuration() {
        return mPlayer.getDuration();
    }

@Override
    public boolean isPlaying() {
        return mPlayer.isPlaying();
    }

@Override
    public void pause() {
        mPlayer.pause();
    }

@Override
    public void seekTo(int pos) {
        mPlayer.seekTo(pos);
    }

@Override
    public void start() {
        mPlayer.start();
    }

//BufferUpdateListener Methods
    @Override
    public void onBufferingUpdate(MediaPlayer mp, int percent) {
        bufferPercent = percent;     }

//Android 2.0+ Target Callbacks
    public boolean canPause() {
        return true;
    }

public boolean canSeekBackward() {
        return true;
    }

public boolean canSeekForward() {
        return true;
    }
}`

这个例子创建了一个简单的音频播放器,它显示与正在播放的音频相关联的艺术家或封面艺术的图像(我们只是在这里将其设置为应用图标)。该示例仍然使用 MediaPlayer 实例,但是这一次我们没有使用create()便利方法来创建它。相反,我们在创建实例后使用setDataSource()来设置内容。当以这种方式附加内容时,播放器不会自动准备好,所以我们还必须调用prepare()来准备好播放器以供使用。

此时,音频准备开始。我们希望MediaController能够处理所有的回放控制,但是MediaController只能附加到实现了MediaController.MediaPlayerControl接口的对象上。奇怪的是,MediaPlayer本身并没有实现这个接口,所以我们指定 Activity 来做这项工作。该接口包含的七个方法中有六个实际上是由MediaPlayer实现的,所以我们直接调用这些方法。

后期添加:如果您的应用面向 API Level 5 或更高版本,那么在MediaController.MediaPlayerControl接口中有三个额外的方法要实现:

canPause() canSeekBackward() canSeekForward()

这些方法只是告诉系统我们是否希望允许这些操作在这个控件中发生,所以我们的例子为所有三个返回true。如果你的目标是一个较低的 API 级别,这些方法不是必需的(这就是为什么我们没有在它们上面提供@Override注释),但是你可以在以后的版本上运行时实现它们以获得最好的结果。

需要使用MediaController的最后一个方法是getBufferPercentage()。为了获得这些数据,该活动还负责实现MediaPlayer.OnBufferingUpdateListener,它会随着缓冲百分比的变化而更新。

MediaController 的实现有一个技巧。它被设计成一个小部件,在自己的窗口中浮动在一个活动视图之上,一次只能看到几秒钟。因此,我们没有在内容视图的 XML 布局中实例化小部件,而是在代码中实例化。通过调用setAnchorView()在媒体控制器和内容视图之间建立链接,这也决定了控制器在屏幕上的显示位置。在这个例子中,我们将它锚定到根布局对象,因此它将显示在屏幕的底部。如果MediaController锚定到层次结构中的子视图,它将显示在该子视图的旁边。

此外,由于控制器的独立窗口,不得从onCreate()内部调用MediaController.show(),这样做会导致致命的异常。
MediaController设计为默认隐藏,由用户激活。在这个例子中,我们覆盖了活动的onTouchEvent()方法,以便每当用户点击屏幕时显示控制器。除非用参数 0 调用show(),否则它会在该参数标注的时间后淡出。在没有任何参数的情况下调用show()告诉它在默认超时(大约三秒)后淡出。参见图 4–4。

images

图 4–4。 使用媒体控制器的活动

现在,音频回放的所有功能都由标准控制器小部件处理。本例中使用的版本setDataSource()采用了一个 Uri,使得适合于从 ContentProvider 或远程位置加载音频。请记住,所有这些都可以很好地处理本地音频文件和使用备用形式的setDataSource()的资源。

视频播放器

播放视频时,通常需要一整套播放控件来播放、暂停和查找内容。此外,MediaPlayer 必须有一个对 SurfaceHolder 的引用,它可以在该 surface holder 上绘制视频帧。正如我们在前面的例子中提到的,Android 提供 API 来完成所有这些工作,并创建自定义的视频播放体验。然而,在许多情况下,最有效的前进方式是让 SDK 提供的类,即MediaControllerVideoView,来完成所有繁重的工作。

我们来看一个在活动中创建视频播放器的例子。参见清单 4–26。

清单 4–26。 活动播放视频内容

`public class VideoActivity extends Activity {

VideoView videoView;
    MediaController controller;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        videoView = new VideoView(this);

videoView.setVideoURI( Uri.parse("URI_TO_REMOTE_VIDEO") );
        controller = new MediaController(this);
        videoView.setMediaController(controller);
        videoView.start();

setContentView(videoView);
    }

@Override
    public void onDestroy() {
        super.onDestroy();
        videoView.stopPlayback();
    }
}`

此示例将远程视频位置的 URI 传递给 VideoView,并告诉它处理其余部分。VideoView 也可以嵌入到更大的 XML 布局层次结构中,尽管它通常是惟一的东西,并且是全屏显示的,所以在代码中设置为布局树中的惟一视图并不少见。

有了VideoView,和MediaController的交互就简单多了。VideoView实现了MediaController.MediaPlayerControl接口,因此不需要额外的粘合逻辑来使控件起作用。VideoView也在内部处理控制器到自身的锚定,所以它显示在屏幕上适当的位置。

处理重定向

关于使用 MediaPlayer 类处理远程内容,我们还有最后一点要注意。如今,网络上的许多媒体内容服务器并不公开展示视频容器的直接 URL。出于跟踪或安全的目的,公共媒体 URL 通常会在到达真正的媒体内容之前重定向一次或多次。

MediaPlayer 不处理此重定向过程,当显示重定向的 URL 时会返回错误。

如果您无法直接检索要在应用中显示的内容的位置,该应用必须在将 URL 传递给 MediaPlayer 之前跟踪重定向路径。清单 4–27 是一个简单的 AsyncTask 跟踪程序的例子。

清单 4–27。 RedirectTracerTask

`public class RedirectTracerTask extends AsyncTask<Uri, Void, Uri> {

private VideoView mVideo;
    private Uri initialUri;

public RedirectTracerTask(VideoView video) {
        super();
        mVideo = video;
    }

@Override
    protected Uri doInBackground(Uri... params) {
        initialUri = params[0];
        String redirected = null;
        try {
          URL url = new URL(initialUri.toString());
          HttpURLConnection connection = (HttpURLConnection)url.openConnection();
          //Once connected, see where you ended up
          redirected = connection.getHeaderField("Location");

return Uri.parse(redirected);
        } catch (Exception e) {
          e.printStackTrace();
          return null;
        }
    }

@Override
    protected void onPostExecute(Uri result) {
        if(result != null) {
            mVideo.setVideoURI(result);
        } else {
            mVideo.setVideoURI(initialUri);
        }
    }

}`

这个助手类通过从 HTTP 头中检索最终位置来跟踪它。如果提供的 Uri 中没有重定向,后台操作将返回 null,在这种情况下,原始 Uri 将被传递给 VideoView。使用这个助手类,您现在可以将位置传递给视图,如下所示:

`VideoView videoView = new VideoView(this);
RedirectTracerTask task = new RedirectTracerTask(videoView);
Uri location = Uri.parse("URI_TO_REMOTE_VIDEO");

task.execute(location);`

4–9。创建倾斜监视器

问题

您的应用需要来自设备加速度计的反馈,而不仅仅是了解设备是纵向还是横向。

解决方案

(API 三级)

使用SensorManager接收来自加速度传感器的持续反馈。SensorManager提供一个通用抽象接口,用于在 Android 设备上使用传感器硬件。加速度计只是应用可以注册以接收定期更新的众多传感器之一。

它是如何工作的

重要提示:设备传感器,比如加速度计,不存在于模拟器中。如果您无法在 Android 设备上测试SensorManager代码,您将需要使用 SensorSimulator 等工具将传感器事件注入系统。SensorSimulator 要求修改此示例以使用不同的SensorManager接口进行测试;请参阅本章末尾的“有用的工具:传感器模拟器”了解更多信息。

该示例活动向SensorManager注册加速度计更新,并在屏幕上显示数据。原始的 X/Y/Z 数据显示在屏幕底部的TextView中,但此外,设备的“倾斜”通过一个简单的图形在TableLayout中的四个视图中可视化。参见列表 4–28 和 4–29。

注意:我们还建议您将android:screenOrientation=“portrait”android:screenOrientation=“landscape”添加到应用的清单中,以防止活动在您移动和倾斜设备时试图旋转。

清单 4–28。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <TableLayout     android:layout_width="fill_parent"     android:layout_height="fill_parent"     android:stretchColumns="0,1,2">     <TableRow       android:layout_weight="1">       <View         android:id="@+id/top"         android:layout_column="1"       />     </TableRow>     <TableRow       android:layout_weight="1">       <View         android:id="@+id/left"         android:layout_column="0"       />       <View         android:id="@+id/right"         android:layout_column="2"       />     </TableRow>     <TableRow       android:layout_weight="1">       <View         android:id="@+id/bottom"         android:layout_column="1"       />     </TableRow>   </TableLayout>   <TextView       android:id="@+id/values"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:layout_alignParentBottom="true"   /> </RelativeLayout>

清单 4–29。 倾斜监控活动

`public class TiltActivity extends Activity implements SensorEventListener {

private SensorManager mSensorManager;
    private Sensor mAccelerometer;
    private TextView valueView;
    private View mTop, mBottom, mLeft, mRight;

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
        mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

valueView = (TextView)findViewById(R.id.values);
        mTop = findViewById(R.id.top);
        mBottom = findViewById(R.id.bottom);
        mLeft = findViewById(R.id.left);
        mRight = findViewById(R.id.right);
    }

protected void onResume() {
        super.onResume();         mSensorManager.registerListener(this, mAccelerometer,
            SensorManager.SENSOR_DELAY_UI);
    }

protected void onPause() {
        super.onPause();
        mSensorManager.unregisterListener(this);
    }

public void onAccuracyChanged(Sensor sensor, int accuracy) { }

public void onSensorChanged(SensorEvent event) {
        float[] values = event.values;
        float x = values[0]/10;
        float y = values[1]/10;
        int scaleFactor;

if(x > 0) {
            scaleFactor = (int)Math.min(x255, 255);
            mRight.setBackgroundColor(Color.TRANSPARENT);
            mLeft.setBackgroundColor(Color.argb(scaleFactor, 255, 0, 0));
        } else {
            scaleFactor = (int)Math.min(Math.abs(x)
255, 255);
            mRight.setBackgroundColor(Color.argb(scaleFactor, 255, 0, 0));
            mLeft.setBackgroundColor(Color.TRANSPARENT);
        }

if(y > 0) {
            scaleFactor = (int)Math.min(y255, 255);
            mTop.setBackgroundColor(Color.TRANSPARENT);
            mBottom.setBackgroundColor(Color.argb(scaleFactor, 255, 0, 0));
        } else {
            scaleFactor = (int)Math.min(Math.abs(y)
255, 255);
            mTop.setBackgroundColor(Color.argb(scaleFactor, 255, 0, 0));
            mBottom.setBackgroundColor(Color.TRANSPARENT);
        }
        //Display the raw values
        valueView.setText(String.format("X: %11.2f,Y:1.2f, Z: %3$1.2f",
                values[0], values[1], values[2]));
    }
}`

从纵向观看设备屏幕的角度来看,设备加速计上三个轴的方向如下:

  • x:水平轴,正指向右侧
  • y:正向上的垂直轴
  • z:正对着你的垂直轴

当活动对用户可见时(在onResume()onPause()之间),它向SensorManager注册以接收关于加速度计的更新。注册时,registerListener()的最后一个参数定义了更新速率。所选的值SENSOR_DELAY_UI,是接收更新并在每次更新时直接修改用户界面的最快推荐速率。

对于每个新的传感器值,用一个SensorEvent值调用我们注册的监听器的onSensorChanged()方法;该事件包含 X/Y/Z 加速度值。

快速科学笔记:加速度计测量由于施加的力而产生的加速度。当设备处于静止状态时,唯一作用于其上的力是重力(~9.8 米/秒 2 )。每个轴上的输出值是这个力(向下指向地面)和每个方向向量的乘积。当两者平行时,该值将达到最大值(9.8-10)。当两者垂直时,该值将处于最小值(0.0)。因此,平放在桌子上的设备的 X 和 Y 读数都为~0.0,z 读数为~9.8。

示例应用在屏幕底部的 TextView 中显示每个轴的原始加速度值。此外,还有一个由四个View组成的网格,以上/下/左/右的模式排列,我们根据方向按比例调整这个网格的背景颜色。当设备完全平坦时,X 和 Y 都应该接近零,整个屏幕将是黑色的。当设备倾斜时,倾斜位置低侧的方块将开始发出红光,直到设备方向在任何一个位置达到直立时,方块完全变成红色。

提示:试着用其他的比率值修改这个例子,比如SENSOR_DELAY_NORMAL。请注意示例中的更改如何影响更新速率。

此外,您可以摇动设备,并在设备向各个方向加速时看到交替的网格框高亮显示。

4–10。监控指南针方向

问题

您的应用希望通过监控设备的指南针传感器来了解用户面对的主要方向。

解决方案

(API 三级)

再次前来救援。Android 并不完全提供“指南针”传感器,而是包括必要的方法来根据其他传感器数据收集设备指向的位置。在这种情况下,设备的磁场传感器将与加速度计结合使用,以确定用户面对的位置。

然后,我们可以使用getOrientation()向 SensorManager 询问用户相对于地球的方位。

工作原理

重要提示:模拟器中不存在加速度计这样的设备传感器。如果您无法在 Android 设备上测试SensorManager代码,您将需要使用 SensorSimulator 等工具将传感器事件注入系统。SensorSimulator 要求修改此示例以使用不同的SensorManager接口进行测试;请参阅本章末尾的“有用的工具:传感器模拟器”了解更多信息。

与前面的加速度计示例一样,我们使用 SensorManager 注册所有感兴趣的传感器(在本例中有两个)的更新,并在onSensorChanged()中处理结果。此示例从设备摄像头的视角计算并显示用户方向,因为这是增强现实等应用所需要的。参见列表 4–30 和 4–31。

清单 4–30。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <TextView     android:id="@+id/direction"     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:layout_centerInParent="true"     android:textSize="64dip"     android:textStyle="bold"   />   <TextView       android:id="@+id/values"     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:layout_alignParentBottom="true"   /> </RelativeLayout>

清单 4–31。 活动监控用户定位

`public class CompassActivity extends Activity implements SensorEventListener {

private SensorManager mSensorManager;
    private Sensor mAccelerometer, mField;
    private TextView valueView, directionView;

privatefloat[] mGravity;
    privatefloat[] mMagnetic;

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
        mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mField = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

valueView = (TextView)findViewById(R.id.values);
        directionView = (TextView)findViewById(R.id.direction);
    }

protected void onResume() {
        super.onResume();
        mSensorManager.registerListener(this, mAccelerometer,             SensorManager.SENSOR_DELAY_UI);
        mSensorManager.registerListener(this, mField, SensorManager.SENSOR_DELAY_UI);
    }

protected void onPause() {
        super.onPause();
        mSensorManager.unregisterListener(this);
    }

privatevoid updateDirection() {
        float[] temp = newfloat[9];
        float[] R = newfloat[9];
        //Load rotation matrix into R
        SensorManager.getRotationMatrix(temp, null, mGravity, mMagnetic);
        //Map to camera's point-of-view
        SensorManager.remapCoordinateSystem(temp, SensorManager.AXIS_X,             SensorManager.AXIS_Z, R);
        //Return the orientation values
        float[] values = newfloat[3];
        SensorManager.getOrientation(R, values);
        //Convert to degrees
        for (int i=0; i < values.length; i++) {
            Double degrees = (values[i] * 180) / Math.PI;
            values[i] = degrees.floatValue();
        }
        //Display the compass direction
        directionView.setText( getDirectionFromDegrees(values[0]) );
        //Display the raw values
        valueView.setText(String.format("Azimuth: %11.2f,Pitch:1.2f, Roll: %3$1.2f",
                values[0], values[1], values[2]));
    }

private String getDirectionFromDegrees(float degrees) {
        if(degrees >= -22.5 && degrees < 22.5) { return "N"; }
        if(degrees >= 22.5 && degrees < 67.5) { return "NE"; }
        if(degrees >= 67.5 && degrees < 112.5) { return "E"; }
        if(degrees >= 112.5 && degrees < 157.5) { return "SE"; }
        if(degrees >= 157.5 || degrees < -157.5) { return "S"; }
        if(degrees >= -157.5 && degrees < -112.5) { return "SW"; }
        if(degrees >= -112.5 && degrees < -67.5) { return "W"; }
        if(degrees >= -67.5 && degrees < -22.5) { return "NW"; }

return null;
    }

public void onAccuracyChanged(Sensor sensor, int accuracy) { }     public void onSensorChanged(SensorEvent event) {
        switch(event.sensor.getType()) {
        case Sensor.TYPE_ACCELEROMETER:
            mGravity = event.values.clone();
            break;
        case Sensor.TYPE_MAGNETIC_FIELD:
            mMagnetic = event.values.clone();
            break;
        default:
            return;
        }

if(mGravity != null&& mMagnetic != null) {
            updateDirection();
        }
    }
}`

本示例活动在屏幕底部实时显示传感器计算返回的三个原始值。此外,与用户当前面对的位置相关联的罗盘方向被转换并显示在舞台中央。当从传感器接收到更新时,维护来自每个传感器的最新值的本地副本。一旦我们从两个感兴趣的传感器收到至少一个读数,我们就允许 UI 开始更新。

所有繁重的工作都在这里进行。
SensorManager.getOrientation()提供了我们需要的输出信息显示方向。该方法不返回任何数据,而是传入一个空的浮点数组供该方法填充三个角度值,它们表示(按顺序):

  • 方位角
    • 绕直接指向地球的轴的旋转角度。
    • 这是该示例的感兴趣的值。
    • 绕指向西方的轴旋转的角度。
    • 绕磁北极旋转的角度和指向磁北极的轴。

传递给getOrientation()的参数之一是一个表示旋转矩阵的浮点数组。旋转矩阵是设备的当前坐标系如何定向的表示,因此该方法可以基于其参考坐标提供适当的旋转角度。使用getRotationMatrix()获得设备方向的旋转矩阵,该矩阵将来自加速度计和磁场传感器的最新值作为输入。和getOrientation()一样,它也返回 void 长度为 9 或 16 的空浮点数组(表示 3×3 或 4×4 的矩阵)必须作为第一个参数传入,以便该方法填充。

最后,我们希望方向计算的输出特定于摄像机的视角。为了进一步转换获得的旋转,我们使用remapCoordinateSystem()方法。该方法接受四个参数(按顺序):

  1. 表示要转换的矩阵的输入数组
  2. 如何相对于世界坐标转换设备的 X 轴
  3. 如何相对于世界坐标转换设备的 Y 轴
  4. 用于填充结果的空数组

在我们的示例中,我们希望 X 轴保持不变,因此我们将 X 映射到 X。但是,我们希望将设备的 Y 轴(垂直轴)与世界的 Z 轴(指向地球的轴)对齐。这将使我们接收到的旋转矩阵定向,以匹配垂直拿着的设备,就好像用户正在使用相机并在屏幕上观看预览一样。

计算出角度数据后,我们进行一些数据转换,并将结果显示在屏幕上。getOrientation()的单位输出是弧度,所以在显示之前我们首先要把每个结果转换成度数。此外,我们需要将方位值转换为罗盘方向;getDirectionFromDegrees()是一个助手方法,根据当前读数所在的范围返回正确的方向。顺时针旋转一整圈,从北到南的方位角读数为 0 到 180 度。继续绕着圆圈,方位角将从南到北旋转-180 到 0 度。

需要了解的有用工具:传感器模拟器

谷歌的 Android 模拟器不支持传感器,因为大多数计算机没有指南针、加速度计,甚至没有模拟器可以利用的光传感器。虽然这种限制对于需要与传感器交互的应用来说是有问题的,并且模拟器是唯一可行的测试选项,但它可以通过使用传感器模拟器来克服。

传感器模拟器 ( [code.google.com/p/openintents/wiki/SensorSimulator](http://code.google.com/p/openintents/wiki/SensorSimulator))是一个开源工具,让你模拟传感器数据,并使这些数据可用于你的应用进行测试。目前支持加速度计、磁场(指南针)、方位、温度、条码阅读器传感器;这些传感器的行为可以通过各种配置设置来定制。

注意: Sensor Simulator 是由 OpenIntents ( [code.google.com/p/openintents/wiki/OpenIntents](http://code.google.com/p/openintents/wiki/OpenIntents))向 Android 开发者提供的几个项目之一,这是一个由谷歌托管的为 Android 平台创建可重用组件和工具的项目。

获取传感器模拟器

传感器模拟器分布在一个单独的 ZIP 存档中。将浏览器指向[code.google.com/p/openintents/downloads/list?q=sensorsimulator](http://code.google.com/p/openintents/downloads/list?q=sensorsimulator),点击sensorsimulator-1.1.0-rc1.zip链接,然后点击下一页的sensorsimulator-1.1.0-rc1.zip链接,下载这个 284Kb 的文件。

解压缩这个归档文件后,您会发现一个包含以下子目录的sensorsimulator-1.1.0-rc1主目录:

  • bin : 包含sensorsimulator-1.1.0-rc1.jar(让您生成测试数据的传感器模拟器独立 Java 应用)和SensorSimulatorSettings-1.1.0-rc1.apk(设置默认 IP 地址/端口设置并测试传感器模拟器 Java 应用连接的 Android 应用)可执行文件以及这些可执行文件的自述文件。
  • lib :包含sensorsimulator-lib-1.1.0-rc1.jar库,您的 Android 应用使用该库从传感器模拟器 Java 应用访问传感器设置。
  • release : 包含 Apache Ant 构建脚本来组装sensorsimulator-1.1.0-rc1.zip版本。
  • samples : 包含一个关于如何从 Android 应用访问传感器模拟器的SensorDemo Android 应用示例。
  • SensorSimulator : 包含传感器模拟器 Java 应用的源代码。
  • SensorSimulatorSettings : 包含传感器模拟器设置 Android 应用的源代码和用于构建其 APK 和库文件的项目设置。

启动传感器模拟器设置和传感器模拟器

既然您已经下载并解压缩了 Sensor Simulator 发行版,那么您需要启动这个软件。完成以下步骤来完成此任务:

  1. 启动 Android 模拟器,如果还没有运行;比如在命令行执行emulator -avdtest_AVD。这个例子假设你已经在第一章中创建了test_AVD
  2. 在模拟器上安装SensorSimulatorSettings-1.1.0-rc1.apk;比如执行adb install SensorSimulatorSettings-1.1.0-rc1.apk。这个例子假设通过您的PATH环境变量可以访问adb工具,并且bin目录是最新的。当 APK 成功安装在模拟器上时,它会输出一条成功消息。
  3. 点击应用启动器屏幕的传感器模拟器图标,启动传感器模拟器应用。
  4. 启动bin目录的传感器模拟器 Java 应用,它位于sensorsimulator-1.1.0-rc1.jar中。例如,在 Windows 下,双击该文件名。

图 4–5 显示了模拟器的应用启动器屏幕,其中传感器模拟器图标高亮显示。

images

图 4–5。 传感器模拟器图标在应用启动器屏幕上高亮显示。

单击传感器模拟器图标。图 4–6 显示了分为两个活动的传感器模拟器设置屏幕:设置和测试。

images

图 4–6。 默认设置活动提示为 IP 地址和套接字端口。

设置活动提示您输入传感器模拟器 Java 应用的 IP 地址和套接字端口号,其用户界面显示在图 4–7 中。

images

图 4–7。 使用传感器模拟器应用的用户界面将传感器数据发送到传感器模拟器设置和您自己的应用。

Sensor Simulator 提供了一个选项卡式用户界面,每个选项卡都允许您将测试数据发送到不同的仿真器实例。目前,只有一个默认的传感器模拟器选项卡,但您可以添加更多的选项卡,并通过从文件菜单中选择新建选项卡和关闭选项卡菜单项来删除它们。

每个选项卡分为三个窗格:

  • 左侧窗格显示设备的图形,该图形显示了设备的方向和位置。它还允许您选择套接字端口和 Telnet 套接字端口,显示连接信息,并且(默认情况下)仅显示加速度计、磁场和方向传感器数据。
  • 中间窗格允许您调整设备的偏航、俯仰和滚动,选择支持哪些传感器,启用合适的传感器进行测试,并选择其他传感器数据(如选择当前温度值)以及传感器数据发送到仿真器的频率。
  • 右侧窗格允许您通过 Telnet 与模拟器实例通信。您可以交流电池状态(例如电池是否存在以及电池的健康状况——是否过热?)连同 GPS 数据一起发送到模拟器实例。

左侧窗格显示要在设置活动的 IP 地址文本字段中输入的 IP 地址(本例中为 192.168.100.100)。因为 Sensor Simulator 使用的端口号(8010)与 Settings 活动的 Socket textfield 中显示的端口号相同,所以您不需要更改该字段的值。

注意:如果 8010 正被您计算机上运行的其他应用使用,您可能需要更改设置活动的套接字文本字段和传感器模拟器的套接字文本字段中的端口号。

在设置活动的 IP 地址字段中输入该 IP 地址后(参见图 4–6,点击测试选项卡选择测试活动。图 4–8 显示了结果。

images

图 4–8。 点击连接,连接到传感器模拟器 app,开始接收测试数据。

根据此屏幕,您必须单击 Connect 按钮来建立与 Sensor Simulator Java 应用的连接,该应用此时必须正在运行。(您稍后可以单击“断开”来断开连接。)

点按“连接”后,“测试”标签会显示加速计、磁场和方向复选框,其下方带有标签以显示测试值。它不显示温度和条形码读取器的复选框,因为这些传感器既不被支持也不被启用(参见传感器模拟器应用的中间面板)。

选中 acclerometer 复选框,如图 4–9 所示,复选框下方的标签显示从传感器模拟器获得的当前偏航、俯仰和横滚值。

images

图 4–9。 传感器模拟器设置应用正在从传感器模拟器应用接收加速度计数据。

从您的应用访问传感器模拟器

虽然传感器模拟器设置可以帮助您学习如何使用传感器模拟器将测试数据发送到应用,但它不能替代您自己的应用。在某种程度上,您会希望将代码合并到访问该工具的活动中。Google 为修改您的应用以访问 Sensor Simulator 提供了以下指南:

  1. lib目录的 JAR 文件(例如sensorsimulator-lib-1.1.0-rc1.jar)添加到您的项目中。

  2. 将该库中的以下传感器模拟器类型导入源代码:import org.openintents.sensorsimulator.hardware.Sensor; import org.openintents.sensorsimulator.hardware.SensorEvent; import org.openintents.sensorsimulator.hardware.SensorEventListener; import org.openintents.sensorsimulator.hardware.SensorManagerSimulator;

  3. 用等效的SensorManagerSimulator.getSystemService()方法调用替换活动的onCreate()方法的现有SensorManager.getSystemService()方法调用。例如,你可以用mSensorManager = SensorManagerSimulator.getSystemService(this, SENSOR_SERVICE);代替mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);

  4. 例如,使用之前通过SensorSimulatorSettings : mSensorManager.connectSimulator();设置的设置连接到传感器模拟器 Java 应用。

  5. 所有其他代码保持不变。但是,记得在onResume()中注册传感器,在onStop() : @Override protected void onResume() {     super.onResume();     mSensorManager.registerListener(this,         mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),         SensorManager.SENSOR_DELAY_FASTEST);     mSensorManager.registerListener(this,         mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),         SensorManager.SENSOR_DELAY_FASTEST);     mSensorManager.registerListener(this,         mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION),         SensorManager.SENSOR_DELAY_FASTEST);     mSensorManager.registerListener(this,         mSensorManager.getDefaultSensor(Sensor.TYPE_TEMPERATURE),         SensorManager.SENSOR_DELAY_FASTEST); } @Override protected void onStop() {     mSensorManager.unregisterListener(this);     super.onStop(); }中取消注册

  6. 最后,您必须实现SensorEventListener接口:`class MySensorActivity extends Activity implements SensorEventListener
    {
       public void onAccuracyChanged(Sensor sensor, int accuracy)
       {
       }

    public void onSensorChanged(SensorEvent event)
       {
          int sensor = event.type;
          float[] values = event.values;
          // do something with the sensor data
       }
    }`

注意: OpenIntents 的SensorManagerSimulator类是从 Android 的SensorManager类派生出来的,实现的功能和SensorManager完全一样。对于回调,新的SensorEventListener界面已经实现,类似于标准的 Android SensorEventListener界面。

每当您没有连接到 Sensor Simulator Java 应用时,您将获得真实的设备传感器数据:org.openintents.hardware.SensorManagerSimulator类透明地调用由系统服务返回的SensorManager实例来实现这一点。

总结

这些秘籍展示了如何使用 Android 来使用地图、用户位置和设备传感器数据,将用户周围的信息集成到您的应用中。我们还讨论了如何利用设备的摄像头和麦克风,允许用户捕捉,有时解释他们周围的事情。最后,通过使用媒体 API,您学习了如何获取媒体内容,无论是用户在本地捕获的还是从 Web 上远程下载的,并在您的应用中回放这些内容。在下一章,我们将讨论如何使用 Android 的许多持久性技术来存储设备上的非易失性数据。

五、持久化数据

即使在将尽可能多的用户数据转移到云中的宏伟架构中,移动应用的短暂性也总是要求至少一些用户数据在设备上本地持久存储。这些数据可能包括来自保证离线访问的 web 服务的缓存响应,也可能包括用户为特定应用行为设置的首选项。Android 提供了一系列有用的框架来消除使用文件和数据库来保存信息的痛苦。

5–1。制作首选项屏幕

问题

您需要创建一种简单的方法来存储、更改和显示应用中的用户首选项和设置。

解决办法

(API 一级)

使用PreferenceActivity和 XML Preference层次结构一次性提供用户界面、键/值组合和持久性。使用这种方法将创建一个与 Android 设备上的设置应用一致的用户界面,保持用户的体验与他们的期望一致。

在 XML 中,可以定义一个或多个屏幕的完整集合,显示相关的设置,并使用PreferenceScreenPreferenceCategory和相关的Preference元素对其进行分类。然后,活动可以使用很少的代码为用户加载这个层次结构。

它是如何工作的

清单 5–1 和 5–2 提供了一个 Android 应用的基本设置示例。XML 定义了两个屏幕,其中包含这个框架支持的所有常见首选项类型。请注意,一个屏幕嵌套在另一个屏幕中;当用户从根屏幕点击相关列表项时,将显示内部屏幕。

清单 5–1。??【RES/XML/settings . XML】

<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">   <EditTextPreference     android:key="namePref"     android:title="Name"     android:summary="Tell Us Your Name"     android:defaultValue="Apress"   />   <CheckBoxPreference       android:key="morePref"       android:title="Enable More Settings"       android:defaultValue="false"   />   <PreferenceScreen     android:key="moreScreen"     android:title="More Settings"     android:dependency="morePref">     <ListPreference       android:key="colorPref"       android:title="Favorite Color"       android:summary="Choose your favorite color"       android:entries="@array/color_names"       android:entryValues="@array/color_values"       android:defaultValue="GRN"     />     <PreferenceCategory       android:title="Location Settings">       <CheckBoxPreference         android:key="gpsPref"         android:title="Use GPS Location"         android:summary="Use GPS to Find You"         android:defaultValue="true"       />       <CheckBoxPreference         android:key="networkPref"         android:title="Use Network Location"         android:summary="Use Network to Find You"         android:defaultValue="true"       />     </PreferenceCategory>   </PreferenceScreen> </PreferenceScreen>

清单 5–2。RES/values/arrays . XML

<?xml version="1.0" encoding="utf-8"?> <resources>     <string-array name="color_names">       <item>Black</item>       <item>Red</item>       <item>Green</item>     </string-array>
    <string-array name="color_values">       <item>BLK</item>       <item>RED</item>       <item>GRN</item>     </string-array> </resources>

首先注意用于创建 XML 文件的约定。虽然这个资源可以从任何目录(比如 res/layout)展开,但是惯例是将它们放在项目的一个通用目录中,这个目录的名称简单地为“xml”

另外,请注意,我们为每个Preference对象提供了一个android:key属性,而不是android:id。当每个存储的值通过一个SharedPreferences对象在应用的其他地方被引用时,将使用键来访问它。此外,PreferenceActivity还包含了findPreference()方法,用于获取对 Java 代码中一个膨胀的Preference的引用,这比使用findViewById();更高效,而且findPreference()也将键作为参数。

展开后,根首选项屏幕显示一个列表,其中包含以下三个选项(按顺序排列):

  1. 标题为“名称”的项目
    1. EditTextPreference的实例,存储一个字符串值。
    2. 点击此项将显示一个文本框,供用户键入新的首选项值。
  2. 标题为“启用更多设置”的项目,旁边有一个复选框
    1. CheckBoxPreference 的实例,它存储一个布尔值。
    2. 点击此项将切换复选框的选中状态。
  3. 标题为“更多设置”的项目
    1. 点击此项将加载另一个包含更多项目的首选项屏幕。

当用户点击“更多设置”项目时,第二个屏幕显示三个以上的项目:一个ListPreference项目和另外两个由PreferenceCategory组合在一起的CheckBoxPreferencePreferenceCategory是一种在列表中创建分节符和标题的简单方法,用于对实际的首选项进行分组。

ListPreference是示例中使用的最终首选项类型。这个项目需要两个数组参数(尽管它们可以被设置为同一个数组),这两个参数代表用户可以从中选择的一组选项。android:entries数组是要显示的可读项目列表,而android:entryValues数组表示要存储的实际值。

所有偏好项也可以选择性地为它们设置默认值。但是,该值不会自动加载。当显示PreferenceActivity或调用PreferenceManager.setDefaultValues()时,它将第一次加载这个 XML 文件。

现在让我们看看PreferenceActivity将如何加载和管理它。参见清单 5–3。

清单 5–3。 偏好行动中的活动

`public class SettingsActivity extends PreferenceActivity {

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //Load preference data from XML
        addPreferencesFromResource(R.xml.settings);
    }
}`

向用户显示首选项并允许他们进行更改所需要的只是调用addPreferencesFromResource()。不需要用PreferenceActivity来调用setContentView(),因为addPreferencesFromResource()会放大 XML 并显示它。然而,可以提供一个自定义布局,只要它包含一个设置了android:id="@android:id/list"属性的ListView,这是PreferenceActivity将加载首选项的地方。

出于控制访问的唯一目的,也可以将偏好项放在列表中。在本例中,我们将“启用更多设置”项放在列表中,只是为了允许用户启用或禁用对第二个PreferenceScreen的访问。为了实现这一点,我们的嵌套PreferenceScreen包含了android:dependency属性,该属性将其启用状态链接到另一个首选项的状态。每当引用的首选项未设置或为假时,该首选项将被禁用。

当这个活动加载时,您会看到类似于 Figure 5–1 的内容。

images

图 5–1。 首选屏幕在行动

PreferenceScreen(左)首先显示。如果用户点击“更多设置”,将显示第二个屏幕(右侧)。

加载默认值和访问首选项

通常,像这样的PreferenceActivity不是应用的根。通常,如果设置了默认值,在用户访问设置之前,应用的其余部分可能需要访问这些值(第一种情况下将加载默认值)。因此,在应用中的其他地方调用下面的方法会很有帮助,这样可以确保在使用默认值之前将其加载。

PreferenceManager.setDefaultValues(Context context, int resId, boolean readAgain);

这个方法可能会被调用多次,默认值将不会被再次加载。它可以放在主活动中,以便在第一次启动时调用,或者放在一个公共位置,在访问共享首选项之前调用。

使用这种机制存储的首选项被放入默认的共享首选项对象中,可以使用任何Context指针访问该对象

PreferenceManager.getDefaultSharedPreferences(Context context);

一个示例活动将加载我们上一个示例中设置的默认值,并访问一些存储的当前值,看起来类似于清单 5–4。

清单 5–4。 活动加载偏好默认值

`public class HomeActivity extends Activity {

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

//Load the preference defaults
        PreferenceManager.setDefaultValues(this, R.xml.settings, false);
    }

@Override
    public void onResume() {
        super.onResume();
        //Access the current settings
        SharedPreferences settings =
            PreferenceManager.getDefaultSharedPreferences(this);

String name = settings.getString("namePref", "");
         boolean isMoreEnabled = settings.getBoolean("morePref", false);
    }
}`

调用setDefaultValues()将在首选项存储中为 XML 文件中包含android:defaultValue属性的任何项目创建一个值。这将使应用可以访问它们,即使用户尚未访问设置屏幕。

然后可以使用一组类型化的访问器函数在SharedPreferences对象上访问这些值。如果 preference key 的值尚不存在,这些访问器方法中的每一个都需要返回 preference key 的名称和默认值。

5–2。持久化简单数据

问题

您的应用需要一种简单、低开销的方法来将基本数据(如数字和字符串)存储在持久存储中。

解决办法

(API 一级)

使用SharedPreferences对象,应用可以快速创建一个或多个持久存储,数据可以保存在这些存储中,供以后检索。在底层,这些对象实际上作为 XML 文件存储在应用的用户数据区。然而,与直接从文件中读写数据不同,SharedPreferences为持久化基本数据类型提供了一个有效的框架。

创建多个 SharedPreferences(而不是将所有数据都转储到默认对象中)可能是一个好习惯,尤其是当您存储的数据有保质期时。请记住,使用 XML 和PreferenceActivity框架存储的所有首选项也存储在默认位置——如果您想存储一组与登录用户相关的项目,该怎么办?当该用户注销时,您将需要删除随之而来的所有持久化数据。如果您将所有数据存储在默认首选项中,您很可能需要单独删除每个项目。但是,如果您只是为这些设置创建一个首选项对象,那么注销就像调用SharedPreferences.clear()一样简单。

它是如何工作的

让我们看一个使用SharedPreferences持久化简单数据的实际例子。清单 5–5 和 5–6 为用户创建一个数据输入表单,向远程服务器发送一条简单的消息。为了帮助用户,我们将记住他们在每个字段中输入的所有数据,直到发出成功的请求。这将允许用户离开屏幕(或被短信或电话打断),而不必再次输入他们的所有信息。

清单 5–5。 res/layout/form.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">

  <TextView     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Email:"     android:padding="5dip"   />   <EditText     android:id="@+id/email"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:singleLine="true"   />   <CheckBox     android:id="@+id/age"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Are You Over 18?"   />   <TextView       android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Message:"     android:padding="5dip"   />   <EditText     android:id="@+id/message"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:minLines="3"     android:maxLines="3"   />   <Button     android:id="@+id/submit"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Submit"   /> </LinearLayout>

清单 5–6。 有持久性的录入表单

`public class FormActivity extends Activity implements View.OnClickListener {

EditText email, message;
    CheckBox age;
    Button submit;

SharedPreferences formStore;

boolean submitSuccess = false;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.form);

email = (EditText)findViewById(R.id.email);
        message = (EditText)findViewById(R.id.message);`

`        age = (CheckBox)findViewById(R.id.age);

submit = (Button)findViewById(R.id.submit);
        submit.setOnClickListener(this);

//Retrieve or create the preferences object
        formStore = getPreferences(Activity.MODE_PRIVATE);
    }

@Override
    public void onResume() {
        super.onResume();
        //Restore the form data
        email.setText(formStore.getString("email", ""));
        message.setText(formStore.getString("message", ""));
        age.setChecked(formStore.getBoolean("age", false));
    }

@Override
    public void onPause() {
        super.onPause();
        if(submitSuccess) {
            //Editor calls can be chained together
            formStore.edit().clear().commit();
        } else {
            //Store the form data
            SharedPreferences.Editor editor = formStore.edit();
            editor.putString("email", email.getText().toString());
            editor.putString("message", message.getText().toString());
            editor.putBoolean("age", age.isChecked());
            editor.commit();
        }
    }

@Override
    public void onClick(View v) {

//DO SOME WORK SUBMITTING A MESSAGE

//Mark the operation successful
        submitSuccess = true;
        //Close
        finish();
    }
}`

我们从一个典型的用户表单开始,两个简单的EditText输入字段和一个CheckBox。当创建活动时,我们使用Activity.getPreferences()收集一个SharedPreferences对象,这是所有持久化数据将被存储的地方。如果在任何时候活动由于除成功提交之外的原因(由布尔成员控制)而暂停,表单的当前状态将被快速加载到首选项中并持久化。

注意:当使用Editor将数据保存到SharedPreferences中时,务必记住在更改完成后调用commit()apply()。否则,您的更改将不会被保存。

相反,每当活动变得可见时,onResume()用存储在 preferences 对象中的最新信息加载用户界面。如果不存在首选项,因为它们已被清除或从未创建(第一次启动),则表单被设置为空白。

当用户按下 Submit 并且假表单成功提交时,随后对onPause()的调用将清除 preferences 中任何存储的表单数据。因为所有这些操作都是在私有首选项对象上完成的,所以清除数据不会影响可能已使用其他方式存储的任何用户设置。

注意:Editor调用的方法总是返回同一个Editor对象,允许它们在某些地方链接在一起,这样做可以让你的代码更具可读性。

共享共享的首选项

前面的例子说明了在单个活动的上下文中使用单个SharedPreferences对象,该活动具有从Activity.getPreferences()获得的对象。说实话,这个方法实际上只是一个方便的Context.getSharedPreferences()包装器,它将活动名作为首选商店名传递。如果您存储的数据最好在两个或多个 Activity 实例之间共享,那么调用getSharedPreferences()并传递一个更通用的名称可能更有意义,这样就可以很容易地从代码中的不同位置访问它。参见清单 5–7。

清单 5–7。 使用相同偏好的两个活动

`public class ActivityOne extends Activity {
    public static final String PREF_NAME = "myPreferences";
    private SharedPreferences mPreferences;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPreferences = getSharedPreferences(PREF_NAME, Activity.MODE_PRIVATE);
    }
}

public class ActivityTwo extends Activity {

private SharedPreferences mPreferences;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPreferences = getSharedPreferences(ActivityOne.PREF_NAME,
            Activity.MODE_PRIVATE);
    }

}`

在这个例子中,两个活动类使用相同的名称(定义为常量字符串)检索SharedPreferences对象,因此它们将访问相同的偏好数据集。此外,两个引用甚至指向首选项的同一个实例,因为框架为每组SharedPreferences(一组由其名称定义)创建了一个单例对象。这意味着在一方所做的更改会立即反映到另一方。

A Note About Mode

Context.getSharedPreferences()也需要一个模式参数。传递 0 或MODE_PRIVATE提供了默认行为,只允许创建首选项的应用(或另一个具有相同用户 ID 的应用)获得读写访问权。此方法支持两个以上的模式参数;MODE_WORLD_READABLEMODE_WORLD_WRITEABLE。这些模式允许其他应用通过对其创建的文件设置适当的用户权限来访问这些首选项。但是,外部应用仍然需要一个有效的上下文,指向创建首选项文件的包。

例如,假设您用包com.examples.myfirstapplication在应用中创建了SharedPreferences with world readable permission。为了从第二个应用访问这些首选项,第二个应用将使用以下代码获取它们:

Context otherContext = createPackageContext("com.examples.myfirstapplication", 0); SharedPreferences externalPreferences = otherContext.getSharedPreferences(PREF_NAME, 0);

注意:如果您选择使用 mode 参数来允许外部访问,请确保您在调用getSharedPreferences()时提供的模式是一致的。该模式仅在第一次创建首选项文件时使用,因此在不同时间使用不同的模式参数调用SharedPreferences只会导致混淆。

5–3 岁。读写文件

问题

您的应用需要从外部文件读入数据,或者写出更复杂的数据以实现持久性。

解决办法

(API 一级)

有时,使用文件系统是无可替代的。Android 支持所有用于创建、读取、更新和删除(CRUD)操作的标准 Java 文件 I/O,以及一些额外的助手,使访问特定位置的文件更加方便。应用可以在三个主要位置处理文件:

  • 内存储器
    • 受保护的目录空间来读写文件数据。
  • 外部存储器
    • 用于读写文件数据的外部可安装空间。
    • 需要 API 级别 4+中的WRITE_EXTERNAL_STORAGE权限。
    • 通常,这是设备中的物理 SD 卡。
  • 素材
    • APK 包中受保护的只读空间。
    • 对不能/不应该编译的本地资源有好处。

虽然处理文件数据的底层机制保持不变,但我们将研究使处理每个目的地略有不同的细节。

它是如何工作的

如前所述,传统的 Java FileInputStreamFileOutputStream类构成了访问文件数据的主要方法。事实上,您可以随时使用绝对路径位置创建一个File实例,并开始传输数据。然而,由于不同设备上的根路径不同,并且某些目录受到应用的保护,我们推荐一些稍微更有效的方法来处理文件。

内部存储

为了创建或修改文件在内存中的位置,请使用Context.openFileInput()Context.openFileOutput()方法。这些方法只需要文件名作为参数,而不是整个路径,并且将引用与应用的受保护目录空间相关的文件,而不考虑特定设备上的确切路径。参见清单 5–8。

清单 5–8。 在内存上 CRUD 一个文件

`public class InternalActivity extends Activity {

private static final String FILENAME = "data.txt";

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView tv = new TextView(this);
        setContentView(tv);

//Create a new file and write some data`

`        try {
            FileOutputStream mOutput = openFileOutput(FILENAME, Activity.MODE_PRIVATE);
            String data = "THIS DATA WRITTEN TO A FILE";
            mOutput.write(data.getBytes());
            mOutput.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

//Read the created file and display to the screen
        try {
            FileInputStream mInput = openFileInput(FILENAME);
            byte[] data = newbyte[128];
            mInput.read(data);
            mInput.close();

String display = new String(data);
            tv.setText(display.trim());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

//Delete the created file
        deleteFile(FILENAME);    
    }
}`

这个例子使用Context.openFileOutput()将一些简单的字符串数据写到一个文件中。使用此方法时,如果文件不存在,将会创建该文件。它有两个参数,一个文件名和一个操作模式。在这种情况下,我们通过将模式定义为MODE_PRIVATE来使用默认操作。这种模式会用每个新的写操作覆盖文件;如果你喜欢每次写在现有文件的末尾,使用MODE_APPEND

写操作完成后,该示例使用Context.openFileInput()打开 InputStream 并读取文件数据,它只需要再次将文件名作为参数。数据被读入一个字节数组,并通过 TextView 显示给用户界面。完成操作后,Context.deleteFile()用于从存储器中删除文件。

注意:数据以字节的形式写入文件流,因此更高级别的数据(甚至字符串)必须转换成这种格式或从这种格式转换出来。

这个例子没有留下文件的痕迹,但是我们鼓励你尝试同样的例子,不要在最后运行deleteFile()来保存文件。将 DDMS 与仿真器或解锁的设备一起使用,您可以查看文件系统,并可以在相应的应用数据文件夹中找到该应用创建的文件。

因为这些方法是Context的一部分,并不绑定到 Activity,所以这种类型的文件访问可以发生在应用中你需要的任何地方,比如一个BroadcastReceiver或者甚至一个定制类。许多系统构造要么是Context的子类,要么在它们的回调中传递对它的引用。这允许在任何地方进行相同的打开/关闭/删除操作。

外部存储

内部存储和外部存储的主要区别在于外部存储是可装载的。这意味着用户可以将他们的设备连接到计算机,并可以选择将外部存储作为可移动磁盘安装在 PC 上。通常,存储本身是物理可移动的(如 SD 卡),但这不是平台的要求。

重要提示:写入设备的外部存储将需要您向应用清单添加一个android.permission.WRITE_EXTERNAL_STORAGE声明。

在外部安装或物理移除设备的外部存储器期间,应用无法访问该存储器。因此,通过检查Environment.getExternalStorageState()来检查外部存储器是否准备好总是谨慎的。

让我们修改文件示例,对设备的外部存储进行同样的操作。参见清单 5–9。

清单 5–9。 在外部存储器上创建一个文件

`public class ExternalActivity extends Activity {

private static final String FILENAME = "data.txt";

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView tv = new TextView(this);
        setContentView(tv);

//Create the file reference
        File dataFile = new File(Environment.getExternalStorageDirectory(), FILENAME);

//Check if external storage is usable
        if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            Toast.makeText(this, "Cannot use storage.", Toast.LENGTH_SHORT).show();
            finish();
            return;
        }

//Create a new file and write some data
        try {
            FileOutputStream mOutput = new FileOutputStream(dataFile, false);
            String data = "THIS DATA WRITTEN TO A FILE";
            mOutput.write(data.getBytes());             mOutput.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

//Read the created file and display to the screen
        try {
            FileInputStream mInput = new FileInputStream(dataFile);
            byte[] data = newbyte[128];
            mInput.read(data);
            mInput.close();

String display = new String(data);
            tv.setText(display.trim());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

//Delete the created file
        dataFile.delete();
    }
}`

对于外部存储,我们利用了更多的传统 Java 文件 I/O。使用外部存储的关键是调用Environment.getExternalStorageDirectory()来检索设备外部存储位置的根路径。

在进行任何操作之前,首先用Environment.getExternalStorageState()检查设备外部存储器的状态。如果返回值是除了Environment.MEDIA_MOUNTED之外的任何值,我们将不会继续,因为存储不能被写入,所以活动被关闭。否则,可以创建新文件并开始操作。

输入和输出流现在必须使用默认的 Java 构造函数,而不是使用Context方便的方法。输出流的默认行为是覆盖当前文件,如果当前文件不存在,则创建它。如果您的应用每次写入都必须追加到现有文件的末尾,请将FileOutputStream构造函数中的布尔参数更改为 true。

通常,在外部存储器上为应用文件创建一个特殊的目录是有意义的。我们可以简单地使用更多的 Java 文件 API 来实现这一点。参见清单 5–10。

清单 5–10。 在新目录下 CRUD 一个文件

`public class ExternalActivity extends Activity {

private static final String FILENAME = "data.txt";
    private static final String DNAME = "myfiles";

@Override
    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);
        TextView tv = new TextView(this);
        setContentView(tv);

//Create a new directory on external storage
        File rootPath = new File(Environment.getExternalStorageDirectory(), DNAME);
        if(!rootPath.exists()) {
            rootPath.mkdirs();
        }
        //Create the file reference
        File dataFile = new File(rootPath, FILENAME);

//Create a new file and write some data
        try {
            FileOutputStream mOutput = new FileOutputStream(dataFile, false);
            String data = "THIS DATA WRITTEN TO A FILE";
            mOutput.write(data.getBytes());
            mOutput.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

//Read the created file and display to the screen
        try {
            FileInputStream mInput = new FileInputStream(dataFile);
            byte[] data = newbyte[128];
            mInput.read(data);
            mInput.close();

String display = new String(data);
            tv.setText(display.trim());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

//Delete the created file
        dataFile.delete();
    }
}`

在本例中,我们在外部存储目录中创建新的目录路径,并将该新位置用作数据文件的根位置。一旦使用新的目录位置创建了文件引用,示例的其余部分是相同的。

5–4 岁。将文件用作资源

问题

您的应用必须利用 Android 无法编译成资源 id 的格式的资源文件。

解决方案

(API 一级)

使用 Assets 目录存放应用需要读取的文件,例如本地 HTML、CSV 或专有数据。素材目录是 Android 应用中文件的受保护资源位置。放置在此目录中的文件将与最终的 APK 捆绑在一起,但不会被处理或编译。像所有其他应用资源一样,素材中的文件是只读的。

它是如何工作的

我们在本书中已经看到了一些具体的例子,可以使用素材将内容直接加载到小部件中,比如WebViewMediaPlayer。然而,在大多数情况下,最好使用传统的InputStream来访问素材。清单 5–11 和 5–12 提供了一个从素材中读取私有逗号分隔值(CSV)文件并显示在屏幕上的示例。

清单 5–11。 素材/数据. csv

John,38,Red Sally,42,Blue Rudy,31,Yellow

清单 5–12。 从素材文件中读取

`public class AssetActivity extends Activity {

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView tv = new TextView(this);
        setContentView(tv);

try {
            //Access application assets
            AssetManager manager = getAssets();
            //Open our data file
            InputStream mInput = manager.open("data.csv");
            //Read data in
            byte[] data = newbyte[128];
            mInput.read(data);
            mInput.close();

//Parse the CSV data and display
            String raw = new String(data);
            ArrayList cooked = parse(raw.trim());
            StringBuilder builder = new StringBuilder();
            for(Person piece : cooked) {
              builder.append(String.format("%s is %s years old, and likes the color %s",
                        piece.name, piece.age, piece.color));
              builder.append('\n');
            }
            tv.setText(builder.toString());`

`        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

}

/* Simple CSV Parser */
    private static finalintCOL_NAME = 0;
    private static finalintCOL_AGE = 1;
    private static finalintCOL_COLOR = 2;

private ArrayList parse(String raw) {
        ArrayList results = new ArrayList();
        Person current = null;

StringTokenizer st = new StringTokenizer(raw,",\n");
        int state = COL_NAME;
        while(st.hasMoreTokens()) {
            switch(state) {
            case COL_NAME:
                current = new Person();
                current.name = st.nextToken();
                state = COL_AGE;
                break;
            case COL_AGE:
                current.age = st.nextToken();
                state = COL_COLOR;
                break;
            case COL_COLOR:
                current.color = st.nextToken();
                results.add(current);
                state = COL_NAME;
                break;
            }
        }

return results;
    }

privateclass Person {
        public String name;
        public String age;
        public String color;

public Person() { }
    }
}`

访问 Assets 中的文件的关键在于使用AssetManager,这将允许应用打开当前驻留在 Assets 目录中的任何资源。将我们感兴趣的文件名传递给AssetManager.open()会返回一个 InputStream 供我们读取文件数据。将流读入内存后,该示例将原始数据传递给解析例程,并在用户界面上显示结果。

解析 CSV

这个例子还展示了一个简单的方法,从一个 CSV 文件中获取数据,并将其解析成一个模型对象(在本例中称为Person)。这里使用的方法将整个文件读入一个字节数组,作为单个字符串进行处理。当要读取的数据量非常大时,这种方法不是最有效的内存方法,但是对于像这样的小文件,这种方法就可以了。

原始字符串被传递到 StringTokenizer 实例中,同时传递的还有用作标记断点的必需字符:逗号和换行符。此时,可以按顺序处理文件的每个单独的块。使用基本的状态机方法,来自每一行的数据被插入到新的Person实例中,并被加载到结果列表中。

5–5 岁。管理数据库

问题

您的应用需要持久化数据,这些数据可以作为子集或单个记录进行查询或修改。

解决办法

(API 一级)

在一个SQLiteOpenHelper的帮助下创建一个SQLiteDatabase来管理您的数据存储。SQLite 是一种快速、轻量级的数据库技术,它利用 SQL 语法来构建查询和管理数据。对 SQLite 的支持内置于 Android SDK 中,这使得在应用中设置和使用 SQLite 变得非常容易。

它是如何工作的

定制SQLiteOpenHelper允许您管理数据库模式本身的创建和修改。它也是一个很好的地方,可以在创建数据库时将您可能需要的任何初始值或默认值插入到数据库中。清单 5–13 是一个定制助手的例子,它创建一个数据库,用一个表来存储关于人的基本信息。

清单 5–13。 自定义 SQLiteOpenHelper

`public class MyDbHelper extends SQLiteOpenHelper {

private static final String DB_NAME = "mydb";
    private static final int DB_VERSION = 1;

public static final String TABLE_NAME = "people";
    public static final String COL_NAME = "pName";
    public static final String COL_DATE = "pDate";
    private static final String STRING_CREATE =`

`        "CREATE TABLE "+TABLE_NAME+" (_id INTEGER PRIMARY KEY AUTOINCREMENT, "
        +COL_NAME+" TEXT, "+COL_DATE+" DATE);";

public MyDbHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

@Override
    public void onCreate(SQLiteDatabase db) {
        //Create the database table
        db.execSQL(STRING_CREATE);

//You may also load initial values into the database here
        ContentValues cv = new ContentValues(2);
        cv.put(COL_NAME, "John Doe");
        //Create a formatter for SQL date format
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        cv.put(COL_DATE, dateFormat.format(new Date())); //Insert 'now' as the date
        db.insert(TABLE_NAME, null, cv);
    }

@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        //For now, clear the database and re-create
        db.execSQL("DROP TABLE IF EXISTS "+TABLE_NAME);
        onCreate(db);
    }
}`

数据库需要的关键信息是名称和版本号。创建和升级 SQLiteDatabase 确实需要一点 SQL 知识,所以如果您不熟悉一些语法,我们建议您浏览一下 SQL 参考资料。助手将在任何时候访问这个特定的数据库时调用onCreate(),使用SQLiteOpenHelper.getReadableDatabase()SQLiteOpenHelper.getWritableDatabase(),如果它还不存在的话。

该示例将表名和列名抽象为常量以供外部使用(这是一个很好的习惯)。下面是实际的 SQL 创建字符串,它在onCreate()中用来创建我们的表:

CREATE TABLE people (_id INTEGER PRIMARY KEY AUTOINCREMENT, pName TEXT, pAge INTEGER, pDate DATE);

在 Android 中使用 SQLite 时,数据库必须进行少量的格式化,以便与框架一起正常工作。它的大部分是为您创建的,但是您创建的表必须有一部分是用于_id的列。该字符串的其余部分为表中的每条记录再创建两列:

  • 人员姓名的文本字段
  • 输入此记录的日期的日期字段

使用ContentValues对象将数据插入数据库。该示例说明了如何在创建数据库时使用ContentValues向数据库中插入一些默认数据。SQLiteDatabase.insert()采用表名、空列 hack 和表示要插入的记录的ContentValues作为参数。

这里没有使用空列黑客,但是它有一个可能对您的应用至关重要的目的。SQL 不能将一个完全为空的值插入到数据库中,尝试这样做将会导致错误。如果您的实现有可能将一个空的ContentValues传递给insert(),则使用空列 hack 来插入一个记录,其中引用列的值为空。

关于升级的说明

SQLiteOpenHelper也很好地帮助您在应用的未来版本中迁移数据库模式。每当数据库被访问,但是磁盘上的版本与当前版本不匹配(意味着构造函数中传递的版本),就会调用onUpgrade()

在我们的例子中,我们采用了懒人的方式,简单地删除现有的数据库并重新创建它。实际上,如果数据库包含用户输入的数据,这可能不是合适的方法;他们可能不会太高兴看到它消失。所以让我们暂时离题,看一个可能更有用的onUpgrade()的例子。例如,在应用的整个生命周期中使用以下三个数据库:

  • 版本 1:应用的首次发布
  • 版本 2:应用升级,包括电话号码字段
  • 版本 3:应用升级,包括插入的日期条目

我们可以利用onUpgrade()来改变现有的数据库,而不是删除当前所有的信息。参见清单 5–14。

清单 5–14。on upgrade()的样本

@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {     //Upgrade from v1\. Adding phone number     if(oldVersion<= 1) {         db.execSQL("ALTER TABLE "+TABLE_NAME+" ADD COLUMN phone_number INTEGER;");     }     //Upgrade from v2\. Add entry date     if(oldVersion <= 2) {         db.execSQL("ALTER TABLE "+TABLE_NAME+" ADD COLUMN entry_date DATE;");     } }

在这个例子中,如果用户的现有数据库版本是 1,那么这两个语句都将被调用来向数据库添加列。如果他们已经有了版本 2,那么只需要调用后面的语句来添加条目日期列。在这两种情况下,应用数据库中的任何现有数据都将被保留。

使用数据库

回到我们最初的示例,让我们看看一个活动如何利用我们创建的数据库。参见清单 5–15 和清单 5–16。

清单 5–15。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <EditText     android:id="@+id/name"     android:layout_width="fill_parent"     android:layout_height="wrap_content"   />   <Button     android:id="@+id/add"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Add New Person"   />   <ListView     android:id="@+id/list"     android:layout_width="fill_parent"     android:layout_height="fill_parent"   /> </LinearLayout>

清单 5–16。 查看和管理活动数据库

`public class DbActivity extends Activity implements View.OnClickListener,
        AdapterView.OnItemClickListener {

EditText mText;
    Button mAdd;
    ListView mList;

MyDbHelper mHelper;
    SQLiteDatabase mDb;
    Cursor mCursor;
    SimpleCursorAdapter mAdapter;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

mText = (EditText)findViewById(R.id.name);
        mAdd = (Button)findViewById(R.id.add);
        mAdd.setOnClickListener(this);
        mList = (ListView)findViewById(R.id.list);
        mList.setOnItemClickListener(this);

mHelper = new MyDbHelper(this);
    }

@Override
    public void onResume() {
        super.onResume();
        //Open connections to the database
        mDb = mHelper.getWritableDatabase();         String[] columns = new String[] {"_id", MyDbHelper.COL_NAME, MyDbHelper.COL_DATE};
        mCursor = mDb.query(MyDbHelper.TABLE_NAME, columns, null, null, null, null, null);
        //Refresh the list
        String[] headers = new String[] {MyDbHelper.COL_NAME, MyDbHelper.COL_DATE};
        mAdapter = new SimpleCursorAdapter(this, android.R.layout.two_line_list_item,
                mCursor, headers, newint[]{android.R.id.text1, android.R.id.text2});
        mList.setAdapter(mAdapter);
    }

@Override
    public void onPause() {
        super.onPause();
        //Close all connections
        mDb.close();
        mCursor.close();
    }

@Override
    public void onClick(View v) {
        //Add a new value to the database
        ContentValues cv = new ContentValues(2);
        cv.put(MyDbHelper.COL_NAME, mText.getText().toString());
        //Create a formatter for SQL date format
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        cv.put(MyDbHelper.COL_DATE, dateFormat.format(new Date())); //Insert 'now' as the date
        mDb.insert(MyDbHelper.TABLE_NAME, null, cv);
        //Refresh the list
        mCursor.requery();
        mAdapter.notifyDataSetChanged();
        //Clear the edit field
        mText.setText(null);
    }

@Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        //Delete the item from the database
        mCursor.moveToPosition(position);
         //Get the id value of this row
        String rowId = mCursor.getString(0); //Column 0 of the cursor is the id
        mDb.delete(MyDbHelper.TABLE_NAME, "_id = ?", new String[]{rowId});
        //Refresh the list
        mCursor.requery();
        mAdapter.notifyDataSetChanged();
    }
}`

在这个例子中,我们利用我们的客户SQLiteOpenHelper来访问数据库实例,并将数据库中的每条记录作为列表显示给用户界面。来自数据库的信息如果以Cursor的形式返回,这是一个设计用来读取、写入和遍历查询结果的接口。

当活动变得可见时,进行数据库查询以返回“people”表中的所有记录。必须将列名数组传递给查询,以告诉数据库要返回哪些值。query()的其余参数旨在缩小选择数据集,我们将在下一个秘籍中对此进行进一步研究。当不再需要数据库和游标连接时,关闭它们是很重要的。在本例中,我们在onPause()中这样做,此时活动不再处于前台。

SimpleCursorAdapter用于将数据库中的数据映射到标准的 Android 两行列表项目视图中。string 和 int 数组参数构成映射;string 数组中每一项的数据都将插入到视图中,并在 int 数组中显示相应的 id 值。注意,这里传递的列名列表与传递给查询的数组略有不同。这是因为我们将需要知道其他操作的记录 id,但是在将数据映射到用户界面时这是不必要的。

用户可以在文本字段中输入姓名,然后按“添加新人”按钮来创建新的内容值并将其插入到数据库中。此时,为了让 UI 显示更改,我们调用了Cursor.requery()ListAdapter.notifyDataSetChanged()

相反,点击列表中的项目将从数据库中删除该指定项目。为了实现这一点,我们必须构造一个简单的 SQL 语句,告诉数据库只删除 _id 值与该选择匹配的记录。此时,光标和列表适配器再次被刷新。

通过将光标移动到所选位置并调用getString(0)来获得列索引零的值,从而获得选择的 _id 值。此请求返回 _id,因为在列列表中传递给查询的第一个参数(索引 0)是“_id”delete 语句由两个参数组成:语句字符串和参数。对于字符串中出现的每个问号,将在语句中插入传递的数组中的一个参数。

5 至 6 岁。查询数据库

问题

您的应用使用 SQLiteDatabase,您需要返回其中包含的数据的特定子集。

解决办法

(API 一级)

使用完全结构化的 SQL 查询,为特定数据创建过滤器并从数据库返回这些子集非常简单。有几种重载形式的SQLiteDatabase.query()可以从数据库中收集信息。我们将在这里检查其中最冗长的。

public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)

前两个参数简单地定义了查询数据的表,以及我们想要访问的每条记录的列。剩下的参数定义了我们将如何缩小结果的范围。

  • 选择
    • 给定查询的 SQL WHERE 子句。
  • 选择 Args
    • 如果选择了问号,则这些项目会填充这些字段。
  • 群组依据
    • 给定查询的 SQL GROUP BY 子句。
  • 拥有
    • 给定查询的 SQL ORDER BY 子句。
  • 排序依据
    • 给定查询的 SQL ORDER BY 子句。
  • 限制
    • 查询返回的最大结果数。

如您所见,所有这些参数都旨在为数据库查询提供 SQL 的全部功能。

它是如何工作的

让我们来看一些示例查询,可以构造这些查询来完成一些常见的实用查询。

  • 返回值与给定参数匹配的所有行。

String[] COLUMNS = new String[] {COL_NAME, COL_DATE}; String selection = COL_NAME+" = ?"; String[] args = new String[] {"NAME_TO_MATCH"}; Cursor result = db.query(TABLE_NAME, COLUMNS, selection, args, null, null, null, null);

这个查询相当简单。selection 语句只是告诉数据库将 name 列中的任何数据与所提供的参数相匹配(该参数是在“?”位置插入的)在选择字符串中)。

  • 返回插入数据库的最后 10 行。

String orderBy = "_id DESC"; String limit = "10"; Cursor result = db.query(TABLE_NAME, COLUMNS, null, null, null, null, orderBy, limit);

这个查询没有特殊的选择标准,而是告诉数据库按照自动递增的 id 值对结果进行排序,最新的(最高的 id)记录排在第一位。limit 子句将返回结果的最大数量设置为 10。

  • 返回日期字段在指定范围内的行(在本例中为 2000 年)。

String[] COLUMNS = new String[] {COL_NAME, COL_DATE}; String selection = "datetime("+COL_DATE+") > datetime(?)"+         " AND datetime("+COL_DATE+") < datetime(?)"; String[] args = new String[] {"2000-1-1 00:00:00","2000-12-31 23:59:59"}; Cursor result = db.query(TABLE_NAME, COLUMNS, selection, args, null, null, null, null);

SQLite 没有为日期保留特定的数据类型,尽管它们允许在创建表时使用 DATE 作为声明类型。但是,标准的 SQL 日期和时间函数可用于创建文本、整数或实数形式的数据表示。这里,我们比较数据库中的值和该范围的开始和结束日期的格式化字符串的返回值。

  • 返回整数字段在指定范围内(在本例中为 7 到 10)的行。

String[] COLUMNS = new String[] {COL_NAME, COL_AGE}; String selection = COL_AGE+"> ? AND "+COL_AGE+"< ?"; String[] args = new String[] {"7","10"}; Cursor result = db.query(TABLE_NAME, COLUMNS, selection, args, null, null, null, null);

这与上一个示例类似,但没有那么冗长。这里,我们只需创建选择语句来返回大于下限但小于上限的值。这两个限制都是作为要插入的参数提供的,因此它们可以在应用中动态设置。

5 至 7 岁。备份数据

问题

您的应用将数据保存在设备上,当用户更换设备或被迫重新安装应用时,您需要为用户提供一种备份和恢复这些数据的方法。

解决办法

(API 一级)

使用设备的外部存储作为安全位置来复制数据库和其他文件。外部存储通常是物理可移动的,允许用户将其放在另一个设备中并进行恢复。即使在不可能的情况下,当用户将其设备连接到计算机时,也可以安装外部存储器,从而进行数据传输。

工作原理

清单 5–17 展示了AsyncTask的一个实现,它在设备的外部存储器和它在应用数据目录中的位置之间来回复制数据库文件。它还为要实现的活动定义了一个接口,以便在操作完成时得到通知。

清单 5–17。 异步请求备份和恢复

`public class BackupTask extends AsyncTask<String,Void,Integer> {

public interface CompletionListener {
        void onBackupComplete();
        void onRestoreComplete();
        void onError(int errorCode);
    }

public static final int BACKUP_SUCCESS = 1;
    public static final int RESTORE_SUCCESS = 2;
    public static final int BACKUP_ERROR = 3;
    public static final int RESTORE_NOFILEERROR = 4;

public static final String COMMAND_BACKUP = "backupDatabase";
    public static final String COMMAND_RESTORE = "restoreDatabase";

private Context mContext;
    private CompletionListener listener;

public BackupTask(Context context) {
        super();
        mContext = context;
    }

public void setCompletionListener(CompletionListener aListener) {
        listener = aListener;
    }

@Override
    protected Integer doInBackground(String... params) {

//Get a reference to the database
        File dbFile = mContext.getDatabasePath("mydb");
        //Get a reference to the directory location for the backup
        File exportDir = new File(Environment.getExternalStorageDirectory(), "myAppBackups");
        if (!exportDir.exists()) {
            exportDir.mkdirs();
        }
        File backup = new File(exportDir, dbFile.getName());

//Check the required operation
        String command = params[0];
        if(command.equals(COMMAND_BACKUP)) {
            //Attempt file copy
            try {
                backup.createNewFile();
                fileCopy(dbFile, backup);                 returnBACKUP_SUCCESS;
            } catch (IOException e) {
                returnBACKUP_ERROR;
            }
        } elseif(command.equals(COMMAND_RESTORE)) {
            //Attempt file copy
            try {
                if(!backup.exists()) {
                    returnRESTORE_NOFILEERROR;
                }
                dbFile.createNewFile();
                fileCopy(backup, dbFile);
                return RESTORE_SUCCESS;
            } catch (IOException e) {
                return BACKUP_ERROR;
            }
        } else {
            return BACKUP_ERROR;
        }
    }

@Override
    protected void onPostExecute(Integer result) {

switch(result) {
        case BACKUP_SUCCESS:
            if(listener != null) {
                listener.onBackupComplete();
            }
            break;
        case RESTORE_SUCCESS:
            if(listener != null) {
                listener.onRestoreComplete();
            }
            break;
        case RESTORE_NOFILEERROR:
            if(listener != null) {
                listener.onError(RESTORE_NOFILEERROR);
            }
            break;
        default:
            if(listener != null) {
                listener.onError(BACKUP_ERROR);
            }
        }
    }

private void fileCopy(File source, File dest) throws IOException {
        FileChannel inChannel = new FileInputStream(source).getChannel();
        FileChannel outChannel = new FileOutputStream(dest).getChannel();
        try {
            inChannel.transferTo(0, inChannel.size(), outChannel);
        } finally {
            if (inChannel != null)
                inChannel.close();
            if (outChannel != null)
                outChannel.close();         }
    }
}`

如您所见,当COMMAND_BACKUP被传递给execute()时,BackupTask 将命名数据库的当前版本复制到外部存储中的特定目录,当COMMAND_RESTORE被传递时,将文件复制回来。

一旦执行,任务使用Context.getDatabasePath()来检索我们需要备份的数据库文件的引用。这一行可以很容易地替换为对Context.getFilesDir()的调用,访问系统内部存储器上的文件进行备份。还获得了对我们在外部存储器上创建的备份目录的引用。

使用传统的 Java 文件 I/O 复制文件,如果一切成功,注册的监听器会得到通知。在此过程中,任何抛出的异常都会被捕获,并向侦听器返回一个错误。现在让我们来看一个利用该任务备份数据库的活动——参见清单 5–18。

清单 5–18。 活动使用备份任务

`public class BackupActivity extends Activity implements BackupTask.CompletionListener {

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        //Dummy example database
        SQLiteDatabase db = openOrCreateDatabase("mydb", Activity.MODE_PRIVATE, null);
        db.close();
    }

@Override
    public void onResume() {
        super.onResume();
        if( Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ) {
            BackupTask task = new BackupTask(this);
            task.setCompletionListener(this);
            task.execute(BackupTask.COMMAND_RESTORE);
        }
    }

@Override
    public void onPause() {
        super.onPause();
        if( Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ) {
            BackupTask task = new BackupTask(this);
            task.execute(BackupTask.COMMAND_BACKUP);
        }
    }

@Override
    public void onBackupComplete() {
        Toast.makeText(this, "Backup Successful", Toast.LENGTH_SHORT).show();
    }`

`    @Override
    public void onError(int errorCode) {
        if(errorCode == BackupTask.RESTORE_NOFILEERROR) {
            Toast.makeText(this, "No Backup Found to Restore",
                Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "Error During Operation: "+errorCode,
                Toast.LENGTH_SHORT).show();
        }
    }

@Override
    public void onRestoreComplete() {
        Toast.makeText(this, "Restore Successful", Toast.LENGTH_SHORT).show();
    }
}`

该活动实现了由BackupTask,定义的CompletionListener,因此当操作完成或发生错误时,它会得到通知。出于示例的目的,在应用的数据库目录中创建了一个虚拟数据库。我们调用openOrCreateDatabase()只是为了允许创建一个文件,所以连接在创建之后会立即关闭。在正常情况下,这个数据库已经存在,这些行是不必要的。

该示例在每次活动恢复时执行一个恢复操作,向任务注册自己,以便它可以得到通知,并向用户提示状态结果。请注意,检查外部存储是否可用的任务也落到了活动上,如果外部存储不可访问,则不会执行任何任务。当活动暂停时,执行备份操作,这一次不注册回调。这是因为用户对该活动不再感兴趣,所以我们将不需要举杯指出操作结果。

额外积分

这个后台任务可以扩展到将数据保存到基于云的服务中,以获得最大的安全性和数据可移植性。有许多选项可以实现这一点,包括 Google 自己的 web APIs,我们建议您尝试一下。

从 API Level 8 开始,Android 还包括一个用于将数据备份到云服务的 API。这个 API 可能适合您的目的,但是我们不会在这里讨论它。Android 框架不能保证该服务在所有 Android 设备上都可用,并且在撰写本文时还没有 API 来确定用户拥有的设备是否支持 Android 备份,因此不建议对关键数据使用该服务。

5–8。共享您的数据库

问题

您的应用希望将其维护的数据库内容提供给设备上的其他应用。

解决办法

(API 一级)

创建一个ContentProvider作为应用数据的外部接口。ContentProvider通过类似数据库的接口query()insert()update()delete()向外部请求公开任意一组数据;尽管实现者可以自由设计接口如何映射到实际的数据模型。创建一个 ContentProvider 来公开来自SQLiteDatabase的数据简单明了。除了一些小的例外,开发人员只需要将调用从提供者传递到数据库。

关于操作哪个数据集的参数通常编码在传递给ContentProvider的 Uri 中。例如,发送查询 Uri,如

content://com.examples.myprovider/friends

会告诉提供者返回其数据集中“friends”表的信息,而

content://com.examples.myprovider/friends/15

将只指示记录 id 15 从查询中返回。应该注意的是,这些只是系统其余部分使用的约定,您有责任让您创建的ContentProvider以这种方式运行。ContentProvider本身并没有为您提供这种功能。

它是如何工作的

首先,要创建一个与数据库交互的ContentProvider,我们必须有一个可以与之交互的数据库。清单 5–19 是一个示例SQLiteOpenHelper实现,我们将使用它来创建和访问数据库本身。

清单 5–19。 示例 SQLiteOpenHelper

`public class ShareDbHelper extends SQLiteOpenHelper {

private static final String DB_NAME = "frienddb";
    private static final int DB_VERSION = 1;

public static final String TABLE_NAME = "friends";
    public static final String COL_FIRST = "firstName";
    public static final String COL_LAST = "lastName";
    public static final String COL_PHONE = "phoneNumber";`

`    private static final String STRING_CREATE =
        "CREATE TABLE "+TABLE_NAME+" (_id INTEGER PRIMARY KEY AUTOINCREMENT, "
        +COL_FIRST+" TEXT, "+COL_LAST+" TEXT, "+COL_PHONE+" TEXT);";

public ShareDbHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

@Override
    public void onCreate(SQLiteDatabase db) {
        //Create the database table
        db.execSQL(STRING_CREATE);

//Inserting example values into database
        ContentValues cv = new ContentValues(3);
        cv.put(COL_FIRST, "John");
        cv.put(COL_LAST, "Doe");
        cv.put(COL_PHONE, "8885551234");
        db.insert(TABLE_NAME, null, cv);
        cv = new ContentValues(3);
        cv.put(COL_FIRST, "Jane");
        cv.put(COL_LAST, "Doe");
        cv.put(COL_PHONE, "8885552345");
        db.insert(TABLE_NAME, null, cv);
        cv = new ContentValues(3);
        cv.put(COL_FIRST, "Jill");
        cv.put(COL_LAST, "Doe");
        cv.put(COL_PHONE, "8885553456");
        db.insert(TABLE_NAME, null, cv);
    }

@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        //For now, clear the database and re-create
        db.execSQL("DROP TABLE IF EXISTS "+TABLE_NAME);
        onCreate(db);
    }
}`

总的来说,这个助手相当简单,创建一个表来保存我们的朋友列表,其中只有三列用于存放文本数据。出于此示例的目的,插入了三个行值。现在让我们来看看将这个数据库暴露给其他应用的ContentProvider——参见清单 5–20 和 5–21。

清单 5–20。 内容提供者的清单声明

<manifest xmlns:android="http://schemas.android.com/apk/res/android" …>     <application …>       <provider android:name=".FriendProvider"           android:authorities="com.examples.sharedb.friendprovider">       </provider>     </application> </manifest>

清单 5–20。 为一个数据库提供内容

`public class FriendProvider extends ContentProvider {

public static final Uri CONTENT_URI =
           Uri.parse("content://com.examples.sharedb.friendprovider/friends");

public static finalclass Columns {
        public static final String _ID = "_id";
        public static final String FIRST = "firstName";
        public static final String LAST = "lastName";
        public static final String PHONE = "phoneNumber";
    }

/* Uri Matching */
    private static final int FRIEND = 1;
    private static final int FRIEND_ID = 2;

private static final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
    static {
        matcher.addURI(CONTENT_URI.getAuthority(), "friends", FRIEND);
        matcher.addURI(CONTENT_URI.getAuthority(), "friends/#", FRIEND_ID);
    }

SQLiteDatabase db;

@Override
    publicint delete(Uri uri, String selection, String[] selectionArgs) {
        int result = matcher.match(uri);
        switch(result) {
        case FRIEND:
            return db.delete(ShareDbHelper.TABLE_NAME, selection, selectionArgs);
        case FRIEND_ID:
            return db.delete(ShareDbHelper.TABLE_NAME, "_ID = ?",
                    new String[]{uri.getLastPathSegment()});
        default:
            return 0;
        }
    }

@Override
    public String getType(Uri uri) {
        returnnull;
    }

@Override
    public Uri insert(Uri uri, ContentValues values) {
        long id = db.insert(ShareDbHelper.TABLE_NAME, null, values);
        if(id >= 0) {
            return Uri.withAppendedPath(uri, String.valueOf(id));
        } else {
            returnnull;
        }
    }

@Override
    publicboolean onCreate() {
        ShareDbHelper helper = new ShareDbHelper(getContext());         db = helper.getWritableDatabase();
        returntrue;
    }

@Override
    public Cursor query(Uri uri, String[] projection, String selection,             String[] selectionArgs,String sortOrder) {
            int result = matcher.match(uri);
            switch(result) {
            case FRIEND:
                return db.query(ShareDbHelper.TABLE_NAME, projection, selection,
                    selectionArgs,null, null, sortOrder);
        case FRIEND_ID:
            return db.query(ShareDbHelper.TABLE_NAME, projection, "_ID = ?",
                    new String[]{uri.getLastPathSegment()}, null, null, sortOrder);
        default:
            returnnull;
        }
    }

@Override
    publicint update(Uri uri, ContentValues values, String selection,
        String[] selectionArgs) {
        int result = matcher.match(uri);
        switch(result) {
        case FRIEND:
            return db.update(ShareDbHelper.TABLE_NAME, values, selection,
                selectionArgs);
        case FRIEND_ID:
            return db.update(ShareDbHelper.TABLE_NAME, values, "_ID = ?",
                    new String[]{uri.getLastPathSegment()});
        default:
            return 0;
        }
    }

}`

ContentProvider 必须在应用的清单中用它所表示的授权字符串来声明。这允许从外部应用访问该提供程序,但即使您只在应用内部使用该提供程序,这也是必需的。权限是 Android 用来将Uri请求匹配到提供者的,因此它应该匹配公共CONTENT_URI的权限部分。

扩展ContentProvider时需要覆盖的六种方法是query()insert()update()delete()getType()onCreate()。这些方法中的前四个在SQLiteDatabase中有直接对应的方法,所以只需用适当的参数调用数据库方法。这两者之间的主要区别是ContentProvider方法传入了一个Uri,提供者应该检查这个方法以确定操作数据库的哪一部分。

当一个活动或其他系统组件调用其内部ContentResolver上的相应方法时(您可以在清单 5–21 中看到这一点),或者在活动的情况下,当调用managedQuery()时,这四个主要的 CRUD 方法在提供者上被调用。

为了遵守本菜谱第一部分提到的Uri约定,insert()返回一个Uri对象,它是通过将新创建的记录 id 附加到路径的末尾而创建的。这个Uri应该被它的请求者认为是对刚刚创建的记录的直接引用。

其余的方法(query()update()delete())遵循惯例,通过检查传入的Uri来查看它是引用特定的记录,还是引用整个表。这个任务是在UriMatcher便利类的帮助下完成的。UriMatcher.match()方法将Uri与一组提供的模式进行比较,并将匹配的模式作为 int 返回,如果没有找到匹配的模式,则返回UriMatcher.NO_MATCH。如果一个Uri被附加了一个记录 id,那么对数据库的调用将被修改为只影响那个特定的行。

应该通过用UriMatcher.addURI()提供一组模式来初始化一个UriMatcher;谷歌建议这一切都在ContentProvider的静态环境中完成。添加的每个模式还被赋予一个常量标识符,当进行匹配时,该标识符将作为返回值。在提供的模式中可以使用两个通配符:井号(#)将匹配任何数字,星号(*)将匹配任何文本。

我们的例子创建了两个匹配的模式。初始模式与提供的CONTENT_URI直接匹配,并被用来引用整个数据库表。第二种模式寻找路径的附加数字,该数字将被用来引用该 id 处的记录。

通过onCreate()中的ShareDbHelper给出的引用获得对数据库的访问。在决定这种方法是否适用于您的应用时,应该考虑所用数据库的大小。我们的数据库在创建时非常小,但是更大的数据库可能需要很长时间来创建,在这种情况下,在这个操作发生时,主线程不应该被占用;getWritableDatabase()在这些情况下,可能需要包装在 AsyncTask 中并在后台完成。现在让我们来看一个访问数据的示例活动——参见清单 5–23 和 5–24。

清单 5–23。 AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"     package="com.examples.sharedb" android:versionCode="1" android:versionName="1.0">     <uses-sdk android:minSdkVersion="1" />     <application android:icon="@drawable/icon" android:label="@string/app_name">       <activity android:name=".ShareActivity" android:label="@string/app_name">         <intent-filter>           <action android:name="android.intent.action.MAIN" />           <category android:name="android.intent.category.LAUNCHER" />         </intent-filter>       </activity>       <provider android:name=".FriendProvider"           android:authorities="com.examples.sharedb.friendprovider">       </provider>     </application> </manifest>

清单 5–24。 活动访问内容提供者

`public class ShareActivity extends ListActivity implements AdapterView.OnItemClickListener {

Cursor mCursor;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

//List of column names to return from the query for each record
        String[] projection = new String[]{FriendProvider.Columns._ID,             FriendProvider.Columns.FIRST};
        mCursor = managedQuery(FriendProvider.CONTENT_URI, projection, null, null,             null);

SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
            android.R.layout.simple_list_item_1,
            mCursor,
            new String[]{FriendProvider.Columns.FIRST},
            newint[]{android.R.id.text1});

ListView list = getListView();
        list.setOnItemClickListener(this);
        list.setAdapter(adapter);
    }

@Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        mCursor.moveToPosition(position);

Uri uri = Uri.withAppendedPath(FriendProvider.CONTENT_URI,             mCursor.getString(0));
        String[] projection = new String[]{FriendProvider.Columns.FIRST,
                FriendProvider.Columns.LAST,
                FriendProvider.Columns.PHONE};
        //Get the full record
        Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
        cursor.moveToFirst();

String message = String.format("%s %s, %s", cursor.getString(0),
            cursor.getString(1),cursor.getString(2));
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
    }
}`

该示例查询FriendsProvider中的所有记录,并将它们放入一个列表中,只显示名字列。为了让Cursor正确地适应列表,我们的投影必须包括 ID 列,即使它没有显示。

如果用户点击列表中的任何一项,就会使用在末尾附加了记录 ID 的 Uri 对提供者进行另一次查询,迫使提供者只返回那一条记录。此外,还提供了一个扩展投影来获取关于这个朋友的所有列数据。

返回的数据被放入一个Toast中,并被提交给用户查看。光标中的各个字段通过它们的列索引进行访问,对应于传递给查询的投影中的索引。Cursor.getColumnIndex()方法也可以用来查询游标,查找与给定列名相关联的索引。

当不再需要某个Cursor时,它应该总是被关闭,就像我们在用户点击时创建的Cursor一样。成员mCursor从未被显式关闭,因为它是由活动管理的。每当使用managedQuery()创建一个Cursor时,该活动将打开、关闭和刷新数据以及它自己的正常生命周期。

Figure 5–2 显示了运行此示例以显示提供者内容的结果。

images

图 5–2。 来自内容提供商的信息

5–9 岁。共享您的其他数据

问题

您希望您的应用将它维护的文件或其他数据提供给设备上的其他应用。

解决方案

(API 三级)

创建一个ContentProvider作为应用数据的外部接口。ContentProvider通过类似数据库的接口query()insert()update()delete()向外部请求公开任意一组数据,尽管实现可以自由设计数据如何从这些方法传递到实际模型。

ContentProvider可用于向外部请求公开任何类型的应用数据,包括应用的资源和素材。

它是如何工作的

让我们来看一个ContentProvider实现,它公开了两个数据源:位于内存中的字符串数组,以及存储在应用的 assets 目录中的一系列图像文件。和以前一样,我们必须在清单中使用一个<provider>标签向 Android 系统声明我们的提供者。参见清单 5–25 和清单 5–26。

清单 5–25。 内容提供者的清单声明

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" …>     <application …>       <provider android:name=".ImageProvider"           android:authorities="com.examples.share.imageprovider">       </provider>     </application> </manifest>

清单 5–26。 自定义内容提供者公开素材

`public class ImageProvider extends ContentProvider {

public static final Uri CONTENT_URI =
        Uri.parse("content://com.examples.share.imageprovider");

public static final String COLUMN_NAME = "nameString";
    public static final String COLUMN_IMAGE = "imageUri";

private String[] mNames;

@Override
    publicint delete(Uri uri, String selection, String[] selectionArgs) {
        thrownew UnsupportedOperationException("This ContentProvider is read-only");
    }

@Override
    public String getType(Uri uri) {
        returnnull;
    }

@Override     public Uri insert(Uri uri, ContentValues values) {
        thrownew UnsupportedOperationException("This ContentProvider is read-only");
    }

@Override
    publicboolean onCreate() {
        mNames = new String[] {"John Doe", "Jane Doe", "Jill Doe"};
        returntrue;
    }

@Override
    public Cursor query(Uri uri, String[] projection, String selection,         String[] selectionArgs,String sortOrder) {
        MatrixCursor cursor = new MatrixCursor(projection);
        for(int i = 0; i < mNames.length; i++) {
            //Insert only the columns they requested
            MatrixCursor.RowBuilder builder = cursor.newRow();
            for(String column : projection) {
                if(column.equals("_id")) {
                    //Use the array index as a unique id
                    builder.add(i);
                }
                if(column.equals(COLUMN_NAME)) {
                    builder.add(mNames[i]);
                }
                if(column.equals(COLUMN_IMAGE)) {
                    builder.add(Uri.withAppendedPath(CONTENT_URI, String.valueOf(i)));
                }
            }
        }
        return cursor;
    }

@Override
    publicint update(Uri uri, ContentValues values, String selection,         String[] selectionArgs) {
        thrownew UnsupportedOperationException("This ContentProvider is read-only");
    }

@Override
    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws         FileNotFoundException {
        int requested = Integer.parseInt(uri.getLastPathSegment());
        AssetFileDescriptor afd;
        AssetManager manager = getContext().getAssets();
        //Return the appropriate asset for the requested item
        try {
            switch(requested) {
            case 0:
                afd = manager.openFd("logo1.png");
                break;
            case 1:
                afd = manager.openFd("logo2.png");
                break;
            case 2:
                afd = manager.openFd("logo3.png");
                break;             default:
                afd = manager.openFd("logo1.png");
            }
            return afd;
        } catch (IOException e) {
            e.printStackTrace();
            returnnull;
        }
    }
}`

正如您可能已经猜到的,该示例公开了三个徽标图像素材。我们为此示例选择的图像如图 5–3 所示。

images

图 5–3。 示例 logo1.png(左)、logo2.png(中)和 logo3.png(右)存储在素材中

首先注意,因为我们在素材目录中公开只读内容,所以不需要支持继承的方法insert()update()delete(),所以我们让这些方法简单地抛出一个UnsupportedOperationException

创建提供者时,保存人名的字符串数组被创建,onCreate()返回 true 这向系统发出信号,表明提供程序已成功创建。提供者为它的Uri和所有可读的列名公开常量。外部应用将使用这些值来请求数据。

此提供程序仅支持对其中所有数据的查询。为了支持对特定记录或所有内容的子集的条件查询,应用可以处理传入到query()中的selectionselectionArgs的值。在这个例子中,对query()的任何调用都将构建一个包含所有三个元素的游标。

该提供程序中使用的游标实现是一个MatrixCursor,它是一个设计用于围绕不在数据库中保存的数据构建的游标。该示例遍历所请求的列列表(投影),并根据所包含的这些列构建每一行。每一行都是通过调用MatrixCursor.newRow()创建的,它还返回一个用于添加列数据的Builder实例。应该始终注意将列数据的顺序添加到所请求的投影顺序中。它们应该总是匹配的。

name 列中的值是本地数组中相应的字符串,而 _id 值(Android 需要它来利用返回的带有大多数ListAdapter的光标)只是作为数组索引返回。每行的 image 列中显示的数据实际上是代表每行图像文件的内容Uri,它是以提供者的内容Uri为基础创建的,并附加了数组索引。

当一个外部应用实际上通过ContentResolver.openInputStream()去检索这个内容时,将调用openAssetFile(),这个调用已经被覆盖以返回一个指向素材目录中的一个图像文件的AssetFileDescriptor。这个实现通过再次解构内容Uri并从末尾检索附加的索引值来确定返回哪个图像文件。

用法举例

让我们看看在 Android 应用的上下文中应该如何实现和访问这个提供者。参见清单 5–27。

清单 5–27。 AndroidManifest.xml

`

    


        
            
                
                
            

        

        
        

    

`

要实现这个提供者,拥有内容的应用的清单必须声明一个<provider>标记,指出发出请求时要匹配的ContentProvider名称和授权。权限值应该与暴露内容的基本部分相匹配Uri。必须在清单中声明提供程序,以便系统可以实例化并运行它,即使拥有它的应用没有运行。参见清单 5–28 和清单 5–29。

清单 5–28。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <TextView       android:id="@+id/name"     android:layout_width="wrap_content"     android:layout_height="20dip"     android:layout_gravity="center_horizontal"   />   <ImageView     android:id="@+id/image"     android:layout_width="wrap_content"     android:layout_height="50dip"
    android:layout_gravity="center_horizontal"   />   <ListView     android:id="@+id/list"     android:layout_width="fill_parent"     android:layout_height="fill_parent"   />   </LinearLayout>

清单 5–29。 从 ImageProvider 读取活动

`public class ShareActivity extends Activity implements AdapterView.OnItemClickListener {

Cursor mCursor;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

String[] projection = new String[]{"_id", ImageProvider.COLUMN_NAME,
            ImageProvider.COLUMN_IMAGE};
        mCursor = managedQuery(ImageProvider.CONTENT_URI, projection, null, null, null);

SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,             android.R.layout.simple_list_item_1,mCursor, new String[]{ImageProvider.COLUMN_NAME},
            new int[]{android.R.id.text1});

ListView list = (ListView)findViewById(R.id.list);
        list.setOnItemClickListener(this);
        list.setAdapter(adapter);
    }

@Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        //Seek the cursor to the selection
        mCursor.moveToPosition(position);

//Load the name column into the TextView
        TextView tv = (TextView)findViewById(R.id.name);
        tv.setText(mCursor.getString(1));

ImageView iv = (ImageView)findViewById(R.id.image);
        try {
            //Load the content from the image column into the ImageView
            InputStream in =
                getContentResolver().openInputStream(Uri.parse(mCursor.getString(2)));
            Bitmap image = BitmapFactory.decodeStream(in);
            iv.setImageBitmap(image);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

}
}`

在本例中,从自定义的ContentProvider中获得一个托管游标,引用数据的公开 Uri 和列名。然后使用一个SimpleCursorAdapter将数据连接到一个ListView,只显示名称值。

当用户点击列表中的任何项目时,光标移动到该位置,相应的名称和图像显示在上面。这是活动调用ContentResolver.openInputStream()通过存储在列字段中的 Uri 访问素材图像的地方。

Figure 5–4 显示了运行该应用并选择列表中最后一项的结果(Jill Doe)。

images

图 5–4。 活动从 ContentProvider 提取资源

请注意,到Cursor的连接没有被显式关闭,因为它是使用managedQuery()创建的,这意味着活动将把光标作为其正常生命周期的一部分来管理,包括在活动离开前台时关闭它。

需要了解的有用工具:SQLite3

Android 提供了sqlite3工具(在 Android SDK 主目录的tools子目录中),用于在您的托管平台上创建新数据库和管理现有数据库,或者(当与 Android 调试桥工具adb结合使用时)在 Android 设备上创建新数据库和管理现有数据库。如果你不熟悉sqlite3,将你的浏览器指向[sqlite.org/sqlite.html](http://sqlite.org/sqlite.html),阅读这个命令行工具的简短教程。

您可以用一个数据库文件名参数指定sqlite3(例如sqlite3 employees)来创建数据库文件(如果它不存在的话)或者打开现有的文件,并进入这个工具的 shell,从这里您可以执行特定于sqlite3的点前缀命令和 SQL 语句。如图 Figure 5–5 所示,您也可以不带参数地指定sqlite3并输入 shell。

images

图 5–5。 调用sqlite3时不带数据库文件名参数

Figure 5–5 揭示了进入sqlite3 shell 后欢迎您的开场白,它由您输入命令的sqlite>提示符指示。当您键入特定于sqlite3的“.help命令时,它还会显示部分帮助文本。

提示:您可以在没有参数的情况下指定sqlite3之后创建一个数据库,方法是输入适当的 SQL 语句来创建和填充所需的表(并可能创建索引),然后在退出sqlite3之前调用.backup*filename*(其中 filename 标识存储数据库的文件)。

在你的托管平台上创建了数据库之后,你会想把它上传到你的 Android 设备上。您可以根据以下命令行语法,通过使用push命令调用adb工具来完成这项任务:

adb [-s <*serialNumber*>] push *local*.db /data/data/<*application package*>/databases/*remote*.db

该命令将标识为 local.db 的本地托管数据库推送到名为 remote.db 的文件中,该文件位于连接的 Android 设备上的/data/data/<*application package*>/databases目录中。

注意: Localremote 是实际数据库文件名的占位符。按照惯例,文件名与一个.db文件扩展名相关联(尽管扩展名不是强制性的)。另外,/data/data/<*application package*>是指应用自己的私有存储区, application package 是指应用唯一的包名。

如果只有一个设备连接到托管平台,则不需要-s <*serialNumber*>,本地数据库被推送到该设备上。如果连接了多个设备,需要使用-s <*serialNumber*>来识别特定设备(例如-s emulator-5556)。

或者,您可能希望将设备的数据库下载到您的托管平台,也许是为了与设备应用的桌面版本一起使用。您可以根据下面的语法通过调用带有pull命令的adb来完成这个任务:

adb [-s <*serialNumber*>] pull /data/data/<*application package*>/databases/*remote*.db *local*.db

如果您想使用sqlite3来管理存储在设备上的 SQLite 数据库,您需要从该设备的adb远程 shell 中调用这个工具。您可以根据以下语法通过调用adbsqlite3来完成这项任务:

`adb [-s <serialNumber>] shell

sqlite3 /data/data/<application package>/databases/remote.db`

adb外壳由#提示符指示。输入sqlite3,后跟现有设备托管的数据库文件的路径和名称,以操作数据库,或创建新的数据库。或者,您可以不带参数地输入sqlite3

sqlite3命令呈现了与您在图 5–1 中看到的相同的序言。输入sqlite3命令,发出 SQL 语句来管理 remote.db (或者创建一个新的数据库),然后退出sqlite3 ( .exit或者.quit),再退出adb shell ( exit)。

SQLite3 和 UC

第一章向你介绍了一个名为UC的应用。这个单位转换应用允许你在不同的单位之间进行转换(例如,从华氏温度到摄氏温度)。

虽然很有用,UC也有缺陷,因为每次有新的转换添加到它的转换列表中时,都必须重新构建它。我们可以通过在数据库中存储UC的转换来消除这个缺陷,这就是我们在本节中要做的。

我们将首先创建一个数据库来存储转换列表。数据库将由一个带有conversionmultiplier列的conversions表组成。此外,数据库将存储在一个conversions.db文件中。

Table 5–1 列出了将存储在conversionmultiplier列中的值。

表 5–1。 ConversionMultiplier的值

| **转换** | 乘法器 | | :-- | :-- | | 英亩到平方英里 | 0.0015625 | | 大气压至帕斯卡 | One hundred and one thousand three hundred and twenty-five | | 巴到帕斯卡 | One hundred thousand | | 摄氏温度到华氏温度 | 0(占位符) | | 华氏度到摄氏度 | 0(占位符) | | 达因到牛顿 | 0.00001 | | 英尺/秒到米/秒 | 0.3048 | | 液体盎司(英国)到升 | 0.0284130625 | | 液体盎司(美国)到升 | 0.0295735295625 | | 马力(电力)至瓦特 | Seven hundred and forty-six | | 马力(公制)到瓦特 | Seven hundred and thirty-five point four nine nine | | 千克到吨(英制或长制) | 1/1016.0469088 | | 千克到吨(美制或短制) | 1/907.18474 | | 升到液体盎司(英国) | 1/0.0284130625 | | 升到液体盎司(美国) | 1/0.0295735295625 | | 马赫数到米/秒 | Three hundred and thirty-one point five | | 米/秒到英尺/秒 | 1/0.3048 | | 米/秒到马赫数 | 1/331.5 | | 英里/加仑(英国)到英里/加仑(美国) | Zero point eight three three | | 英里/加仑(美国)到英里/加仑(英国) | 1/0.833 | | 牛顿至达因 | One hundred thousand | | 帕斯卡至大气压 | 1/101325.0 | | 帕斯卡到巴 | 0.00001 | | 平方英里到英亩 | Six hundred and forty | | 吨(英制或长制)到千克 | 1016.0469088 | | 吨(美制或短制)到千克 | 907.18474 | | 瓦特/马力 _ 电动) | 1/746.0 | | 瓦特对马力(公制) | 1/735.499 |

在命令行执行sqlite3 conversions.db创建conversions.db并进入 shell,然后执行 SQL 语句create table conversions(conversion varchar(50), mutliplier float);创建该数据库的conversions表。

继续,输入一系列 insert 语句,将表 5–1 的值行插入到conversions中。例如,SQL 语句insert into conversions values('Acres to square miles', 0.0015625);将第一行的值插入到表中。

注意:您必须按照它们在表 5–1 中出现的顺序插入行,因为Degrees Celsius to Degrees FahrenheitDegrees Fahrenheit to Degrees Celsius必须出现在从零开始的位置 3 和 4,因为这些位置在UC2.java中是硬编码的。

接下来,我们将创建一个类似于UCUC2应用,但是从conversions.db获得它的转换。按照第一章的秘籍 1-10(用 Eclipse 开发UC)中的说明完成这项任务,但要做以下更改(参见清单 5–30):

  • 将包名从com.apress.uc更改为com.apress.uc2
  • 忽略arrays.xml文件。UC2不需要这个文件。
  • 用清单 5–26 替换框架UC2.java源代码。

清单 5–30。 执行从Conversions.db 获取的单位换算的活动

`public class UC2 extends Activity {
   private int position = 0;
   private String[] conversions;
   private double[] multipliers;

private class DBHelper extends SQLiteOpenHelper
   {
      private final static String DB_PATH = "data/data/com.apress.uc2/databases/";
      private final static String DB_NAME = "conversions.db";
      private final static int CONVERSIONS_COLUMN_ID = 0;
      private final static int MULTIPLIERS_COLUMN_ID = 1;

private SQLiteDatabase db;

public DBHelper(Context context)
      {
         super(context, DB_NAME, null, 1);
      }

@Override
      public void onCreate(SQLiteDatabase db)
      {
         // Do nothing ... we don't create a new database.
      }

@Override
      public void onUpgrade(SQLiteDatabase db, int oldver, int newver)       {
         // Do nothing ... we don't upgrade a database.
      }

public boolean populateArrays()
      {
         try
         {
            String path = DB_PATH+DB_NAME;
              db = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY|
                                             SQLiteDatabase.NO_LOCALIZED_COLLATORS);
              Cursor cur = db.query("conversions", null, null, null, null, null, null);
              if (cur.getCount() == 0)
              {
                 Toast.makeText(UC2.this, "conversions table is empty",
                              Toast.LENGTH_LONG).show();
                 return false;
              }
              conversions = new String[cur.getCount()];
              multipliers = new double[cur.getCount()];
              int i = 0;
              while (cur.moveToNext())
              {
                  conversions[i] = cur.getString(CONVERSIONS_COLUMN_ID);
                  multipliers[i++] = cur.getFloat(MULTIPLIERS_COLUMN_ID);
              }
              return true;
         }
         catch (SQLException sqle)
         {
            Toast.makeText(UC2.this, sqle.getMessage(), Toast.LENGTH_LONG).show();
         }
         finally
         {
            if (db != null)
               db.close();
         }
         return false;
      }
   }

@Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);

DBHelper dbh = new DBHelper(this);
      if (!dbh.populateArrays())
          finish();

final EditText etUnits = (EditText) findViewById(R.id.units);

final Spinner spnConversions = (Spinner) findViewById(R.id.conversions);
      ArrayAdapter aa;
      aa = new ArrayAdapter(this, android.R.layout.simple_spinner_item,
                                          conversions);       aa.setDropDownViewResource(android.R.layout.simple_spinner_item);
      spnConversions.setAdapter(aa);

AdapterView.OnItemSelectedListener oisl;
      oisl = new AdapterView.OnItemSelectedListener()
      {
         @Override
         public void onItemSelected(AdapterView<?> parent, View view,
                                    int position, long id)
         {
            UC2.this.position = position;
         }

@Override
         public void onNothingSelected(AdapterView<?> parent)
         {
            System.out.println("nothing");
         }
      };
      spnConversions.setOnItemSelectedListener(oisl);

final Button btnClear = (Button) findViewById(R.id.clear);
      AdapterView.OnClickListener ocl;
      ocl = new AdapterView.OnClickListener()
      {
         @Override
         public void onClick(View v)
         {
            etUnits.setText("");
         }
      };
      btnClear.setOnClickListener(ocl);
      btnClear.setEnabled(false);

final Button btnConvert = (Button) findViewById(R.id.convert);
      ocl = new AdapterView.OnClickListener()
      {
         @Override
         public void onClick(View v)
         {
            String text = etUnits.getText().toString();
            double input = Double.parseDouble(text);
            double result = 0;
            if (position == 3)
               result = input9.0/5.0+32; // Celsius to Fahrenheit
            else
            if (position == 4)
               result = (input-32)
5.0/9.0; // Fahrenheit to Celsius
            else
               result = input*multipliers[position];
            etUnits.setText(""+result);
         }
      };
      btnConvert.setOnClickListener(ocl);
      btnConvert.setEnabled(false);

Button btnClose = (Button) findViewById(R.id.close);       ocl = new AdapterView.OnClickListener()
      {
         @Override
         public void onClick(View v)
         {
            finish();
         }
      };
      btnClose.setOnClickListener(ocl);

TextWatcher tw;
      tw = new TextWatcher()
      {
         public void afterTextChanged(Editable s)
         {
         }
         public void beforeTextChanged(CharSequence s, int start, int count,
                                       int after)
         {
         }
         public void onTextChanged(CharSequence s, int start, int before,
                                   int count)
         {
            if (etUnits.getText().length() == 0)
            {
               btnClear.setEnabled(false);
               btnConvert.setEnabled(false);
            }
            else
            {
               btnClear.setEnabled(true);
               btnConvert.setEnabled(true);
            }
         }
      };
      etUnits.addTextChangedListener(tw);
   }
}`

UC2UC的不同之处主要在于依靠DBHelper内部类从conversions.db数据库的conversions表中的conversionmultiplier列获取其conversionsmultipliers数组的值。

DBHelper扩展android.database.sqlite.SQLiteOpenHelper并覆盖它的抽象onCreate()onUpgrade()方法。重写方法什么都不做;重要的是数据库是否可以打开。

数据库以populateArrays()方法打开。如果成功打开,查询conversions表以返回所有行。如果返回的android.database.Cursor对象包含至少一行,数组由Cursor值填充。

如果出现问题,会显示一条提示消息。虽然对于这个简单的例子来说很方便,但是您可能希望显示一个对话框并将其字符串存储在资源文件中。无论是否显示 toast 消息,数据库都是关闭的。

UC2UC的区别还在于,它直接实例化了android.widget.ArrayAdapter,而不是调用这个类的createFromResource()方法。这样做是为了将字符串名称的conversions数组传递给ArrayAdapter实例。

假设您已经构建了这个应用,从 Eclipse 启动它。UC2将短暂显示一个空白屏幕,然后在结束前显示一条提示信息。图 5–6 显示这个吐司出现在应用启动屏幕上。

images

图 5–6。 因为conversions.db数据库还没有出现在设备上,所以显示了一个 toast。

出现 toast 是因为在/data/data/com.apress.uc2/databases/路径中不存在conversions.db数据库。我们可以通过将之前创建的conversions.db文件上传到这个路径来纠正这种情况,如下所示:

adb push conversions.db /data/data/com.apress.uc2/databases/conversions.db

这一次,当您启动该应用时,您应该会看到出现在 Figure 5–7 中的屏幕。

images

图 5–7。 单位转换器的单独屏幕让你执行各种单位转换。

遭受以下一对缺陷的困扰——考虑将修复这些缺陷作为要完成的练习:

  • 由于在UC2.java中对这些位置进行了硬编码,因此Degrees Celsius to Degrees FahrenheitDegrees Fahrenheit to Degrees Celsius转换必须出现在从零开始的位置 3 和 4。这个硬编码位于下面的摘录中,该摘录摘自在清单 5–30 中分配给点击按钮的点击监听器中的onClick()方法:if (position == 3)    result = input*9.0/5.0+32; // Celsius to Fahrenheit else if (position == 4)    result = (input-32)*5.0/9.0; // Fahrenheit to Celsius else    result = input*multipliers[position];
  • DBHelperpopulateArrays()方法填充应用主线程上的conversionsmultipliers数组。这应该不成问题,因为conversions表只包含 28 行。然而,如果向该表中添加更多的行,主线程可能会被占用足够长的时间,以至于可怕的应用不响应对话框出现(参见附录 C)。此外,这也是为什么 Android 文档指出SQLiteOpenHelpergetReadableDatabase()getWritableDatabase()方法不应该在主线程上调用的原因。然而,对于小型数据库,在主线程上调用这些方法应该不成问题。

总结

在这一章中,你已经研究了许多在 Android 设备上持久化数据的实用方法。您了解了如何快速创建首选项屏幕,以及如何使用首选项和简单的方法来持久化基本数据类型。您已经看到了文件的放置方式和位置,既可用于参考,也可用于存储。您甚至学习了如何与其他应用共享持久化数据。在下一章中,我们将研究如何利用操作系统的服务来进行后台操作和应用之间的通信。

六、与系统交互

Android 操作系统提供了许多应用可以利用的有用服务。这些服务中的许多都是为了让您的应用能够在移动系统中运行,而不仅仅是与用户进行短暂的交互。应用可以为自己安排警报,运行后台服务,并相互发送消息;所有这些都允许 Android 应用最大程度地与移动设备集成。此外,Android 提供了一套标准接口,旨在向您的软件公开其核心应用收集的所有数据。通过这些接口,任何应用都可以集成、添加和改进平台的核心功能,从而增强用户体验。

6–1。从后台通知

问题

您的应用在后台运行,当前没有对用户可见的界面,但必须通知用户发生了重要事件。

解决方案

(API 一级)

使用NotificationManager发布状态栏通知。Notifications是一种不引人注目的方式,告诉用户你想引起他们的注意。也许新消息已经到达,更新可用,或者长时间运行的作业已经完成;Notifications非常适合完成所有这些任务。

工作原理

一个Notification可以从任何系统组件发送到NotificationManager,比如一个服务、广播接收器或活动。在这个例子中,我们将看到一个使用延迟来模拟长时间运行的操作的活动,当它完成时会产生一个Notification

清单 6–1。 活动触发通知

`public class NotificationActivity extends Activity implements View.OnClickListener {

private static final intNOTE_ID = 100;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Button button = new Button(this);
        button.setText("Post New Notification");
        button.setOnClickListener(this);
        setContentView(button);
    }

@Override
    public void onClick(View v) {
        //Run 10 seconds after click
        handler.postDelayed(task, 10000);
        Toast.makeText(this, "Notification will post in 10 seconds",             Toast.LENGTH_SHORT).show();
    }

private Handler handler = new Handler();
    private Runnable task = new Runnable() {
        @Override
        public void run() {
            NotificationManager manager =
                    (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
            Intent launchIntent = new Intent(getApplicationContext(),                 NotificationActivity.class);
            PendingIntent contentIntent =
                PendingIntent.getActivity(getApplicationContext(), 0, launchIntent, 0);

//Create notification with the time it was fired
            Notification note = new Notification(R.drawable.icon, "Something Happened",
                    System.currentTimeMillis());
            //Set notification information
            note.setLatestEventInfo(getApplicationContext(), "We're Finished!",                 "Click Here!", contentIntent);
            note.defaults |= Notification.DEFAULT_SOUND;
            note.flags |= Notification.FLAG_AUTO_CANCEL;

manager.notify(NOTE_ID, note);
        }
    };
}`

这个例子使用了一个Handler来调度一个任务,通过调用按钮监听器中的Handler.postDelayed()在按钮被点击十秒钟后发送Notification。不管活动是否在前台,这个任务都会执行,所以如果用户厌倦了并离开应用,他们仍然会得到通知。

当计划任务执行时,会创建一个新的通知。可以提供图标资源和标题字符串,这些项目将在通知发生时显示在状态栏中。此外,我们传递一个时间值(以毫秒为单位)作为事件时间显示在通知列表中。这里,我们将该值设置为通知触发的时间,但是在您的应用中它可能有不同的含义。

一旦Notification被创建,我们用一些有用的参数填充它。使用Notification.setLatestEventInfo(),当用户下拉状态栏时,我们在通知列表中显示更详细的文本。

传递给这个方法的参数之一是一个指向我们活动的PendingIntent。这种意图使得通知是交互式的,允许用户点击列表中的通知并启动活动。

注意:这个意向将为每个事件发起一个新的活动。如果您希望活动的现有实例响应启动,如果堆栈中存在一个实例,请确保适当地包含意图标志和清单参数来实现这一点,例如Intent.FLAG_ACTIVITY_CLEAR_TOPandroid:launchMode="singleTop."

为了增强状态栏中视觉动画之外的Notification,修改了Notification.defaults位掩码,以包括当Notification触发时系统默认的通知声音。也可以添加诸如Notification.DEFAULT_VIBRATIONNotification.DEFAULT_LIGHTS的值。

提示:如果您想定制用Notification播放的声音,将Notification.sound参数设置为引用文件的Uri或要读取的ContentProvider

Notification.flags位掩码添加一系列标志允许进一步定制Notification。这个例子使Notification.FLAG_AUTO_CANCEL能够表示一旦用户选择了通知,就应该取消通知,或者从列表中删除。如果没有此标志,通知将保留在列表中,直到通过调用NotificationManager.cancel()NotificationManager.cancelAll()手动取消。

以下是其他一些有用的标志:

  • FLAG_INSISTENT
    • 重复Notification声音,直到用户做出响应。
  • FLAG_NO_CLEAR
    • 不允许用用户的“清除通知”按钮清除Notification;只能通过调用cancel()

一旦通知准备好了,就用NotificationManager.notify()发送给用户,它也带有一个 ID 参数。应用中的每个Notification类型都应该有一个惟一的 ID。管理器一次只允许列表中有一个具有相同 ID 的Notification,具有相同 ID 的新实例将取代现有的实例。另外,手动取消特定的Notification需要 ID。

当我们运行这个例子时,像 Figure 6–1 这样的活动向用户显示一个按钮。按下按钮后,您可以在一段时间后看到通知帖子,即使该活动不再可见(参见图 6–2)。

images

图 6–1。 通知从按钮按下开始张贴

images

图 6–2。 通知发生(左),并显示在列表中(右)

6–2。创建定时和周期性任务

问题

您的应用需要在计时器上运行一个操作,比如定期更新 UI。

解决方案

(API 一级)

使用由Handler提供的定时操作。有了Handler,可以有效地安排操作在特定的时间发生,或者在指定的延迟之后发生。

它是如何工作的

让我们看一个在TextView中显示当前时间的示例活动。参见清单 6–2。

清单 6–2。 用处理程序更新的活动

`public class TimingActivity extends Activity {

TextView mClock;`

`    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mClock = new TextView(this);
        setContentView(mClock);
    }

private Handler mHandler = new Handler();
    private Runnable timerTask = new Runnable() {
        @Override
        public void run() {
            Calendar now = Calendar.getInstance();
            mClock.setText(String.format("%02d:%02d:%02d",
                    now.get(Calendar.HOUR),
                    now.get(Calendar.MINUTE),
                    now.get(Calendar.SECOND)) );
            //Schedule the next update in one second
            mHandler.postDelayed(timerTask,1000);
        }
    };

@Override
    public void onResume() {
        super.onResume();
        mHandler.post(timerTask);
    }

@Override
    public void onPause() {
        super.onPause();
        mHandler.removeCallbacks(timerTask);
    }
}`

这里我们已经将读取当前时间和更新 UI 的操作封装到一个名为timerTaskRunnable中,它将由已经创建的Handler触发。当活动变得可见时,调用Handler.post()尽快执行任务。在更新了TextView之后,timerTask的最后一个操作是调用处理程序,使用Handler.postDelayed()来调度从现在起一秒钟(1000 毫秒)后的另一次执行。

只要活动没有中断,这个循环就会继续,UI 每秒都会更新。一旦活动暂停(用户离开或其他事情吸引了他们的注意力),Handler.removeCallbacks()删除所有挂起的操作,并确保任务不会被进一步调用,直到活动再次可见。

提示:在这个例子中,我们更新 UI 是安全的,因为Handler是在主线程上创建的。操作将总是在发布它们的Handler所连接的同一个线程上执行。

6–3。计划周期性任务

问题

您的应用需要注册才能定期运行任务,例如检查服务器的更新或提醒用户做一些事情。

解决方案

(API 一级)

利用AlarmManager来管理和执行你的任务。AlarmManager对于调度未来的单个或重复操作非常有用,即使您的应用没有运行,这些操作也需要发生。每当闹钟设定好的时候,AlarmManager就会被交给一个PendingIntent去启动。这个意图可以指向任何系统组件,比如一个ActivityBroadcastReceiverService,当警报触发时执行。

应该注意的是,这种方法最适合于即使在应用代码可能没有运行时也需要发生的操作。AlarmManager需要太多的开销,对于在应用使用时可能需要的简单计时操作来说是无用的。使用HandlerpostAtTime()postDelayed()方法可以更好地处理这些问题。

它是如何工作的

让我们看看如何使用AlarmManager定期触发广播接收器。参见清单 6–3 到清单 6–5。

清单 6–3。 待触发的广播接收器

public class AlarmReceiver extends BroadcastReceiver {     @Override     public void onReceive(Context context, Intent intent) {         //Perform an interesting operation, we'll just display the current time         Calendar now = Calendar.getInstance();         DateFormat formatter = SimpleDateFormat.getTimeInstance();         Toast.makeText(context, formatter.format(now.getTime()),             Toast.LENGTH_SHORT).show();     } }

提醒:必须在清单中用一个<receiver>标签声明一个 BroadcastReceiver ( AlarmReceiver),以便AlarmManager能够触发它。确保在您的<application>标签中包含一个,如下所示:

<application>   …   <receiver android:name=".AlarmReceiver"></receiver> </application>

清单 6–4。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <Button     android:id="@+id/start"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Start Alarm"   />   <Button     android:id="@+id/stop"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Cancel Alarm"   /> </LinearLayout>

清单 6–5。 注册/注销报警的活动

`public class AlarmActivity extends Activity implements View.OnClickListener {

private PendingIntent mAlarmIntent;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        //Attach the listener to both buttons
        findViewById(R.id.start).setOnClickListener(this);
        findViewById(R.id.stop).setOnClickListener(this);
        //Create the launch sender
        Intent launchIntent = new Intent(this, AlarmReceiver.class);
        mAlarmIntent = PendingIntent.getBroadcast(this, 0, launchIntent, 0);
    }

@Override
    public void onClick(View v) {
        AlarmManager manager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
        long interval = 5*1000; //5 seconds

switch(v.getId()) {
        case R.id.start:             Toast.makeText(this, "Scheduled", Toast.LENGTH_SHORT).show();
            manager.setRepeating(AlarmManager.ELAPSED_REALTIME,
                    SystemClock.elapsedRealtime()+interval,
                    interval,
                    mAlarmIntent);
            break;
        case R.id.stop:
            Toast.makeText(this, "Canceled", Toast.LENGTH_SHORT).show();
            manager.cancel(mAlarmIntent);
            break;
        default:
            break;
        }
    }
}`

在这个例子中,我们提供了一个非常基本的 BroadcastReceiver,当它被触发时,会简单地将当前时间显示为 Toast。该接收器必须用一个<receiver>标签在应用的清单中注册。否则,应用外部的AlarmManager将不会知道如何触发它。示例活动提供了两个按钮:一个开始触发常规警报,另一个取消警报。

要触发的操作由PendingIntent引用,它将用于设置和取消报警。我们创建一个直接引用应用的 BroadcastReceiver 的 Intent,然后使用getBroadcast()从该 Intent 创建一个PendingIntent(因为我们正在创建一个对 BroadcastReceiver 的引用)。

提醒: PendingIntent有创建者方法getActivity()也有getService()。确保在创建这个部分时引用正确的应用组件。

当按下开始按钮时,活动使用AlarmManager.setRepeating()记录一个重复报警。除了 PendingIntent 之外,该方法还需要一些参数来确定何时触发警报。第一个参数根据使用的时间单位以及设备处于睡眠模式时是否应该发出警报来定义警报类型。在本例中,我们选择了ELAPSED_REALTIME,它表示自上次设备启动以来的值(单位为毫秒)。此外,还有三种其他模式可供使用:

  • ELAPSED_REALTIME_WAKEUP
    • 报警时间是指经过的时间,如果设备处于睡眠状态,将唤醒设备触发报警。
  • RTC
    • 参照 UTC 时间的报警时间。
  • RTC_WAKEUP
    • 参考 UTC 时间的闹钟时间,如果设备处于睡眠状态,将唤醒设备进行触发。

以下参数(分别)指的是警报第一次触发的时间和重复的时间间隔。因为选择的警报类型是 ELAPSED_REALTIME,开始时间也必须相对于经过时间;SystemClock.elapsedRealtime()以此格式提供当前时间。

示例中的警报被注册为在按下按钮 5 秒后触发,之后每隔 5 秒触发一次。每五秒钟,屏幕上会出现一个带有当前时间值的Toast,即使该应用不再运行或不在用户面前。当用户显示活动并按下停止按钮时,任何与我们的PendingIntent匹配的未决警报都会被立即取消…停止Toast的流程

一个更精确的例子

如果我们想安排一个闹铃在特定的时间发生呢?也许每天早上 9 点一次?用一些稍微不同的参数设置AlarmManager可以实现这一点。参见清单 6–6。

清单 6–6。 精确报警

`    long oneDay = 2436001000; //24 hours
    long firstTime;

//Get a Calendar (defaults to today)
    //Set the time to 09:00:00
    Calendar startTime = Calendar.getInstance();
    startTime.set(Calendar.HOUR_OF_DAY, 9);
    startTime.set(Calendar.MINUTE, 0);
    startTime.set(Calendar.SECOND, 0);

//Get a Calendar at the current time
    Calendar now = Calendar.getInstance();

if(now.before(startTime)) {
        //It's not 9AM yet, start today
        firstTime = startTime.getTimeInMillis();
    } else {
        //Start 9AM tomorrow
        startTime.add(Calendar.DATE, 1);
        firstTime = startTime.getTimeInMillis();
    }

//Set the alarm
    manager.setRepeating(AlarmManager.RTC_WAKEUP,
                   firstTime,
                   oneDay,
                   mAlarmIntent);`

这个例子使用了一个实时报警。确定上午 9:00 的下一次发生是在今天还是明天,并且返回该值作为警报的初始触发时间。然后,以毫秒为单位的 24 小时的计算值作为时间间隔,这样从该时间点开始,每天触发一次警报。

重要提示:警报不会在设备重启后持续存在。如果设备关闭后又重新打开,则必须重新安排任何先前注册的警报。

6–4 岁。创建粘性操作

问题

您的应用需要执行一个或多个后台操作,即使用户暂停应用,这些操作也会运行到完成。

解决方案

(API 三级)

创建一个IntentService的实现来处理这项工作。IntentService是 Android 基础服务实现的包装器,是在后台工作而无需用户交互的关键组件。IntentService对传入的工作进行排队(用 Intents 表示),依次处理每个请求,然后在队列为空时自行停止。

IntentService还处理后台工作所需的工作线程的创建,因此不必使用 AsyncTask 或 Java 线程来确保操作在后台正常进行。

这个菜谱研究了一个使用IntentService创建后台操作的中央管理器的例子。在本例中,将通过调用Context.startService()从外部调用管理器。经理会将收到的所有请求排队,并通过给onHandleIntent()打电话来单独处理它们。

它是如何工作的

让我们来看看如何构造一个简单的IntentService实现来处理一系列后台操作。参见清单 6–7。

清单 6–7。 IntentService 搬运操作

`public class OperationsManager extends IntentService {

public static final String ACTION_EVENT = "ACTION_EVENT";
    public static final String ACTION_WARNING = "ACTION_WARNING";
    public static final String ACTION_ERROR = "ACTION_ERROR";
    public static final String EXTRA_NAME = "eventName";

private static final String LOGTAG = "EventLogger";

private IntentFilter matcher;`

`    public OperationsManager() {
        super("OperationsManager");
        //Create the filter for matching incoming requests
        matcher = new IntentFilter();
        matcher.addAction(ACTION_EVENT);
        matcher.addAction(ACTION_WARNING);
        matcher.addAction(ACTION_ERROR);
    }

@Override
    protectedvoid onHandleIntent(Intent intent) {
        //Check for a valid request
        if(!matcher.matchAction(intent.getAction())) {
            Toast.makeText(this, "OperationsManager: Invalid Request", Toast.LENGTH_SHORT).show();
            return;
        }

//Handle each request directly in this method. Don't create more threads.
        if(TextUtils.equals(intent.getAction(), ACTION_EVENT)) {
            logEvent(intent.getStringExtra(EXTRA_NAME));
        }
        if(TextUtils.equals(intent.getAction(), ACTION_WARNING)) {
            logWarning(intent.getStringExtra(EXTRA_NAME));
        }
        if(TextUtils.equals(intent.getAction(), ACTION_ERROR)) {
            logError(intent.getStringExtra(EXTRA_NAME));
        }
    }

private void logEvent(String name) {
        try {
            //Simulate a long network operation by sleeping
            Thread.sleep(5000);
            Log.i(LOGTAG, name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

private void logWarning(String name) {
        try {
            //Simulate a long network operation by sleeping
            Thread.sleep(5000);
            Log.w(LOGTAG, name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

private void logError(String name) {
        try {
            //Simulate a long network operation by sleeping
            Thread.sleep(5000);
            Log.e(LOGTAG, name);
        } catch (InterruptedException e) {
            e.printStackTrace();         }
    }
}`

注意IntentService没有默认的构造函数(没有参数),所以自定义实现必须实现一个构造函数,用服务名调用 super。这个名称在技术上没有什么重要性,因为它只对调试有用;Android 使用提供的名称来命名它创建的工作线程。

服务通过onHandleIntent()方法处理所有请求。这个方法是在提供的 worker 线程上调用的,所以所有的工作都应该直接在这里完成;不应创建新的线程或操作。当onHandleIntent()返回时,这是 IntentService 开始处理队列中下一个请求的信号。

这个示例提供了三个日志记录操作,可以在请求意图上使用不同的操作字符串来请求这些操作。出于演示目的,每个操作都使用特定的日志记录级别(信息、警告或错误)将提供的消息写入设备日志。请注意,消息本身是作为请求意图的额外内容传递的。使用每个意图的数据和额外字段来保存操作的任何参数,让操作字段来定义操作类型。

示例服务维护一个 IntentFilter,它用于方便地确定是否发出了有效的请求。当创建服务时,所有有效的动作都被添加到过滤器中,允许我们对任何传入的请求调用IntentFilter.matchAction()来确定它是否包括我们可以在这里处理的动作。

清单 6–8 是一个调用这个服务来执行工作的活动的例子。

清单 6–8。 AndroidManifest.xml

`

    


        
            
                
                
            

        

        
    

`

提醒:Android manifest . XML 中的package属性必须与您为应用选择的包相匹配;"com.examples.sticky"只是我们在这里的例子中选择的包。

注意:因为IntentService是作为服务调用的,所以必须使用<service>标签在应用清单中声明它。

清单 6–9。 活动调用 IntentService

`public class ReportActivity extends Activity {

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        logEvent("CREATE");
    }

@Override
    public void onStart() {
        super.onStart();
        logEvent("START");
    }

@Override
    public void onResume() {
        super.onResume();
        logEvent("RESUME");
    }

@Override
    public void onPause() {
        super.onPause();
        logWarning("PAUSE");
    }

@Override
    public void onStop() {
        super.onStop();
        logWarning("STOP");
    }

@Override
    public void onDestroy() {
        super.onDestroy();
        logWarning("DESTROY");
    }

private void logEvent(String event) {
        Intent intent = new Intent(this, OperationsManager.class);
        intent.setAction(OperationsManager.ACTION_EVENT);         intent.putExtra(OperationsManager.EXTRA_NAME, event);

startService(intent);
    }

private void logWarning(String event) {
        Intent intent = new Intent(this, OperationsManager.class);
        intent.setAction(OperationsManager.ACTION_WARNING);
        intent.putExtra(OperationsManager.EXTRA_NAME, event);

startService(intent);
    }
}`

这个活动没什么好看的,因为所有有趣的事件都是通过设备日志发送出去的,而不是发送到用户界面。然而,它有助于说明我们在前一个示例中创建的服务的队列处理行为。当活动变得可见时,它将调用所有正常的生命周期方法,导致对日志服务的三个请求。在处理每个请求时,日志中将输出一行,服务将继续。

提示:这些日志语句可以通过 SDK 提供的logcat工具看到。从大多数开发环境(包括 Eclipse)中都可以看到来自设备或仿真器的logcat输出,或者通过在命令行键入adblogcat.就可以看到

还要注意,当服务完成所有三个请求时,系统会在日志中发出通知,指出服务已经停止。仅在完成作业所需的时间内存中存在;这是一个非常有用的特性,让你的服务成为系统的好公民。

按下 HOME 或 BACK 按钮将导致更多的生命周期方法生成服务请求,并注意暂停/停止/销毁部分调用服务中的单独操作,导致它们的消息被记录为警告;简单地将请求意图的动作字符串设置为不同的值就可以控制这一点。

请注意,即使应用不再可见(或者打开了另一个应用),消息仍会继续输出到日志中。这就是 Android 服务组件的强大之处。无论用户行为如何,这些操作在完成之前都会受到系统保护。

可能的缺点

在每种操作方法中,都设置了五秒钟的延迟,以模拟发出远程 API 或一些类似操作的实际请求所需的时间。当运行这个例子时,它也有助于说明IntentService用单个工作线程以串行方式处理发送给它的所有请求。该示例对来自每个生命周期方法的多个连续请求进行排队,但是结果仍然是每五秒钟一条日志消息,因为 IntentService 在当前请求完成之前不会启动一个新请求(实际上是在onHandleIntent()返回时)。

如果您的应用需要粘性后台任务的并发性,您可能需要创建一个更加定制的服务实现,使用线程池来执行工作。Android 开源的美妙之处在于,如果需要的话,你可以直接找到IntentService的源代码,并将其作为实现的起点,从而最大限度地减少所需的时间和定制代码。

6–5 岁。运行持久的后台操作

问题

您的应用有一个组件,它必须在后台无限期运行,执行某些操作或监视某些事件的发生。

解决方案

(API 一级)

将组件构建成服务。服务被设计为后台组件,应用可以启动这些组件并让它们无限期地运行。就防止在内存不足的情况下被终止而言,服务还被赋予了高于其他后台进程的更高的地位。

对于不需要直接连接到另一个组件的操作(如活动),可以显式地启动和停止服务。但是,如果应用必须直接与服务交互,则提供一个绑定接口来传递数据。在这些情况下,服务可以由系统隐式地启动和停止,这是实现其所请求的绑定所需要的。

对于服务实现,要记住的关键是始终保持用户友好。除非用户明确要求,否则不确定操作很可能不应该启动。整个应用可能应该包含一个界面或设置,允许用户控制启用或禁用这样的服务。

它是如何工作的

清单 6–10 是一个持久化服务的例子,用于在一定时期内跟踪和记录用户的位置。

清单 6–10。 持久跟踪服务

`public class TrackerService extends Service implements LocationListener {

private static final String LOGTAG = "TrackerService";

private LocationManager manager;
    private ArrayList storedLocations;`

`    privateboolean isTracking = false;

/* Service Setup Methods */
    @Override
    public void onCreate() {
        manager = (LocationManager)getSystemService(LOCATION_SERVICE);
        storedLocations = new ArrayList();
        Log.i(LOGTAG, "Tracking Service Running...");
    }

@Override
    public void onDestroy() {
        manager.removeUpdates(this);
        Log.i(LOGTAG, "Tracking Service Stopped...");
    }

public void startTracking() {
        if(!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            return;
        }
        Toast.makeText(this, "Starting Tracker", Toast.LENGTH_SHORT).show();
        manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 30000, 0, this);

isTracking = true;
    }

public void stopTracking() {
        Toast.makeText(this, "Stopping Tracker", Toast.LENGTH_SHORT).show();
        manager.removeUpdates(this);
        isTracking = false;
    }

publicboolean isTracking() {
        return isTracking;
    }

/* Service Access Methods */
    public class TrackerBinder extends Binder {
        TrackerService getService() {
            return TrackerService.this;
        }
    }

private final IBinder binder = new TrackerBinder();

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

publicint getLocationsCount() {
        return storedLocations.size();
    }

public ArrayList getLocations() {
        return storedLocations;     }

/* LocationListener Methods */
    @Override
    public void onLocationChanged(Location location) {
        Log.i("TrackerService", "Adding new location");
        storedLocations.add(location);
    }

@Override
    public void onProviderDisabled(String provider) { }

@Override
    public void onProviderEnabled(String provider) { }

@Override
    public void onStatusChanged(String provider, int status, Bundle extras) { }
}`

该服务的工作是监控和跟踪它从LocationManager接收的更新。当创建服务时,它准备一个空白的Location条目列表,并等待开始跟踪。一个外部组件,比如一个活动,可以调用startTracking()stopTracking()来启用和禁用位置更新到服务的流程。此外,还公开了访问服务已记录的位置列表的方法。

因为这个服务需要来自活动或其他组件的直接交互,所以需要一个 Binder 接口。当服务必须跨越流程边界进行通信时,绑定器的概念可能会变得复杂,但是对于像这样的情况,所有东西都位于同一个流程的本地,使用一个方法getService()创建一个非常简单的绑定器,将服务实例本身返回给调用者。我们稍后将从活动的角度对此进行更详细的讨论。

当在服务上启用跟踪时,它向LocationManager注册更新,并将收到的每个更新存储在其位置列表中。请注意,调用requestLocationUpdates()的最短时间为 30 秒。由于这项服务预计将运行很长时间,谨慎的做法是留出更新时间,让 GPS(以及电池)休息一会儿。

现在让我们来看一个允许用户访问该服务的简单活动。参见清单 6–11 至清单 6–13。

清单 6–11。 AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"       package="com.examples.service"       android:versionCode="1"       android:versionName="1.0">     <uses-sdk android:minSdkVersion="1" />     <application android:icon="@drawable/icon" android:label="@string/app_name">         <activity android:name=".ServiceActivity"                   android:label="@string/app_name">             <intent-filter>                 <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />             </intent-filter>         </activity>         <service android:name=".TrackerService"></service>     </application>     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> </manifest>

提醒:服务必须在应用清单中使用<service>标签声明,这样 Android 就知道如何以及在哪里调用它。此外,对于本例,权限android.permission.ACCESS_FINE_LOCATION是必需的,因为我们正在使用 GPS。

清单 6–12。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <Button     android:id="@+id/enable"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Start Tracking"   />   <Button     android:id="@+id/disable"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Stop Tracking"   />   <TextView     android:id="@+id/status"     android:layout_width="fill_parent"     android:layout_height="wrap_content"   /> </LinearLayout>

清单 6–13。 活动与服务交互

`public class ServiceActivity extends Activity implements View.OnClickListener {

Button enableButton, disableButton;
    TextView statusView;

TrackerService trackerService;
    Intent serviceIntent;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);`

`        enableButton = (Button)findViewById(R.id.enable);
        enableButton.setOnClickListener(this);
        disableButton = (Button)findViewById(R.id.disable);
        disableButton.setOnClickListener(this);
        statusView = (TextView)findViewById(R.id.status);

serviceIntent = new Intent(this, TrackerService.class);
    }

@Override
    public void onResume() {
        super.onResume();
        //Starting the service makes it stick, regardless of bindings
        startService(serviceIntent);
        //Bind to the service
        bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

@Override
    public void onPause() {
        super.onPause();
        if(!trackerService.isTracking()) {
            //Stopping the service let's it die once unbound
            stopService(serviceIntent);
        }
        //Unbind from the service
        unbindService(serviceConnection);
    }

@Override
    public void onClick(View v) {
        switch(v.getId()) {
        case R.id.enable:
            trackerService.startTracking();
            break;
        case R.id.disable:
            trackerService.stopTracking();
            break;
        default:
            break;
        }
        updateStatus();
    }

private void updateStatus() {
        if(trackerService.isTracking()) {
            statusView.setText(
                String.format("Tracking enabled. %d locations                     logged.",trackerService.getLocationsCount()));
        } else {
            statusView.setText("Tracking not currently enabled.");
        }
    }

private ServiceConnection serviceConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            trackerService = ((TrackerService.TrackerBinder)service).getService();             updateStatus();
        }

public void onServiceDisconnected(ComponentName className) {
            trackerService = null;
        }
    };
}`

Figure 6–3 显示了用户启用和禁用位置跟踪行为的两个按钮,以及当前服务状态的文本显示。

images

图 6–3。 服务活动布局

当活动可见时,它被绑定到TrackerService。这是在ServiceConnection接口的帮助下完成的,该接口在绑定和解除绑定操作完成时提供回调方法。将服务绑定到活动后,我们现在可以直接调用服务公开的所有公共方法。

然而,单靠绑定无法让服务长期运行;仅通过绑定器接口访问服务会导致服务随着活动的生命周期自动创建和销毁。在这种情况下,我们希望服务持续到该活动在内存中之后。为了实现这一点,服务在绑定之前通过startService()显式启动。向已经运行的服务发送启动命令没有坏处,所以我们也可以在onResume()中安全地这样做。

服务现在将继续在内存中运行,即使在活动解除自身绑定之后。在onPause()中,这个例子总是检查用户是否激活了跟踪,如果没有,它首先停止服务。这允许服务在不需要跟踪的情况下终止,从而防止服务在没有实际工作要做的情况下永远挂在内存中。

运行这个例子,并按下 Start Tracking 按钮将会启动持久服务和LocationManager。用户可以在这一点上离开应用,并且服务将保持运行,同时记录来自 GPS 的所有输入位置更新。当用户返回到这个应用时,他们可以看到服务仍然在运行,并且显示当前存储的位置点的数量。按 Stop Tracking 将结束该过程,并允许服务在用户再次离开活动时立即终止。

6–6 岁。启动其他应用

问题

您的应用需要特定的功能,而设备上的另一个应用已经对该功能进行了编程。为了避免重叠功能,您希望启动该作业的另一个应用。

解决方案

(API 一级)

使用一个隐含的意图来告诉系统你想做什么,并确定是否有任何应用可以满足需要。大多数情况下,开发人员以明确的方式使用意图来开始另一个活动或服务,就像这样:

Intent intent = new Intent(this, NewActivity.class); startActivity(intent);

通过声明我们想要启动的特定组件,其交付意图非常明确。我们也有能力根据意图的动作、类别、数据和类型来定义意图,以定义我们想要完成什么任务的更隐含的需求。

当以这种方式启动时,外部应用总是在与您的应用相同的 Android 任务中启动,因此一旦操作完成(或者如果用户退出),用户就会返回到您的应用。这保持了无缝的体验,从用户的角度来看,允许多个应用作为一个整体。

它是如何工作的

当以这种方式定义意图时,可能不清楚您必须包括什么信息,因为没有发布的标准,并且提供相同服务(例如,读取 PDF 文件)的两个应用可能定义稍微不同的过滤器来监听传入的意图。您希望确保并为系统(或用户)提供足够的信息,以选择处理所需任务的最佳应用。

定义几乎所有隐含意图的核心数据是动作;在构造函数中或通过Intent.setAction()传递的字符串值。这个值告诉 Android 你想做什么,是查看一段内容,发送一条消息,选择一个选项,还是你有什么。由此,所提供的字段是特定于场景的,并且通常多种组合可以得到相同的结果。让我们来看看一些有用的例子。

阅读 PDF 文件

显示 PDF 文档的组件不包括在核心 SDK 中,尽管今天市场上几乎每个消费 Android 设备都附带了 PDF 阅读器应用,Android Market 上还有许多其他应用。因此,在应用中嵌入 PDF 显示功能可能没有意义。

相反,下面的清单 6–14 说明了如何找到并启动另一个应用来查看 PDF。

清单 6–14。 查看 PDF 的方法

private void viewPdf(Uri file) {         Intent intent;         intent = new Intent(Intent.ACTION_VIEW);         intent.setDataAndType(file, "application/pdf");         try {             startActivity(intent);         } catch (ActivityNotFoundException e) {             //No application to view, ask to download one             AlertDialog.Builder builder = new AlertDialog.Builder(this);             builder.setTitle("No Application Found");             builder.setMessage("We could not find an application to view PDFs."                     +"  Would you like to download one from Android Market?");             builder.setPositiveButton("Yes, Please",                 new DialogInterface.OnClickListener() {                 @Override                 public void onClick(DialogInterface dialog, int which) {                     Intent marketIntent = new Intent(Intent.ACTION_VIEW);                     marketIntent.setData(Uri.parse("market://details?id=com.adobe.reader"));                     startActivity(marketIntent);                 }             });             builder.setNegativeButton("No, Thanks", null);             builder.create().show();         }     }

此示例方法将使用找到的最佳应用打开设备(内部或外部存储器)上的任何本地 PDF 文件。如果在设备上找不到查看 pdf 的应用,我们鼓励用户去 Android Market 下载一个。

我们为此创建的意图是使用通用的Intent.ACTION_VIEW动作字符串构建的,告诉系统我们想要查看意图中提供的数据。数据文件本身及其 MIME 类型也被设置为告诉系统我们想要查看哪种数据。

提示: Intent.setData()Intent.setType()使用时互相清零对方以前的值。如果您需要同时设置两者,请使用示例中的Intent.setDataAndType(),

如果startActivity()ActivityNotFoundException而失败,这意味着用户的设备上没有安装可以查看 pdf 的应用。我们希望我们的用户有完整的体验,所以如果发生这种情况,我们会显示一个对话框告诉他们问题,并询问他们是否愿意去市场上买一个阅读器。如果用户按下 Yes,我们使用另一个隐含的意图来请求 Android Market 直接打开到 Adobe Reader 的应用页面,这是一个用户可以下载来查看 PDF 文件的免费应用。我们将在下一个秘籍中讨论用于这个目的的Uri方案。

注意,示例方法将一个Uri参数传递给本地文件。以下是如何检索位于内部存储上的文件的Uri的示例:

String filename = NAME_OF YOUR_FILE; File internalFile = getFileStreamPath(filename); Uri internal = Uri.fromFile(internalFile);

方法getFileStreamPath()是从Context调用的,所以如果这个代码不在活动中,你必须引用一个Context对象来调用。以下是如何为位于外部存储器上的文件创建一个Uri:

String filename = NAME_OF YOUR_FILE; File externalFile = new File(Environment.getExternalStorageDirectory(), filename); Uri external = Uri.fromFile(externalFile);

这个例子也适用于任何其他文档类型,只需简单地改变附加到意图的 MIME 类型。

与朋友分享

开发人员在他们的应用中包含的另一个流行特性是与他人共享应用内容的方法;通过电子邮件、短信和著名的社交网络。所有的 Android 设备都包括电子邮件和短信应用,大多数希望通过社交网络(如脸书或 Twitter)分享的用户也在他们的设备上安装了这些移动应用。

事实证明,这项任务也可以使用隐式意图来完成,因为大多数应用都会以某种方式响应Intent.ACTION_SEND动作字符串。 清单 6–15 是一个允许用户通过一个意向请求向任何媒体发帖的例子。

清单 6–15。 分享意向

private void shareContent(String update) {         Intent intent = new Intent(Intent.ACTION_SEND);         intent.setType("text/plain");         intent.putExtra(Intent.EXTRA_TEXT, update);         startActivity(Intent.createChooser(intent, "Share..."));     }

这里,我们告诉系统我们有一段文本要发送,作为额外的内容传入。这是一个非常普通的请求,我们希望不止一个应用能够处理它。默认情况下,Android 会给用户一个应用列表,让用户选择想要打开的应用。此外,一些设备为用户提供了一个复选框,将他们的选择设置为默认值,这样列表就不会再显示了!

我们希望对这个过程有更多一点的控制,因为我们也希望每次都有多个结果。因此,我们没有将意图直接传递给startActivity(),而是首先通过Intent.createChooser()传递,这允许我们定制标题,并保证选择列表将始终显示。

当用户选择一个选项时,特定的应用将启动,并在消息输入框中预填充EXTRA_TEXT,准备共享!

6–7 岁。启动系统应用

问题

您的应用需要特定的功能,而设备上的某个系统应用已经对该功能进行了编程。为了避免重叠功能,您希望启动作业的系统应用。

解决方案

(API 一级)

使用隐含的意图告诉系统你对哪个应用感兴趣。每个系统应用订阅一个定制的Uri方案,该方案可以作为数据插入到一个隐含的意图中,以表示您需要启动的特定应用。

以这种方式启动时,外部应用总是在与您的应用相同的任务中启动,因此一旦任务完成(或者如果用户退出),用户就会返回到您的应用。这保持了无缝的体验,从用户的角度来看,允许多个应用作为一个整体。

工作原理

下面所有的例子都将构造可以用来在不同状态下启动系统应用的意图。一旦构建完成,您应该通过将所述意图传递给startActivity().来启动这些应用

浏览器

可以启动浏览器应用来显示网页或运行网络搜索。

要显示网页,请构建并启动以下意图:

Intent pageIntent = new Intent(); pageIntent.setAction(Intent.ACTION_VIEW); pageIntent.setData(Uri.parse(“http://WEB_ADDRESS_TO_VIEW”));

这将数据字段中的Uri替换为您想要查看的页面。要在浏览器中启动 web 搜索,请构建并启动以下意图:

Intent searchIntent = new Intent(); searchIntent.setAction(Intent.ACTION_WEB_SEARCH); searchIntent.putExtra(SearchManager.QUERY, STRING_TO_SEARCH);

这将把您想要执行的搜索查询作为额外内容放在意图中。

电话拨号程序

可以启动拨号器应用,使用以下意图向特定号码发出呼叫:

Intent dialIntent = new Intent(); dialIntent.setAction(Intent.ACTION_DIAL); dialIntent.setData(Uri.Parse(“tel:8885551234”);

这将数据 Uri 中的电话号码替换为要呼叫的号码。

注意:这个动作只是调出拨号器;它实际上并不发出呼叫。Intent.ACTION_CALL可以用来直接拨打电话,尽管谷歌不鼓励在大多数情况下使用它。使用ACTION_CALL还需要在清单中声明android.permission.CALL_PHONE权限。

地图

可以启动设备上的地图应用来显示位置或提供两点之间的方向。如果您知道要绘制地图的位置的纬度和经度,则创建以下意图:

Intent mapIntent = new Intent(); mapIntent.setAction(Intent.ACTION_VIEW); mapIntent.setData(Uri.parse(“geo:latitude,longitude”));

这将替换您所在位置的经纬度坐标。例如,Uri

"geo:37.422,122.084"

会标出谷歌总部的位置。如果您知道要显示的位置的地址,则创建以下意图:

Intent mapIntent = new Intent(); mapIntent.setAction(Intent.ACTION_VIEW); mapIntent.setData(Uri.parse(“geo:0,0?q=ADDRESS”));

这将插入您想要映射的地址。例如,Uri

"geo:0,0?q=1600 Amphitheatre Parkway, Mountain View, CA 94043"

会绘制谷歌总部的地址。

提示:地图应用也将接受一个Uri,其中地址查询中的空格将被替换为“+”字符。如果对包含空格的字符串进行编码有困难,请尝试用“+”替换它们。

如果您想要显示至位置之间的方向,请创建以下意图:

Intent mapIntent = new Intent(); mapIntent.setAction(Intent.ACTION_VIEW); mapIntent.setData(Uri.parse(“http://maps.google.com/maps?saddr=lat,lng&daddr=lat,lng”));

这将插入起始和结束地址的位置。

如果您希望打开一个地址开放的地图应用,也可以只包含其中一个参数。例如,Uri

"http://maps.google.com/maps?&daddr=37.422,122.084"

将显示地图应用与目的地位置预填充,但允许用户输入自己的起始地址。

电子邮件

设备上的任何电子邮件应用都可以使用以下意图启动到撰写模式:

Intent mailIntent = new Intent(); mailIntent.setAction(Intent.ACTION_SEND); mailIntent.setType(“message/rfc822”); mailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"recipient@gmail.com"}); mailIntent.putExtra(Intent.EXTRA_CC, new String[] {"carbon@gmail.com"}); mailIntent.putExtra(Intent.EXTRA_BCC, new String[] {"blind@gmail.com"}); mailIntent.putExtra(Intent.EXTRA_SUBJECT, "Email Subject"); mailIntent.putExtra(Intent.EXTRA_TEXT, "Body Text"); mailIntent.putExtra(Intent.EXTRA_STREAM, URI_TO_FILE);

在这种情况下,action 和 type 字段是显示空白电子邮件的唯一必需部分。所有剩余的 extras 都预先填充了电子邮件消息的特定字段。请注意,EXTRA_EMAIL(填充 To:字段)、EXTRA_CCEXTRA_BCC被传递给了字符串数组,即使那里只放置了一个收件人。也可以使用EXTRA_STREAM在意向中指定文件附件。这里传递的值应该是一个指向要附加的本地文件的Uri

如果您需要在电子邮件中附加多个文件,要求会稍有变化,如下所示:

`Intent mailIntent = new Intent();
mailIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
mailIntent.setType(“message/rfc822”);
mailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"recipient@gmail.com"});
mailIntent.putExtra(Intent.EXTRA_CC, new String[] {"carbon@gmail.com"});
mailIntent.putExtra(Intent.EXTRA_BCC, new String[] {"blind@gmail.com"});
mailIntent.putExtra(Intent.EXTRA_SUBJECT, "Email Subject");
mailIntent.putExtra(Intent.EXTRA_TEXT, "Body Text");

ArrayList files = new ArrayList();
files.add(URI_TO_FIRST_FILE);
files.add(URI_TO_SECOND_FILE);
//...Repeat add() as often as necessary to add all the files you need
mailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, files);`

注意,意向的动作字符串现在是ACTION_SEND_MULTIPLE。除了作为EXTRA_STREAM添加的数据之外,所有的主字段保持不变。这个例子创建了一个指向您想要附加的文件的 uri 列表,并使用putParcelableArrayListExtra()添加它们。

对于用户来说,在他们的设备上有多个应用可以处理这些内容并不少见,所以通常谨慎的做法是在将这些构建的意图传递给startActivity()之前用Intent.createChooser()包装它们。

短信(Messages)

可以使用以下意图将消息应用启动到新 SMS 消息的撰写模式:

Intent smsIntent = new Intent(); smsIntent.setAction(Intent.ACTION_VIEW); smsIntent.setType(“vnd.android-dir/mms-sms”); smsIntent.putExtra(“address”, “8885551234”); smsIntent.putExtra(“sms_body”, “Body Text”);

与撰写电子邮件一样,您必须至少设置操作和类型,以启动带有空白消息的应用。包括地址和 sms_body extras 允许应用预先填充消息的收件人(地址)和正文文本(sms_body)。

请注意,这两个键都没有在 Android 框架中定义的常量,这意味着它们将来会发生变化。然而,在撰写本文时,这些键在所有版本的 Android 上都表现正常。

联系提货人

应用可以启动默认联系人选取器,以便用户使用以下意图从他们的联系人数据库中进行选择:

Intent pickIntent = new Intent(); pickIntent.setAction(Intent.ACTION_PICK); pickIntent.setData(URI_TO_CONTACT_TABLE);

这个意图要求将您感兴趣的 Contacts 表的CONTENT_URI传递到数据字段中。由于在 API Level 5 (Android 2.0)和更高版本中对 Contacts API 进行了重大更改,如果您支持跨边界的版本,这可能与Uri不同。

例如,要在 2.0 之前的设备上从联系人列表中选择一个人,我们将传递

android.provider.Contacts.People.CONTENT_URI

但是,在 2.0 和更高版本中,类似的数据将通过传递

android.provider.ContactsContract.Contacts.CONTENT_URI

关于您需要访问的联系数据,请务必查阅 API 文档。

安卓市场

Android Market 可以从应用中启动,以显示特定应用的详细信息页面或运行特定关键字的搜索。要启动特定的应用市场页面,请使用以下意图:

Intent marketIntent = new Intent(); marketIntent.setAction(Intent.ACTION_VIEW); marketIntent.setData(Uri.parse(“market://details?id=PACKAGE_NAME_HERE”));

这将插入您要显示的应用的唯一包名(如“com.adobe.reader”)。如果您想通过搜索查询打开市场,请使用以下意图:

Intent marketIntent = new Intent(); marketIntent.setAction(Intent.ACTION_VIEW); marketIntent.setData(Uri.parse(“market://search?q=SEARCH_QUERY”));

插入要搜索的查询字符串。搜索查询本身可以采取三种主要形式之一:

  • q=<simple text string here>
    • 在这种情况下,搜索将是市场的关键字风格搜索。
  • q=pname:<package name here>
    • 在这种情况下,将搜索包名,只返回完全匹配的包名。
  • q=pub:<developer name here>
    • 在这种情况下,将搜索开发人员姓名字段,只返回完全匹配的内容。

6–8 岁。让其他应用启动您的应用

问题

您已经创建了一个绝对擅长完成特定任务的应用,并且您希望为设备上的其他应用提供一个接口,以便能够运行您的应用。

解决方案

(API 一级)

为您想要公开的活动或服务创建一个IntentFilter,然后公开记录正确访问它所需的动作、数据类型和附加内容。回想一下,意图的动作、类别和数据/类型都可以用作将请求匹配到您的应用的标准。任何额外的必需或可选参数都应该作为额外参数传入。

它是如何工作的

假设您已经创建了一个应用,其中包含一个播放视频的活动,并在播放过程中在屏幕顶部选择视频的标题。您希望允许其他应用使用您的应用播放视频,因此我们需要为应用定义一个有用的意图结构来传递所需的数据,然后在应用清单中的活动上创建一个IntentFilter来匹配。

这个假设的活动需要两个数据来完成它的工作:

  1. 本地或远程视频的Uri
  2. 一个代表视频标题的String

如果应用专门处理某种类型的视频,我们可以定义一个通用的动作(比如 ACTION_VIEW ),并根据我们想要处理的视频内容的数据类型进行更具体的过滤。清单 6–16 是一个如何在清单中定义活动的例子,以这种方式过滤意图。

清单 6–16。androidmanifest . XML元素带数据类型过滤器

    <activity android:name=".PlayerActivity">         <intent-filter>             <action android:name="android.intent.action.VIEW" />             <category android:name="android.intent.category.DEFAULT" />             <data android:mimeType="video/h264" />         </intent-filter>     </activity>

该过滤器将匹配任何带有Uri数据的意图,这些数据要么被明确声明为 H.264 视频剪辑,要么在检查Uri文件时被确定为 H.264。然后,外部应用将能够调用此活动,使用以下代码行播放视频:

Uri videoFile = A_URI_OF_VIDEO_CONTENT; Intent playIntent = new Intent(Intent.ACTION_VIEW); playIntent.setDataAndType(videoFile, “video/h264”); playIntent.putExtra(Intent.EXTRA_TITLE, “My Video”); startActivity(playIntent);

在某些情况下,外部应用直接引用这个播放器作为目标可能更有用,而不管它们想要传入的视频类型。在这种情况下,我们将为意图实现创建一个唯一的自定义操作字符串。清单中附加到活动的过滤器只需要匹配定制的动作字符串。参见清单 6–17。

清单 6–17。Android manifest . XML元素带自定义动作过滤器

    <activity android:name=".PlayerActivity">         <intent-filter>             <action android:name="com.examples.myplayer.PLAY" />             <category android:name="android.intent.category.DEFAULT" />         </intent-filter>     </activity>

外部应用可以调用此活动,使用以下代码播放视频:

Uri videoFile = A_URI_OF_VIDEO_CONTENT; Intent playIntent = new Intent(“com.examples.myplayer.PLAY”); playIntent.setData(videoFile); playIntent.putExtra(Intent.EXTRA_TITLE, “My Video”); startActivity(playIntent);

处理成功的发射

不管意图如何与活动相匹配,一旦活动启动,我们希望检查活动完成其预期目的所需的两条数据的传入意图。参见清单 6–18。

清单 6–18。 活动考察意向

`public class PlayerActivity extends Activity {

public static final String ACTION_PLAY = "com.examples.myplayer.PLAY";

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

//Inspect the Intent that launched us
        Intent incoming = getIntent();         //Get the video URI from the data field
        Uri videoUri = incoming.getData();
        //Get the optional title extra, if it exists
        String title;
        if(incoming.hasExtra(Intent.EXTRA_TITLE)) {
            title = incoming.getStringExtra(Intent.EXTRA_TITLE);
        } else {
            title = "";
        }

/* Begin playing the video and displaying the title */
    }

/* Remainder of the Activity Code */

}`

活动发起时,可以通过Activity.getIntent()检索调用意图。因为视频内容的Uri是在 Intent 的数据字段中传递的,所以通过调用Intent.getData()对其进行解包。我们已经确定视频的标题是调用意图的可选值,所以我们检查 extras 包,首先查看调用者是否决定传递它;如果存在,该值也会从意图中解包。

注意,本例中的PlayerActivity的确将定制动作字符串定义为一个常量,但是在我们上面构建的启动活动的示例意图中没有引用它。因为这个调用来自外部应用,所以它不能访问这个应用中定义的共享公共常量。

因此,尽可能重用 SDK 中已经存在的 Intent extra 键也是一个好主意,而不是定义新的常量。在本例中,我们选择了标准意图。EXTRA_TITLE 来定义要传递的可选 EXTRA,而不是为该值创建一个自定义键。

6–9 岁。与联系人交互

问题

您的应用需要直接与 Android 向用户联系人公开的ContentProvider进行交互,以添加、查看、更改或删除数据库中的信息。

解决方案

(API 等级 5)

使用ContactsContract公开的接口访问数据。ContactsContract是一个庞大的ContentProvider API,它试图将存储在系统中的来自多个用户账户的联系信息聚合到一个数据存储中。结果是一个由Uris、表和列组成的迷宫,从中可以访问和修改数据。

联系人结构是一个具有三层的层次结构:联系人、原始联系人和数据。

  • 联系人在概念上代表一个人,是 Android 认为代表同一个人的所有RawContacts的集合。
  • RawContacts表示存储在设备中的来自特定设备帐户的数据集合,例如用户的电子邮件地址簿、脸书帐户或其他。
  • 数据元素是附加到每个RawContacts的特定信息,比如电子邮件地址、电话号码或邮政地址。

完整的 API 有太多的组合和选项,我们无法在这里一一介绍,所以请查阅 SDK 文档了解所有的可能性。我们将研究如何构建执行查询和更改 contacts 数据集的基本构建块。

它是如何工作的

Android Contacts API 归结为一个包含多个表和连接的复杂数据库。因此,访问数据的方法与从应用访问任何其他 SQLite 数据库的方法没有什么不同。

列出/查看联系人

让我们看一个示例活动,它列出了数据库中的所有联系人条目,当选择一个条目时,会显示更多的细节。参见清单 6–19。

重要提示:为了在应用中显示联系人 API 的信息,您需要在应用清单中声明android.permission.READ_CONTACTS

清单 6–19。 活动显示联系人

`public class ContactsActivity extends ListActivity implements AdapterView.OnItemClickListener {

Cursor mContacts;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Return all contacts, ordered by name
        String[] projection = new String[] { ContactsContract.Contacts._ID,
                ContactsContract.Contacts.DISPLAY_NAME };
        mContacts = managedQuery(ContactsContract.Contacts.CONTENT_URI,
                projection, null, null, ContactsContract.Contacts.DISPLAY_NAME);

// Display all contacts in a ListView
        SimpleCursorAdapter mAdapter = new SimpleCursorAdapter(this,
                android.R.layout.simple_list_item_1, mContacts,
                new String[] { ContactsContract.Contacts.DISPLAY_NAME },
                newint[] { android.R.id.text1 });
        setListAdapter(mAdapter);         // Listen for item selections
        getListView().setOnItemClickListener(this);
    }

@Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        if (mContacts.moveToPosition(position)) {
            int selectedId = mContacts.getInt(0); // _ID column
            // Gather email data from email table
            Cursor email = getContentResolver().query(
                    CommonDataKinds.Email.CONTENT_URI,
                    new String[] { CommonDataKinds.Email.DATA },
                    ContactsContract.Data.CONTACT_ID + " = " + selectedId, null, null);
            // Gather phone data from phone table
            Cursor phone = getContentResolver().query(
                    CommonDataKinds.Phone.CONTENT_URI,
                    new String[] { CommonDataKinds.Phone.NUMBER },
                    ContactsContract.Data.CONTACT_ID + " = " + selectedId, null, null);
            // Gather addresses from address table
            Cursor address = getContentResolver().query(
                    CommonDataKinds.StructuredPostal.CONTENT_URI,
                    new String[] { CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS },
                    ContactsContract.Data.CONTACT_ID + " = " + selectedId, null, null);

//Build the dialog message
            StringBuilder sb = new StringBuilder();
            sb.append(email.getCount() + " Emails\n");
            if (email.moveToFirst()) {
                do {
                    sb.append("Email: " + email.getString(0));
                    sb.append('\n');
                } while (email.moveToNext());
                sb.append('\n');
            }
            sb.append(phone.getCount() + " Phone Numbers\n");
            if (phone.moveToFirst()) {
                do {
                    sb.append("Phone: " + phone.getString(0));
                    sb.append('\n');
                } while (phone.moveToNext());
                sb.append('\n');
            }
            sb.append(address.getCount() + " Addresses\n");
            if (address.moveToFirst()) {
                do {
                    sb.append("Address:\n" + address.getString(0));
                } while (address.moveToNext());
                sb.append('\n');
            }

AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle(mContacts.getString(1)); // Display name
            builder.setMessage(sb.toString());
            builder.setPositiveButton("OK", null);
            builder.create().show();

// Finish temporary cursors             email.close();
            phone.close();
            address.close();
        }
    }
}`

正如您所看到的,在这个 API 中引用所有的表和列会导致非常冗长的代码。本例中对Uris、表和列的所有引用都是源于ContactsContract的内部类。在与 Contacts API 交互时,验证您引用的是正确的类是很重要的,因为任何不是源于ContactsContract的 Contacts 类都是不推荐的和不兼容的。

创建活动后,我们通过用Contacts.CONTENT_URI调用Activity.managedQuery()对核心 Contacts 表进行简单的查询,只请求我们需要将光标放在ListAdapter中的列。结果光标显示在用户界面上的列表中。这个例子利用了ListActivity的便利行为来提供一个ListView作为内容视图,这样我们就不必管理这些组件了。

此时,用户可以滚动设备上的所有联系人条目,并点击其中一个条目以获得更多信息。当一个列表项被选中时,该特定联系人的 _ID 值被记录下来,应用转到其他的ContactsContract.Data表来收集更详细的信息。请注意,这个联系人的数据分布在多个表中(电子邮件表中的电子邮件、电话表中的电话号码等等),需要多次查询才能获得。

每个CommonDataKinds表都有一个惟一的CONTENT_URI供查询引用,还有一组惟一的列别名用于请求数据。这些数据表中的所有行都通过Data.CONTACT_ID链接到特定的联系人,因此每个游标都要求只返回值匹配的行。

收集了所选联系人的所有数据后,我们遍历结果并在对话框中显示给用户。由于这些表中的数据是多个来源的集合,因此所有这些查询返回多个结果的情况并不少见。对于每个光标,我们显示结果的数量,然后追加每个包含的值。当所有的数据组成后,对话框被创建并显示给用户。

最后一步,所有临时和非托管游标在不再需要时立即关闭。

Running the Application

在设置了任意数量帐户的设备上运行该应用时,您可能会注意到的第一件事是,该列表似乎非常长,肯定比运行与设备捆绑的联系人应用时显示的要长得多。联系人 API 允许存储分组条目,这些条目可能对用户隐藏并用于内部目的。Gmail 经常使用它来存储收到的电子邮件地址,以便快速访问,即使该地址与真实的联系人无关。

在下一个例子中,我们将展示如何过滤这个列表,但是现在我们要惊叹于联系人表中真正存储的数据量。

更改/添加联系人

现在让我们看一个操作特定联系人数据的示例活动。参见清单 6–20。

重要提示:为了与应用中的联系人 API 进行交互,您必须在应用清单中声明android.permission.READ_CONTACTSandroid.permission.WRITE_CONTACTS

清单 6–20。 活动写入联系人 API

`public class ContactsEditActivity extends ListActivity implements
            AdapterView.OnItemClickListener, DialogInterface.OnClickListener {

private static final String TEST_EMAIL = "test@email.com";

private Cursor mContacts, mEmail;
    private int selectedContactId;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Return all contacts, ordered by name
        String[] projection = new String[] { ContactsContract.Contacts._ID,
                ContactsContract.Contacts.DISPLAY_NAME };
        //List only contacts visible to the user
        mContacts = managedQuery(ContactsContract.Contacts.CONTENT_URI,
                projection,
                ContactsContract.Contacts.IN_VISIBLE_GROUP+" = 1",
                null, ContactsContract.Contacts.DISPLAY_NAME);

// Display all contacts in a ListView
        SimpleCursorAdapter mAdapter = new SimpleCursorAdapter(this,
                android.R.layout.simple_list_item_1, mContacts,
                new String[] { ContactsContract.Contacts.DISPLAY_NAME },
                newint[] { android.R.id.text1 });

setListAdapter(mAdapter);
        // Listen for item selections
        getListView().setOnItemClickListener(this);
    }

@Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        if (mContacts.moveToPosition(position)) {
            selectedContactId = mContacts.getInt(0); // _ID column
            // Gather email data from email table
            String[] projection = new String[] { ContactsContract.Data._ID,
                    ContactsContract.CommonDataKinds.Email.DATA };
            mEmail = getContentResolver().query(                     ContactsContract.CommonDataKinds.Email.CONTENT_URI,
                    projection,
                    ContactsContract.Data.CONTACT_ID+" = "+selectedContactId, null, null);
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("Email Addresses");
            builder.setCursor(mEmail, this, ContactsContract.CommonDataKinds.Email.DATA);
            builder.setPositiveButton("Add", this);
            builder.setNegativeButton("Cancel", null);
            builder.create().show();
        }
    }

@Override
    public void onClick(DialogInterface dialog, int which) {
        //Data must be associated with a RAW contact, retrieve the first raw ID
        Cursor raw = getContentResolver().query(
                ContactsContract.RawContacts.CONTENT_URI,
                new String[] { ContactsContract.Contacts._ID },
                ContactsContract.Data.CONTACT_ID+" = "+selectedContactId, null, null);
        if(!raw.moveToFirst()) {
            return;
        }

int rawContactId = raw.getInt(0);
        ContentValues values = new ContentValues();
        switch(which) {
        case DialogInterface.BUTTON_POSITIVE:
            //User wants to add a new email
            values.put(ContactsContract.CommonDataKinds.Email.RAW_CONTACT_ID, rawContactId);
            values.put(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
            values.put(ContactsContract.CommonDataKinds.Email.DATA, TEST_EMAIL);
            values.put(ContactsContract.CommonDataKinds.Email.TYPE,
                    ContactsContract.CommonDataKinds.Email.TYPE_OTHER);
            getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);
            break;
        default:
            //User wants to edit selection
            values.put(ContactsContract.CommonDataKinds.Email.DATA, TEST_EMAIL);
            values.put(ContactsContract.CommonDataKinds.Email.TYPE,
                    ContactsContract.CommonDataKinds.Email.TYPE_OTHER);
            getContentResolver().update(ContactsContract.Data.CONTENT_URI, values,
                    ContactsContract.Data._ID+" = "+mEmail.getInt(0), null);
            break;
        }

//Don't need the email cursor anymore
        mEmail.close();
    }
}`

在这个例子中,我们像以前一样开始,对 Contacts 数据库中的所有条目执行查询。这一次,我们提供了单一的选择标准:

ContactsContract.Contacts.IN_VISIBLE_GROUP+" = 1"

这一行的作用是将返回的条目限制为只包括那些通过联系人用户界面对用户可见的条目。这将(在某些情况下,极大地)减小活动中显示的列表的大小,并使其与联系人应用中显示的列表更加匹配。

当用户从该列表中选择一个联系人时,将显示一个对话框,其中列出了该联系人的所有电子邮件条目。如果从列表中选择了特定的地址,则编辑该条目;并且如果按下添加按钮,则添加新的电子邮件地址条目。为了简化示例,我们不提供输入新电子邮件地址的界面。而是插入一个常数值,作为新记录或对所选记录的更新。

电子邮件地址等数据元素只能与一个RawContact相关联。因此,当我们想要添加一个新的电子邮件地址时,我们必须获得由用户选择的更高级别联系人表示的 RawContacts 之一的 ID。出于示例的目的,我们对哪一个不太感兴趣,所以我们检索第一个匹配的 RawContact 的 ID。只有在执行插入时才需要该值,因为更新引用了表中已经存在的电子邮件记录的不同行 ID。

还要注意的是,CommonDataKinds中提供的用于读取该数据的别名Uri不能用于进行更新和更改。插入和更新必须直接在ContactsContract.DataUri上调用。这意味着(除了在操作方法中引用不同的Uri之外)还必须指定一个额外的元数据MIMETYPE。如果没有为插入的数据设置MIMETYPE字段,后续查询可能不会将其识别为联系人的电子邮件地址。

Aggregation at Work

因为这个示例通过添加或编辑具有相同值的电子邮件地址来更新记录,所以它提供了一个独特的机会来实时查看 Android 的聚合操作。当您运行这个示例应用时,您可能会注意到这样一个事实,即添加或编辑联系人以给他们相同的电子邮件地址经常会触发 Android 开始认为以前分开的联系人现在是同一个人。即使在这个示例应用中,当附加到核心 Contacts 表的托管查询更新时,请注意,某些联系人会随着它们聚合在一起而消失。

注意:Android 模拟器上没有完全实现联系人聚合行为。要完全看到这种效果,您需要在真实设备上运行代码。

维护参考

Android Contacts API 引入了另一个重要的概念,这取决于应用的范围。由于这种聚合过程的发生,引用联系人的不同行 ID 变得非常不稳定;当某个联系人与另一个联系人聚合在一起时,该联系人可以接收新的 ID。

如果您的应用需要对特定联系人的长期引用,建议您的应用保留ContactsContract.Contacts.LOOKUP_KEY,而不是行 ID。当使用该键查询联系人时,还会提供一个特殊的Uri作为ContactsContract.Contacts.CONTENT_LOOKUP_URI。使用这些值来长期查询记录将保护您的应用不会被自动聚合过程所混淆。

6 到 10 岁。挑选设备媒体

问题

您的应用需要导入用户选择的媒体项目(音频、视频或图像)以供显示或回放。

解决方案

(API 一级)

使用以Intent.ACTION_GET_CONTENT为目标的隐含意图,调出系统媒体选择器界面。用感兴趣的媒体(音频、视频或图像)的匹配内容类型激发这个意图,将为用户提供一个选择器界面来选择一个项目,并且意图结果将包括一个指向他们所做选择的 Uri。

它是如何工作的

让我们看看在一个示例活动的上下文中使用的这种技术。参见清单 6–21 和清单 6–22。

清单 6–21。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <Button     android:id="@+id/imageButton"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Images"   />   <Button     android:id="@+id/videoButton"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Video"   />
  <Button     android:id="@+id/audioButton"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Audio"   /> </LinearLayout>

清单 6–22。 活动挑选媒体

`public class MediaActivity extends Activity implements View.OnClickListener {

private static final intREQUEST_AUDIO = 1;
    private static final intREQUEST_VIDEO = 2;
    private static final intREQUEST_IMAGE = 3;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

Button images = (Button)findViewById(R.id.imageButton);
        images.setOnClickListener(this);
        Button videos = (Button)findViewById(R.id.videoButton);
        videos.setOnClickListener(this);
        Button audio = (Button)findViewById(R.id.audioButton);
        audio.setOnClickListener(this);

}

@Override
    protectedvoid onActivityResult(int requestCode, int resultCode, Intent data) {

if(resultCode == Activity.RESULT_OK) {
            //Uri to user selection returned in the Intent
            Uri selectedContent = data.getData();

if(requestCode == REQUEST_IMAGE) {
                //Display the image
            }
            if(requestCode == REQUEST_VIDEO) {
                //Play the video clip
            }
            if(requestCode == REQUEST_AUDIO) {
                //Play the audio clip
            }
        }
    }

@Override
    public void onClick(View v) {
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_GET_CONTENT);
        switch(v.getId()) {
        case R.id.imageButton:
            intent.setType("image/");
            startActivityForResult(intent, REQUEST_IMAGE);
            return;         case R.id.videoButton:
            intent.setType("video/
");
            startActivityForResult(intent, REQUEST_VIDEO);
            return;
        case R.id.audioButton:
            intent.setType("audio/*");
            startActivityForResult(intent, REQUEST_AUDIO);
            return;
        default:
            return;
        }
    }
}`

这个例子有三个按钮供用户按下,每个按钮针对一种特定类型的媒体。当用户按下这些按钮中的任何一个时,带有Intent.ACTION_GET_CONTENT动作字符串的意图被发送给系统,启动适当的选取器活动。如果用户选择了一个有效的条目,指向该条目的内容Uri将在结果意图中返回,状态为RESULT_OK。如果用户取消或退出选取器,状态将为RESULT_CANCELED,并且意向的数据字段将为空。

随着媒体的Uri被接收,应用现在可以自由地播放或显示被认为合适的内容。像MediaPlayerVideoView这样的类将直接获取一个 Uri 来播放媒体内容,而Uri.getPath()方法将返回一个可以传递给BitmapFactory.decodeFile()的图像的文件路径。

6 至 11 日。保存到媒体商店

问题

您的应用希望存储媒体并将其插入设备的全局媒体存储中,以便所有应用都可以看到它。

解决方案

(API 一级)

利用 MediaStore 公开的 ContentProvider 接口来执行插入。除了媒体内容本身,该界面还允许您插入元数据来标记每个项目,例如标题、描述或创建时间。ContentProvider 插入操作的结果是一个 Uri,应用可以将它用作新媒体的目的地。

它是如何工作的

让我们来看一个将图像或视频剪辑插入 MediaStore 的例子。参见清单 6–23 和清单 6–24。

清单 6–23。 res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <Button     android:id="@+id/imageButton"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Images"   />   <Button     android:id="@+id/videoButton"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="Video"   /> </LinearLayout>

清单 6–24。 在 MediaStore 中保存数据的活动

`public class StoreActivity extends Activity implements View.OnClickListener {

private static final intREQUEST_CAPTURE = 100;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

Button images = (Button)findViewById(R.id.imageButton);
        images.setOnClickListener(this);
        Button videos = (Button)findViewById(R.id.videoButton);
        videos.setOnClickListener(this);
    }

@Override
    protectedvoid onActivityResult(int requestCode, int resultCode, Intent data) {
        if(requestCode == REQUEST_CAPTURE&& resultCode == Activity.RESULT_OK) {
            Toast.makeText(this, "All Done!", Toast.LENGTH_SHORT).show();
        }
    }

@Override
    public void onClick(View v) {
        ContentValues values;
        Intent intent;
        Uri storeLocation;

switch(v.getId()) {
        case R.id.imageButton:
            //Create any metadata for image
            values = new ContentValues(2);
            values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, System.currentTimeMillis());
            values.put(MediaStore.Images.ImageColumns.DESCRIPTION, "Sample Image");
            //Insert metadata and retrieve Uri location for file             storeLocation = getContentResolver().insert(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            //Start capture with new location as destination
            intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            intent.putExtra(MediaStore.EXTRA_OUTPUT, storeLocation);
            startActivityForResult(intent, REQUEST_CAPTURE);
            return;
        case R.id.videoButton:
            //Create any metadata for video
            values = new ContentValues(2);
            values.put(MediaStore.Video.VideoColumns.ARTIST, "Yours Truly");
            values.put(MediaStore.Video.VideoColumns.DESCRIPTION, "Sample Video Clip");
            //Insert metadata and retrieve Uri location for file
            storeLocation = getContentResolver().insert(
                    MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
            //Start capture with new location as destination
            intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
            intent.putExtra(MediaStore.EXTRA_OUTPUT, storeLocation);
            startActivityForResult(intent, REQUEST_CAPTURE);
            return;
        default:
            return;
        }
    }
}`

注意:由于这个例子与相机硬件交互,您应该在真实设备上运行它以获得完整的效果。事实上,在运行 Android 2.2 或更高版本的模拟器中有一个已知的错误,如果摄像机被访问,它将导致该示例崩溃。早期的仿真器会适当地执行代码,但是如果没有真正的硬件,这个例子就不那么有趣了。

在这个例子中,当用户点击任一按钮时,将与媒体本身相关联的元数据被插入到ContentValues实例中。图像和视频共有的一些更常见的元数据列有:

  • TITLE:内容标题的字符串值
  • DESCRIPTION:内容描述的字符串值
  • DATE_TAKEN:描述媒体捕获日期的整数值。用System.currentTimeMillis()填充该字段,表示“现在”的时间

然后使用适当的CONTENT_URI引用将ContentValues插入媒体存储。请注意,元数据是在实际采集媒体本身之前插入的。成功插入的返回值是一个完全限定的 Uri,应用可以将它用作媒体内容的目的地。

在前面的例子中,我们使用了第四章中的简化方法,通过请求系统应用处理这个过程来捕获音频和视频。回想一下第四章中的内容,音频和视频捕获意图都可以通过传递,并额外声明结果的目的地。这是我们传递从 insert 返回的 Uri 的地方。

从捕获活动成功返回后,应用就不再需要做什么了。外部应用已将捕获的图像或视频保存到 MediaStore 插页引用的位置。这些数据现在对所有应用都可见,包括系统的图库应用。

总结

在这一章中,你学习了你的应用如何与 Android 操作系统直接交互。我们讨论了将操作置于背景中不同时间长度的几种方法。您了解了应用如何分担责任,互相启动以最好地完成手头的任务。最后,我们展示了系统如何公开其核心应用套件收集的内容供您的应用使用。在下一章,也是最后一章,我们将探讨如何利用大量公开可用的 Java 库来进一步增强您的应用。

七、使用库

聪明的 Android 开发者通过利用库来更快地将他们的应用交付给市场,库通过提供先前创建和测试的代码来减少开发时间。开发人员可以创建和使用自己的库,也可以使用他人创建的库,或者两者兼而有之。

本章的初始秘籍向你介绍创建和使用你自己的库。随后的菜谱向您介绍了 Kidroid 的 skiChart 图表库,用于呈现条形图和折线图,以及 IBM 的 MQTT 库,用于在您的应用中实现轻量级推送消息。

提示:OpenIntents.org 发布了一个来自不同厂商的库列表,你可能会发现它对你的应用开发有所帮助([www.openintents.org/en/libraries](http://www.openintents.org/en/libraries))。

7–1。创建 Java 库 jar

问题

您希望创建一个库,存储与 Android 无关的代码,并且可以在您的 Android 和非 Android 项目中使用。

解决办法

创建一个基于 JAR 的库,通过 JDK 命令行工具或 Eclipse 只访问 Java 5(和更早版本)API。

它是如何工作的

假设您计划创建一个简单的面向数学的工具库。这个库将由一个带有各种static方法的MathUtils类组成。清单 7–1 展示了这个类的早期版本。

清单 7–1。 MathUtils通过static方法实现面向数学的工具

`// MathUtils.java

package com.apress.mathutils;

public class MathUtils
{
   public static long factorial(long n)
   {
      if (n <= 0)
         return 1;
      else
         return n*factorial(n-1);
   }
}`

MathUtils目前由一个用于计算和返回阶乘的static factorial()方法组成(可能用于计算排列和组合)。您可能最终会扩展这个类来支持快速傅立叶变换和其他不受java.lang.Math类支持的数学运算。

注意:当创建一个存储 Android 无关代码的库时,确保只访问 Android 支持的标准 Java API(如 collections 框架)——不要访问不支持的 Java API(如 Swing)或特定于 Android 的 API(如 Android widgets)。另外,不要访问任何比 Java 版本 5 更新的标准 Java APIs。

用 JDK 创造数学

用 JDK 开发一个基于 JAR 的库是很简单的。执行以下步骤创建一个包含MathUtils类的mathutils.jar文件:

  1. 在当前目录中,创建一个包目录结构,由一个包含apress子目录的com子目录和一个包含mathutils子目录的apress子目录组成。
  2. 将清单 7–1 的MathUtils.java源代码复制到存储在mathutils中的MathUtils.java文件中。
  3. 假设当前目录包含com子目录,执行javac com/apress/mathutils/MathUtils.java编译MathUtils.java。一个MathUtils.class文件存储在com/apress/mathutils中。
  4. 通过执行jar cf mathutils.jar com/apress/mathutils/*.class创建mathutils.jar。产生的mathutils.jar文件包含一个com/apress/mathutils/MathUtils.class条目。
使用 Eclipse 创建数学工具

用 Eclipse 开发一个基于 JAR 的库有点复杂。执行以下步骤创建一个包含MathUtils类的mathutils.jar文件:

  1. 假设您已经安装了在第一章中讨论的 Eclipse 版本,如果还没有运行的话,启动这个 IDE。
  2. 从“文件”菜单中选择“新建”,从出现的弹出菜单中选择“Java 项目”。
  3. 在出现的 New Java Project 对话框中,将 mathutils 输入到项目名称文本字段中,然后单击 Finish 按钮。
  4. 展开包资源管理器的 mathutils 节点。
  5. 右键单击 src 节点(在 mathutils 下面),并选择“新建”,然后从出现的弹出菜单中选择“包”。
  6. 在出现的 New Java Package 对话框中,在 Name 字段中输入 com.apress.mathutils 并点击 Finish。
  7. 右键单击生成的 com.apress.mathutils 节点,选择“新建”,然后在生成的弹出菜单中选择“类”。
  8. 在出现的 New Java Class 对话框中,在 Name 字段中输入 MathUtils 并点击 Finish。
  9. 用清单 7–1 中的替换生成的 MathUtils.java 编辑器窗口中的框架内容。
  10. 右键单击 mathutils 项目节点,并从出现的弹出菜单中选择“构建项目”。(您可能必须先从“项目”菜单中取消选择“自动构建”。)
  11. 右键单击 mathutils 项目节点,并从出现的弹出菜单中选择“导出”。
  12. 在出现的 Export 对话框中,选择 Java 节点下的 JAR 文件并点击 Next 按钮。
  13. 在生成的 JAR 导出窗格中,保留默认值,但在 JAR 文件文本字段中输入mathutils.jar。单击完成。产生的mathutils.jar文件创建在 Eclipse 工作区的根目录中。

7–2。使用 Java 库 jar

问题

您已经成功构建了mathutils.jar,并且想要学习如何将这个 JAR 文件集成到您的基于 Eclipse 的 Android 项目中。

解决办法

您将创建一个带有libs目录的基于 Eclipse 的 Android 项目,并将mathutils.jar复制到这个目录中。

注意:通常的做法是将库(.jar文件和 Linux 共享对象库,.so文件)存储在 Android 项目目录的libs子目录中。Android build 系统自动获取在libs中找到的文件,并将它们集成到 apk 中。如果这个库是一个共享对象库,它被存储在一个以lib(不是libs)开始的.apk文件中。

它是如何工作的

现在你已经创建了mathutils.jar,你需要一个 Android 应用来测试这个库。清单 7–2 将源代码呈现给一个UseMathUtils基于单个活动的应用,该应用计算 5 阶乘,活动随后输出该阶乘。

清单 7–2。 UseMathUtils 调用MathUtil``factorial()方法计算 5 阶乘

`// UseMathUtils.java

package com.apress.usemathutils;

import android.app.Activity;

import android.os.Bundle;

import android.widget.TextView;

import com.apress.mathutils.MathUtils;

public class UseMathUtils extends Activity
{
   @Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      TextView tv = new TextView(this);
      tv.setText("5! = "+MathUtils.factorial(5));
      setContentView(tv);
   }
}`

假设 Eclipse 正在运行,完成以下步骤来创建一个UseMathUtils项目:

  1. 从“文件”菜单中选择“新建”,并从出现的弹出菜单中选择“项目”。
  2. New Project 对话框中,展开向导树中的 Android 节点,选择该节点下的 Android 项目分支,点击 Next 按钮。
  3. 在出现的新 Android 项目对话框中,在项目名称文本字段中输入 UseMathUtils 。输入的名称标识了存储UseMathUtils项目的文件夹/目录。
  4. 如果“在工作区中创建新项目”单选按钮尚未选中,请选中它。
  5. 在构建目标下,选中要用作UseMathUtils构建目标的适当 Android 目标的复选框。这个目标指定了您希望您的应用在哪个 Android 平台上构建。假设您只安装了 Android 2.3 平台,那么只有这个构建目标应该出现,并且应该已经被选中。
  6. 在属性下,在应用名称文本字段中输入 Use MathUtils 。这个人类可读的标题将出现在 Android 设备上。继续,在包名文本字段中输入 com.apress.usemathutils 。该值是包名称空间(遵循与 Java 编程语言中的包相同的规则),所有源代码都将驻留在该名称空间中。如果未选中创建活动复选框,请选中它,并在此复选框旁边的文本字段中输入 UseMathUtils 作为应用的开始活动的名称。未选中此复选框时,文本字段被禁用。最后,在 Min SDK Version 文本字段中输入整数 9 ,以确定在 Android 2.3 平台上正确运行UseMathUtils所需的最低 API 级别。
  7. 单击完成。

Eclipse 在 Package Explorer 窗口中创建一个UseMathUtils节点。完成以下步骤来设置所有文件:

  1. 展开 UseMathUtils 节点,然后展开src节点,再展开com.apress.usemathutils节点。
  2. 双击 UseMathUtils.java 节点(在 com.apress.usemathutils 下面)并用清单 7–2 替换结果窗口中的框架内容。
  3. 右键单击 UseMathUtils 节点,在弹出的菜单中选择 New,然后选择 Folder。在出现的新文件夹对话框中,将libs输入到文件夹名称文本框中,并点击完成按钮。
  4. 使用您平台的文件管理器程序(如 Windows XP 的 Windows 资源管理器)选择先前创建的mathutils.jar文件并将其拖到 libs 节点。如果出现文件操作对话框,保持选择复制文件单选按钮并点击确定按钮。
  5. 右键单击 mathutils.jar 并在弹出菜单中选择“构建路径”,然后选择“配置构建路径”。
  6. 在出现的 UseMathUtils 的属性对话框中,选择 Libraries 选项卡并单击 Add Jars 按钮。
  7. 在出现的 JAR 选择对话框中,展开 UseMathUtils 节点,然后展开 libs 节点。选择 mathutils.jar,点击 OK 关闭 JAR 选择。第二次点击确定关闭 UseMathUtils 的属性。

您现在已经准备好运行这个项目了。从菜单栏中选择运行,然后从下拉菜单中选择运行。如果出现运行方式对话框,选择 Android 应用并点击确定。Eclipse 启动模拟器,安装该项目的 APK,并运行应用,其输出显示在图 7–1 中。

images

图 7–1。 UseMathUtils的简单用户界面可以扩展到让用户输入任意数字。

注意:检查这个应用的UseMathUtils.apk文件(jar tvf UseMathUtils.apk,你不会找到一个mathutils.jar条目。相反,您会发现classes.dex,它包含应用的 Dalvik 可执行字节码。classes.dex还包含了MathUtils classfile 的 Dalvik 等价物,因为 Android 构建系统解包 JAR 文件,用dx工具处理它们的内容,将它们的 Java 字节码转换成 Dalvik 字节码,并将等价的 Dalvik 代码合并到classes.dex

7–3。创建 Android 库项目

问题

您希望创建一个库来存储 Android 特定的代码,比如定制的小部件或有或没有资源的活动。

解决办法

Android 2.2 和后续版本允许您创建 Android 库项目,这些项目是 Eclipse 项目,描述包含 Android 特定代码甚至资源的库。

它是如何工作的

假设您想要创建一个库,其中包含一个可重用的定制小部件,描述一个游戏棋盘(用于下棋、跳棋,甚至是井字游戏)。清单 7–3 揭示了这个库的GameBoard类。

清单 7–3。 GameBoard描述一个可重复使用的自定义控件,用于绘制不同的游戏棋盘

`// GameBoard.java

package com.apress.gameboard;

import android.content.Context;

import android.graphics.Canvas;
import android.graphics.Paint;

import android.view.View;

public class GameBoard extends View
{
   private int nSquares, colorA, colorB;

private Paint paint;
   private int squareDim;

public GameBoard(Context context, int nSquares, int colorA, int colorB)
   {
      super(context);
      this.nSquares = nSquares;
      this.colorA = colorA;
      this.colorB = colorB;
      paint = new Paint();
   }

@Override
   protected void onDraw(Canvas canvas)
   {
      for (int row = 0; row < nSquares; row++)
      {          paint.setColor(((row & 1) == 0) ? colorA : colorB);
         for (int col = 0; col < nSquares; col++)
         {
            int a = colsquareDim;
            int b = row
squareDim;
            canvas.drawRect(a, b, a+squareDim, b+squareDim, paint);
            paint.setColor((paint.getColor() == colorA) ? colorB : colorA);
         }
      }   
   }

@Override
   protected void onMeasure(int widthMeasuredSpec, int heightMeasuredSpec)
   {
      // keep the view squared
      int width = MeasureSpec.getSize(widthMeasuredSpec);
      int height = MeasureSpec.getSize(heightMeasuredSpec);
      int d = (width == 0) ? height : (height == 0) ? width :
              (width < height) ? width : height;
      setMeasuredDimension(d, d);
      squareDim = width/nSquares;
   }
}`

Android 定制小部件基于子类android.view.View或其一个子类(如android.widget.TextView)的视图。GameBoard直接子类化View,因为它不需要任何子类功能。

GameBoard提供了几个字段,包括如下:

  • nSquares存储游戏棋盘每边的方块数。典型值包括 3(3x 3 板)和 8(8x 8 板)。
  • colorA存储偶数行上偶数方块的颜色,奇数行上奇数方块的颜色——行列编号从 0 开始。
  • colorB存储偶数行奇数方块的颜色,奇数行偶数方块的颜色。
  • paint存储对android.graphics.Paint对象的引用,该对象用于在绘制游戏板时指定方块颜色(colorAcolorB)。
  • squareDim存储正方形的尺寸——每边的像素数。

GameBoard的构造函数通过在同名字段中存储其nSquarescolorAcolorB参数来初始化这个小部件,并且还实例化了Paint类。然而,在这样做之前,它将其context参数传递给其View超类。

注意: V iew子类需要将一个android.content.Context实例传递给它们的View超类。这样做可以识别定制小部件运行的上下文(例如,一个活动)。定制小部件子类可以随后调用ViewContext getContext()方法来返回这个Context对象,这样它们就可以调用Context方法来访问当前主题、资源等等。

Android 通过调用小部件的覆盖方法protected void onDraw(Canvas canvas)来告诉定制小部件绘制自己。GameBoardonDraw(Canvas)方法通过调用android.graphics.Canvasvoid drawRect(float left, float top, float right, float bottom, Paint paint)方法来响应,为每个行/列交叉点绘制每个方块。最后一个paint参数决定了那个方块的颜色。

Android 在调用onDraw(Canvas)之前,必须对 widget 进行测量。它通过调用小部件的 overriding protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法来完成这个任务,其中传递的参数指定了父视图强加的水平和垂直空间需求。小部件通常将这些参数传递给View.MeasureSpec嵌套类的static int getSize(int measureSpec)方法,根据传递的measureSpec参数返回小部件的精确宽度或高度。然后,必须将返回值或这些值的修改版本传递给Viewvoid setMeasuredDimension(int measuredWidth, int measuredHeight)方法,以存储测得的宽度和高度。调用此方法失败会导致在运行时引发异常。因为游戏板应该是正方形的,GameBoardonMeasure(int, int)方法将宽度和高度的最小值传递给setMeasuredDimension(int, int)以确保游戏板是正方形的。

现在您已经知道了GameBoard是如何工作的,您已经准备好创建一个存储这个类的库了。您将通过创建一个 Android 库项目来创建这个库。这样一个项目的好处是它是一个标准的 Android 项目,所以你可以像创建一个新的 app 项目一样创建一个新的 Android 库项目。

完成以下步骤来创建GameBoard项目:

  1. 从“文件”菜单中选择“新建”,并从出现的弹出菜单中选择“项目”。
  2. New Project 对话框中,展开向导树中的 Android 节点,选择该节点下的 Android 项目分支,点击 Next 按钮。
  3. 在出现的新 Android 项目对话框中,在项目名称文本字段中输入 GameBoard 。输入的名称标识了存储GameBoard项目的文件夹。
  4. 如果“在工作区中创建新项目”单选按钮尚未选中,请选中它。
  5. 在构建目标下,选中要用作GameBoard构建目标的适当 Android 目标的复选框。这个目标指定了您希望您的应用在哪个 Android 平台上构建。假设您只安装了 Android 2.3 平台,那么只有这个构建目标应该会出现,并且它应该已经被选中了。
  6. 在“属性”下,将应用名称文本字段留空——该库不是一个应用,因此没有必要在此字段中输入值。继续,在包名文本字段中输入 com.apress.gameboard 。该值是包名称空间(遵循与 Java 编程语言中的包相同的规则),所有库源代码都将驻留在该名称空间中。如果选中了创建活动复选框,则取消选中它。未选中此复选框时,文本字段被禁用。最后,在 Min SDK 版本文本字段中输入整数 9 ,以确定在 Android 2.3 平台上正确运行GameBoard所需的最低 API 级别。
  7. 单击完成。

尽管您创建 Android 库项目的方式与创建常规应用项目的方式相同,但您必须调整GameBoard的一些项目属性,以表明它是一个库项目:

  1. 在包浏览器中,右键单击 GameBoard 并从弹出菜单中选择 Properties。
  2. 在出现的游戏板属性对话框中,选择 Android 属性组并选中是库复选框。
  3. 单击应用按钮,然后单击确定。

新的GameBoard项目现在被标记为 Android 库项目。然而,它还不包含包含清单 7–3 内容的GameBoard.java源文件。在包浏览器的 game board/src/com/a press/game board 节点下创建这个源文件。

如果您愿意,您可以构建这个库(例如,右键单击 GameBoard 节点并从弹出菜单中选择 Build Project)。然而,没有必要这样做。当您生成使用此库的项目时,将自动生成该项目。你将在下一个秘籍中学习如何做这件事。

注意:如果你构建了GameBoard库,你会发现一个com/apress/gameboard目录结构,其中gameboard包含GameBoard.class和几个面向资源的类文件(即使GameBoard.java不引用资源)。这就是基于 Android 库项目的库的本质。

7–4。使用 Android 库项目

问题

您已经成功构建了GameBoard库,并且想要学习如何将这个库集成到您的基于 Eclipse 的 Android 项目中。

解决办法

在正在构建的 app 项目的属性中标识出要 Eclipse 的GameBoard库,并构建 app。

它是如何工作的

现在你已经创建了GameBoard,你需要一个 Android 应用来测试这个库。清单 7–4 将源代码呈现给一个UseGameBoard基于单个活动的应用,该应用实例化这个库的GameBoard类,并将其放置在活动的视图层次结构中。

清单 7–4。 UseGameBoardGameBoard小部件放入活动的视图层次

`// UseGameBoard.java

package com.apress.usegameboard;

import android.app.Activity;

import android.graphics.Color;

import android.os.Bundle;

import com.apress.gameboard.GameBoard;

public class UseGameBoard extends Activity
{
   @Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      GameBoard gb = new GameBoard(this, 8, Color.BLUE, Color.WHITE);
      setContentView(gb);
   }
}`

假设 Eclipse 正在运行,完成以下步骤来创建一个UseGameBoard项目:

  1. 从“文件”菜单中选择“新建”,并从出现的弹出菜单中选择“项目”。
  2. 新建项目对话框中,展开向导树中的 Android 节点,选择该节点下的 Android 项目分支,点击下一步按钮。
  3. 在出现的新 Android 项目对话框中,在项目名称文本字段中输入 UseGameBoard 。输入的名称标识了存储UseGameBoard项目的文件夹。
  4. 如果“在工作区中创建新项目”单选按钮尚未选中,请选中它。
  5. 在构建目标下,选中要用作UseGameBoard构建目标的适当 Android 目标的复选框。这个目标指定了你希望你的应用在哪个 Android 平台上构建。假设您只安装了 Android 2.3 平台,那么只有这个构建目标应该会出现,并且它应该已经被选中了。
  6. 在属性下,在应用名称文本字段中输入 Use GameBoard 。这个人类可读的标题将出现在 Android 设备上。继续,在包名文本字段中输入 com.apress.usegameboard 。该值是包名称空间(遵循与 Java 编程语言中的包相同的规则),所有源代码都将驻留在该名称空间中。如果未选中创建活动复选框,请选中它,并在此复选框旁边的文本字段中输入 UseGameBoard 作为应用的开始活动的名称。未选中此复选框时,文本字段被禁用。最后,在 Min SDK Version 文本字段中输入整数 9 ,以确定在 Android 2.3 平台上正确运行UseGameBoard所需的最低 API 级别。
  7. 单击完成。

Eclipse 在 Package Explorer 窗口中创建一个UseGameBoard节点。完成以下步骤来设置所有文件:

  1. 展开 UseGameBoard 节点,然后展开src节点,再展开com.apress.usegameboard节点。
  2. 双击 UseGameBoard.java 节点(在 com.apress.usegameboard 下面)并用清单 7–4 替换结果窗口中的框架内容。
  3. 右键单击“使用游戏板”节点,并从弹出菜单中选择“属性”。
  4. 在随后出现的UseGameBoard的属性对话框中,选择 Android 类别并点击添加按钮。
  5. 在弹出的项目选择对话框中,选择游戏板并点击确定。
  6. 点击应用,然后点击确定关闭使用游戏板的属性。

您现在已经准备好运行这个项目了。从菜单栏中选择运行,然后从下拉菜单中选择运行。如果出现运行方式对话框,选择 Android 应用并点击确定。Eclipse 启动模拟器,安装该项目的 APK,并运行应用,其输出显示在图 7–2 中。

images

图 7–2。 UseGameBoard展示了一个蓝白相间的棋盘,可用作跳棋或国际象棋等游戏的背景。

注意:如果你有兴趣创建和使用一个基于 Android library 项目的包含一个活动的库,可以查看 Google 的TicTacToe示例库项目([developer.android.com/guide/developing/projects/projects-eclipse.html#SettingUpLibraryProject](http://developer.android.com/guide/developing/projects/projects-eclipse.html#SettingUpLibraryProject))。

7–5。制图

问题

您正在寻找一个简单的库,让您的应用生成条形图或折线图。

解决办法

虽然有几个 Android 库可以生成图表,但你可能更喜欢 Kidroid.com 的 kiChart 产品([www.kidroid.com/kichart/](http://www.kidroid.com/kichart/))的简单性。0.1 版本支持条形图和折线图,Kidroid 承诺在后续版本中添加新的图表类型。

到 kiChart 主页的链接提供了下载kiChart-0.1.jar(库)和kiChart-Help.pdf(描述库的文档)的链接。

它是如何工作的

kiChart 的文档指出条形图和折线图支持多个数据系列。此外,它还声明可以将图表导出为图像文件,并且可以定义图表参数(如字体颜色、字体大小、边距等)。

然后,该文档显示了一对由演示应用呈现的示例线图和条形图的截图。这些截图后面是来自这个演示的代码——特别是,LineChart图表活动类。

LineChart的源代码揭示了建立图表的基本原理,解释如下:

  1. 创建一个扩展com.kidroid.kichart.ChartActivity类的活动。此活动呈现条形图或折线图。
  2. 在活动的onCreate(Bundle)方法中,创建一个横轴标签的String数组,并为每组条或每条线创建一个浮点数据数组。
  3. 创建一个由com.kidroid.kichart.model.Aitem (axis item)实例组成的数组,并用存储数据数组的Aitem对象填充这个数组。每个Aitem构造函数调用都要求您传递一个android.graphics.Color值来标识与数据数组相关联的颜色(其显示的值和条或线都以该颜色显示)、一个String值来将标签与颜色和数据数组以及数据数组本身相关联。
  4. 如果想显示条形图,实例化com.kidroid.kichart.view.BarView类;如果想显示折线图,实例化com.kidroid.kichart.view.LineView类。
  5. 调用该类的public void setTitle(String title)方法来指定图表的标题。
  6. 调用该类的public void setAxisValueX(String[] labels)方法来指定图表的水平标签。
  7. 调用该类的public void setItems(Aitem[] items)方法来指定图表的数据项数组。
  8. 用图表实例作为参数调用setContentView()来显示图表。
  9. 您不必担心为垂直轴选择一系列值,因为 kiChart 会替您完成这项任务。

源代码后面有一个类图,展示了 kiChart 的类并显示了它们之间的关系。例如,com.kidroid.kichart.view.ChartViewcom.kidroid.kichart.view.AxisView的超类,它超类BarViewLineView

然后记录每个类的属性和ChartViewpublic boolean exportImage(String filename)方法。此方法允许您将图表输出到文件中,如果成功则返回 true,如果不成功则返回 false。

提示:要影响垂直轴上显示的值的范围,您需要使用AxisViewintervalCountintervalValuevalueGenerate属性。

在实践中,您会发现 kiChart 很容易使用。例如,考虑一个ChartDemo应用,它的主要活动(也称为ChartDemo)提供了一个用户界面,让用户通过它的八个文本字段输入 2010 年和 2011 年每一年的季度销售额。主活动还提供了一对按钮,允许用户通过单独的BarChartLineChart活动在条形图或折线图的上下文中查看这些数据。

清单 7–5 展示了ChartDemo的源代码。

清单 7–5。 ChartDemo描述输入图表数据值并启动条形图或折线图活动的活动

`// ChartDemo.java

package com.apress.chartdemo;

import android.app.Activity;

import android.content.Intent;

import android.os.Bundle;

import android.view.View;

import android.widget.AdapterView;
import android.widget.Button; import android.widget.EditText;

public class ChartDemo extends Activity
{
   @Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);

Button btnViewBC = (Button) findViewById(R.id.viewbc);
      AdapterView.OnClickListener ocl;
      ocl = new AdapterView.OnClickListener()
      {
         @Override
         public void onClick(View v)
         {
            final float[] data2010 = new float[4];
            int[] ids = { R.id.data2010_1, R.id.data2010_2, R.id.data2010_3,
                          R.id.data2010_4 };
            for (int i = 0; i < ids.length; i++)
            {
               EditText et = (EditText) findViewById(ids[i]);
               String s = et.getText().toString();
               try
               {
                  float input = Float.parseFloat(s);
                  data2010[i] = input;
               }
               catch (NumberFormatException nfe)
               {
                  data2010[i] = 0;
               }
            }
            final float[] data2011 = new float[4];
            ids = new int[] { R.id.data2011_1, R.id.data2011_2,
                              R.id.data2011_3, R.id.data2011_4 };
            for (int i = 0; i < ids.length; i++)
            {
               EditText et = (EditText) findViewById(ids[i]);
               String s = et.getText().toString();
               try
               {
                  float input = Float.parseFloat(s);
                  data2011[i] = input;
               }
               catch (NumberFormatException nfe)
               {
                  data2011[i] = 0;
               }
            }
            Intent intent = new Intent(ChartDemo.this, BarChart.class);
            intent.putExtra("2010", data2010);
            intent.putExtra("2011", data2011);
            startActivity(intent);
         }
      };       btnViewBC.setOnClickListener(ocl);

Button btnViewLC = (Button) findViewById(R.id.viewlc);
      ocl = new AdapterView.OnClickListener()
      {
         @Override
         public void onClick(View v)
         {
            final float[] data2010 = new float[4];
            int[] ids = { R.id.data2010_1, R.id.data2010_2, R.id.data2010_3,
                          R.id.data2010_4 };
            for (int i = 0; i < ids.length; i++)
            {
               EditText et = (EditText) findViewById(ids[i]);
               String s = et.getText().toString();
               try
               {
                  float input = Float.parseFloat(s);
                  data2010[i] = input;
               }
               catch (NumberFormatException nfe)
               {
                  data2010[i] = 0;
               }
            }
            final float[] data2011 = new float[4];
            ids = new int[] { R.id.data2011_1, R.id.data2011_2,
                              R.id.data2011_3, R.id.data2011_4 };
            for (int i = 0; i < ids.length; i++)
            {
               EditText et = (EditText) findViewById(ids[i]);
               String s = et.getText().toString();
               try
               {
                  float input = Float.parseFloat(s);
                  data2011[i] = input;
               }
               catch (NumberFormatException nfe)
               {
                  data2011[i] = 0;
               }
            }
            Intent intent = new Intent(ChartDemo.this, LineChart.class);
            intent.putExtra("2010", data2010);
            intent.putExtra("2011", data2011);
            startActivity(intent);
         }
      };
      btnViewLC.setOnClickListener(ocl);
   }
}`

ChartDemo在它的onCreate(Bundle)方法中实现它的所有逻辑。这个方法主要是设置它的内容视图,并在视图的两个按钮上附加一个点击监听器。

因为这些监听器几乎相同,我们将只考虑附加到viewbc(查看条形图)按钮的监听器的代码。作为对这个按钮被点击的响应,监听器的onClick(View)方法被调用来执行以下任务:

  1. 用对应于 2010 年数据的四个文本字段的值填充一个data2010浮点数组。
  2. 用对应于 2011 年数据的四个文本字段的值填充一个data2011浮点数组。
  3. 创建一个Intent对象,将BarChart.class指定为要启动的活动的类文件。
  4. data2010data2011数组存储在该对象中,以便可以从BarChart活动中访问它们。
  5. 发起BarChart活动。

清单 7–6 展示了BarChart的源代码。

清单 7–6。 BarChart描述条形图活动

`// BarChart.java

package com.apress.chartdemo;

import com.kidroid.kichart.ChartActivity;

import com.kidroid.kichart.model.Aitem;

import com.kidroid.kichart.view.BarView;

import android.graphics.Color;

import android.os.Bundle;

public class BarChart extends ChartActivity
{
   @Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      Bundle bundle = getIntent().getExtras();
      float[] data2010 = bundle.getFloatArray("2010");
      float[] data2011 = bundle.getFloatArray("2011");
      String[] arrX = new String[4];
      arrX[0] = "2010.1";
      arrX[1] = "2010.2";
      arrX[2] = "2010.3";
      arrX[3] = "2010.4";
      Aitem[] items = new Aitem[2];
      items[0] = new Aitem(Color.RED, "2010", data2010);
      items[1] = new Aitem(Color.GREEN, "2011", data2011);
      BarView bv = new BarView(this);
      bv.setTitle("Quarterly Sales (Billions)");
      bv.setAxisValueX(arrX);       bv.setItems(items);
      setContentView(bv);
   }
}`

BarChart首先通过调用其继承的Intent getIntent()方法获得对传递给它的Intent对象的引用。然后,它使用这个方法检索对Intent对象的Bundle对象的引用,该对象存储数据项的浮点数组。通过调用Bundlefloat[] getFloatArray(String key)方法来检索每个数组。

BarChart接下来为图表的 X 轴构建一个标签的String数组,并创建一个用两个Aitem对象填充的Aitem数组。第一个对象存储 2010 年的数据值,并将这些值与红色和作为图例值的 2010 相关联;第二个对象用绿色和图例值 2011 存储 2011 数据值。

在实例化BarView之后,BarChart调用这个对象的setTitle(String)方法来建立图表的标题,setAxisValueX(String[])方法将 X 轴标签的数组传递给对象,setItems(Aitem[])方法将Aitem数组传递给对象。然后将BarView对象传递给setContentView()以显示条形图。

注意:因为LineChartBarChart几乎相同,所以这个类的源代码不在本章中介绍。您可以通过将BarView bv = new BarView(this);改为LineView bv = new LineView(this);来轻松创建LineChart。此外,为了最佳实践,您可能应该将变量bv重命名为lv。还有别忘了把import com.kidroid.kichart.view.BarView;改成import com.kidroid.kichart.view.LineView;

清单 7–7 展示了main.xml,它描述了构成ChartDemo用户界面的布局和小部件。

清单 7–7。 main.xml描述图表演示活动的布局

`

  
    
    
    
  


    
         
  


    
         
  


    
    
    
  


    
    
    
  


    
    

问题

谷歌的云到设备消息传递(C2DM)框架([code.google.com/android/c2dm/index.html](http://code.google.com/android/c2dm/index.html))旨在实现设备的推送消息传递,它有许多缺点,这些缺点可能会影响它作为推送消息传递的实用解决方案。你的应用需要一个更通用的推送解决方案。

谷歌 C2DM 的局限性

C2DM 是谷歌开发的一项技术,通过可扩展消息和存在协议(XMPP)在 Android 设备上运行,XMPP 是聊天客户端的常见实现。通过进一步的观察,C2DM 有许多必需的属性,这些属性通常会降低它在应用中的有用性:

  • 要求最低 API 级别为 8: 虽然这一限制不会永远成为一个重大限制,但现在希望在运行 2.2 之前版本的 Android 设备上支持推送消息的应用将无法使用 C2DM。
  • 需要设备上的 Google 帐户和 Google API:C2DM 在 GTalk 聊天服务创建的 XMPP 通道上运行。如果用户在不包含 Google APIs(以及 GTalk 应用)的 Android 设备上运行,或者如果他们没有在设备上输入有效的 Google 帐户,您的应用将无法在该设备上注册 C2DM 消息。
  • 利用 HTTP POST 进行主机应用和 C2DM 服务器之间的事务:从应用的服务器端,要发送到设备的消息通过对每条消息使用单独的 HTTP POST 请求被传递到 C2DM 服务器。随着需要发送的消息数量的增加,这种机制变得越来越慢,以至于 C2DM 在某些时间关键的应用中可能不是可行的选择。
解决办法

利用 IBM 的 MQTT 库在您的应用中实现轻量级推送消息。MQTT 客户端库由 IBM 以纯 Java 实现的形式提供,这意味着它可以在任何 Android 设备上使用,没有特定 API 级别的限制。

MQTT 系统由三个主要组件组成:

  • 客户端应用:在设备上运行,并向消息代理注册一组给定的“主题”来接收消息。
  • 消息代理:处理客户端的注册,并根据客户端的“主题”将来自服务器应用的消息分发到每个客户端
  • 服务器应用:负责向代理发布消息。

邮件按主题过滤。主题以树形格式定义,由路径字符串表示。通过提供适当的路径,客户端可以订阅特定的主题或子主题组。例如,假设我们为应用定义了两个主题,如下所示:

examples/one examples/two

客户端可以通过订阅精确的完整路径字符串来订阅任一主题。但是,如果客户希望订阅这两个主题(以及该组中稍后可能创建的任何其他主题),则可以通过以下方式方便地进行订阅:

examples/#

通配符“#”表示该客户对示例组中的所有主题都感兴趣。

在这个菜谱中,我们将重点关注在 Android 设备上使用 MQTT 库实现客户端应用。IBM 为其他组件的开发和测试提供了优秀的工具,我们也将在这里展示这些工具。

它是如何工作的

MQTT Java 库可以从 IBM 的以下位置免费下载:www-01.ibm.com/support/docview.wss?uid=swg24006006。除了库 JAR 之外,下载档案还包含示例代码、API Javadoc 和使用文档。

从下载档案中找到wmqtt.jar文件。这是 Android 项目中必须包含的库。按照惯例,这意味着应该在您的项目目录中创建一个/libs目录,并且应该在那里插入这个 JAR。

为了测试您的客户机实现,IBM 提供了非常小的消息代理(RSMB)。RSMB 可以在以下位置下载:[www.alphaworks.ibm.com/tech/rsmb](http://www.alphaworks.ibm.com/tech/rsmb)

RSMB 是一个多平台下载,包括用于消息代理和发布消息的应用的命令行工具。IBM 为此工具提供的许可证禁止在生产环境中使用它;此时,您将需要推出自己的解决方案,或者使用众多可用的开源实现之一。然而,对于移动客户端的开发,RSMB 再好不过了。

客户端示例

因为监视传入的推送消息是一个不确定的、长期的操作,所以让我们看一个将基本功能放入服务的例子。

注意:提醒一下,您的项目目录中应该有libs/wmqtt.jar,并在您的项目构建路径中被引用。

清单 7–9 展示了一个示例 MQTT 服务的源代码。

清单 7–9。 MQTT 示例服务

`//ClientService.java
package com.apress.pushclient;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.widget.Toast;
//Imports required from the MQTT Library JAR
import com.ibm.mqtt.IMqttClient;
import com.ibm.mqtt.MqttClient;
import com.ibm.mqtt.MqttException;
import com.ibm.mqtt.MqttPersistenceException;
import com.ibm.mqtt.MqttSimpleCallback;

public class ClientService extends Service implements MqttSimpleCallback {

//Location where broker is running
    privatestaticfinal String HOST = HOSTNAME_STRING_HERE;
    privatestaticfinal String PORT = "1883";
    //30 minute keep-alive ping
    privatestaticfinalshortKEEP_ALIVE = 60 * 30;
    //Unique identifier of this device
    privatestaticfinal String CLIENT_ID = "apress/"+System.currentTimeMillis();
    //Topic we want to watch for
    privatestaticfinal String TOPIC = "apress/examples";

privatestaticfinal String ACTION_KEEPALIVE = "com.examples.pushclient.ACTION_KEEPALIVE";

private IMqttClient mClient;
    private AlarmManager mManager;
    private PendingIntent alarmIntent;

@Override
    public void onCreate() {
        super.onCreate();
        mManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);

Intent intent = new Intent(ACTION_KEEPALIVE);
        alarmIntent = PendingIntent.getBroadcast(this, 0, intent, 0);         registerReceiver(mReceiver, new IntentFilter(ACTION_KEEPALIVE));

try {
            //Format: tcp://hostname@port
            String connectionString = String.format("%s%s@%s", MqttClient.TCP_ID, HOST, PORT);
            mClient = MqttClient.createMqttClient(connectionString, null);
        } catch (MqttException e) {
            e.printStackTrace();
            //Can't continue without a client
            stopSelf();
        }
    }

@Override
    public void onStart(Intent intent, int startId) {
        //Callback on Android devices prior to 2.0
        handleCommand(intent);
    }

@Override
    publicint onStartCommand(Intent intent, int flags, int startId) {
        //Callback on Android devices 2.0 and later
        handleCommand(intent);
        //If Android kills this service, we want it back when possible
        return START_STICKY;
    }

private void handleCommand(Intent intent) {
        try {
            //Make a connection
            mClient.connect(CLIENT_ID, true, KEEP_ALIVE);
            //Target MQTT callbacks here
            mClient.registerSimpleHandler(this);
            //Subscribe to a topic
            String[] topics = new String[] { TOPIC };
            //QoS of 0 indicates fire once and forget
            int[] qos = newint[] { 0 };
            mClient.subscribe(topics, qos);

//Schedule a ping
            scheduleKeepAlive();
        } catch (MqttException e) {
            e.printStackTrace();
        }
    }

@Override
    public void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mReceiver);
        unscheduleKeepAlive();

if(mClient != null) {
            try {
                mClient.disconnect();
                mClient.terminate();             } catch (MqttPersistenceException e) {
                e.printStackTrace();
            }
            mClient = null;
        }
    }

//Handle incoming message from remote
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            String incoming = (String)msg.obj;
            Toast.makeText(ClientService.this, incoming, Toast.LENGTH_SHORT).show();
        }
    };

//Handle ping alarms to keep the connection alive
    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if(mClient == null) {
                return;
            }
            //Ping the MQTT service
            try {
                mClient.ping();
            } catch (MqttException e) {
                e.printStackTrace();
            }
            //Schedule the next alarm
            scheduleKeepAlive();
        }
    };

private void scheduleKeepAlive() {
        long nextWakeup = System.currentTimeMillis() + (KEEP_ALIVE * 1000);
        mManager.set(AlarmManager.RTC_WAKEUP, nextWakeup, alarmIntent);
    }

private void unscheduleKeepAlive() {
        mManager.cancel(alarmIntent);
    }

/* MqttSimpleCallback Methods */

@Override
    public void connectionLost() throws Exception {
        mClient.terminate();
        mClient = null;
        stopSelf();
    }

@Override
    public void publishArrived(String topicName, byte[] payload, int qos, boolean retained) throws Exception {
        //Be wary of UI related code here!
        //Best to use a Handler for UI or Context operations         StringBuilder builder = new StringBuilder();
        builder.append(topicName);
        builder.append('\n');
        builder.append(new String(payload));
        //Pass the message up to our handler
        Message receipt = Message.obtain(mHandler, 0, builder.toString());
        receipt.sendToTarget();
    }

/Unused method/
    //We are not using this service as bound
    //It is explicitly started and stopped with no direct connection
    @Override
    public IBinder onBind(Intent intent) { returnnull; }
}`

重要提示:这个Service很可能会与远程服务器通信,因此您必须在应用清单中声明android.permission.INTERNET,以及带有<service>标签的Service本身。

为了子类化Service,必须提供onBind()的实现。在这种情况下,我们的例子不需要提供一个Binder接口,因为活动永远不需要直接挂钩到调用方法中。因此,这个必需的方法只返回 null。这个Service被设计成接收启动和停止的明确指令,在其间运行一段不确定的时间。

Service被创建时,一个MqttClient对象也被使用createMqttClient()实例化;这个客户机将消息代理主机的位置作为一个字符串。连接字符串的格式为tcp://hostname@port。在本例中,选择的端口号是 1883,这是 MQTT 通信的默认端口号。如果您选择不同的端口号,您应该验证您的服务器实现是否在匹配的端口上运行。

从这一点开始,Service保持空闲,直到发出启动命令。一旦收到开始命令(通过调用Context.startService()从外部发出),将调用onStart()onStartCommand()(取决于设备上运行的 Android 版本)。在后一种情况下,服务返回START_STICKY,这是一个常量,告诉系统应该让这个服务继续运行,如果它因为内存原因被提前终止,就重新启动它。

一旦启动,服务将向 MQTT 消息代理注册,传递一个惟一的客户机 ID 和一个保活时间。为了简单起见,这个例子根据服务创建时的当前时间来定义客户机 ID。在生产中,更独特的标识符,如 Wi-Fi MAC 地址或TelephonyManager.getDeviceId()可能更合适,记住这两种选择都不能保证出现在所有设备上。

keep-alive 参数是时间(以秒为单位),代理应该使用该时间使到该客户端的连接超时。为了避免这种超时,客户应该发布消息或定期 ping 代理。我们将很快更全面地讨论这项任务。

在启动过程中,客户端还订阅了一个主题。注意,subscribe()方法将数组作为参数;一个客户端可以在一个方法调用中订阅多个主题。每个主题还订阅有请求的服务质量(QoS)值。对移动设备请求的最委婉的值是零,告诉代理只发送一次消息而不需要确认。这样做减少了代理和设备之间所需的握手次数。

随着连接的激活和注册,来自远程代理的任何传入消息都将导致对publishArrived()的调用,并传递关于该消息的数据。这个方法可以在任何由MqttClient创建和维护的后台线程上调用,所以不要在这里直接做任何与主线程相关的事情是很重要的。在本例中,所有传入的消息都被传递到一个本地的Handler,以保证结果Toast被发送到主线程上进行显示。

实现 MQTT 客户机时需要一项维护任务,那就是 ping 代理以保持连接活动。为了完成这个任务,ServiceAlarmManager注册,以根据匹配保活参数的时间表触发广播。即使设备当前处于睡眠状态,也必须完成该任务,因此每次使用AlarmManager.RTC_WAKEUP设置闹铃。当每个警报触发时,Service简单地调用MqttClient.ping()并安排下一次保活更新。

由于这一要求的持续性质,为保活定时器选择低频间隔是谨慎的;在这个例子中,我们选择了 30 分钟。该定时器值代表了减少设备上所需更新的频率(以节省功率和带宽)和远程代理意识到远程设备不再存在并超时之前的等待时间之间的平衡。

当不再需要推送服务时,对Context.stopService()的外部调用将导致对onDestroy()的调用。在这里,Service拆除 MQTT 连接,删除任何未决的警报,并释放所有资源。作为MqttSimpleCallback接口的一部分实现的第二个回调是onConnectionLost(),表示意外的断开。在这些情况下,Service会像手动停止请求一样自行停止。

测试客户端

为了测试设备的消息传递,您需要在您的机器上启动一个 RSMB 实例。从命令行中,导航到您解压缩下载的位置,然后导航到与您的计算机平台(Windows、Linux、Mac OS X)匹配的目录。从这里,只需执行broker命令,代理服务将开始在您的机器上运行,位于localhost:1883:

CWNAN9999I Really Small Message Broker CWNAN9997I Licensed Materials - Property of IBM CWNAN9996I Copyright IBM Corp. 2007, 2010 All Rights Reserved …
CWNAN0014I MQTT protocol starting, listening on port 1883

此时,您可以连接到服务并发布消息或注册接收消息。为了对这个Service进行测试,清单 7–10 和清单 7–11 创建了一个简单的Activity,可以用来启动和停止服务。

清单 7–10。 res/menu/home.xml

`

      `

清单 7–11。 活动控制 MQTT 服务

`//ClientActivity.java
package com.apress.pushclient;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

public class ClientActivity extends Activity {

private Intent serviceIntent;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

serviceIntent = new Intent(this, ClientService.class);
    }

@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.home, menu);
        return true;
    }

@Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch(item.getItemId()) {
        case R.id.menu_start:
            startService(serviceIntent);
            return true;
        case R.id.menu_stop:
            stopService(serviceIntent);
            return true;
        }         return super.onOptionsItemSelected(item);
    }
}`

清单 7–11 创建了一个Intent,它将被两个菜单选项用来随意启动和停止服务(参见图 7–5)。通过按下菜单按钮并选择“启动服务”,MQTT 连接将启动并为设备注册主题为“进程/示例”的消息

images

图 7–5。 活动控制服务

注意:示例服务中的HOST值需要指向运行您的 RSMB 实例的机器。即使你在同一台机器上的模拟器中测试,这个值也是而不是 localhost!至少,您必须将模拟器或设备指向运行您的代理的机器的 IP 地址。

Android 设备成功注册了来自代理的推送消息后,打开另一个命令行窗口,导航到执行broker的同一个目录。另一个命令stdinpub可用于连接到代理实例,并将消息发布到设备。从命令行键入以下命令:

stdinpub apress/examples

该命令将注册一个客户端,以发布主题与我们的示例相匹配的消息。您将看到以下结果:

Using topic apress/examples Connecting

现在你可以输入任何你喜欢的信息,然后回车。按下 Enter 键后,消息将被发送到代理,并被推送到注册的设备。尽可能多次这样做,然后使用 CTRL-C 退出程序。CTRL-C 还将终止代理服务。

提示:【RSMB】还包含第三个命令stdoutsub,用于向您的本地代理服务订阅一组主题。这个命令让您完全关闭循环,并测试问题是发生在测试套件中还是您的 Android 应用中。

总结

聪明的 Android 开发者通过利用库来更快地将他们的应用交付给市场,库通过提供先前创建和测试的代码来减少开发时间。

本章的初始秘籍向您介绍了创建和使用您自己的库的主题。具体来说,您学习了如何创建和使用 Java 库 jar,其代码仅限于 Java 5(或更早版本)API 和 Android 库项目。

尽管您可能会创建自己的库来避免重复劳动,但您可能也需要使用其他人的库。例如,如果您需要一个简单的图表库,您可能想看看 kiChart,它有助于条形图和折线图的显示。

如果你正在使用云,你可能会决定使用谷歌的 C2DM 框架。但是,因为这个框架有许多缺点(比如要求最低 API 级别为 8),所以您可以考虑利用 IBM 的 MQTT 库在您的应用中实现轻量级的推送消息。

八、附录 A:Android 脚本层

Android 脚本层(SL4A),之前被称为 Android 脚本环境,是一个在 Android 设备上安装脚本语言解释器并通过这些解释器运行脚本的平台。脚本可以访问 Android 应用可用的许多 API,但有一个大大简化的界面,使事情更容易完成。

注意: SL4A 目前只支持 Python、Perl、JRuby、Lua、BeanShell、Rhino JavaScript 和 Tcl 脚本语言。

您可以在终端窗口(命令窗口)、后台或通过区域设置([www.twofortyfouram.com/](http://www.twofortyfouram.com/))交互运行脚本。 Locale 是一款 Android 应用,可以让你在预定的时间或者满足其他条件时运行脚本(例如,当你进入剧院或法庭时,运行脚本将你的手机铃声模式改为振动)。

安装 SL4A

在使用 SL4A 之前,您必须安装它。你可以从谷歌托管的项目网站([code.google.com/p/android-scripting](http://code.google.com/p/android-scripting))下载最新版本的 APK 文件(sl4a_r3.apk)到你的设备上。为此,请使用您的条形码阅读器应用扫描网站上显示的条形码图像。

如果你使用的是 Android 模拟器,点击条形码图片下载sl4a_r3.apk。然后执行adb install sl4a_r3.apk在当前运行的仿真设备上安装这个应用。(如果收到设备离线消息,您可能需要尝试多次。)图 A–1 显示了 SL4A 在应用启动器屏幕上的图标。

images

图 A–1。 点击 SL4A 图标,开始探索 Android 应用的脚本层。

探索 SL4A

现在你已经安装了 SL4A,你会想要学习如何使用这个应用。单击 SL4A 图标,您将被带到一个显示已安装脚本(和其他项目)列表的脚本屏幕。单击菜单按钮,SL4A 将显示脚本菜单。Figure A–2 显示了一个最初为空的列表和该菜单的选项。

images

图 A–2。 SL4A 的脚本屏幕显示还没有安装脚本。

脚本菜单分为以下六类:

  • 添加:将文件夹(用于组织脚本和其他项目)、嵌入 JavaScript 代码的 HTML 页面、外壳脚本和通过扫描条形码图像获得的脚本添加到脚本屏幕。文件夹和其他项目存储在设备的/sdcard/sl4a/scripts目录中。
  • View: 查看已安装的解释器(如 Python 解释器) triggers (一种无论设备是否睡眠都重复运行脚本的意图,或者根据振铃模式变化有条件运行脚本的意图) logcat (查看系统调试输出的工具)。SL4A 只附带了 shell 解释器以及 HTML 和 JavaScript。此外,Android 模拟器似乎不支持触发器。
  • Search: 创建并显示与输入的搜索文本相匹配的脚本和其他项目的列表。当没有匹配时,搜索逻辑输出“没有找到匹配”。
  • 首选项:配置通用、脚本管理器、脚本编辑器和终端选项。
  • 刷新:重新显示脚本屏幕以显示任何更改;也许在后台运行的脚本已经更新了这个列表。
  • 帮助:从 SL4A 的 wiki 文档([code.google.com/p/android-scripting/wiki/TableOfContents?tm=6](http://code.google.com/p/android-scripting/wiki/TableOfContents?tm=6))、YouTube 截屏和终端帮助文档中获得关于使用 SL4A 的帮助。

添加外壳脚本

让我们向脚本屏幕添加一个简单的 shell 脚本。通过完成以下步骤来完成此任务:

  1. 单击电话控件中的菜单按钮。
  2. 单击屏幕底部出现的菜单中的添加菜单项。
  3. 从弹出的添加菜单中单击 Shell。
  4. hw.sh 输入到脚本编辑器屏幕顶部的单行文本字段中;这是 shell 脚本的文件名。
  5. 在多行文本字段中输入 #! /system/bin/sh ,然后输入 echo "hello, world" 。前一行告诉 Android 在哪里可以找到sh(shell 程序),但看起来并不重要;第二行告诉 Android 向标准输出设备输出一些文本。
  6. 单击电话控件中的菜单按钮。
  7. 从出现的菜单中单击保存并退出菜单项。

图 A–3 显示了点击保存&退出前编辑屏幕的样子。

images

图 A–3。 SL4A 的脚本编辑器屏幕提示输入文件名和脚本。

脚本屏幕现在应该显示一个 hw.sh 项目。点击此项,您将看到出现在图 A–4 中的图标菜单。

images

图 A–4。 图标菜单可让您在终端窗口中运行脚本、在后台运行脚本、编辑脚本、重命名脚本或删除脚本。

您可以选择在终端窗口(最左边的图标)或后台(紧挨着最左边的“齿轮”图标)运行脚本。单击任一图标运行该 shell 脚本。然而,如果您在装有 Android 模拟器的 Windows 平台上运行这个脚本,您可能看不到任何输出(可能是由于 SL4A 本身的一个错误)。

访问 Linux Shell

如果您不能通过以前面提到的方式运行这个脚本来观察hw.sh的输出,您仍然可以通过 Linux shell 运行这个脚本来观察它的输出。按照以下步骤完成此任务:

  1. 从脚本屏幕的菜单中选择查看。
  2. 从弹出的可视列表中选择口译员。
  3. 从解释器屏幕中选择 Shell 以显示终端窗口。
  4. 在终端窗口的$提示符下执行cd /sdcard/sl4a/scripts,切换到包含hw.sh的目录。
  5. 在$提示符下执行sh hw.sh,运行hw.sh

图 A–5 向您展示了如何从 shell 运行hw.sh。它还揭示了当您单击电话控制中的后退按钮时会发生什么。

images

图 A–5。 点击返回按钮得到一个“确认退出。杀死过程?”消息,并单击 Yes 按钮退出 shell。

安装 Python 解释器

虽然你不能用 SL4A 做很多事情,但是你可以用这个特殊的 app 来安装 Python 或者另一种脚本语言。完成以下步骤来安装 Python:

  1. 从主菜单中选择查看。

  2. 从弹出的可视列表中选择口译员。

  3. 按菜单电话控制按键。

  4. Select Add from the menu. Figure A–6 reveals the Add interpreters list. images

    图 A–6。 添加菜单让你选择想要安装的脚本语言解释器。

  5. 点击 Python 2.6.2。SL4A 会开始从 SL4A 网站下载这个解释器。下载完成后,SL4A 呈现图 A–7 的通知。

images

图 A–7。 点击通知告诉 SL4A 你要安装 Python。

单击通知,SL4A 会显示一个对话框(参见图 A–8)询问您是否真的要安装 Python 应用。

images

图 A–8 点击安装开始安装。

单击安装按钮。SL4A 呈现图 A–9 的安装屏幕。

images

图 A–9。 安装屏幕让你在安装过程中尽情娱乐。

最后,当安装完成时,SL4A 显示如图图 A–10 所示的应用安装屏幕。

images

图 A–10。 点击打开按钮下载支持文件。

虽然安装了 Python 应用,但尚未安装包含示例脚本等项目的支持归档。单击“打开”按钮下载这些档案。Figure A–11 显示了结果屏幕的一部分,其中仅包含一个安装按钮。

images

图 A–11。 点击安装按钮开始下载安装支持文件。

点击安装后,SL4A 开始下载这些归档文件并提取它们的文件的任务。例如,图 A–12 显示了正在提取的python_r7.zip文件的内容。

images

图 A–12。 下载并解压 Android 模拟器上的所有支持文件需要几分钟时间。

当此过程完成时,您将看到一个类似于图 A–11 所示的屏幕,但带有一个卸载按钮。此时不要单击卸载。但是,如果您单击 BACK 按钮,您现在应该看到 Python 2.6.2 出现在解释器列表中,如图 Figure A–13 所示。

images

图 A–13。 点击 Python 2.6.2 运行 Python 解释器。

如果您现在单击 Python 2.6.2,则可以运行 Python 解释器。图 A–14 显示了介绍性屏幕。

images

图 A–14。 继续输入一些 Python 代码。如果你是 Python 新手,输入 help

独立于 SL4A 安装解释器

当你访问 SL4A 的项目网站([code.google.com/p/android-scripting](http://code.google.com/p/android-scripting))时,你会发现几个独立的解释器 apk,比如PythonForAndroid_r4.apk。这些 apk 包含比您从 SL4A 中安装解释器时获得的版本更新的版本。

例如,如果您想要安装最新的 Python 版本(在撰写本文时),请单击PythonForAndroid_r4.apk链接。在生成的网页上,用您的 Android 设备扫描条形码,或者(对于 Android 模拟器)单击PythonForAndroid_r4.apk链接将这个 APK 保存到您的硬盘上,然后执行adb install PythonForAndroid_r4.apk将这个 APK 安装到模拟设备上。图 A–15 显示了生成的图标。

images

图 A–15。 点击 Python for Android 图标,安装支持文件并执行其他操作。

单击 Python for Android 图标,该应用会显示用于安装支持文件和执行其他任务的按钮(参见图 A–16)。

images

图 A–16。 Python for Android 的屏幕可以让你安装支持文件和执行其他操作。它还显示版本信息等。

您可以用类似的方式安装其他独立的解释器 apk。

用 Python 编写脚本

现在您已经安装了 Python 2.6.2,您会想要尝试这个解释器。图 A–17 展示了 Python 的一个示例会话,包括打印版本号(从sys模块的version成员获得)、打印math模块的pi常量,以及执行exit()函数来终止 Python 解释器。

images

图 A–17。 终止 Python 解释器的一种方法是执行 Python 的exit()函数。

您还想从这个解释器访问 Android API。您可以通过导入android模块、实例化该模块的Android类并调用该类的方法来完成这项任务。图 A–18 展示了一个遵循此方法的会议,以展示祝酒词。

images

图 A–18。 Android方法返回带有标识符、结果和错误信息的Result对象。

Android类的方法返回Result对象。每个对象都提供了idresulterror字段:id惟一地标识对象,result包含方法的返回值(如果方法不返回值,则为None),而error标识可能发生的任何错误(如果没有发生错误,则为None)。

如果你对一个更有雄心的 Python 脚本感兴趣,你会想看看随 Python 解释器一起安装的示例脚本,这些脚本可以从脚本屏幕访问(见图 A–2)。例如,say_time.py脚本(其代码如以下代码所示)使用AndroidttsSpeak()函数说出当前时间:

import android; import time droid = android.Android() droid.ttsSpeak(time.strftime("%_I %M %p on %A, %B %_e, %Y "))

九、附录 B:Android NDK

Android 原生开发套件(NDK)通过将 C/C++ 源代码(在其中编写应用的性能关键部分)转换为运行在 Android 设备上的原生代码库,帮助您提升应用的性能。NDK 为构建活动、处理用户输入、使用硬件传感器等提供了头文件和库。您的应用文件(包括您创建的任何本机代码库)打包到 apk 中;它们在 Android 设备的 Dalvik 虚拟机中执行。

注意:仔细考虑是否需要在 app 中集成原生代码。即使应用的一部分基于本机代码,也会增加其复杂性,并使其更难调试。此外,并不是每个应用都经历了性能提升(除了在 Android 2.2 中引入的 Dalvik 的即时编译器已经提供的性能提升)。本机代码通常最适用于处理器密集型应用,但只有在性能分析发现存在瓶颈的情况下,才能通过在本机代码中重新编码这部分应用来解决。例如,一个具有计算密集型物理模拟的游戏应用,分析显示运行不佳,将受益于本地执行这些计算。

安装 NDK

如果你认为你的应用可以从用 C/C++ 部分表达中获益,你需要安装 NDK。在此之前,请完成以下准备工作:

  • 验证您的开发平台是 Windows XP (32 位)或 Vista (32 位或 64 位)、Mac OS X 10.4.8 或更高版本(仅限 x86)还是 Linux (32 位或 64 位,在 Linux Ubuntu Dapper Drake 上测试)。NDK 官方只支持这些开发平台。
  • 如果尚未安装 Android SDK(NDK 支持 1.5 或更高版本),请安装该软件。
  • 验证您的平台包含 GNU Make 3.81 或更高版本以及 GNU Awk 的最新版本。要在 Windows 平台上运行 Make 和 Awk,必须先安装 Cygwin ,这是一个基于命令行的、类似 Unix 的 shell 工具,用于在 Windows 上运行类似 Linux 的程序。

安装 CYGWIN

必须安装 Cygwin1.7 或更高版本才能在 Windows 平台上运行 Make 和 Awk。完成以下步骤来安装 Cygwin:

  1. 将浏览器指向[cygwin.com/](http://cygwin.com/)

  2. 点击setup.exe链接,将该文件保存到硬盘上。

  3. 在您的 Windows 平台上运行这个程序,开始安装 Cygwin 版本 1.7.8-1(撰写本文时的最新版本)。如果选择不同的安装位置,请确保目录路径不包含空格。

  4. When you reach the Select Packages screen, select the Devel category and look for an entry in this category whose Package column presents make: The GNU version of the ‘make' utility. In the entry's New column, click the word Skip; this word should change to 3.81-2. Also, the Bin? column's checkbox should be checked – see Figure B–1. images

    图 B–1。 确保 3.81-2 出现在新列中,并且复选框在 Bin?在单击“下一步”之前,请检查列。

  5. 单击“下一步”按钮,继续安装。

Cygwin 在开始菜单中安装一个条目,并在桌面上安装一个图标。点击这个图标,您将看到 Cygwin 控制台(基于 Bash shell),如图图 B–2 所示。

images

图 B–2。 Cygwin 的控制台第一次开始运行时显示初始化信息。

如果您想验证 Cygwin 是否提供了对 GNU Make 3.81 或更高版本以及 GNU Awk 的访问,请输入图 B–3 中所示的命令来完成这项任务。

images

图 B–3。??awk工具不显示版本号。

你可以通过查看cygwin.com以及维基百科的 Cygwin 条目([en.wikipedia.org/wiki/Cygwin](http://en.wikipedia.org/wiki/Cygwin))来了解更多关于 Cygwin 的信息。

继续,将您的浏览器指向[developer.android.com/sdk/ndk/index.html](http://developer.android.com/sdk/ndk/index.html)并为您的平台下载以下 NDK 软件包之一——修订版 2011 年 1 月)是撰写本文时的最新版本:

  • android-ndk-r5B–windows.zip (Windows)
  • android-ndk-r5B–darwin-x86.tar.bz2 (Mac OS X:英特尔)
  • android-ndk-r5B–linux-x86.tar.bz2 (Linux 32/64 位:x86)

下载完您选择的包后,将其解压缩并将其android-ndk-r5b主目录移动到一个更合适的位置,也许是包含 Android SDK 主目录的同一个目录。

探索 NDK

现在,您已经在平台上安装了 NDK,您可能想要浏览它的主目录以发现 NDK 提供了什么。以下列表描述了位于基于 Windows 的 NDK 主目录中的目录和文件:

  • 包含组成 NDK 构建系统的文件。
  • docs包含 NDK 的基于 HTML 的文档文件。
  • Platforms包含子目录,这些子目录包含 Android SDK 安装的每个 Android 平台的头文件和共享库。
  • samples包含展示 NDK 不同方面的各种示例应用。
  • sources包含各种共享库的源代码和预构建的二进制文件,比如cpufeatures(检测目标设备的 CPU 家族及其支持的可选特性)和stlport(多平台 C++ 标准库)。Android NDK 1.5 要求开发者在这个目录下组织他们的本地代码库项目。从 Android NDK 1.6 开始,原生代码库存储在其 Android SDK 项目目录的jni子目录中。
  • tests包含执行 NDK 自动化测试的脚本和源代码。它们对于测试定制的 NDK 非常有用。
  • toolchains包含用于在 Linux、OS X 和 Windows(使用 Cygwin)平台上生成原生 ARM(高级 Risc 机器,Android 使用的 CPU,参见[en.wikipedia.org/wiki/ARM_architecture](http://en.wikipedia.org/wiki/ARM_architecture))二进制文件的编译器、连接器和其他工具。
  • 是进入 NDK 文档的入口。
  • GNUmakefile是 GNU make 使用的默认 Make 文件。
  • ndk-build是一个简化构建机器码的 shell 脚本。
  • ndk-gdb是一个 shell 脚本,用于轻松启动 NDK 生成的机器码的本地调试会话。
  • README.TXT欢迎您来到 NDK,并确定各种文档文件,通知您当前版本的变化,提供 NDK 的概述,等等。
  • RELEASE.TXT包含 NDK 的发布号。

每个platforms目录的子目录都包含头文件和面向稳定的本地 API 的共享库。Google 保证该平台的所有后续版本将支持以下 API:

  • 安卓日志(liblog)
  • Android 原生应用 API
  • C 库(libc)
  • C++ 最小支持(stlport)
  • JNI 接口 API
  • 数学库(libm)
  • OpenGL ES 1.1 和 OpenGL ES 2.0 (3D 图形库)API
  • OpenSL ES 本地音频库 API
  • Android 2.2 及以上版本的像素缓冲区访问(libjnigraphics)
  • Zlib 压缩(libz)

注意:此列表中未列出的本机系统库不稳定,可能会在 Android 平台的未来版本中发生变化。不要使用它们。

来自 NDK 的问候

也许熟悉 NDK 编程最简单的方法是创建一个调用返回 Java String对象的本地函数的小应用。例如,清单 B–1 的NDKGreetings基于单个活动的应用调用一个native getGreetingMessage()方法来返回一条问候消息,该消息通过一个对话框显示。

清单 B–1。??NDKGreetings.java

`// NDKGreetings.java

package com.apress.ndkgreetings;

import android.app.Activity;
import android.app.AlertDialog;

import android.os.Bundle;

public class NDKGreetings extends Activity
{
   static
   {
      System.loadLibrary("NDKGreetings");
   }
  private native String getGreetingMessage();
   @Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      String greeting = getGreetingMessage();
      new AlertDialog.Builder(this).setMessage(greeting).show();
   }
}`

清单 B–1 的NDKGreetings类揭示了每个包含本机代码的应用的以下三个重要特性:

  • 本机代码存储在外部库中,必须在调用其代码之前加载该库。库通常在类加载时通过调用System.loadLibrary()方法来加载。这个方法使用一个单独的String参数来标识库,没有前缀lib和后缀.so。在这个例子中,实际的库文件被命名为libNDKGreetings.so
  • 声明了一个或多个对应于位于库中的函数的本机方法。Java 通过在返回类型前加上关键字native来识别本地方法。
  • 本地方法像任何其他 Java 方法一样被调用。在幕后,Dalvik 确保在库中调用相应的本机函数(用 C/C++ 表示)。

清单 B–2 将 C 源代码呈现给通过 Java 本地接口(JNI)实现getGreetingMessage()的本地代码库。

清单 B–2。??NDKGreetings.c

`// NDKGreetings.c

#include <jni.h>

jstring
   Java_com_apress_ndkgreetings_NDKGreetings_getGreetingMessage(JNIEnv env,*
                                                                jobject this)
{
   return (*env)->NewStringUTF(env, "Greetings from the NDK!");
}`

这个清单首先指定了一个#include预处理器指令,它包含了编译源代码时的jni.h头文件的内容。

清单然后声明了 Java 的getGreetingMessage()方法的本地函数等价物。这个本地函数的头揭示了几个重要的项目:

  • 本机函数的返回类型被指定为jstring。这个类型在jni.h中定义,代表 Java 在本地代码级别的String对象类型。
  • 函数名必须以 Java 包名和类名开头,这些名称标识了关联的本机方法的声明位置。
  • 函数的第一个参数env的类型被指定为一个JNIEnv指针。jni.h中定义的JNIEnv,是一个 C 结构,标识可以被调用与 Java 交互的 JNI 函数。
  • 函数的第二个参数this的类型被指定为jobject。这种类型在jni.h中定义,在本地代码级别标识任意 Java 对象。传递给这个参数的实参是 JVM 传递给任何 Java 实例方法的隐式this实例。

该函数的单行代码取消了对其参数env的引用,以便调用NewStringUTF() JNI 函数。NewStringUTF()将它的第二个参数,一个 C 字符串,转换成它的等价的jstring(这个字符串是通过 Unicode UTF 编码标准编码的),并返回这个等价的 Java 字符串,然后这个字符串被返回给 Java。

注意:当在 C 语言的上下文中使用 JNI 时,您必须取消引用JNIEnv参数(例如*env,以便调用 JNI 函数。此外,您必须将JNIEnv参数作为第一个参数传递给 JNI 函数。相比之下,C++ 不需要这种冗长:您不必解引用JNIEnv参数,也不必将该参数作为第一个参数传递给 JNI 函数。例如,清单 B–2 的基于 C 的(*env)->NewStringUTF(env, "Greetings from the NDK!")函数调用在 C++ 中表示为env->NewStringUTF("Greetings from the NDK!")

使用 Android SDK 构建和运行 NDKGreetings

要用 Android SDK 构建NDKGreetings,首先使用 SDK 的android工具创建一个NDKGreetings项目。假设一个 Windows XP 平台,一个存储NDKGreetings项目的C:\prj\dev层次结构(在C:\prj\dev\NDKGreetings中),并且 Android 2.3 平台目标对应于整数 ID 1,从文件系统中的任何地方调用下面的命令(为了可读性分成两行)来创建NDKGreetings:

android create project -t 1 -p C:\prj\dev\NDKGreetings -a NDKGreetings                        -k com.apress.ndkgreetings

该命令在C:\prj\dev\NDKGreetings中创建各种目录和文件。例如,src目录包含com\apress\ndkgreetings目录结构,最后的ndkgreetings目录包含一个骨架NDKGreetings.java源文件。用清单 B–1 替换这个框架文件的内容。

继续,在C:\prj\dev\NDKGreetings中创建一个jni目录,并将清单 B–2 复制到C:\prj\dev\NDKGreetings\jni。另外,将清单 B–3 复制到C:\prj\dev\NDKGreetings\jni\Android.mk,这是一个 GNU make 文件(在 NDK 文档中有解释),用于创建libNDKGreetings.so库。

清单 B–3。??Android.mk

`LOCAL_PATH := ./jni

include $(CLEAR_VARS)

LOCAL_MODULE    := NDKGreetings LOCAL_SRC_FILES := NDKGreetings.c

include $(BUILD_SHARED_LIBRARY)`

如果您在 Windows 平台上工作,运行 Cygwin(如果没有运行),在 Cygwin 中,将当前目录设置为C:\prj\dev\NDKGreetings。参见图 B–4。

images

图 B–4。 /prj/dev/NDKGreetings的路径以前缀/cygdrive/c开始。

假设 NDK 主目录是android-ndk-r5b,并且它位于驱动器 C 的根目录中,执行以下命令来构建库:

../../../android-ndk-r5b/ndk-build

如果 Cygwin 成功构建了库,它会显示以下消息:

Compile thumb  : NDKGreetings <= NDKGreetings.c SharedLibrary  : libNDKGreetings.so Install        : libNDKGreetings.so => libs/armeabi/libNDKGreetings.so

这个输出表明libNDKGreetings.so位于您的NDKGreetings项目目录的libs子目录的armeabi子目录中。

提示:如果该命令输出包含短语No rule to make target的消息,编辑Android.mk删除多余的空格字符,然后重试。

假设C:\prj\dev\NDKGreetings是当前的,执行ant debug(从 Cygwin 的 shell 或普通的 Windows 命令窗口)来创建NDKGreetings-debug.apk

这个 APK 文件放在NDKGreetings项目目录的bin子目录中。要验证libNDKGreetings.so是否是该 APK 的一部分,请从bin运行以下命令:

jar tvf NDKGreetings-debug.apk

您应该在jar命令的输出中看到包含lib/armeabi/libNDKGreetings.so的一行。

要验证应用是否工作,请启动模拟器,这可以通过在命令行执行以下命令来完成:

emulator -avd test_AVD

该命令假设您已经创建了第一章中指定的test_AVD设备配置。

继续,通过以下命令在仿真设备上安装NDKGreetings-debug.apk:

adb install NDKGreetings-debug.apk

该命令假设adb位于您的路径中。它还假设bin是当前目录。

adb指示已经安装了NDKGreetings-debug.apk时,导航到应用启动器屏幕并点击 NDKGreetings 图标。figure B–5 向您展示了结果。

images

图 B–5。 按 Esc 键(在 Windows 上)使对话框消失。

对话框显示“来自 NDK 的问候!”通过调用本机代码库中的本机函数获得的消息。它还在屏幕顶部附近显示了一条微弱的“Hello World,NDKGreetings”消息。该消息源自项目的默认main.xml文件,该文件由android工具创建。

用 Eclipse 构建和运行 NDKGreetings

要用 Eclipse 构建NDKGreetings,首先创建一个新的 Android 项目,如第一章的秘籍 1-10 所述。为了方便起见,下面列出了完成此任务所需的步骤:

  1. 从“文件”菜单中选择“新建”,并从出现的弹出菜单中选择“项目”。
  2. New Project 对话框中,展开向导树中的 Android 节点,选择该节点下的 Android 项目分支,点击 Next 按钮。
  3. 在弹出的新 Android 项目对话框中,在项目名称文本框中输入 NDKGreetings ,取消勾选使用默认位置,在位置文本框中输入不带空格的路径; C:\prj\dev\NDKGreetings (假设 Windows),例如。这个输入的名称标识了存储NDKGreetings项目的文件夹。
  4. 如果未选中,请选择“在工作区中创建新项目”单选按钮。
  5. 在构建目标下,选中要用作NDGreetings构建目标的适当 Android 目标的复选框。这个目标指定了您希望您的应用在哪个 Android 平台上构建。假设您只安装了 Android 2.3 平台,那么只有这个构建目标应该出现,并且应该已经被选中。
  6. 在属性下,在应用名称文本字段中输入 NDK Greetings 。这个人类可读的标题将出现在 Android 设备上。继续,在包名文本字段中输入 com.apress.ndkgreetings 。该值是包名称空间(遵循与 Java 编程语言中的包相同的规则),所有源代码都将驻留在该名称空间中。如果未选中,请选中“创建活动”复选框,并在该复选框旁边的文本字段中输入 NDKGreetings 作为应用的启动活动的名称。未选中此复选框时,文本字段被禁用。最后,在 Min SDK Version 文本字段中输入整数 9 ,以确定在 Android 2.3 平台上正确运行NDKGreetings所需的最低 API 级别。
  7. 单击完成。

接下来,使用 Eclipse 的包资源管理器来定位NDKGreetings.java源文件节点。双击这个节点,用清单 B–1 替换编辑窗口中显示的框架内容。

使用 Package Explorer,在 NDKGreetings 项目节点下创建一个 jni 节点,添加一个 jni 的 NDKGreetings.csubnode,用清单 B–2 替换这个节点的空内容,添加一个新的 jni 的 Android.mk 子节点,用清单 B–3 替换它的空内容。

启动 Cygwin 并使用cd命令切换到项目的文件夹;比如cd /cygdrive/c/prj/dev/NDKGreetings。然后,如前一节所示执行ndk-build;比如../../../android-ndk-r5b/ndk-build。如果一切顺利,NDKGreetings项目目录的libs子目录应该包含一个armeabi子目录,其中应该包含一个libNDKGreetings.so库文件。

最后,从项目菜单中选择构建项目;bin子目录应该包含一个NDKGreetings.apk文件(如果成功)。您可能想要执行jar tvfNDKGreetings.apk来验证这个文件是否包含lib/armeabi/libNDKGreetings.so

要从 Eclipse 运行NDKGreetings,从菜单栏中选择 run,并从下拉菜单中选择 Run。如果出现 运行方式】对话框,选择 Android 应用并点击确定。Eclipse 使用test_AVD设备启动emulator,安装NDKGreetings.apk,并运行这个应用,其输出显示在图 B–5 中。

NDK 采样

NDK 安装主目录的samples子目录包含几个示例应用,展示了 NDK 的不同方面:

  • bitmap-plasma:一个应用,演示了如何从本机代码访问 Android android.graphics.Bitmap对象的像素缓冲区,并使用该功能生成一个老派的“等离子体”效果。
  • hello-gl2:一款使用 OpenGL ES 2.0 顶点和片段着色器渲染三角形的 app。(如果您在 Android 模拟器上运行此应用,您可能会收到一条错误消息,指出应用已意外停止,因为模拟器不支持 OpenGL ES 2.0 硬件模拟。)
  • hello-jni:一个应用,从共享库中实现的本地方法中加载一个字符串,然后将其显示在应用的用户界面中。这个 app 和NDKGreetings很像。
  • hello-neon:一个展示如何使用cpufeatures库在运行时检查 CPU 能力,然后在 CPU 支持的情况下使用 NEON(ARM 架构的 SIMD 指令集的市场名称)内部函数的应用。具体来说,该应用为 FIR 滤波器环路([en.wikipedia.org/wiki/Finite_impulse_response](http://en.wikipedia.org/wiki/Finite_impulse_response))实现了两个版本的微型基准,一个 C 版本和一个支持它的设备的 NEON 优化版本。
  • native-activity:演示如何使用native-app-glue静态库创建本地活动(完全用本地代码实现的活动)的应用。
  • native-audio:演示如何使用原生方法通过 OpenSL ES 播放声音的 app。
  • native-plasma:用本地活动实现的bitmap-plasma的一个版本。
  • san-angeles:通过原生 OpenGL ES APIs 渲染 3D 图形的应用,同时用android.opengl.GLSurfaceView对象管理活动生命周期。
  • two-libs:动态加载共享库,调用库提供的原生方法的 app。在这种情况下,该方法在由共享库导入的静态库中实现。

您可以使用 Eclipse 以类似于NDKGreetings的方式构建这些应用。例如,执行以下步骤来构建san-angeles:

  1. 从“文件”菜单中选择“新建”,并从出现的弹出菜单中选择“项目”。
  2. New Project 对话框中,展开向导树中的 Android 节点,选择该节点下的 Android 项目分支,点击 Next 按钮。
  3. 在出现的 New Android Project 对话框中,在项目名称文本字段中输入 san-angeles ,并选择 Create Project from existing source 单选按钮。
  4. 点击位置字段旁边的浏览按钮,通过浏览文件夹对话框,选择 NDK 安装主目录下samples子目录下的san-angeles子目录。单击确定。
  5. 选中构建目标区域中的 Android 2.3 目标复选框(或 Android 2.3.1 或 2.3.3 复选框,如果这是您的版本)。单击完成。

Eclipse 通过创建一个包含这个示例应用文件的DemoActivity项目做出响应,并在其包浏览器中显示这个项目的名称。

启动 Cygwin 并切换到项目的文件夹;比如cd/cygdrive/c/android-ndk-r5b/samples/san-angeles。然后,执行ndk-build;例如,../../ndk-build。如果一切顺利,san-angeles项目目录的libs子目录应该包含一个包含libsanangeles.soarmeabi子目录。

最后,从包资源管理器中选择DemoActivity,从项目菜单中选择构建项目;bin子目录应该包含一个DemoActivity.apk文件(如果成功)。您可能想要执行jar tvfDemoActivity.apk来验证这个文件是否包含lib/armeabi/libsanangeles.so

从菜单栏中选择运行,并从下拉菜单中选择运行。如果出现运行方式对话框,选择 Android 应用并点击确定。Eclipse 用test_AVD设备启动emulator,安装DemoActivity.apk,并运行这个应用。如果成功,您应该会看到类似于图 B–6 中所示的屏幕。

images

图 B–6。 DemoActivity带你畅游立体城市。

十、附录 C:应用设计指南

这本书关注于使用各种 Android 技术开发应用的机制。然而,如果你想成为一名成功的 Android 开发者,知道如何创建一个应用是不够的。你还必须知道如何设计只有兼容设备的用户才能使用的应用,这些应用性能良好,响应用户,并能与其他应用正常交互。这个附录的秘籍给你必要的设计知识,让你的应用大放异彩。

C–1。设计过滤的应用

问题

当您将应用发布到 Google 的 Android Market 时,您不希望该应用对不兼容的设备可见。你希望 Android Market 过滤你的应用,让这些不兼容设备的用户无法下载应用。

解决办法

Android 运行在许多设备上,这给了开发者一个巨大的潜在市场。但是,并非所有设备都包含相同的功能(例如,一些设备有摄像头,而其他设备没有),因此某些应用可能无法在某些设备上正常运行。

认识到这个问题,谷歌提供了各种市场过滤器,每当用户通过 Android 设备访问 Android Market 时都会触发这些过滤器。如果一个应用不满足过滤器,该应用不会对用户可见。Table C–1 确定了当特定元素出现在应用清单文件中时触发的三个市场过滤器。

表 C–1。基于清单元素的市场过滤器

| **过滤器名称** | **清单元素** | **过滤器如何工作** | | :-- | :-- | :-- | | 最低框架版本 | `` | 一个应用需要最低的 API 级别。不支持该级别的设备将无法运行该应用。 API 等级用整数表示。比如整数 9 对应 Android 2.3(API 9 级)。 举例:``告知 Android Market 该应用仅支持 Android 2.3 及更高版本。 如果不声明该属性,Android Market 会假设默认值为`"` 1,`"`,表示该应用兼容所有版本的 Android。 | | 设备功能 | `` | 一个应用可以要求某些设备功能出现在设备上。这个功能是在 Android 2.0 (API Level 5)中引入的。 例子:``告诉 Android Market 设备必须有指南针。 抽象的`android.content.pm.PackageManager`类为`"android.hardware.sensor.compass"`和其他特性 id 定义了 Java 常量。 | | 屏幕大小 | `` | 一个应用通过设置``元素的属性来指示它能够支持的屏幕尺寸。当应用发布时,Android Market 根据用户设备的屏幕大小,使用这些属性来决定是否向用户显示应用。 示例:``告知 Android Market,该应用无法在 QVGA(240 x 320 像素)屏幕的设备上运行。 使用 API 级或更高级别的 app 默认`smallScreens`到`"true;"`之前的级别默认此属性为`"false."` Android Market 一般假设设备可以使较小的布局适应较大的屏幕,但不能使较大的布局适应较小的屏幕。因此,如果一个应用声明只支持“正常”屏幕尺寸,Android Market 会让该应用适用于正常和大屏幕设备,但会过滤该应用,使其不适用于小屏幕设备。 |

Android Market 还使用其他应用特征(如使用该设备的用户当前所在的国家)来确定是否显示或隐藏应用。表 C–2 确定了三种市场过滤器,当这些附加特征中的一些出现时,这些过滤器就会被触发。

表 C–2。基于清单元素的市场过滤器

| **过滤器名称** | **过滤器如何工作** | | :-- | :-- | | 发布状态 | 只有已发布的应用才会出现在 Android Market 的搜索中。即使应用未发布,如果用户可以在他们购买、安装或最近卸载的应用的下载区域中看到它,也可以安装它。如果应用已被暂停,用户将无法重新安装或更新它,即使它出现在他们的下载中。 | | 定价状态 | 并非所有用户都能看到付费应用。要显示付费应用,设备必须有 SIM 卡,运行 Android 1.1 或更高版本,并且必须位于可以使用付费应用的国家(由 SIM 卡运营商确定)。 | | 国家/运营商目标 | 当你将应用上传到 Android Market 时,你可以选择特定的国家作为目标。该应用仅对您选择的国家/地区(运营商)可见,如下:
  • The operator of the device (if available) determines its country/region. If the operator cannot be determined, Android Market tries to determine the country based on IP.
  • Determine the operator according to the SIM card of the device (applicable to GSM devices), not the currently roaming operator.

|

]

C–2。设计高性能应用

问题

应用应该表现良好,尤其是在内存有限的设备上。此外,性能更好的应用消耗的电池电量更少。你想知道如何设计你的应用以获得良好的性能。

解决办法

Android 设备在很多方面都有所不同。一些设备可能具有比其他设备更快的处理器,一些设备可能具有比其他设备更大的内存,并且一些设备可能包括实时(JIT)编译器,而其他设备不具有通过将字节码指令序列动态转换为等效的本机代码序列来加速可执行代码的技术。以下列表列出了编写代码时需要考虑的一些事项,以便您的应用能够在任何设备上良好运行:

  • 仔细优化你的代码:在考虑优化代码之前,努力用一个坚实的架构来编写不影响性能的应用。一旦应用正确运行,在各种设备上分析其代码,并寻找使应用变慢的瓶颈。请记住,模拟器会给你一个错误的应用性能的印象。例如,它的网络连接基于您的开发平台的网络连接,比您可能在许多 Android 设备上遇到的要快得多。
  • 最小化对象创建:对象创建会影响性能,尤其是在垃圾收集方面。你应该尽可能地重用现有的对象,以尽量减少垃圾收集周期,垃圾收集周期会暂时降低应用的速度。例如,使用一个java.lang.StringBuilder对象(或者当多个线程可能访问这个对象时使用一个java.lang.StringBuffer对象)来构建字符串,而不是在一个循环中使用字符串连接操作符,这会导致创建不必要的中间String对象。
  • 尽量减少浮点运算:浮点运算在 Android 设备上比整数运算慢一倍左右;例如,无浮点单元和无 JIT 的 G1 设备。此外,请记住,一些设备缺乏基于硬件的整数除法指令,这意味着整数除法是在软件中执行的。当涉及到哈希表(依赖于余数操作符)时,由此产生的缓慢尤其令人烦恼。
  • 在任何需要执行复制的地方使用 system . array copy():java.lang.System类的static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)方法比用 JIT 在 Nexus One 上手工编码的循环快 9 倍左右。
  • 避免枚举:枚举虽然方便,但会增加.dex文件的大小并影响速度。例如,public enum Directions { UP, DOWN, LEFT, RIGHT }向一个.dex文件添加了几百个字节,相比之下,等效的类有四个public static final int
  • 使用增强的 for 循环语法:一般来说,在没有 JIT 的设备上,增强的 for 循环(如for (String s: strings) {})比常规的 for 循环(如for (int i = 0; i < strings.length; i++))要快,当涉及 JIT 时,也不比常规的 for 循环慢。因为增强的 for 循环在迭代一个java.util.ArrayList实例时会慢一些,所以应该使用常规的 for 循环来代替 arraylist 遍历。

您还需要仔细选择算法和数据结构。例如,线性搜索算法(从头到尾搜索一系列项目,将每个项目与一个搜索值进行比较)平均检查一半的项目,而二分搜索法算法使用递归除法技术来定位搜索值,只需很少的比较。例如,对 40 亿个条目的线性搜索平均有 20 亿次比较,而二分搜索法最多执行 32 次比较。

C–3。设计响应式应用

问题

对用户响应缓慢的应用,或者看起来挂起或冻结的应用,有触发应用不响应对话框的风险(见图 C–1),这给用户机会杀死应用(并可能卸载它)或继续等待,希望应用最终会响应。

images

图 C–1。 可怕的应用不响应对话框可能会导致用户卸载应用。

你想知道如何设计响应性应用,这样你就可以避免这个对话框(很可能会给不感兴趣的用户带来坏名声)。

解决办法

当应用无法响应用户输入时,Android 会显示应用不响应对话框。例如,应用阻塞 I/O 操作(通常是网络访问)会阻止主应用线程处理传入的用户输入事件。在 Android 确定的时间长度后,Android 得出应用被冻结的结论,并显示此对话框,让用户选择终止应用。

同样,当一个应用花费太多时间来构建一个复杂的内存数据结构,或者该应用正在执行一个密集的计算(例如计算象棋或其他一些游戏的下一步棋),Android 会认为该应用已经挂起。因此,使用方法 C–2 中描述的技术来确保这些计算是有效的,这一点很重要。

在这些情况下,应用应该创建另一个线程,并在该线程上执行大部分工作。对于活动来说尤其如此,活动应该在关键的生命周期回调方法中做尽可能少的工作,比如onCreate(Bundle)onResume()。因此,主线程(驱动用户界面事件循环)保持运行,Android 不会得出应用冻结的结论。

注:活动管理器和窗口管理器(见第一章、图 1-1 )监控 app 响应性。当他们检测到在 5 秒内没有对输入事件(例如,按键或触摸屏幕)做出响应,或者广播接收器在 10 秒内没有完成执行时,他们断定应用已经冻结,并显示应用没有响应对话框。

C–4。设计无缝应用

问题

你想知道如何设计你的应用,以便与其他应用正常交互。具体来说,你想知道你的应用应该避免做哪些事情,这样才不会给用户带来问题(并面临被卸载的可能性)。

解决办法

你的应用必须与其他应用公平竞争,这样它们就不会在用户与某个活动交互时弹出对话框之类的事情来打扰用户。此外,您不希望应用的某个活动在暂停时丢失状态,让用户在返回到该活动时困惑于为什么之前输入的数据会丢失。换句话说,你希望你的应用与其他应用很好地协作,这样它就不会扰乱用户的体验。

实现无缝体验的应用必须考虑以下规则:

  • 不要丢弃数据:因为 Android 是一个移动平台,所以可以在你的应用的活动上弹出另一个活动(可能是一个来电触发了电话应用)。当这种情况发生时,你的 activity 的void onSaveInstanceState(Bundle outState)onPause()回调方法被调用,你的 app 很可能会被杀死。如果用户当时正在编辑数据,除非通过onSaveInstanceState()保存,否则数据将会丢失。数据随后以onCreate()void onRestoreInstanceState(Bundle savedInstanceState)方式恢复。
  • 不要暴露原始数据:暴露原始数据不是一个好主意,因为其他 app 必须理解你的数据格式。如果您更改格式,这些其他应用将会中断,除非进行更新以考虑格式更改。相反,您应该创建一个通过精心设计的 API 公开数据的ContentProvider实例。
  • 不要打断用户:当用户正在与一个活动交互时,如果被一个弹出对话框打断(可能是由于startActivity(Intent)方法调用而通过后台服务激活),用户会不高兴。通知用户的首选方式是通过android.app.NotificationManager类发送消息。该消息出现在状态栏上,用户可以在方便时查看该消息。
  • 将线程用于长时间的活动:执行长时间计算或涉及其他耗时活动的组件应该将这项工作转移到另一个线程。这样做可以防止应用不响应对话框出现,并减少用户从设备上卸载应用的机会。
  • 不要让单个活动屏幕过载:用户界面复杂的应用应该通过多个活动来呈现用户界面。这样,用户就不会被屏幕上出现的大量项目淹没。此外,您的代码变得更易于维护,并且它也可以很好地与 Android 的活动堆栈模型兼容。
  • 设计您的用户界面以支持多种屏幕分辨率:不同的 Android 设备通常支持不同的屏幕分辨率。一些设备甚至可以动态改变屏幕分辨率,例如切换到横向模式。因此,重要的是要确保你的布局和绘图能够灵活地在不同的设备屏幕上正确显示。通过为关键屏幕分辨率提供不同版本的图稿(如果有),然后设计布局以适应各种尺寸,可以完成这项任务。(例如,避免使用硬编码的位置,而是使用相对布局。)做这么多,系统处理其他任务;结果是一个在任何设备上都很棒的应用。
  • 假设网络很慢: Android 设备有多种网络连接选项,有些设备比其他设备快。然而,最小公分母是 GPRS(GSM 网络的非 3G 数据服务)。即使支持 3G 的设备也要在非 3G 网络上花费大量时间,因此在未来很长一段时间内,慢速网络仍将是现实。因此,在编写应用时,一定要尽量减少网络访问和带宽。不要假设网络快;计划它是缓慢的。如果你的用户碰巧在更快的网络上,他们的体验只会得到改善。
  • 不要假设触摸屏或键盘: Android 支持各种类型的输入设备:一些 Android 设备具有完整的“QWERTY”键盘,而其他设备具有 40 键、12 键或其他键配置。同样,一些设备有触摸屏,但许多设备没有。设计应用时,请记住这些差异。不要假设特定的键盘布局,除非你想限制你的应用只能在某些设备上使用。
  • 节省设备电池:移动设备由电池供电,最大限度地减少电池消耗非常重要。两个最大的电池功耗是处理器和无线电,这就是为什么编写使用尽可能少的处理器周期和尽可能少的网络活动的应用很重要。最大限度地减少应用占用的处理器时间归结为编写高效的代码。最大限度地减少使用无线电的功耗归结为优雅地处理错误条件和只获取需要的数据。例如,如果一次尝试失败,不要不断地重试网络操作。如果它失败了一次,另一次立即尝试很可能会失败,因为用户没有接收;你只会浪费电池的能量。请记住,用户会注意到一个耗电的应用,并且最有可能卸载该应用。

posted @   绝不原创的飞龙  阅读(36)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示