安卓秘籍-问题解决方法-全-
安卓秘籍:问题解决方法(全)
零、前言
欢迎来到安卓秘籍!
如果你正在读这本书,你可能不需要被告知移动设备对软件开发者和用户来说代表着巨大的机会。近年来,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 显示了这种分层架构。
图 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)。
图 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 组件标签的父标签。icon
和label
属性指的是 Android 设备显示的代表应用的图标和标签资源。(秘籍 1–5 中简要讨论了资源。)
注意:资源由前缀@
标识,后跟资源类别名称(如string
或drawable
)、/
和资源 ID(如app_name
或icon
)。
<application>
标签的icon
和label
属性指定缺省值,这些缺省值由标签没有指定这些属性的组件继承。
嵌套在<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 用这七种方法说明了活动的生命周期。
图 1–3。 一个活动的生命周期表明不能保证onDestroy()
会被调用。
因为onDestroy()
可能不会被调用,你不应该指望使用这个方法作为保存数据的地方。例如,如果一个活动正在编辑内容提供商的数据,这些编辑通常应该在onPause()
中提交。
相比之下,onDestroy()
通常被实现来释放与活动相关联的资源(比如线程),以便被销毁的活动不会在它的应用的其余部分仍在运行时留下这些东西。
图 1–3 显示一个活动是通过调用startActivity()
开始的。更具体地说,活动是通过创建一个描述显式或隐式意图的Intent
对象,并将该对象传递给Context
的void startActivity(Intent intent)
方法(启动一个新活动;完成时不返回任何结果)。
或者,可以通过调用Activity
的void startActivityForResult(Intent intent, int requestCode)
方法来启动活动。指定的int
结果作为参数返回给Activity
的void onActivityResult(int requestCode, int resultCode, Intent data)
回调方法。
注意:响应活动可以通过调用Activity
的Intent getIntent()
方法来查看导致其启动的初始意图。Android 调用活动的void onNewIntent(Intent intent)
方法(也位于Activity
类中)将任何后续意图传递给活动。
假设你已经创建了一个名为SimpleActivity
的应用,这个应用由SimpleActivity
(在清单 1–2 中描述)和SimpleActivity2
类组成。现在假设您想从SimpleActivity
的onCreate(Bundle)
方法中启动SimpleActivity2
。以下代码片段向您展示了如何启动SimpleActivity2
:
Intent intent = new Intent(SimpleActivity.this, SimpleActivity2.class); SimpleActivity.this.startActivity(intent);
第一行创建一个描述明确意图的Intent
对象。它通过将当前SimpleActivity
实例的引用和SimpleActivity2
的Class
实例传递给Intent(Context packageContext, Class<?> cls)
构造函数来初始化这个对象。
第二行将这个Intent
对象传递给startActivity(Intent)
,后者负责启动由SimpleActivity2.class
描述的活动。如果startActivity(Intent)
无法找到指定的活动(这不应该发生),它将抛出一个android.content.ActivityNotFoundException
实例。
活动必须在应用的AndroidManifest.xml
文件中声明,否则无法启动(因为它们对 Android 是不可见的)。例如,清单 1–3 中的AndroidManifest.xml
文件声明了SimpleActivity
和SimpleActivity2
——省略号表示与本次讨论无关的内容。
清单 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 揭示了SimpleActivity
和SimpleActivity2
中的每一个都通过嵌套在<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
对象。传递给Intent
的Intent 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()
来执行初始化和清理。与Activity
的onCreate(Bundle)
和onDestroy()
方法不同,Service
的onCreate()
方法不会被重复调用,它的onDestroy()
方法总是被调用。
服务的生命周期发生在调用时间onCreate()
和返回时间onDestroy()
之间。与活动一样,服务在onCreate()
中初始化,在onDestroy()
中清理。例如,音乐回放服务可以在onCreate()
中创建播放音乐的线程,并在onDestroy()
中停止该线程。
本地服务通常通过Context
的ComponentName startService(Intent intent)
方法启动,该方法返回一个android.content.ComponentName
实例来标识启动的服务组件,如果服务不存在,则返回空引用。此外,startService(Intent)
导致了如图图 1–4 所示的生命周期。
图 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
是描述该启动请求的唯一整数。服务可以将这个值传递给Service
的boolean stopSelfResult(int startId)
方法来停止自己。
onStartCommand(Intent, int, int)
处理Intent
对象,通常返回常量Service.START_STICKY
来表示服务将继续运行,直到被显式停止。此时,服务正在运行并将继续运行,直到发生以下事件之一:
- 另一个组件通过调用
Context
的boolean stopService(Intent intent)
方法来停止服务。无论startService(Intent)
被呼叫的频率如何,只需要一次stopService(Intent)
呼叫。 - 服务通过调用
Service
的重载stopSelf()
方法之一,或者调用Service
的stopSelfResult(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);
远程服务通过Context
的boolean bindService(Intent service, ServiceConnection conn, int flags)
方法启动,该方法连接到一个正在运行的服务,在必要时创建服务,当成功连接时返回‘true’。bindService(Intent, ServiceConnection, int)
导致了由图 1–5 所示的生命周期。
图 1–5。 由bindService(Intent, ServiceConnection, int)
启动的服务的生命周期不包括对onStartCommand(Intent, int, int)
的调用。
对bindService(Intent, ServiceConnection, int)
的调用导致对onCreate()
的调用,然后是对onBind(Intent)
的调用,后者返回客户端用来与服务交互的通信通道(实现android.os.IBinder
接口的类的实例)。
客户端与服务的交互如下:
-
The client subclasses
android.content.ServiceConnection
and overrides this class's abstractvoid onServiceConnected(ComponentName className, IBinder service)
andvoid onServiceDisconnected(ComponentName name)
methods in order to receive information about the service as the service is started and stopped. WhenbindService(Intent, ServiceConnection, int)
returns true, the former method is called when a connection to the service has been established; theIBinder
argument passed to this method is the same value returned fromonBind(Intent)
. The latter method is called when a connection to the service has been lost.当承载服务的进程崩溃或被终止时,通常会发生连接丢失。
ServiceConnection
实例本身并没有被删除——到服务的绑定将保持活动状态,当服务下次运行时,客户端将收到对onServiceConnected(ComponentName, IBinder)
的调用。 -
客户端将
ServiceConnection
子类对象传递给bindService(Intent, ServiceConnection, int)
。
客户端通过调用Context
的void 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()
方法。
注: Binder
与IBinder
接口一起支持远程过程调用机制,用于进程间的通信。尽管这个例子假设服务和应用的其他部分在同一个进程中运行,但是仍然需要Binder
和IBinder
。
清单 1–6 接下来实例化SimpleBinder
并将实例的引用分配给私有binder
字段。该字段的值从随后覆盖的onBind(Intent)
方法返回。
让我们假设清单 1–2 中的SimpleActivity
类声明了一个名为ss
( private SimpleService ss;
)的私有SimpleService
字段。继续,让我们假设下面的代码片段包含在SimpleActivity
的onCreate(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
参数调用SimpleBinder
的getService()
方法并保存结果。
虽然它必须存在,但是覆盖的onServiceDisconnected(ComponentName)
方法永远不应该被调用,因为SimpleService
和SimpleActivity
在同一个进程中运行。
接下来,代码片段将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);
调用Intent
的Intent 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 组件。
图 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 显示了产生的安装档案对话框。
图 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 管理器:
- 从列表框中选择可用的包。
- 单击结果窗格上的添加附加站点按钮,并将网站的
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 中所示的窗格。
图 1–8。 初始没有安装 avd。
单击新建按钮。图 1–9 显示了产生的创建新的 Android 虚拟设备(AVD) 对话框。
图 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
装置的这些窗格。
图 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 上生成的命令窗口。
图 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 = 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
。这个类首先声明了position
和multipliers
字段:
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
文件。main
是UC
唯一的布局资源。
R.layout.main
被传递给Activity
的void 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);
。该语句首先调用View
的View findViewById(int id)
方法,找到main.xml
中声明的标识为units
的EditText
视图,实例化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)
调用ArrayAdapter
的void 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
接口的匿名类,然后通过调用AdapterView
的void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener)
方法向 spinner 注册这个实例。
每当用户选择一个新项目时,OnItemSelectedListener
的void 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)
方法。每个监听器通过调用AdapterView
的void setOnItemClickListener(AdapterView.OnItemClickListener listener)
方法注册到它的Button
对象。
Clear 按钮的 click listener 简单地执行etUnits.setText("")
来清除用户输入或来自etUnits
文本字段的转换结果。关闭按钮的点击监听器同样简单;它调用finish()
终止当前活动和UC
app。相比之下,Convert 按钮的 click listener 有更多的工作要完成:
- 获取
etUnits
文本字段的内容作为String
对象:String text = etUnits.getText().toString();
。 - 将这个
String
对象解析为双精度浮点值:double input = Double.parseDouble(text);
。 - 根据
position
的值:result = input*9.0/5.0+32;
、result = (input-32)*5.0/9.0;
或result = input*multipliers[position];
进行转换并保存结果。 - 用
result
:etUnits.setText(""+result);
更新etUnits
。
还有一项任务需要onCreate(Bundle)
执行:确保当etUnits
为空时,清除和转换按钮被禁用。毕竟,清除一个空的文本字段是没有意义的,当试图解析一个空的文本字段时,parseDouble()
会抛出一个异常。
onCreate(Bundle)
通过android.widget.TextView
的void addTextChangedListener(TextWatcher watcher)
方法向etUnits
文本字段注册一个文本观察器(其类实现android.text.TextWatcher
接口的对象)来完成这项任务。TextView
是EditText
的超类。
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_width
和layout_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>
标签根据渐变定义形状的颜色,渐变是通过startColor
、endColor
和angle
属性指定的。angle
属性指定渐变扫过矩形的方向。如果angle
不存在,角度默认为 0 度。
嵌套的<corners>
标签决定了矩形是否有角。如果此标签存在,其属性标识每个或所有角的圆度。例如,清单 1–11 中的radius
属性指定每个角的半径为 10 个与密度无关的像素—dp
是dip
的同义词。
字符串应该分开存储,以便于文本的本地化。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
,后跟debug
或release
,以指示构建模式:
- 调试模式:搭建 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 显示了单位转换器应用入口。
图 1–12。 高亮显示的单位转换器应用条目显示一个自定义图标(在一个icon.png
文件中,该文件包含在本书的代码中),该图标也存储在drawable-mdpi
中。
点击单位转换器图标,您应该会看到如图图 1–13 所示的屏幕。
图 1–13。??【单位】文本框提示用户输入一个数字。
在单位文本框中输入 37 ,你会看到如图图 1–14 所示的屏幕。
图 1–14。 清除和转换按钮不再被禁用。
点击微调按钮,您将看到如图图 1–15 所示的屏幕。
图 1–15。 微调器在其转换名称下拉列表的顶部显示提示。
选择“摄氏度至华氏度”,您将看到类似于 Figure 1–16 的屏幕。
图 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 允许你通过其versionCode
和versionName
属性在AndroidManifest.xml
的<manifest>
标签中指定版本信息来给你的应用添加版本信息。
versionCode
被赋予一个表示应用代码版本的整数值。该值是一个整数,以便其他应用可以通过编程对其进行评估,例如检查升级或降级关系。尽管您可以将该值设置为任何所需的整数,但您应该确保应用的每个后续版本都使用更大的值。Android 不强制执行这种行为,但是在后续版本中增加值是规范的。
versionName
被赋予一个字符串值,表示应用代码的发布版本,应该显示给用户(由应用显示)。这个值是一个字符串,因此您可以将应用版本描述为一个<*major*>.<*minor*>.<*point*>
字符串,或者任何其他类型的绝对或相对版本标识符。和android:versionCode
一样,Android 不把这个值用于任何内部目的。发布服务可以提取versionName
值以显示给用户。
UC
的AndroidManifest.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
指定生成密钥时使用的加密算法;虽然支持DSA
和RSA
,但是RSA
是在命令行中指定的。-keysize
指定每个生成密钥的大小(以比特为单位);因为 Google 建议使用 2048 位或更高的密钥大小(默认大小为 1024 位),所以在命令行中指定了2048
。-validity
指定密钥保持有效的期限(以天为单位)(Google 推荐值为 10000 或更大);10000
是在命令行指定的。
keytool
提示您输入密码(以保护对密钥库的访问),并再次输入相同的密码。然后,它会提示您输入姓名、您的组织单位名称、您的组织名称、您的城市或地区名称、您的州或省的名称,以及您的组织单位的两个字母的国家代码。
keytool
随后提示您指出该信息是否正确(通过键入yes
并按 Enter,或按 Enter 表示否)。假设您输入了yes
,keytool
允许您为密钥选择不同的密码,或者使用与密钥库相同的密码。
注意:保管好你的私人密钥。否则,您的应用创作身份和用户信任可能会受到影响。以下是保护您的私钥安全的一些提示:
*为密钥库和密钥选择强密码。
*用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.apk
的bin
子目录,执行以下命令行对该文件进行签名:
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.apk
与UC.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 应用之前,您必须至少完成以下三项任务中的前两项:
- 安装 Android SDK 和至少一个 Android 平台(参见方法 1–1 和 1–2)。还必须安装 JDK 5 或 JDK 6。
- 为 Eclipse IDE 安装一个与 Android SDK 和 Android 开发工具(ADT)插件兼容的 Eclipse 版本。
- 安装 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:
- 将浏览器指向位于
[www.eclipse.org/downloads/packages/eclipse-classic-361/heliossr1](http://www.eclipse.org/downloads/packages/eclipse-classic-361/heliossr1)
的 Eclipse Classic 3.6.1 页面。 - 通过单击本页右侧下载链接框中的链接之一,选择适当的分发文件。例如,您可以单击 Windows 32 位平台。
- 单击下载链接,将分发文件保存到硬盘上。例如,你可以将
eclipse-SDK-3.6.1–win32.zip
保存到你的硬盘上。 - 解压缩分发文件并将
eclipse
主目录移动到一个方便的位置。例如,您可以将eclipse
移动到您的C:\Program Files
目录中。 - 您可能还想为位于
eclipse
主目录中的eclipse
应用创建一个桌面快捷方式。
完成以下步骤以安装 ADT 插件的最新版本:
- 启动 Eclipse。
- 第一次启动 Eclipse 时,您会在闪屏后发现一个工作区启动器对话框。您可以使用此对话框选择存储项目的工作空间文件夹。您还可以告诉 Eclipse 在以后的启动中不要显示这个对话框。更改或保留默认文件夹设置,然后单击确定。
- 一旦 Eclipse 显示了它的主窗口,从 Help 菜单中选择 Install New Software。
- 在出现的安装对话框的可用软件窗格中点击添加按钮。
- 在随后出现的添加存储库对话框中,在名称字段中输入远程站点的名称(例如
Android Plugin
),并在位置字段中输入[
dl-ssl.google.com/android/eclipse/](https://dl-ssl.google.com/android/eclipse/)
。单击确定。 - 现在,您应该会在出现在安装对话框中间的列表中看到开发者工具。
- 选中开发者工具旁边的复选框,这将自动选中嵌套的 Android DDMS、Android 开发工具和 Android 层次结构查看器复选框。单击下一步。
- 出现的安装细节窗格列出了 Android DDMS、Android 开发工具和 Android Hierarchy Viewer。单击“下一步”阅读并接受许可协议,安装所有依赖项,然后单击“完成”。
- 出现一个安装软件对话框,并负责安装。如果遇到安全警告对话框,点击确定。
- 最后,Eclipse 会显示一个软件更新对话框,提示您重启这个 IDE。单击“立即重新启动”按钮重新启动。
提示:如果在第 5 步中获取插件有困难,请尝试在位置字段中指定http
而不是https
(出于安全原因,首选https
)。
要完成 ADT 插件的安装,您必须通过修改 Eclipse 中的 ADT 首选项来配置这个插件,以指向 Android SDK 主目录。通过完成以下步骤来完成此任务:
- 从“窗口”菜单中选择“首选项”,打开“首选项”面板。对于 Mac OS X,从 Eclipse 菜单中选择 Preferences。
- 从左侧面板中选择 Android。
- 单击 SDK 位置文本字段旁边的浏览按钮,找到您下载的 SDK 的主目录(例如
C:\android-sdk-windows
)。 - 单击应用,然后单击确定。
注意:关于安装 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 项目。完成以下步骤来创建该项目:
- 如果没有运行,启动 Eclipse。
- 从“文件”菜单中选择“新建”,然后从弹出菜单中选择“项目”。
- 在 New Project 对话框中,展开向导树中的 Android 节点,选择该节点下的 Android 项目分支,点击 Next 按钮。
- 在出现的新 Android 项目对话框中,将
UC
输入到项目名称文本字段中。输入的名称标识了存储UC
项目的文件夹。 - 如果未选中,请选择“在工作区中创建新项目”单选按钮。
- 在构建目标下,选择适当的 Android 目标作为
UC
的构建目标。这个目标指定了您希望您的应用在哪个 Android 平台上构建。假设您只安装了 Android 2.3 平台,那么只有这个构建目标应该出现,并且应该已经被选中。 - 在属性下,在应用名称文本字段中输入
Units Converter
。这个人类可读的标题将出现在 Android 设备上。继续,在包名文本字段中输入com.apress.uc
。该值是包名称空间(遵循与 Java 编程语言中的包相同的规则),所有源代码都将驻留在该名称空间中。选中创建活动复选框(如果未选中),并在此复选框旁边的文本字段中输入UC
作为应用启动活动的名称。未选中此复选框时,文本字段被禁用。最后,在 Min SDK 版本文本字段中输入整数9
,以确定在 Android 2.3 平台上正确运行UC
所需的最低 API 级别。 - 单击完成。
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 所示的用户界面。
图 1–17。 Eclipse 的用户界面围绕着一个菜单栏、一个工具栏、几个窗口(比如 Package Explorer 和 Outline)、一个状态栏和一个为编辑器窗口保留的空白区域。
这个用户界面被称为工作台。“包资源管理器”窗口出现在左侧,并提供一个可展开的节点列表,这些节点标识当前工作区中的各个项目及其组件。图 1–17 显示UC
是工作区中唯一的项目。
要了解 Eclipse 如何组织UC
项目,请单击该节点左侧的+图标。图 1–18 展示了一个扩展的项目层次结构。
图 1–18。 点击额外的+图标显示更多UC
的文件组织。
双击UC.java node
。Eclipse 通过显示图 1–19 中显示的UC.java
窗口做出响应。
图 1–19。【UC.java 揭示骨骼内容。
用清单 1–9 中的替换UC.java
的框架内容,忽略 Eclipse 报告的错误。稍后您将更正这些错误。
完成以下步骤,将必要的资源引入此项目:
- 双击
main.xml
节点。Eclipse 以图形布局模式显示 main.xml 编辑器窗口。 - 点击窗口下方的
main.xml
选项卡,切换到文本模式。用清单 1–10 替换窗口内容。 - 双击
strings.xml
节点。Eclipse 在资源模式下显示一个strings.xml
编辑器窗口。 - 单击窗口下方的 strings.xml 选项卡切换到文本模式。用清单 1–12 替换窗口内容。
- 右键单击“值”节点,从弹出菜单中选择“新建”,然后选择“其他”。出现一个新对话框。
- 展开向导列表中的 XML 节点,选择 XML 文件,然后单击下一步。在下一个窗格中,将文件名字段中的
NewFile.xml
替换为arrays.xml
;单击完成。 - Eclipse 在设计模式下显示一个 arrays.xml 编辑器窗口。点按窗口下方的源标签以切换到文本模式。用清单 1–13 替换窗口内容。
- 右键单击
drawable-mdpi
节点,从弹出菜单中选择新建,然后选择其他。出现一个新的对话框。 - 展开向导列表中的 XML 节点,选择 XML 文件,然后单击下一步。在下一个窗格中,将文件名字段中的
NewFile.xml
替换为gradientbg.xml
;单击完成。 - Eclipse 在设计模式下显示一个
gradientbg.xml
编辑器窗口。点按窗口下方的源标签以切换到文本模式。用清单 1–11 替换窗口内容。 - 右键单击
drawable-mdpi
下的icon.png
节点。从弹出菜单中选择删除,并删除该节点。 - 将本章代码档案中的
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。
窗口颜色是黑色的,在活动的顶部有一个标题栏(通常是灰色的)。状态栏显示在所有东西的上方,下方有轻微的阴影效果。这些都是由窗口控制的应用的可定制方面,可以为整个应用或特定活动进行设置。
图 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 显示了同时启用所有图标和进度功能的活动。请注意此视图中所有元素的相对位置。
图 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()
回调方法得到通知,在这里它可以执行与变更相关的任何必要的手动处理。
为了完全处理旋转,活动应该注册两个配置更改参数:orientation
和keyboardHidden
。orientation
参数记录器件方向改变时的任何事件的活动。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 一级)
显示一个ContextMenu
或AlertDialog
以响应用户操作。
它是如何工作的
上下文菜单
使用一个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 中的所示。
图 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。
图 2–4。 警报对话框动作菜单
2–9。自定义选项菜单
问题
当用户按下硬件菜单按钮时,您的应用需要做的不仅仅是显示标准菜单。
解决方案
(API 一级)
截取菜单按钮的KeyEvent
,代之以一个自定义视图。
它是如何工作的
通过重写onKeyDown()
或onKeyUp()
方法,可以在Activity
或View
内部拦截该事件:
@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 的结果。
图 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:inputType
和android: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:singleLine
和android:scrollHorizontally
,文本视图将不能正确地布局,以允许文本比视图长(这是滚动条滚动的一个关键要求)。设置android:ellipsize
和android: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 参数的单位定义样式。所有可以定义为ABSOULUTE
、RELATIVE_TO_SELF
或RELATIVE_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。
图 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 所示。
图 2–7。 带边框的圆角矩形作为视图背景
2–16。创建自定义状态绘图
问题
您想要定制一个元素,比如具有多种状态(默认、按下、选中等等)的Button
或CheckBox
。
解决方案
(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
,你可以将任意蒙版(以另一个位图的形式)应用于位图图像。该秘籍的基本步骤如下:
- 创建一个可变位图(空白),并在其中绘制一个画布。
- 首先在画布上绘制蒙版图案。
- 将
PorterDuffXferMode
涂在油漆上。 - 使用传输模式在画布上绘制源图像。
其中的关键成分是PorterDuffXferMode
,它考虑了绘制操作过程中源对象和目标对象的当前状态。目标是现有的画布数据,源是当前操作中应用的图形数据。
有许多模式参数可以与此相关联,它们对结果产生不同的影响,但是对于掩蔽,我们感兴趣的是使用PorterDuff.Mode.SRC_IN
模式。该模式将只在源和目的重叠的位置绘制,绘制的像素将来自源;换句话说,源被目标的边界截断。
它是如何工作的
圆角位图
这种技术的一个非常常见的用途是在将位图图像显示在ImageView
中之前对其应用圆角。对于这个例子,Figure 2–8 是我们将要遮罩的原始图像。
图 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 中。
图 2–9。 应用了圆角矩形蒙版的图像
任意遮罩图像
让我们看一个更有趣的例子。在这里,我们拍摄了两幅图像,一幅是源图像,另一幅是代表我们想要应用的蒙版的图像——在这种情况下,是一个倒置的三角形(见图 2–10)。
图 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。
图 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 所示的屏幕。
图 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
`
注:可伸缩柱是一种可以在宽度上扩展以适应任何可用空间的柱。要指定哪些列是可伸缩的,请将一个以逗号分隔的从 0 开始的整数列表分配给 嵌套在 注:为简洁起见,字符串值直接存储在 清单 7–8 展示了这个应用的 清单 7–8。 在清单中包含每个 图 7–3 显示了 图 7-3。 输入上述数据值后,点击查看条形图按钮启动 图 7–4。 除了呈现一个条形图,图 7–4 揭示了正在使用 kiChart 的试用版。你需要联系 Kidroid.com,了解许可以及如何获得不显示此消息的 kiChart 版本。 谷歌的云到设备消息传递(C2DM)框架( 谷歌 C2DM 的局限性 C2DM 是谷歌开发的一项技术,通过可扩展消息和存在协议(XMPP)在 Android 设备上运行,XMPP 是聊天客户端的常见实现。通过进一步的观察,C2DM 有许多必需的属性,这些属性通常会降低它在应用中的有用性: 利用 IBM 的 MQTT 库在您的应用中实现轻量级推送消息。MQTT 客户端库由 IBM 以纯 Java 实现的形式提供,这意味着它可以在任何 Android 设备上使用,没有特定 API 级别的限制。 MQTT 系统由三个主要组件组成: 邮件按主题过滤。主题以树形格式定义,由路径字符串表示。通过提供适当的路径,客户端可以订阅特定的主题或子主题组。例如,假设我们为应用定义了两个主题,如下所示: 客户端可以通过订阅精确的完整路径字符串来订阅任一主题。但是,如果客户希望订阅这两个主题(以及该组中稍后可能创建的任何其他主题),则可以通过以下方式方便地进行订阅: 通配符“#”表示该客户对示例组中的所有主题都感兴趣。 在这个菜谱中,我们将重点关注在 Android 设备上使用 MQTT 库实现客户端应用。IBM 为其他组件的开发和测试提供了优秀的工具,我们也将在这里展示这些工具。 MQTT Java 库可以从 IBM 的以下位置免费下载: 从下载档案中找到 为了测试您的客户机实现,IBM 提供了非常小的消息代理(RSMB)。RSMB 可以在以下位置下载: RSMB 是一个多平台下载,包括用于消息代理和发布消息的应用的命令行工具。IBM 为此工具提供的许可证禁止在生产环境中使用它;此时,您将需要推出自己的解决方案,或者使用众多可用的开源实现之一。然而,对于移动客户端的开发,RSMB 再好不过了。 因为监视传入的推送消息是一个不确定的、长期的操作,所以让我们看一个将基本功能放入服务的例子。 注意:提醒一下,您的项目目录中应该有 清单 7–9 展示了一个示例 MQTT 服务的源代码。 清单 7–9。 MQTT 示例服务 `//ClientService.java import android.app.AlarmManager; public class ClientService extends Service implements MqttSimpleCallback { //Location where broker is running privatestaticfinal String ACTION_KEEPALIVE = "com.examples.pushclient.ACTION_KEEPALIVE"; private IMqttClient mClient; @Override Intent intent = new Intent(ACTION_KEEPALIVE); try { @Override @Override private void handleCommand(Intent intent) { //Schedule a ping @Override if(mClient != null) { //Handle incoming message from remote //Handle ping alarms to keep the connection alive private void scheduleKeepAlive() { private void unscheduleKeepAlive() { /* MqttSimpleCallback Methods */ @Override @Override /Unused method/ 重要提示:这个 为了子类化 当 从这一点开始, 一旦启动,服务将向 MQTT 消息代理注册,传递一个惟一的客户机 ID 和一个保活时间。为了简单起见,这个例子根据服务创建时的当前时间来定义客户机 ID。在生产中,更独特的标识符,如 Wi-Fi MAC 地址或 keep-alive 参数是时间(以秒为单位),代理应该使用该时间使到该客户端的连接超时。为了避免这种超时,客户应该发布消息或定期 ping 代理。我们将很快更全面地讨论这项任务。 在启动过程中,客户端还订阅了一个主题。注意, 随着连接的激活和注册,来自远程代理的任何传入消息都将导致对 实现 MQTT 客户机时需要一项维护任务,那就是 ping 代理以保持连接活动。为了完成这个任务, 由于这一要求的持续性质,为保活定时器选择低频间隔是谨慎的;在这个例子中,我们选择了 30 分钟。该定时器值代表了减少设备上所需更新的频率(以节省功率和带宽)和远程代理意识到远程设备不再存在并超时之前的等待时间之间的平衡。 当不再需要推送服务时,对 为了测试设备的消息传递,您需要在您的机器上启动一个 RSMB 实例。从命令行中,导航到您解压缩下载的位置,然后导航到与您的计算机平台(Windows、Linux、Mac OS X)匹配的目录。从这里,只需执行 此时,您可以连接到服务并发布消息或注册接收消息。为了对这个 清单 7–10。 res/menu/home.xml ` 清单 7–11。 活动控制 MQTT 服务 `//ClientActivity.java import android.app.Activity; public class ClientActivity extends Activity { private Intent serviceIntent; @Override serviceIntent = new Intent(this, ClientService.class); @Override @Override 清单 7–11 创建了一个 图 7–5。 活动控制服务 注意:示例服务中的 Android 设备成功注册了来自代理的推送消息后,打开另一个命令行窗口,导航到执行 该命令将注册一个客户端,以发布主题与我们的示例相匹配的消息。您将看到以下结果: 现在你可以输入任何你喜欢的信息,然后回车。按下 Enter 键后,消息将被发送到代理,并被推送到注册的设备。尽可能多次这样做,然后使用 CTRL-C 退出程序。CTRL-C 还将终止代理服务。 提示:【RSMB】还包含第三个命令 聪明的 Android 开发者通过利用库来更快地将他们的应用交付给市场,库通过提供先前创建和测试的代码来减少开发时间。 本章的初始秘籍向您介绍了创建和使用您自己的库的主题。具体来说,您学习了如何创建和使用 Java 库 jar,其代码仅限于 Java 5(或更早版本)API 和 Android 库项目。 尽管您可能会创建自己的库来避免重复劳动,但您可能也需要使用其他人的库。例如,如果您需要一个简单的图表库,您可能想看看 kiChart,它有助于条形图和折线图的显示。 如果你正在使用云,你可能会决定使用谷歌的 C2DM 框架。但是,因为这个框架有许多缺点(比如要求最低 API 级别为 8),所以您可以考虑利用 IBM 的 MQTT 库在您的应用中实现轻量级的推送消息。 Android 脚本层(SL4A),之前被称为 Android 脚本环境,是一个在 Android 设备上安装脚本语言解释器并通过这些解释器运行脚本的平台。脚本可以访问 Android 应用可用的许多 API,但有一个大大简化的界面,使事情更容易完成。 注意: SL4A 目前只支持 Python、Perl、JRuby、Lua、BeanShell、Rhino JavaScript 和 Tcl 脚本语言。 您可以在终端窗口(命令窗口)、后台或通过区域设置( 在使用 SL4A 之前,您必须安装它。你可以从谷歌托管的项目网站( 如果你使用的是 Android 模拟器,点击条形码图片下载 图 A–1。 点击 SL4A 图标,开始探索 Android 应用的脚本层。 现在你已经安装了 SL4A,你会想要学习如何使用这个应用。单击 SL4A 图标,您将被带到一个显示已安装脚本(和其他项目)列表的脚本屏幕。单击菜单按钮,SL4A 将显示脚本菜单。Figure A–2 显示了一个最初为空的列表和该菜单的选项。 图 A–2。 SL4A 的脚本屏幕显示还没有安装脚本。 脚本菜单分为以下六类: 让我们向脚本屏幕添加一个简单的 shell 脚本。通过完成以下步骤来完成此任务: 图 A–3 显示了点击保存&退出前编辑屏幕的样子。 图 A–3。 SL4A 的脚本编辑器屏幕提示输入文件名和脚本。 脚本屏幕现在应该显示一个 hw.sh 项目。点击此项,您将看到出现在图 A–4 中的图标菜单。 图 A–4。 图标菜单可让您在终端窗口中运行脚本、在后台运行脚本、编辑脚本、重命名脚本或删除脚本。 您可以选择在终端窗口(最左边的图标)或后台(紧挨着最左边的“齿轮”图标)运行脚本。单击任一图标运行该 shell 脚本。然而,如果您在装有 Android 模拟器的 Windows 平台上运行这个脚本,您可能看不到任何输出(可能是由于 SL4A 本身的一个错误)。 如果您不能通过以前面提到的方式运行这个脚本来观察 图 A–5 向您展示了如何从 shell 运行 图 A–5。 点击返回按钮得到一个“确认退出。杀死过程?”消息,并单击 Yes 按钮退出 shell。 虽然你不能用 SL4A 做很多事情,但是你可以用这个特殊的 app 来安装 Python 或者另一种脚本语言。完成以下步骤来安装 Python: 从主菜单中选择查看。 从弹出的可视列表中选择口译员。 按菜单电话控制按键。 Select Add from the menu. Figure A–6 reveals the Add interpreters list. 图 A–6。 添加菜单让你选择想要安装的脚本语言解释器。 点击 Python 2.6.2。SL4A 会开始从 SL4A 网站下载这个解释器。下载完成后,SL4A 呈现图 A–7 的通知。 图 A–7。 点击通知告诉 SL4A 你要安装 Python。 单击通知,SL4A 会显示一个对话框(参见图 A–8)询问您是否真的要安装 Python 应用。 图 A–8 点击安装开始安装。 单击安装按钮。SL4A 呈现图 A–9 的安装屏幕。 图 A–9。 安装屏幕让你在安装过程中尽情娱乐。 最后,当安装完成时,SL4A 显示如图图 A–10 所示的应用安装屏幕。 图 A–10。 点击打开按钮下载支持文件。 虽然安装了 Python 应用,但尚未安装包含示例脚本等项目的支持归档。单击“打开”按钮下载这些档案。Figure A–11 显示了结果屏幕的一部分,其中仅包含一个安装按钮。 图 A–11。 点击安装按钮开始下载安装支持文件。 点击安装后,SL4A 开始下载这些归档文件并提取它们的文件的任务。例如,图 A–12 显示了正在提取的 图 A–12。 下载并解压 Android 模拟器上的所有支持文件需要几分钟时间。 当此过程完成时,您将看到一个类似于图 A–11 所示的屏幕,但带有一个卸载按钮。此时不要单击卸载。但是,如果您单击 BACK 按钮,您现在应该看到 Python 2.6.2 出现在解释器列表中,如图 Figure A–13 所示。 图 A–13。 点击 Python 2.6.2 运行 Python 解释器。 如果您现在单击 Python 2.6.2,则可以运行 Python 解释器。图 A–14 显示了介绍性屏幕。 图 A–14。 继续输入一些 Python 代码。如果你是 Python 新手,输入 help 。 独立于 SL4A 安装解释器 当你访问 SL4A 的项目网站( 例如,如果您想要安装最新的 Python 版本(在撰写本文时),请单击 图 A–15。 点击 Python for Android 图标,安装支持文件并执行其他操作。 单击 Python for Android 图标,该应用会显示用于安装支持文件和执行其他任务的按钮(参见图 A–16)。 图 A–16。 Python for Android 的屏幕可以让你安装支持文件和执行其他操作。它还显示版本信息等。 您可以用类似的方式安装其他独立的解释器 apk。 现在您已经安装了 Python 2.6.2,您会想要尝试这个解释器。图 A–17 展示了 Python 的一个示例会话,包括打印版本号(从 图 A–17。 终止 Python 解释器的一种方法是执行 Python 的 您还想从这个解释器访问 Android API。您可以通过导入 图 A–18。 如果你对一个更有雄心的 Python 脚本感兴趣,你会想看看随 Python 解释器一起安装的示例脚本,这些脚本可以从脚本屏幕访问(见图 A–2)。例如, Android 原生开发套件(NDK)通过将 C/C++ 源代码(在其中编写应用的性能关键部分)转换为运行在 Android 设备上的原生代码库,帮助您提升应用的性能。NDK 为构建活动、处理用户输入、使用硬件传感器等提供了头文件和库。您的应用文件(包括您创建的任何本机代码库)打包到 apk 中;它们在 Android 设备的 Dalvik 虚拟机中执行。 注意:仔细考虑是否需要在 app 中集成原生代码。即使应用的一部分基于本机代码,也会增加其复杂性,并使其更难调试。此外,并不是每个应用都经历了性能提升(除了在 Android 2.2 中引入的 Dalvik 的即时编译器已经提供的性能提升)。本机代码通常最适用于处理器密集型应用,但只有在性能分析发现存在瓶颈的情况下,才能通过在本机代码中重新编码这部分应用来解决。例如,一个具有计算密集型物理模拟的游戏应用,分析显示运行不佳,将受益于本地执行这些计算。 如果你认为你的应用可以从用 C/C++ 部分表达中获益,你需要安装 NDK。在此之前,请完成以下准备工作: 安装 CYGWIN 必须安装 Cygwin1.7 或更高版本才能在 Windows 平台上运行 Make 和 Awk。完成以下步骤来安装 Cygwin: 将浏览器指向 点击 在您的 Windows 平台上运行这个程序,开始安装 Cygwin 版本 1.7.8-1(撰写本文时的最新版本)。如果选择不同的安装位置,请确保目录路径不包含空格。 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. 图 B–1。 确保 3.81-2 出现在新列中,并且复选框在 Bin?在单击“下一步”之前,请检查列。 单击“下一步”按钮,继续安装。 Cygwin 在开始菜单中安装一个条目,并在桌面上安装一个图标。点击这个图标,您将看到 Cygwin 控制台(基于 Bash shell),如图图 B–2 所示。 图 B–2。 Cygwin 的控制台第一次开始运行时显示初始化信息。 如果您想验证 Cygwin 是否提供了对 GNU Make 3.81 或更高版本以及 GNU Awk 的访问,请输入图 B–3 中所示的命令来完成这项任务。 图 B–3。?? 你可以通过查看 继续,将您的浏览器指向 下载完您选择的包后,将其解压缩并将其 现在,您已经在平台上安装了 NDK,您可能想要浏览它的主目录以发现 NDK 提供了什么。以下列表描述了位于基于 Windows 的 NDK 主目录中的目录和文件: 每个 注意:此列表中未列出的本机系统库不稳定,可能会在 Android 平台的未来版本中发生变化。不要使用它们。 也许熟悉 NDK 编程最简单的方法是创建一个调用返回 Java 清单 B–1。?? `// NDKGreetings.java package com.apress.ndkgreetings; import android.app.Activity; import android.os.Bundle; public class NDKGreetings extends Activity 清单 B–1 的 清单 B–2 将 C 源代码呈现给通过 Java 本地接口(JNI)实现 清单 B–2。?? `// NDKGreetings.c #include <jni.h> jstring 这个清单首先指定了一个 清单然后声明了 Java 的 该函数的单行代码取消了对其参数 注意:当在 C 语言的上下文中使用 JNI 时,您必须取消引用 要用 Android SDK 构建 该命令在 继续,在 清单 B–3。?? `LOCAL_PATH := ./jni include $(CLEAR_VARS) LOCAL_MODULE := NDKGreetings include $(BUILD_SHARED_LIBRARY)` 如果您在 Windows 平台上工作,运行 Cygwin(如果没有运行),在 Cygwin 中,将当前目录设置为 图 B–4。 到 假设 NDK 主目录是 如果 Cygwin 成功构建了库,它会显示以下消息: 这个输出表明 提示:如果该命令输出包含短语 假设 这个 APK 文件放在 您应该在 要验证应用是否工作,请启动模拟器,这可以通过在命令行执行以下命令来完成: 该命令假设您已经创建了第一章中指定的 继续,通过以下命令在仿真设备上安装 该命令假设 当 图 B–5。 按 Esc 键(在 Windows 上)使对话框消失。 对话框显示“来自 NDK 的问候!”通过调用本机代码库中的本机函数获得的消息。它还在屏幕顶部附近显示了一条微弱的“Hello World,NDKGreetings”消息。该消息源自项目的默认 要用 Eclipse 构建 接下来,使用 Eclipse 的包资源管理器来定位 使用 Package Explorer,在 NDKGreetings 项目节点下创建一个 jni 节点,添加一个 jni 的 NDKGreetings.csubnode,用清单 B–2 替换这个节点的空内容,添加一个新的 jni 的 Android.mk 子节点,用清单 B–3 替换它的空内容。 启动 Cygwin 并使用 最后,从项目菜单中选择构建项目; 要从 Eclipse 运行 NDK 安装主目录的 您可以使用 Eclipse 以类似于 Eclipse 通过创建一个包含这个示例应用文件的 启动 Cygwin 并切换到项目的文件夹;比如 最后,从包资源管理器中选择 从菜单栏中选择运行,并从下拉菜单中选择运行。如果出现运行方式对话框,选择 Android 应用并点击确定。Eclipse 用 图 B–6。 这本书关注于使用各种 Android 技术开发应用的机制。然而,如果你想成为一名成功的 Android 开发者,知道如何创建一个应用是不够的。你还必须知道如何设计只有兼容设备的用户才能使用的应用,这些应用性能良好,响应用户,并能与其他应用正常交互。这个附录的秘籍给你必要的设计知识,让你的应用大放异彩。 当您将应用发布到 Google 的 Android Market 时,您不希望该应用对不兼容的设备可见。你希望 Android Market 过滤你的应用,让这些不兼容设备的用户无法下载应用。 Android 运行在许多设备上,这给了开发者一个巨大的潜在市场。但是,并非所有设备都包含相同的功能(例如,一些设备有摄像头,而其他设备没有),因此某些应用可能无法在某些设备上正常运行。 认识到这个问题,谷歌提供了各种市场过滤器,每当用户通过 Android 设备访问 Android Market 时都会触发这些过滤器。如果一个应用不满足过滤器,该应用不会对用户可见。Table C–1 确定了当特定元素出现在应用清单文件中时触发的三个市场过滤器。 表 C–1。基于清单元素的市场过滤器 Android Market 还使用其他应用特征(如使用该设备的用户当前所在的国家)来确定是否显示或隐藏应用。表 C–2 确定了三种市场过滤器,当这些附加特征中的一些出现时,这些过滤器就会被触发。 表 C–2。基于清单元素的市场过滤器 | ] 应用应该表现良好,尤其是在内存有限的设备上。此外,性能更好的应用消耗的电池电量更少。你想知道如何设计你的应用以获得良好的性能。 Android 设备在很多方面都有所不同。一些设备可能具有比其他设备更快的处理器,一些设备可能具有比其他设备更大的内存,并且一些设备可能包括实时(JIT)编译器,而其他设备不具有通过将字节码指令序列动态转换为等效的本机代码序列来加速可执行代码的技术。以下列表列出了编写代码时需要考虑的一些事项,以便您的应用能够在任何设备上良好运行: 您还需要仔细选择算法和数据结构。例如,线性搜索算法(从头到尾搜索一系列项目,将每个项目与一个搜索值进行比较)平均检查一半的项目,而二分搜索法算法使用递归除法技术来定位搜索值,只需很少的比较。例如,对 40 亿个条目的线性搜索平均有 20 亿次比较,而二分搜索法最多执行 32 次比较。 对用户响应缓慢的应用,或者看起来挂起或冻结的应用,有触发应用不响应对话框的风险(见图 C–1),这给用户机会杀死应用(并可能卸载它)或继续等待,希望应用最终会响应。 图 C–1。 可怕的应用不响应对话框可能会导致用户卸载应用。 你想知道如何设计响应性应用,这样你就可以避免这个对话框(很可能会给不感兴趣的用户带来坏名声)。 当应用无法响应用户输入时,Android 会显示应用不响应对话框。例如,应用阻塞 I/O 操作(通常是网络访问)会阻止主应用线程处理传入的用户输入事件。在 Android 确定的时间长度后,Android 得出应用被冻结的结论,并显示此对话框,让用户选择终止应用。 同样,当一个应用花费太多时间来构建一个复杂的内存数据结构,或者该应用正在执行一个密集的计算(例如计算象棋或其他一些游戏的下一步棋),Android 会认为该应用已经挂起。因此,使用方法 C–2 中描述的技术来确保这些计算是有效的,这一点很重要。 在这些情况下,应用应该创建另一个线程,并在该线程上执行大部分工作。对于活动来说尤其如此,活动应该在关键的生命周期回调方法中做尽可能少的工作,比如 注:活动管理器和窗口管理器(见第一章、图 1-1 )监控 app 响应性。当他们检测到在 5 秒内没有对输入事件(例如,按键或触摸屏幕)做出响应,或者广播接收器在 10 秒内没有完成执行时,他们断定应用已经冻结,并显示应用没有响应对话框。 你想知道如何设计你的应用,以便与其他应用正常交互。具体来说,你想知道你的应用应该避免做哪些事情,这样才不会给用户带来问题(并面临被卸载的可能性)。 你的应用必须与其他应用公平竞争,这样它们就不会在用户与某个活动交互时弹出对话框之类的事情来打扰用户。此外,您不希望应用的某个活动在暂停时丢失状态,让用户在返回到该活动时困惑于为什么之前输入的数据会丢失。换句话说,你希望你的应用与其他应用很好地协作,这样它就不会扰乱用户的体验。 实现无缝体验的应用必须考虑以下规则:
`
main.xml
通过<TableLayout>
标签描述了一个表格布局,其中用户界面分为六行三列。这个标签的layout_width
和layout_height
属性的"fill_parent"
赋值告诉这个布局占据活动的整个屏幕。对这个标签的stretchColumns
属性的"*"
赋值告诉这个布局给每一列一个相同的宽度。stretchColumns
。例如,"0, 1"
指定列 0(最左边的列)和列 1 是可拉伸的。"*"
赋值表示所有列都是可拉伸的,这使得它们具有相同的宽度。<TableLayout>
和它的</TableLayout>
伙伴中的是一系列的<TableRow>
标签。每个<TableRow>
标签描述了表格布局中单个行的内容,这些内容是零个或多个视图的变体(例如TextView
和EditText
),其中每个视图构成一列。main.xml
中,而不是存储在单独的strings.xml
文件中。把它当作一个引入strings.xml
的练习,并用对存储在strings.xml
中的字符串的引用替换这些文字字符串。AndroidManifest.xml
文件,它描述了这个应用及其活动。AndroidManifest.xml
为ChartDemo
App 汇集一切<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.apress.chartdemo" android:versionCode="1" android:versionName="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".ChartDemo" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> **<activity android:name=".BarChart"/>** **<activity android:name=".LineChart"/>** </application> <uses-sdk android:minSdkVersion="9" /> </manifest>
BarChart
和LineChart
活动的<activity>
标记是很重要的。否则,运行时对话框会显示以下消息:“The application Chart Demo (process com.apress.chartdemo) has stopped unexpectedly. Please try again.”
ChartDemo
的主要活动,每个季度输入样本值。ChartDemo
允许您输入八个数据值,并选择通过条形图或折线图显示这些值。BarChart
活动,显示图 7–4 中所示的条形图。BarChart
通过一系列彩色条显示每个数组的数据值。7–6 岁。实用推送消息
问题
[
code.google.com/android/c2dm/index.html](http://code.google.com/android/c2dm/index.html)
)旨在实现设备的推送消息传递,它有许多缺点,这些缺点可能会影响它作为推送消息传递的实用解决方案。你的应用需要一个更通用的推送解决方案。
解决办法
examples/one examples/two
examples/#
它是如何工作的
www-01.ibm.com/support/docview.wss?uid=swg24006006
。除了库 JAR 之外,下载档案还包含示例代码、API Javadoc 和使用文档。wmqtt.jar
文件。这是 Android 项目中必须包含的库。按照惯例,这意味着应该在您的项目目录中创建一个/libs
目录,并且应该在那里插入这个 JAR。[www.alphaworks.ibm.com/tech/rsmb](http://www.alphaworks.ibm.com/tech/rsmb)
。客户端示例
libs/wmqtt.jar
,并在您的项目构建路径中被引用。
package com.apress.pushclient;
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;
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";
private AlarmManager mManager;
private PendingIntent alarmIntent;
public void onCreate() {
super.onCreate();
mManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
alarmIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
registerReceiver(mReceiver, new IntentFilter(ACTION_KEEPALIVE));
//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();
}
}
public void onStart(Intent intent, int startId) {
//Callback on Android devices prior to 2.0
handleCommand(intent);
}
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;
}
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);
scheduleKeepAlive();
} catch (MqttException e) {
e.printStackTrace();
}
}
public void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);
unscheduleKeepAlive();
try {
mClient.disconnect();
mClient.terminate();
} catch (MqttPersistenceException e) {
e.printStackTrace();
}
mClient = null;
}
}
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();
}
};
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();
}
};
long nextWakeup = System.currentTimeMillis() + (KEEP_ALIVE * 1000);
mManager.set(AlarmManager.RTC_WAKEUP, nextWakeup, alarmIntent);
}
mManager.cancel(alarmIntent);
}
public void connectionLost() throws Exception {
mClient.terminate();
mClient = null;
stopSelf();
}
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();
}
//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
,这是一个常量,告诉系统应该让这个服务继续运行,如果它因为内存原因被提前终止,就重新启动它。TelephonyManager.getDeviceId()
可能更合适,记住这两种选择都不能保证出现在所有设备上。subscribe()
方法将数组作为参数;一个客户端可以在一个方法调用中订阅多个主题。每个主题还订阅有请求的服务质量(QoS)值。对移动设备请求的最委婉的值是零,告诉代理只发送一次消息而不需要确认。这样做减少了代理和设备之间所需的握手次数。publishArrived()
的调用,并传递关于该消息的数据。这个方法可以在任何由MqttClient
创建和维护的后台线程上调用,所以不要在这里直接做任何与主线程相关的事情是很重要的。在本例中,所有传入的消息都被传递到一个本地的Handler
,以保证结果Toast
被发送到主线程上进行显示。Service
向AlarmManager
注册,以根据匹配保活参数的时间表触发广播。即使设备当前处于睡眠状态,也必须完成该任务,因此每次使用AlarmManager.RTC_WAKEUP
设置闹铃。当每个警报触发时,Service
简单地调用MqttClient.ping()
并安排下一次保活更新。Context.stopService()
的外部调用将导致对onDestroy()
的调用。在这里,Service
拆除 MQTT 连接,删除任何未决的警报,并释放所有资源。作为MqttSimpleCallback
接口的一部分实现的第二个回调是onConnectionLost()
,表示意外的断开。在这些情况下,Service
会像手动停止请求一样自行停止。测试客户端
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
,可以用来启动和停止服务。
package com.apress.pushclient;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.home, menu);
return true;
}
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);
}
}`Intent
,它将被两个菜单选项用来随意启动和停止服务(参见图 7–5)。通过按下菜单按钮并选择“启动服务”,MQTT 连接将启动并为设备注册主题为“进程/示例”的消息HOST
值需要指向运行您的 RSMB 实例的机器。即使你在同一台机器上的模拟器中测试,这个值也是而不是 localhost
!至少,您必须将模拟器或设备指向运行您的代理的机器的 IP 地址。broker
的同一个目录。另一个命令stdinpub
可用于连接到代理实例,并将消息发布到设备。从命令行键入以下命令:stdinpub apress/examples
Using topic apress/examples Connecting
stdoutsub
,用于向您的本地代理服务订阅一组主题。这个命令让您完全关闭循环,并测试问题是发生在测试套件中还是您的 Android 应用中。总结
八、附录 A:Android 脚本层
[www.twofortyfouram.com/](http://www.twofortyfouram.com/)
)交互运行脚本。 Locale 是一款 Android 应用,可以让你在预定的时间或者满足其他条件时运行脚本(例如,当你进入剧院或法庭时,运行脚本将你的手机铃声模式改为振动)。安装 SL4A
[
code.google.com/p/android-scripting](http://code.google.com/p/android-scripting)
)下载最新版本的 APK 文件(sl4a_r3.apk
)到你的设备上。为此,请使用您的条形码阅读器应用扫描网站上显示的条形码图像。sl4a_r3.apk
。然后执行adb install sl4a_r3.apk
在当前运行的仿真设备上安装这个应用。(如果收到设备离线消息,您可能需要尝试多次。)图 A–1 显示了 SL4A 在应用启动器屏幕上的图标。探索 SL4A
/sdcard/sl4a/scripts
目录中。[
code.google.com/p/android-scripting/wiki/TableOfContents?tm=6](http://code.google.com/p/android-scripting/wiki/TableOfContents?tm=6)
)、YouTube 截屏和终端帮助文档中获得关于使用 SL4A 的帮助。添加外壳脚本
hw.sh
输入到脚本编辑器屏幕顶部的单行文本字段中;这是 shell 脚本的文件名。#! /system/bin/sh
,然后输入 echo "hello, world"
。前一行告诉 Android 在哪里可以找到sh
(shell 程序),但看起来并不重要;第二行告诉 Android 向标准输出设备输出一些文本。访问 Linux Shell
hw.sh
的输出,您仍然可以通过 Linux shell 运行这个脚本来观察它的输出。按照以下步骤完成此任务:
cd /sdcard/sl4a/scripts
,切换到包含hw.sh
的目录。sh hw.sh
,运行hw.sh
。hw.sh
。它还揭示了当您单击电话控制中的后退按钮时会发生什么。安装 Python 解释器
python_r7.zip
文件的内容。[
code.google.com/p/android-scripting](http://code.google.com/p/android-scripting)
)时,你会发现几个独立的解释器 apk,比如PythonForAndroid_r4.apk
。这些 apk 包含比您从 SL4A 中安装解释器时获得的版本更新的版本。PythonForAndroid_r4.apk
链接。在生成的网页上,用您的 Android 设备扫描条形码,或者(对于 Android 模拟器)单击PythonForAndroid_r4.apk
链接将这个 APK 保存到您的硬盘上,然后执行adb install PythonForAndroid_r4.apk
将这个 APK 安装到模拟设备上。图 A–15 显示了生成的图标。用 Python 编写脚本
sys
模块的version
成员获得)、打印math
模块的pi
常量,以及执行exit()
函数来终止 Python 解释器。exit()
函数。android
模块、实例化该模块的Android
类并调用该类的方法来完成这项任务。图 A–18 展示了一个遵循此方法的会议,以展示祝酒词。Android
方法返回带有标识符、结果和错误信息的Result
对象。Android
类的方法返回Result
对象。每个对象都提供了id
、result
和error
字段:id
惟一地标识对象,result
包含方法的返回值(如果方法不返回值,则为None
),而error
标识可能发生的任何错误(如果没有发生错误,则为None
)。say_time.py
脚本(其代码如以下代码所示)使用Android
的ttsSpeak()
函数说出当前时间:import android; import time droid = android.Android() droid.ttsSpeak(time.strftime("%_I %M %p on %A, %B %_e, %Y "))
九、附录 B:Android NDK
安装 NDK
[
cygwin.com/](http://cygwin.com/)
。setup.exe
链接,将该文件保存到硬盘上。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
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)
)二进制文件的编译器、连接器和其他工具。GNUmakefile
是 GNU make 使用的默认 Make 文件。ndk-build
是一个简化构建机器码的 shell 脚本。ndk-gdb
是一个 shell 脚本,用于轻松启动 NDK 生成的机器码的本地调试会话。README.TXT
欢迎您来到 NDK,并确定各种文档文件,通知您当前版本的变化,提供 NDK 的概述,等等。RELEASE.TXT
包含 NDK 的发布号。platforms
目录的子目录都包含头文件和面向稳定的本地 API 的共享库。Google 保证该平台的所有后续版本将支持以下 API:
liblog
)libc
)stlport
)libm
)libjnigraphics
)libz
)来自 NDK 的问候
String
对象的本地函数的小应用。例如,清单 B–1 的NDKGreetings
基于单个活动的应用调用一个native getGreetingMessage()
方法来返回一条问候消息,该消息通过一个对话框显示。NDKGreetings.java
import android.app.AlertDialog;
{
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();
}
}`NDKGreetings
类揭示了每个包含本机代码的应用的以下三个重要特性:
System.loadLibrary()
方法来加载。这个方法使用一个单独的String
参数来标识库,没有前缀lib
和后缀.so
。在这个例子中,实际的库文件被命名为libNDKGreetings.so
。native
来识别本地方法。getGreetingMessage()
的本地代码库。NDKGreetings.c
Java_com_apress_ndkgreetings_NDKGreetings_getGreetingMessage(JNIEnv env,*
jobject this)
{
return (*env)->NewStringUTF(env, "Greetings from the NDK!");
}`#include
预处理器指令,它包含了编译源代码时的jni.h
头文件的内容。getGreetingMessage()
方法的本地函数等价物。这个本地函数的头揭示了几个重要的项目:
jstring
。这个类型在jni.h
中定义,代表 Java 在本地代码级别的String
对象类型。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。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
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
库。Android.mk
LOCAL_SRC_FILES := NDKGreetings.cC:\prj\dev\NDKGreetings
。参见图 B–4。/prj/dev/NDKGreetings
的路径以前缀/cygdrive/c
开始。android-ndk-r5b
,并且它位于驱动器 C 的根目录中,执行以下命令来构建库:../../../android-ndk-r5b/ndk-build
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
。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 向您展示了结果。main.xml
文件,该文件由android
工具创建。用 Eclipse 构建和运行 NDKGreetings
NDKGreetings
,首先创建一个新的 Android 项目,如第一章的秘籍 1-10 所述。为了方便起见,下面列出了完成此任务所需的步骤:
NDKGreetings
,取消勾选使用默认位置,在位置文本框中输入不带空格的路径; C:\prj\dev\NDKGreetings
(假设 Windows),例如。这个输入的名称标识了存储NDKGreetings
项目的文件夹。NDGreetings
构建目标的适当 Android 目标的复选框。这个目标指定了您希望您的应用在哪个 Android 平台上构建。假设您只安装了 Android 2.3 平台,那么只有这个构建目标应该出现,并且应该已经被选中。NDK Greetings
。这个人类可读的标题将出现在 Android 设备上。继续,在包名文本字段中输入 com.apress.ndkgreetings
。该值是包名称空间(遵循与 Java 编程语言中的包相同的规则),所有源代码都将驻留在该名称空间中。如果未选中,请选中“创建活动”复选框,并在该复选框旁边的文本字段中输入 NDKGreetings
作为应用的启动活动的名称。未选中此复选框时,文本字段被禁用。最后,在 Min SDK Version 文本字段中输入整数 9
,以确定在 Android 2.3 平台上正确运行NDKGreetings
所需的最低 API 级别。NDKGreetings.java
源文件节点。双击这个节点,用清单 B–1 替换编辑窗口中显示的框架内容。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
。NDKGreetings
,从菜单栏中选择 run,并从下拉菜单中选择 Run。如果出现 运行方式】对话框,选择 Android 应用并点击确定。Eclipse 使用test_AVD
设备启动emulator
,安装NDKGreetings.apk
,并运行这个应用,其输出显示在图 B–5 中。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。在这种情况下,该方法在由共享库导入的静态库中实现。NDKGreetings
的方式构建这些应用。例如,执行以下步骤来构建san-angeles
:
san-angeles
,并选择 Create Project from existing source 单选按钮。samples
子目录下的san-angeles
子目录。单击确定。DemoActivity
项目做出响应,并在其包浏览器中显示这个项目的名称。cd/cygdrive/c/android-ndk-r5b/samples/san-angeles
。然后,执行ndk-build
;例如,../../ndk-build
。如果一切顺利,san-angeles
项目目录的libs
子目录应该包含一个包含libsanangeles.so
的armeabi
子目录。DemoActivity
,从项目菜单中选择构建项目;bin
子目录应该包含一个DemoActivity.apk
文件(如果成功)。您可能想要执行jar tvfDemoActivity.apk
来验证这个文件是否包含lib/armeabi/libsanangeles.so
。test_AVD
设备启动emulator
,安装DemoActivity.apk
,并运行这个应用。如果成功,您应该会看到类似于图 B–6 中所示的屏幕。DemoActivity
带你畅游立体城市。十、附录 C:应用设计指南
C–1。设计过滤的应用
问题
解决办法
C–2。设计高性能应用
问题
解决办法
java.lang.StringBuilder
对象(或者当多个线程可能访问这个对象时使用一个java.lang.StringBuffer
对象)来构建字符串,而不是在一个循环中使用字符串连接操作符,这会导致创建不必要的中间String
对象。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 (String s: strings) {}
)比常规的 for 循环(如for (int i = 0; i < strings.length; i++)
)要快,当涉及 JIT 时,也不比常规的 for 循环慢。因为增强的 for 循环在迭代一个java.util.ArrayList
实例时会慢一些,所以应该使用常规的 for 循环来代替 arraylist 遍历。C–3。设计响应式应用
问题
解决办法
onCreate(Bundle)
和onResume()
。因此,主线程(驱动用户界面事件循环)保持运行,Android 不会得出应用冻结的结论。C–4。设计无缝应用
问题
解决办法
void onSaveInstanceState(Bundle outState)
和onPause()
回调方法被调用,你的 app 很可能会被杀死。如果用户当时正在编辑数据,除非通过onSaveInstanceState()
保存,否则数据将会丢失。数据随后以onCreate()
或void onRestoreInstanceState(Bundle savedInstanceState)
方式恢复。ContentProvider
实例。startActivity(Intent)
方法调用而通过后台服务激活),用户会不高兴。通知用户的首选方式是通过android.app.NotificationManager
类发送消息。该消息出现在状态栏上,用户可以在方便时查看该消息。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律