安卓应用开发学习手册(四)
十五、音频排序:Android SoundPool类
在本章中,我们将了解 Android 中一个更专业的音频播放类,名为: SoundPool 。这个类明显不同于 Android MediaPlayer 类,以至于我决定在本书中专门辟出一章来介绍这个有用的音频类。
本章深入探讨了 Android SoundPool,并定义了它与 MediaPlayer 的不同之处,以及在什么类型的情况下,这些类中的每一个都应该用于数字音频素材播放。
简而言之,SoundPool 的特别之处在于它允许图像合成的音频等价物(分层和混合)。这意味着,像图像合成一样,与音频相关的新媒体素材可以被分解成它们的组成部分并单独控制。
使用 SoundPool,这些组件可以附加到 Java 代码上,甚至可以用 Java 代码进行操作。这使得开发人员可以将这些富媒体数字素材组件作为一个完整的作品呈现给用户,而实际上它们是由您的应用无缝合成的(或者用流行的音频术语来说:混音、移调和排序)。
这使得 Android 开发人员能够将交互性注入到他们的新媒体素材中,而在过去,传统媒体素材(如音乐、电影或电视)只是一个漫长的线性表演;可重复但总是相同的,因此最终用户会厌倦他们的用户体验。
在本章中,我们将首先介绍音频合成和序列的基本原理,然后我们将回顾 SoundPool 类及其功能,就像我们在前一章中对 MediaPlayer 类所做的那样。然后,我们将在 Hello_World 应用中实现 SoundPool 类,看看它如何让我们的声音播放得更快,并让我们能够灵活地组合它们并改变它们的声音方式。
MIDI 和音频序列:概念和原理
音频序列的最早形式利用了 MIDI,我们在前一章中了解到它代表乐器数字接口,允许通过计算机记录和回放演奏数据。
实现这一功能的早期计算机是 Amiga、Atari ST-1040 和 Apple Macintosh,它们运行的软件包名为 MIDI sequencers ,来自 Opcode (Vision)、Steinberg (CuBase)、Cakewalk (Sonar)、Mark of the Unicorn (Performer)、PropellerHead (Reason)和 eMagic (Logic)等软件公司。
这些 MIDI 音序器软件包中的大部分仍然存在(有几个也被其他公司收购了),并且直到今天仍然非常受全球数字音乐人的欢迎。
MIDI 音序器允许演奏数据排序,其中一名作曲家可以使用合成器将每个乐器部分播放到计算机中,合成器设置为给定的乐器样本,比如吉他或钢琴样本,然后计算机会在作曲家为其演奏的计算机回放版本伴奏时回放这些演奏数据。
当电脑播放出到目前为止创作的作曲曲目时,作曲家会在中演奏下一个声部或曲目,使用该歌曲、乐谱或叮当声安排中需要的下一种乐器。
最终,随着计算机处理能力的提高以及专用数字音频适配器(如 Creative Labs 的 SoundBlaster 和 X-Fi)以可承受的价格广泛上市,MIDI 音序器增加了数字音频功能和 MIDI 回放功能。
事实证明,音频序列的概念同样适用于由计算机直接处理的数字音频样本,就像它适用于 MIDI 演奏数据序列一样。随着电脑变得更加强大,更多的数字音频可以被采样和回放,尽管不像 MIDI 那么容易,因为 MIDI 只是演奏数据(音符开,音符关)。
计算机的功能越来越强大,内存越来越大,可以容纳更多的样本(SoundPool 的一个问题,我们很快就会看到),增加了更多的处理器(64 位多核处理器现在允许每个处理器有 4/6/8/16 个 CPU 内核)和更快的处理速度,64 位音频适配器和多核 DSP(数字信号处理器)功能现在都可以买到,而且价格合理。
由于这个原因,音频序列器现在允许的选项比 20 世纪 80 年代早期的 MIDI 序列器多一千倍,尽管它们仍然支持 MIDI 演奏数据,并与音频采样数据一起播放。这是因为 MIDI 非常高效,如果作曲家喜欢这样工作,它允许使用合成器键盘回放样本。音频音序器增加了通常只有合成器才有的过多功能;接下来我们将讨论这些特性、术语和概念。
数字音频合成:基本概念和原理
最初的一些 MIDI 键盘实际上只是数字音频采样器,它使用各种采样速率和采样频率来录制和回放数字音频样本。我们在本书的前几章中学习了样本,因此我们在这里要关注的是如何通过这些样本的进一步音频合成,甚至只是原始波形,如正弦波或锯齿波,将这些样本带到下一个层次。
合成器接收波形音频,无论是消费电子设备中电路板上的振荡器产生的波形,还是更复杂的采样波形,例如弹拨乐器弦的样本,然后对该波形进行进一步的波形处理,以创建新的不同音调、声音甚至特殊效果。我们都熟悉当今流行音乐中新的合成乐器声音;所有这些都是用数学和代码完成的!
可以应用于数字音频领域内音频波形的核心数学操作之一是称为音高移动的,它可以将声音或音调向上或向下移动一个八度音程(或一个八度音程的一小部分,称为音高或键),为我们提供该样本的一个范围,就像我们在合成器键盘的按键上上下弹奏一样。
正如我们之前所了解的,波形的音调可以由该波形本身的频率来确定,因此,通过将波长减半,或者通过将波长加倍,将音调(波)精确地上移一个八度,这就变成了一个相当简单的数学计算。其中的任何部分都会改变音高,这就是我们如何使用单个采样波形在键盘上获得不同的音符。数字音频合成是相当惊人的东西!
SoundPool 可以做到这一点(这就是为什么我们首先在这里学习这些概念),所以它确实有一些音频合成功能,可能会在未来的 Android 操作系统版本中添加更多功能。你需要知道这些概念,以有效和优化地利用它的功能,这就是为什么我们在这里详细讨论所有这些,这样如果你需要使用 SoundPool,你就知道如何正确地使用它,以及为什么你首先需要这样做。
另一个核心的音频合成数学操作是将两个波形的组合 ( 合成)在一起,即从单个振荡器硬件(扬声器)场景中同时播放两个声音。与数字成像或视频合成一样,这里我们将两个不同的样本数据值相加,以获得最终的听觉效果。
今天的音频硬件确实有相当令人印象深刻的多声道支持,并且可能有能力播放立体声(两个声道)或 四声道(四个声道)单个声音(效果、音乐、音轨等)。)直接从消费电子设备内部的音频硬件中分离出来。
如果我们想要像音序器一样实时组合 8 或 16 个轨道的数字音频,会怎么样?这就是 SoundPool 可以在您的应用中为您提供数字音频排序功能的地方。
重要的是,如果你打算尝试 8 到 16 个音频样本的实时音频合成,这些样本中的每一个都是非常好的优化。这使得我们在第十三章中学到的关于数字音频数据优化的知识在使用 Android SoundPool 时变得极其重要。所以你看,我的疯狂是有方法的!
例如,如果您并不真的需要 HD (24 位采样分辨率)音频来获得 CD (16 位)音频的质量效果,那么您可以节省大量内存,同时获得相同的最终效果。
同样,如果您可以使用 32 kHz 采样速率而不是 48 kHz 采样速率获得相同的音频质量,那么您可以少使用 50%的样本(内存)来实现这一点。对于画外音或声音效果,内存节省是显而易见的,因为通常您可以通过使用 8 位分辨率和 11 kHz 采样率来有效地对炸弹或激光爆炸进行采样,而您将无法检测到 16 位 48 kHz 声音效果的任何差异,但您将使用 8.7 倍的内存(16 乘以 48 除以 8 乘以 11)。
就像数字成像和数字视频播放一样,优化您的数字音频素材非常重要,这有两个完全不同但相关的原因。对于数字音频样本,尤其是在使用 Android SoundPool 的情况下,一旦样本被编解码器解压缩,并以原始的未压缩状态放入 Android 设备的内存中,就需要系统内存来容纳每个样本。
优化音频之所以重要的第二个原因是等式的处理部分。很明显,处理的音频越少,即使只是将音频发送到音频硬件,使用的 CPU 周期也就越少。因此,如果您可以使用更低的采样分辨率(每个音频片段更少的位)或更低的采样频率(每秒更少的波形片段)获得相同的基本音频质量结果,您将节省系统内存资源和 CPU 处理周期资源。
对于 SoundPool 而言,这变得越来越重要,因为您的应用中需要使用的数字音频样本的数量在增加。这同样适用于系统内存和系统处理周期的使用情况,因为随着样本的增加,这两种资源的利用率会越来越高,而且不要忘记您的应用还在做其他事情,例如用户界面渲染、成像、视频,甚至可能还有 3D。
高度优化的数字音频样本在使用 SoundPool 类时如此重要的另一个原因是,目前在使用 SoundPool 时,数字音频样本数据有一个一兆字节的限制。虽然这个限制可能会在这个音频类的未来 Android API 版本中增加,但它仍然是有效和高效地优化任何数字音频素材的最佳实践。
SoundPool 介绍:类规则和方法
Android SoundPool 类是 java.lang.Object 类的直接子类,而不是 MediaPlayer 类的子类。
与 MediaPlayer 类一样,它也是 android.media 包的一部分,因此该类的完整路径(在导入语句中使用)应该是: android.media.SoundPool 。
因为 SoundPool 是 java.lang.Object 的子类,所以我们可以推断它是自己的临时代码创建的。还需要注意的是,如果需要,可以同时使用 SoundPool 对象(即 SoundPool 类)和 MediaPlayer 对象(即 MediaPlayer 类)。
事实上,这两种音频播放类有不同的应用。MediaPlayer 最适合用于长格式的音频和视频数据,如歌曲、专辑或电影。SoundPool 最适合用于大量的短格式音频片段,尤其是当它们需要快速连续播放和(或)组合时,比如在游戏或游戏化应用中。
样本的 SoundPool 集合可以从两个位置之一加载到内存中。第一个也是最常见的是来自。APK 文件,我称之为受控新媒体素材,在这种情况下,它们将位于您的/res/raw 项目文件夹中。第二个可以加载样本的地方是 SD 卡或类似的静态内存存储位置(人们称之为 Android OS 文件系统)。
SoundPool 内部使用 Android MediaPlayer 服务将音频素材解码到内存中。它使用未压缩的 16 位 PCM 单声道或立体声音频流来实现这一点。出于这个原因,请确保使用 16 位采样分辨率来优化您的音频,因为,如果您使用 8 位,而 Android 将其上采样到 16 位,您最终会浪费空间。所以好好优化你的采样频率,不到万不得已不要用立体声采样。让您的工作流程符合 SoundPool 的工作方式,以在最大数量的 Android 消费电子设备上获得最佳结果,这一点非常重要。
当 SoundPool 对象用 Java 构建时,正如我们将在本章后面所做的,开发人员可以设置一个 maxStreams 整数参数。此参数确定可以同时合成或渲染多少音频流。使用数字图像合成类比,这将等同于数字图像合成中允许的图像层的数量。
将这个最大流数参数设置为尽可能小的值是一个很好的标准做法。这是因为这样做将有助于最大限度地减少用于处理音频的 CPU 周期,从而降低 SoundPool 音频混合影响应用性能的其他方面(如 3D、图像视觉或 UI 性能)的可能性。
SoundPool 引擎跟踪活动流的数量,以确保它不超过 maxStreams 设置。如果超过了音频流的最大数量,SoundPool 将中止先前播放的音频流。它主要基于您可以为每个音频样本设置的样本优先级值。
如果 SoundPool 找到两个或更多具有相同优先级值的音频样本,它将根据样本年龄决定停止播放哪个样本,这意味着播放时间最长的样本将被删除。我喜欢称之为洛根运行原则。
优先级值从低到高进行评估。这意味着较高(较大)的数字代表较高的优先级。当调用 SoundPool 时,评估优先级。play( ) 方法导致活动流的数量超过创建 SoundPool 对象时设置的 maxStreams 参数所确定的值。
在这种情况下,SoundPool 流分配器停止最低优先级的音频流。正如我提到的,如果有多个流具有相同的低优先级,SoundPool 会选择最早的流停止。在新流的优先级低于所有活动流的情况下,新声音将不会播放,play()函数将返回为零的 streamID ,因此请确保您的应用 Java 代码始终准确跟踪您的音频样本优先级设置。
通过设置任何非零循环值,样本在 SoundPool 中循环。例外情况是, -1 的值会导致样本永远循环,在这种情况下,您的应用代码必须调用 SoundPool。stop()方法来停止循环样本。因此,非零整数值会导致样本重复指定的次数,因此值 7 会导致样本总共回放 8 次,因为计算机开始使用数字 0 而不是 1 来计数。
每个样本回放速率可以通过 SoundPool 来改变,正如前面提到的,sound pool 将这个类变成了一个音频合成工具。因此,等于 1.0 的样本回放速率会导致您的样本以原始频率水平播放(如有必要,会重新采样以匹配硬件输出频率)。
样本回放速率为 2.0 会导致样本以其原始频率的两倍播放,如果是乐器音符,听起来会高出一个全八度。类似地,将样本回放速率设置为 0.5 会导致 SoundPool 以其原始频率的一半播放该样本,听起来好像低了整整一个八度。
SoundPool 的样本回放速率范围目前被限制在 0.5 到 2.0,但这可以在未来的 API 版本中升级到 0.25 到 4,这将为开发人员提供四个八度的样本回放范围。
接下来,我们将回顾一些关于 SoundPool 使用的注意事项,或者更确切地说,如何使用 SoundPool而不是,然后我们将深入一些相当健壮的 Java 编码,以便我们可以在 Hello World Android 应用中实现这个 sound pool 音频引擎,在我们的 Attack a Planet Activity 子类中。
Android 数字音频合成和排序注意事项
在 Android 应用中使用 SoundPool 进行数字音频合成和排序是一个平衡的行为,无论是在您目前测试它的设备中,还是在您的应用将在其上运行的所有设备中。如果给定的硬件平台(智能手机、平板电脑、电子阅读器、iTV)无法处理播放给定的音频数据负载,那么它就无法播放。
到目前为止,我们已经了解到,数字音频合成、排序和合成在很大程度上取决于处理器的速度、可用处理器内核的数量以及可用于存储所有未压缩格式的数字音频样本的内存量。
因此,底线是,你需要非常聪明地处理 SoundPool 中的事情。不在于如何编写代码,虽然这当然很重要,但也在于如何设置音频样本,以便它们使用更少的内存,并可以在应用中进一步利用。
Android 开发者在 SoundPool 方面犯的主要错误是试图将其更多地用作音频音序器,而不是音频合成器。
用户关注 SoundPool 加载多个音频文件波形的能力,但没有利用其通过使用 SoundPool 音高移位功能创建无数新波形的能力。
如果您将 SoundPool 用作音频音序器,系统内存可能会过载,这可能会关闭 SoundPool 的功能。因此,Android 开发人员必须以最佳方式利用 SoundPool 功能,并优化他们的样本。
这里有一个很好的例子。SoundPool 允许在两个完整的八度音程之间进行音高变换,从设置 0.5 (向下一个完整的八度音程,或原始样本波形的一半)到设置 2.0 (向上一个完整的八度音程,或原始波形宽度的两倍)。记住波形宽度等于频率或音高。
大多数用户甚至不使用这种音高变换功能,而是使用不同的样本来实现不同的音符,这将填满内存,结果是该应用在旧设备上的工作越来越差。
使用 SoundPool 的正确方法是获取样本,比如从吉他弹拨一根弦、从萨克斯管吹一次号、一次钢琴击键和一次鼓声,仅使用四个 16 位 32 kHz 高质量样本,您就可以制作一个包含四种基本乐器的基本合成器。
使用这个基本的合成器设置,您的用户可以上下弹奏两个完整的八度音阶。这个应用将只使用兆字节的内存来保存这些 16 位 32 kHz 的未压缩样本。如果您使用高质量麦克风进行采样,您会惊讶于如今使用 16 位 32 kHz 采样格式可以获得的高质量结果。尝试一下,看看你是否能听出 16 位 44 kHz CD 质量音频和 16 位 32 kHz 音频之间的任何真正差异。
使用 SoundPool 进行我们的攻击星球活动
在我们的 Hello_World 应用中,利用 Android SoundPool 的逻辑区域是在我们的 Attack a Planet 活动内部,因为这使用了许多音频样本,当我们的用户单击图标图像按钮时,这些样本应该会快速触发,以提供最专业的用户体验。
我们需要做的第一件事是打开 Eclipse,然后在编辑选项卡中打开我们的 AttackPlanet.java 类,这样我们就可以添加新代码了。
让我们删除在前一章中编写的声明、创建和启动 MediaPlayer 对象的代码。所以,删除类顶部声明四个特效 MediaPlayer 对象的语句,然后删除我们编写的**setaudiopayers()**方法及其方法调用,这样我们又回到了这个 Activity 类中没有音频实现的情况。
现在,我们准备添加所有新的音频处理 Java 代码,使用一个 SoundPool 类加载我们所有的音效样本,而不是使用四个 MediaPlayer 对象。由于我们需要在动画用户界面 ImageButton 元素中实现大量的声音效果,因此对于这个特定的 Activity 类来说,这样做应该更有效。
设置 sound pool:sound pool 对象
为了准备 SoundPool 音频引擎的使用,我们需要在 AttackPlanet Activity 类的顶部做的第一件事是实例化 SoundPool 对象。我们将通过类名声明它,将其命名为 soundPoolFX ,并使用下面的单行 Java 代码应用 private 访问控制:
private SoundPool soundPoolFX;
正如你在图 15-1 中看到的,当我们在我们的类声明代码行下面写这一行代码时,Eclipse 用红色波浪线给 SoundPool 对象加下划线。
图 15-1。声明一个名为 soundPoolFX 的私有 SoundPool 对象,并使用 Eclipse helper 添加导入
将鼠标放在这个突出显示的错误上,弹出 Eclipse ADT 助手对话框。这个对话框为我们提供了几个选项来删除代码中的这个错误标志。
选择第一个选项,**导入“sound pool”(Android . media 包)**作为您想要选择的解决方案,然后 Eclipse 为我们写入导入语句。
打开位于编辑窗格顶部的 import 语句代码块,在类声明的上方,但在包声明的下方,确保**导入 Android . media . media player;**代码语句已被删除。我们不需要显式声明(导入)MediaPlayer 类来与 SoundPool 一起使用,即使我们知道 SoundPool 引擎在幕后使用 Android 的 MediaPlayer 服务,也就是说,来播放我们的数字音频样本。
现在我们已经导入了 SoundPool 库供使用,您会注意到 Eclipse 在我们的 SoundPool 对象名 soundPoolFX 下用黄色下划线标出了一个警告。让我们把鼠标放在上面,看看 Eclipse 认为我们的代码现在有什么问题。
正如你在图 15-2 中看到的,Eclipse 弹出一个帮助对话框,告诉我们名为 soundPoolFX 的新 SoundPool 对象没有被使用。当然,我们知道这一点,所以我们现在不会担心这个警告界限,我们将继续声明我们的其他类和音频样本实例整数变量,我们将需要在 Java 代码中为我们的 AttackPlanet.java 活动子类实现这个新的 SoundPool 音频排序引擎。
图 15-2。检查我们的 Eclipse 警告消息并显示一条导入 android.media.SoundPool 语句
接下来,我们需要声明并实现一个名为散列表的 Android 实用程序类,我们用它来保存我们的数据值对,代表我们的音频样本和它们的文件引用 URI 数据。这样做是为了让 Android 操作系统能够快速轻松地找到并预加载这些音频素材。
SoundPool 使用更复杂的数据结构来加载音频内容;这样可以在运行时快速找到并加载您的样本,因为 SoundPool 的游戏名称就是执行速度。
如果您想更详细地研究 HashMap 实用程序类,您可以在下面的 Android 开发人员网站 URL 找到专门介绍它的整个网页:
http://developer.android.com/reference/java/util/HashMap.html
现在让我们看看如何在 SoundPool 代码中实现 HashMap。
加载 SoundPool 数据:Android HashMap 类
为了准备使用 HashMap 实用程序,我们需要在 AttackPlanet 活动的顶部做的第一件事是实例化一个 HashMap 对象。我们将通过 classname 声明它,将其命名为 soundPoolMap ,并使用下面一小段 Java 代码对 HashMap 应用一个私有访问控制:
private HashMap<Integer, Integer> soundPoolMap;
正如你在图 15-3 中看到的,当我们在我们的类声明代码行下面写这一行代码时,Eclipse 用红色波浪线给这个 HashMap 对象加下划线。将鼠标放在突出显示的错误上,弹出 Eclipse ADT 助手对话框。这个对话框为我们提供了几个选项来删除代码中的这个错误标志。
图 15-3。声明一个名为 soundPoolMap 的私有 HashMap 对象,并使用 Eclipse helper 添加导入语句
选择第一个快速修复选项,导入“HashMap”(Java . util 包),作为您想要选择的解决方案,Eclipse 继续写入我们的导入 Java . util . HashMap; Java 代码为我们导入语句。
接下来,我们需要通过使用以下几行 Java 代码声明四个整数变量来保存我们将在 SoundPool 中使用的样本数:
int sample1 = 1;
int sample2 = 2;
int sample3 = 3;
int sample4 = 4;
现在我们已经在活动的顶部声明了 SoundPool 对象、HashMap 对象和样本整数,在 onCreate()方法之前,如图 15-4 所示。现在我们准备使用 new 关键字并创建一个新的 SoundPool 对象,以便在我们的活动中对音频进行排序。
图 15-4。声明并设置四个样本整数,并显示 import java.util.HashMap 语句
接下来我们将学习 Android AudioManager 类,你可能已经猜到了,它用于在你的 Android 应用中访问音量,以及铃声模式控制。AudioManager 也是 java.lang.Object 的子类,是 Android Media 包的一部分,其导入语句路径为Android . Media . audio manager,我们将在本章下一节创建新的 SoundPool 对象时看到。
AudioManager 是 Android 操作系统常量的集合,这些常量与 Android 操作系统中不同音频相关功能的状态相关。该类还包含一个名为audio manager . onaudiofocuschangelistener的接口,这是一个回调的 Java 接口定义,当操作系统的音频焦点在任何时候被更改或更新时,都会调用该回调。
如果您想更详细地研究 AudioManager 类,并亲自查看 AudioManager SCO、Vibrate 和 Bluetooth 常量中的哪些常量已被弃用,以及它们在哪个 API 级别被弃用,Android 开发者网站有一个专门针对它的网页,网址为:
http://developer.android.com/reference/android/media/AudioManager.html
配置音池:使用 Android AudioManager
我们需要在活动的 onCreate()方法中创建 SoundPool 对象的新实例,该实例指定可以同时播放的声音数量,以及音频的类型和质量级别。这是通过 SoundPool 构造函数完成的,它采用以下格式:
public SoundPool (int maxStreams, int streamType, int srcQuality);
因此,让我们在我们的 setContentView()方法调用后添加一行空格,并使用以下单行 Java 代码,构造一个新的 SoundPool 对象,命名为 soundPoolFX :
soundPoolFX = new SoundPool(4, AudioManager.STREAM_MUSIC, 100);
注意,一旦您在 Eclipse 编辑窗格中键入这一行代码,Eclipse 红色会在 AudioManager 类引用下面加下划线。因此,让我们通过将鼠标放在错误突出显示上并选择**导入“audio manager”(Android . media 包)**选项来消除这个错误,这样 Eclipse 就会为您编写所需的导入 android.media.AudioManager 语句,如图图 15-5 所示。
图 15-5。配置 SoundPool 对象并使用 Eclipse 助手导入 AudioManager 包
接下来,我们需要对 HashMap 对象做同样的工作过程,并调用它的构造函数方法,使用 new 关键字。如果您已经忘记了什么是哈希表或哈希表,这里有一个简短的概述。
哈希表,也称为哈希表,是二维数据结构。这些专门的数据结构用于实现关联数组,这是一种可以快速将键映射到值的数据结构。哈希表利用一个哈希函数来计算一个索引到一个数据条目槽数组中,从中可以快速找到正确的值。
配置 HashMap:使用。put()方法
让我们在刚刚编写的 soundPoolFX 构造函数下添加一行空格,接下来我们将编写 soundPoolMap HashMap 构造函数 Java 代码。
创建空哈希表的构造函数代码行采用一个整数键和一个整数数据值对,编码如下:
soundPoolMap = new HashMap<Integer, Integer>();
现在我们已经定义并创建了一个空的哈希表结构,是时候用我们将在 SoundPool 引擎中使用的音频数据加载它了。这是通过 HashMap 类完成的。put( ) 方法,该方法允许我们将数据对放入(插入)空哈希表结构中,我们现在需要用音频素材数据填充,如图图 15-6 所示。
图 15-6。使用。put()方法来填充 soundPoolMap HashMap 对象
我们将在 HashMap 构造函数下使用四行 Java 代码,这些代码将利用点符号来调用。来自 soundPoolMap HashMap 对象的 put()方法。
的。put()方法传递整数变量 sample1 到 sample4,以及。对我们的 soundPoolFX SoundPool 对象的 load()函数调用,它将传递到我们的每个数字音频素材文件的当前上下文、R.raw 参考数据路径和样本优先级值。
这四个 soundPoolMap.put()方法调用应该类似于以下四行 Java 代码:
soundPoolMap.put(sample1, soundPoolFX.load(this, R.raw.blast, 1));
soundPoolMap.put(sample1, soundPoolFX.load(this, R.raw.blast, 1));
soundPoolMap.put(sample1, soundPoolFX.load(this, R.raw.blast, 1));
soundPoolMap.put(sample1, soundPoolFX.load(this, R.raw.blast, 1));
现在,我们已经创建了 soundPoolFX SoundPool 对象和 soundPoolMap HashMap 对象,并为四个示例中的每一个将这两个对象连接在一起,这四个示例现在已加载到 HashMap 中,并准备好供 Android SoundPool 音频引擎快速访问。
接下来,我们将编写一个方法,它将允许我们使用一个方法和两个参数来配置和播放 SoundPool 音频引擎,这两个参数指定要播放的样本和移动音高的音高移动值。
编码 playSample()方法:使用 SoundPool。play( )
接下来,我们将编写一个名为 playSample()的方法,用于控制 SoundPool 引擎的使用。此方法创建一个 manageAudio AudioManager 对象来获取 AUDIO_SERVICE 系统服务,并使用此对象从操作系统获取当前和最大音量设置,然后使用这些数据值来设置我们的音量设置。play()方法调用我们的 soundPoolFX SoundPool 对象,如图图 15-7 所示。
图 15-7。编写一个 playSample()方法来设置并调用我们的 SoundPool 对象。play()方法
playSample()方法中的第一行 Java 代码创建了一个名为 manageAudio 的 AudioManager 对象,并将其设置为通过上下文调用 getSystemService( ) 方法。AUDIO_SERVICE 常量。这是使用下面一行 Java 代码完成的:
AudioManager manageAudio = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
接下来的三行代码创建了浮点变量。我们将前两个浮点变量 curSampleVolume 和 maxSampleVolume 设置为当前音频流音量和最大音频流音量数据值,这是通过我们在第一行代码中创建的 manageAudio AudioManager 对象获得的。这两个浮点变量是使用以下 Java 代码设置的:
float curSampleVolume = manageAudio.getStreamVolume(AudioManager.STREAM_MUSIC);
float maxSampleVolume = manageAudio.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
然后,我们使用这两个浮点变量数据值来计算我们的第三个浮点变量数据值,使用下面的 Java 代码行,它通过将当前音量除以最大音量来计算我们需要传递给 SoundPool 引擎的 setSampleVolume 音量设置:
float setSampleVolume = curSampleVolume / maxSampleVolume;
最后,我们将把称为音池**。play( )** 方法,并用这些 float volume 变量以及我们传递给 playSample()方法的数据对其进行配置,最后一行代码包含在该方法中。这一行 Java 代码应该如下所示:
soundPoolFX.play(soundPoolMap.get(sample),setSampleVolume,setSampleVolume,0,0,pitchShift);
因此,现在从我们的 soundPoolFX SoundPool 对象调用. play()方法,并传递从 soundPoolMap HashMap 对象中提取的 sample soundID ,这是基于在方法参数列表中传递的样本变量(样本编号是用于索引我们想要的样本数据的关键字)。
其他参数是左右声道的浮点音量水平,由保存在 setSampleVolume 变量中的最终浮点计算指定。也在。play()参数列表是播放优先级,播放次数循环值,最后是变调系数,从 0.5 到 2.0,浮点格式。
注意,在我们的代码中,我们忽略的这个音高移位因子是用每个数字后的小写字母 f 指定的。在我们目前的代码中,这被写成 1.0f 。这个 f 代表浮点并将十进制数指定为浮点值。
当我们讨论我们的 pitchShift 变量的浮点值时,请确保在您的代码中试验这个值,因为您在 Nexus S 模拟器中执行了测试这个代码的下一步工作。
使用作为 Android 应用运行工作流程启动 Nexus S 模拟器,并使用菜单键进入攻击星球活动,单击 ImageButton 图标并触发您的一些示例。请注意,它们播放起来既快又流畅,几乎就像你的用户界面是一个视频游戏。这就是业内所说的 UI 游戏化。
我们需要做的最后一件事是解决 Eclipse 编辑器中的一个警告亮点,即有一个比 HashMap 类更好的类用于存储和访问数据对。我们将解决这个警告,现在我们已经向您展示了如何使用 HashMap,尽管我们的 IDE 编辑器中有一个警告消息,我们的代码仍然可以工作。我们将在下一节详细研究这个警告,因为它要求我们更改 Java 代码。
Android SparseIntArrays:使用 SparseIntArray 类
打开**。单击 IDE 编辑窗格左边的 + 符号,onCreate( )** 方法内容,注意仍然有一条黄色波浪下划线突出显示您的新 HashMap < Integer,Integer>();构建 soundPoolMap HashMap 对象的 Java 代码语句的一部分。将鼠标放在这个警告高亮上,在 Eclipse 中弹出助手对话框,如图图 15-8 所示。
图 15-8。检查 Eclipse 中关于 HashMap 的警告消息,并选择解释问题选项
注意,其中一个选项是:解释问题(使用稀疏数组)。看起来 Eclipse 正在提供给我们一些它所知道的关于在这个特定的实现中使用稀疏数组而不是散列表的东西。
让我们点击这个选项,看看 Eclipse ADT 能为我们提供什么信息。请注意,这一特定信息更多地来自 Eclipse 的 Android ADT 插件部分,而不是来自核心 Eclipse IDE 本身。我们知道这一点,因为对话框中的信息与 Android 类的使用有关,而与 IDE 函数本身的使用无关。
一旦我们点击这个解释问题链接,就会打开另一个对话框,名为更多信息。这个对话框告诉我们,有一个替代 HashMap 的类叫做 SparseIntArray ,对于我们存储和访问 SoundPool 音频引擎的整数键值来说,这个类会更有效。
问题解释本质上是说 Android SparseArray API 比 Android HashMap API 更有效,因为 HashMap 自动将 int 值从 int 装箱为 Integer,而 SparseArray 没有。你可以在图 15-9 中看到这个问题的所有解释文本。
图 15-9。在更多信息对话框中查看稀疏数组问题的解释
根据 Android 开发者网站上找到的更详细的信息,在使用更大的阵列时,这种开关可以节省处理时间。为此,为了向您展示 SparseArrays API,我们将继续升级我们的 Attack a Planet Activity 子类 Java 代码,以利用 SparseIntArray 类,而不是使用 HashMap 类。
如果您想在 Android 开发人员网站上阅读关于这个 SparseIntArray 类的更多详细信息,它有自己的专门页面,可以在以下 URL 找到:
[`developer.android.com/reference/android/util/SparseIntArray.html`](http://developer.android.com/reference/android/util/SparseIntArray.html)
要在我们当前的 Java 代码中进行这种更改,我们需要删除当前构建 HashMap 对象的代码行,并代之以构建 SparseIntArray 对象的新代码行。
让我们也将我们的 SparseIntArray soundPoolMap 命名为,并使用下面一行 Java 代码,使用 new 关键字调用它的构造函数方法:
soundPoolMap = new SparseIntArray(4);
请注意,我们在该表中指定了索引值的数量,使得该表是硬编码的,从而提高了内存和处理效率。
根据开发者网站的说法,这是因为有两种方法来构造稀疏数组。一种是简单构造 SparseIntArray( ) 另一种是构造SparseIntArray(int initial capacity)。
第二种构造方法将创建一个 new SparseIntArray(),它最初不包含任何映射,但是它不需要任何额外的内存分配来存储这个指定数量的映射,因为通过指定这个数量,API 确切地知道要分配多少内存。
在我们的例子中,我们确切地知道 SoundPool 引擎将使用多少数字音频样本,因此我们在 Java 代码中为 soundPoolMap SparseIntArray 对象构造选择了更有效的选项。
一旦我们通过 new 关键字输入构造函数方法调用,我们会看到 Eclipse 在我们的 SparseIntArray 下给了我们一个错误的红色波浪下划线,我们知道这是让 Eclipse 为我们编写更多代码的一个途径。
因此,让我们将鼠标放在这个突出显示的错误上,弹出助手对话框,选择**Import SparseIntArray(Android . util package)**选项,让代码中的突出显示错误永远消失。
既然我们的 soundPoolMap 被构造为一个 SparseIntArray,正如 Android 所希望的那样,我们可以修改接下来的四行代码,以使用需要用于 SparseIntArray 对象的正确方法调用。
这意味着改变**。将()方法调用(与 HashMap 对象一起使用,如图图 15-10 所示)交给一个。append( )** 方法调用,这是用于 SparseIntArray 对象的正确方法调用。
图 15-10。将 HashMap 对象改为 SparseIntArray 对象,并使用 Eclipse helper 添加导入语句
幸运的是,这是对我们现有 Java 代码的一个相当简单的修改,所以我们修改后的四行 Java 代码如下所示:
soundPoolMap.append(sample1, soundPoolFX.load(this, R.raw.blast, 1));
soundPoolMap.append(sample1, soundPoolFX.load(this, R.raw.blast, 1));
soundPoolMap.append(sample1, soundPoolFX.load(this, R.raw.blast, 1));
soundPoolMap.append(sample1, soundPoolFX.load(this, R.raw.blast, 1));
新的 soundPoolMap SparseIntArray 对象现在完全在我们的 SoundPool 引擎逻辑中实现,我们的 IDE 显示零错误或警告,如图 15-11 所示。
图 15-11。将我们的 soundPoolMap.put( ) HashMap 方法调用改为 soundpoolmap . append()SparseIntArray 方法调用
最后,我们准备在 ImageButton onClick()事件处理逻辑结构(代码块)中调用 SoundPool 引擎,这样我们就可以触发我们选择的样本,如果愿意,甚至可以改变它的音调。
调用 SoundPool 对象:使用 playSample()方法
接下来,让我们在每个 ImageButton onClick()事件处理程序方法内部调用我们在本章前面编写的 playSample(int sample,float pitchShift) 方法、。
这包括一行相当简单的代码,分别放在四个 ImageButton onClick()事件处理程序方法中,就在 Toast.makeToast()对象和方法调用之后(或者之前,如果您愿意的话)。
将以下代码行添加到 bombButton、invadeButton、infectButton 和 laserButton 中。setOnClickListener()方法,如下面四行代码所示(每个处理程序一行):
playSample (sample1, 1.0f);
playSample (sample2, 1.0f);
playSample (sample3, 1.0f);
playSample (sample4, 1.0f);
这四行代码在每个 ImageButton onClick()事件处理方法代码块中的位置如图 15-12 所示。
图 15-12。使用样本名称和音高移位参数调用我们的 playSample()方法来播放 SoundPool 样本
现在,在我们的 AttackPlanet.java 活动子类中实现 soundPoolFX SoundPool 对象音频引擎所需的所有 Java 代码构造都已就绪,是时候利用我们的作为 Android 应用运行工作流程,并在 Nexus S 模拟器中测试我们的所有代码了。
一旦 Nexus S 模拟器启动,点击菜单按钮,并选择攻击一个星球菜单选项,并启动我们刚刚在 SoundPool 中实现的 Activity 子类。单击动画 ImageButton 用户界面元素,并触发一些数字音频样本。很酷。
现在请注意,您的示例会立即触发,因为 SoundPool 已经将它们预加载到内存中。另请注意,您现在可以快速连续点击按钮,以更像游戏的方式触发音频。响应性反馈对于这种类型的多媒体用户界面设计很重要,我们在过去几章的活动中已经实现了这一点。
这项活动现在实现了过多的“技巧”新媒体用户界面元素,包括:3D 多态图像按钮、基于帧的动画、矢量或程序动画、数字图像和动画合成以及音频样本引擎,所有这些都无缝集成,效果非常好。
摘要
在这最后一章介绍 Android 中的数字音频时,我们仔细研究了音频排序和音频合成概念,因为它们与强大的 Android SoundPool 数字音频引擎类和 API 相关。
我们从学习 MIDI 和音频序列器的概念开始,这是 Android SoundPool 类的核心。我们还了解了排序是如何产生的,如今在哪里使用,以及与之相关的概念,包括曲目、声部和演奏数据。
接下来,我们进一步了解了数字音频合成,包括波形、音调、音高、八度音程、振荡器、音高移位的基本概念,以及将 SoundPool 类 API 转变为音频合成引擎所需了解的许多基本概念。
接下来,我们专门研究了 SoundPool 类和 API,考察了它的工作原理、方法和内存需求。我们仔细查看了 SoundPool maxStreams 参数,以及当超过 maxStreams 样本流数量时,它如何处理优先级。
下一个合乎逻辑的步骤是让我们了解一些关于使用数字音频排序和合成引擎的注意事项,该引擎会占用大量内存和处理资源,因此,如果我们要在应用中实现这一点,我们必须在代码和新媒体素材中考虑并优化某些因素。
最后,我们准备在 Hello_World Android 应用的 Activity 子类中实现 SoundPool 音频引擎。我们设置了 SoundPool 对象,加载了我们的音频样本数据,并了解了 Android HashMap 类 API,它允许我们创建哈希表。
然后,我们学习了 Android AudioManager 类和 API,它允许我们在 Android 应用和设备中管理音频焦点,然后我们使用这些知识来构建 SoundPool 对象。
接下来,我们编写了一个定制的 playSample( ) 方法,这样我们就可以将样本数据参数传递给我们的 SoundPool,比如我们想要播放的样本以及我们想要对这些样本进行多大程度的音高移动。
为了消除 Eclipse 中令人讨厌的警告标志,我们用一个 SparseIntArray 对象替换了 HashMap,并了解了两个 Android 实用程序之间的差异。然后,我们在 ImageButton onClick()事件处理程序中实现了对 playSample()方法的调用,并且实现了 SoundPool。
在下一章中,我们将开始了解 Android 服务,并使用后台处理来卸载计算密集型任务,以便它们不会以任何方式影响我们的用户界面设计或应用用户体验的响应。
十六、Android 意图:应用间编程
在这一章中,我们将深入探究 Android 的意图 。意图被开发者用来处理组成 Android 应用开发的四个主要功能区域内的模块间通信或指令:活动、服务、广播接收器和内容提供者。
我们已经了解了所有的活动,因为这些活动包含了你的 Android 应用的前端,包括你的设计、内容、新媒体、用户界面等等。在接下来的三章中,我们将讨论 Android 的其他三个主要功能领域:服务(处理)、广播接收器(消息传递)和内容提供者(数据存储)。
为了能够涵盖 Android 应用开发中的这三个更高级的领域,我们首先需要涵盖意图和意图过滤器这个庞大的主题。这是因为 Intents 在实现这些更复杂的“幕后”Android 应用组件时被更多地使用。
意图也可以用于活动,因为我们非常熟悉活动,所以我们将学习如何在活动中使用意图,在接下来的三章中,我们还将学习如何使用服务、广播接收器以及内容供应器来利用意图。
在这一章中,我们将仔细研究 Android Intent 和 Intent Filter 类,以及 Android Intent 的各种特性,以及如何在 Android 应用中声明这些特性、功能、设置、常量和类似的特性。
我们将进一步了解意图过滤器,它允许您在应用被其他应用使用时,自动处理如何在应用中利用您的意图。正如您可能已经猜到的,声明意图过滤器是通过您的 AndroidManifest.xml 文件完成的,使用 <意图过滤器> 标签。
这是 Android 中比较复杂的主题之一,因为它涉及到模块间通信、消息传递、AndroidManifest、过滤器以及 Java 和 XML 格式的类似高级编程主题。
Android 意向信息:首先,全球概述
意图消息是一个 Android OS 工具,在相同或不同应用内的应用组件之间提供后期运行时绑定。
使用 android.content.Intent 类实例化 Android Intent 对象,该类是 java.lang.Object 的子类。这意味着 Intent 仅仅是为了它自己独特的目的而开发的,而不是从任何其他类型的 Android 类派生出来的子类。它被打包在 Android 内容包中,你可以从之前概述的包名称中清楚地看到。
之所以将它与 android.content 包打包在一起,是因为 Intents 可以用来快速访问和操作内容提供者(数据库),这一点我们将在下一章中了解。Intent 对象的用途不仅仅是数据库访问,它还可以用于 Android 服务、活动和广播接收器。
Android Developer Reference 将意图定义为要执行的操作的抽象描述。这意味着 Android 设计意图是创建一个 Java 对象类型,可以用来轻松完成通常需要复杂编程代码的任务。
因此,意图本质上是一种编程捷径,它已经内置于 Android 操作系统和编程环境中,以使事情从长远来看更容易。我说“从长远来看”,因为首先我们需要学习如何使用意图和意图过滤器,然后一旦我们理解了它们,它们就会变得强大,使我们成为更高级的 Android 程序员。
众所周知,Intent 对象结构是一个被动的数据结构对象,因为它只是一个被动的数据和指令的集合,它们被捆绑在一个综合的 Java 对象中,可以很容易地在应用的不同功能模块之间传递。
意图对象数据结构应该包含一个描述,该描述包含需要执行的标准操作系统或开发者创建的动作,并且另外传递那些动作需要处理的数据。这通过其组件名传递给 Java 代码模块,指定哪个类是(接收)意图的目标。
我们将在本章的后面学习所有这些意向对象数据结构格式。除了这些特定的动作和数据,Android Intent 对象还可以指定数据类型 (MIME)规范,以及类别常量、标志,甚至额外的数据对,这些数据对与 Intent 动作需要处理的主数据包相关。在本章的剩余部分,我们将详细了解意图对象的各种功能领域。
Android 意图实现:三种不同类型的意图用法
Intent 对象有三种不同的用途,每一种都可以在 Android 操作系统中调用活动、服务和广播接收器之间的模块间通信。
然而,一种类型的意图,即意图对象,可以用于操作系统的三个特定区域中的每一个。意图对象使用的不同分类类型都不允许互换使用,以避免处理错误。
当然,您可以将您的意图对象命名为您喜欢的任何名称,但是您用来在您的应用模块中传递您的意图对象的意图处理方法调用的类型将最终决定该意图对象将涉及的使用类型,而不管您在构造意图对象时如何命名它。意图的使用类型决定了它是什么类型的意图(或者更准确地说,意图用于什么类型的目标)。
使用不同的意图对象方法调用来保证意图对象的这些不同使用不会彼此混淆,也就是说,它们不会与意图对象的任何其他实现相交、干扰、冲突或错误地被其使用。
例如, startActivity( ) 使用意图对象来启动活动,而 startService( ) 使用意图对象来启动服务。要将意图对象发送到广播接收机,可以使用 broadcastIntent( ) 方法,因此用于分发意图对象的方法决定了它的实现。
出于这个原因,我们将分别讨论意图对象的每个使用场景,以便我们可以看到基于意图的通信与活动、服务和广播接收方消息之间的区别。
在这本书的最后一部分,我们还将练习在 Android 操作系统的四个主要领域中使用意图对象。因为你已经是 Activity 的专家了,所以在这一章中,我们将看看使用 Intent 对象和 Activity 子类的代码例子。在接下来关于 Android 服务、广播接收器和内容提供者的三章中,我们还将看到 Intent 对象是如何被用来启动和控制 Android 中的其他功能区域的。
活动
使用 Context.startActivity( ) 或Activity . startactivityforresult()将与活动一起使用的意图对象传递给每个活动,以启动活动,或要求现有活动执行一些特定于应用的编程任务。
可以使用 Activity.setResult( ) 方法将意图传递回去,以便首先使用 Activity . startactivityforresult()方法将信息返回给发起意图通信的调用活动。
安卓服务
使用 Context.startService( ) 方法调用将与 Android 服务一起使用的意图对象传递给服务子类,以启动 Android 服务,或者向由启动的服务传递新指令。我们将在本书的下一章学习所有关于服务的知识。
还可以使用 Context.bindService( ) 方法传递一个 Intent 对象,以在调用应用组件和绑定的服务之间建立连接(绑定)。如果服务还没有启动并运行,意向者也可以发起绑定服务。
我们将在本书的下一章讨论绑定的服务,以及启动的服务和混合服务。我们将讨论意图在这个上下文中是如何使用的,因此,我在这里讨论它们是为了上下文和概述不同类型的意图。
广播接收机
传递给任何 Android 广播接收器方法的 Intent 对象被传递给所有感兴趣的 Android 广播接收器。大量的 Android 操作系统广播将源自 Android 系统代码,这在复杂的完整版本的 Linux 操作系统中是可以预料的。
Intent 方法调用的广播接收器特定版本包括 Context.sendBroadcast( ) 方法、**context . sendorderedbroadcast()方法和context . sendstickybroadcast()**方法,我们将在第十八章中介绍。
因为每种类型的意图都有一个独特的调用方法,Android 系统可以很容易地定位需要响应每个特定意图对象的适当的应用活动、服务或广播接收器。
由于 Android 意向系统的设置方式,这些意向信息系统之间没有重叠。广播接收机的意图只发送给广播接收机,不会发送给 Android 活动或服务。使用传递的意图对象。startActivity( ) 方法只传递给一个 Activity 子类,而从不传递给 Service 子类或 Broadcast Receiver 子类,等等等等。
安卓意图结构:安卓意图剖析
意图对象包含一个包的信息。从第一次让 Eclipse ADT 为您生成 Android 应用开始,您就已经熟悉了 Android Bundle 对象,因为 Bundle 对象在每个 Android 应用中都有使用,用于保存 Activity 子类的实例状态信息。也许你记得看到下面这行代码:
public void onCreate(Bundle savedInstanceState) {onCreate Method Logic is in here}
意图包含接收该意图的应用组件感兴趣的信息。这包括诸如接收应用组件要采取的动作的信息,以及需要采取行动的数据。意图还可以包括 Android 操作系统本身感兴趣的信息,例如接收组件的哪个类别应该处理意图,或者甚至是关于如何启动目标活动的指令。
一个 Android Intent 可以包含以下七个功能部分:
- **组件名:**目标组件的全限定类名
- **动作:**命名动作或预定义动作常数的字符串
- **数据:**要操作的数据对象(文件)的 URI
- **类别:**字符串命名类别,或安卓类别常量
- **数据(MIME)类型:**要操作的数据的 MIME 类型
- **附加:**用于意图的附加信息的键值对
- **标志:**在意图类中定义的各种标志
让我们深入这七个在意图对象中非常重要的领域,看看为什么每个领域都很重要,以及意图对象的这些领域如何在不同类型的 Android 意图对象中实现。
意图对象组件:指定组件名称参数
您可以为您的意图对象指定的最重要的事情是您显式指定给该意图对象的句柄的应用组件的组件名称。
当您在意图对象中指定该组件名称时,您也创建了一个显式意图对象。我们将在本章接下来的几节中更详细地讨论这个明确的意图。
Intent 对象中的这个字段是一个 ComponentName 对象,它包含目标组件的全限定类名的组合。在我们的 Hello World 应用中,这可能是“chapter.two.Hello_World”。“攻击行星”为攻击行星活动的子类。
注意,包名是在 AndroidManifest XML 文件中为组件所在的每个应用设置的。如果您调用的组件位于不同的应用中,组件名的包部分和 AndroidManifest.xml 中设置的包名不一定要匹配。Intents 足够灵活,可以跨不同的应用进行通信,也可以在单个应用内部进行通信。
这里需要注意的是,组件名是可选的。如果设置了它,您的意图对象将被交付给指定类的实例。如果没有指定组件名称,Android 将查看您的意图对象中的所有其他信息来定位合适的目标。
在这种情况下,意图变成了隐含意图,因为 Android 必须暗示如何应用意图,通过推断将意图应用到哪个组件。这是通过查看意图中的所有其他信息并推断在哪里处理意图来实现的。夏洛克·福尔摩斯肯定会为自己是安卓开发者而自豪。
在你的意图对象中,如果你想指定一个组件名,它将通过使用来设置。setComponent( ) 方法,或者,通过使用**。setClass( )** 方法,或者使用**。setClassName( )** 方法。
另一方面,组件名可以通过使用 Intent 类的从 Intent 对象中读取。getComponent( ) 方法。这用于从 Intent 对象中提取组件名称信息,以便它可以与给定的应用组件匹配,如果匹配就进行处理。
意图对象动作:指定动作参数
使用命名要执行的动作的字符串或用于指定 Android 操作系统内部已经定义的那些动作的 ACTION_ constant 来指定意图动作参数。为所有 Android 活动子类定义的动作常量包括以下动作:
ACTION_MAIN
ACTION_VIEW
ACTION_ATTACH_DATA
ACTION_EDIT
ACTION_PICK
ACTION_CHOOSER
ACTION_GET_CONTENT
ACTION_DIAL
ACTION_CALL
ACTION_SEND
ACTION_SENDTO
ACTION_ANSWER
ACTION_INSERT
ACTION_DELETE
ACTION_RUN
ACTION_SYNC
ACTION_PICK_ACTIVITY
ACTION_SEARCH
ACTION_WEB_SEARCH
ACTION_FACTORY_TEST
在与广播接收器一起使用的意图的情况下,动作实际上指定了在过去发生的动作(已经发生了),并且因此被报告而不是被请求。Intent 类定义了许多广播接收器动作常量,包括:
ACTION_TIME_TICK
ACTION_TIME_CHANGED
ACTION_TIMEZONE_CHANGED
ACTION_BOOT_COMPLETED
ACTION_PACKAGE_ADDED
ACTION_PACKAGE_CHANGED
ACTION_PACKAGE_REMOVED
ACTION_PACKAGE_RESTARTED
ACTION_PACKAGE_DATA_CLEARED
ACTION_UID_REMOVED
ACTION_BATTERY_CHANGED
ACTION_POWER_CONNECTED
ACTION_POWER_DISCONNECTED
ACTION_SHUTDOWN
值得注意的是,您还可以定义自己的自定义操作字符串,以便在应用中激活自己的自定义组件。这允许你在你的应用中开发你自己的定制意图系统。
您自己设计和命名的操作应该包括一个应用包作为前缀,然后是您自己创建的操作常量。以我们自己的 Hello World 应用为例,您可以对操作使用以下完整路径名:
chapter.two.hello_world.ACTION_SHOW_PLANET_STATUS
意图动作参数在确定如何构建意图的其余信息参数时极其重要。
因此,最好使用尽可能具体的动作常数。您还应该尽可能将动作常数与意图对象的其他信息字段紧密关联,如数据、类别、数据类型和标志。
您想要做的是,为您的定制应用组件将要处理的意图对象定义一个完整的协议和一组常量,而不是孤立地定义一个动作。
您的意图对象中的动作常量或字符串值应通过使用来设置。setAction( ) 方法。相反,应该使用从意图对象中读取动作常量或字符串值。getAction( ) 方法。
意图对象数据:发送数据以供操作
意图对象数据参数包含一个 URI 对象,用于使用指定的动作处理数据。正如您可能已经猜到的那样,各种类型的动作与逻辑上对应的数据规范类型一起使用,这些数据规范类型与传递的动作参数非常匹配。
例如,如果传递给意图对象的动作参数的动作是一个 ACTION_DIAL 常量,那么数据参数将包含显示在智能手机拨号区域的电话号码。
如果 Intent action 常量参数是一个 ACTION_CALL ,那么 data 参数将是一个 URI 对象,该对象包含带有电话号码的 tel: 前缀引用,您的应用希望将该电话号码作为一个电话呼叫。
如果意图动作常量是 ACTION_VIEW 并且使用的数据参数是 http: URI,那么接收活动将被调用来下载和显示 URI 引用要查看的任何数据。
意图对象类别:使用类别常量参数
意图对象的类别参数包含一个字符串对象,该对象指定关于处理意图的组件种类的附加信息。任何数量的类别描述都可以放在 Intent 对象中,以帮助接收组件。
Android Intent 类还定义了许多类别常量,就像它为活动和广播接收器定义 ACTION_ constants 一样。这些类别常量都以单词 CATEGORY_ 开头,包括:
CATEGORY_DEFAULT
CATEGORY_BROWSABLE
CATEGORY_TAB
CATEGORY_ALTERNATIVE
CATEGORY_SELECTED_ALTERNATIVE
CATEGORY_LAUNCHER
CATEGORY_INFO
CATEGORY_HOME
CATEGORY_PREFERENCE
CATEGORY_TEST
CATEGORY_CAR_DOCK
CATEGORY_DESK_DOCK
CATEGORY_LE_DESK_DOCK
CATEGORY_HE_DESK_DOCK
CATEGORY_CAR_MODE
CATEGORY_APP_MARKET
Intent 类有几个方法允许您使用类别参数,包括**。addCategory( )** 方法,它在一个意图对象中添加一个类别,。removeCategory()、和,前者在添加类别后将其删除。getCategories(),,它检索当前包含在 Intent 对象中的所有类别的集合。
意图对象数据类型:设置 MIME 数据类型参数
当将您的意图与能够处理给定的数据类型的组件相匹配时,了解您正在处理的数据值的分类或类型(数据的 MIME 类型)通常是非常重要的(当然,除了数据的 URI 位置之外)。
MIME 代表多用途互联网邮件扩展 ,它最初是为电子邮件服务器开发的,用于定义它们对不同类型数据的支持。
MIME 现在已经扩展到支持的数据和内容类型的其他平台定义,以及通信协议(如 HTTP)数据类型定义,还扩展到 Android OS,在这里也定义内容数据类型。可以说 MIME 已经成为无数计算环境中定义内容数据类型的主要标准。MIME 数据类型定义的示例包括以下常用的内容类型:
• Content-Type: text/html (HTML Data)
• Content-Type: video/mp4 (MPEG Data)
• Content-Type: image/png (PNG8 Data)
• Content-Type: audio/mp3 (MPEG Data)
• Content-Type: application/pdf (.PDF Data)
• Content-Type: multipart/x-zip (.ZIP Data)
为了举例说明为什么要在 Intent 对象中声明 MIME 数据类型,您可能希望确保显示视频数据的应用组件不会被错误地调用来播放音频文件,就像您不希望播放音频的应用组件(如我们的 playSample()方法)被错误地调用并传递视频或图像数据文件来播放一样。
在大多数情况下,您的数据类型可以从传递的 URI 中推断出来。对于 Android content:// URIs 来说尤其如此,它表明数据位于你的设备上的什么位置,并且由内容供应器控制。我们将在本书最后一节的后续章节中介绍 Android 内容供应器。
数据类型也可以是在你的意图对象中显式设置的数据类型。利用意向对象**。setData( )** 方法只指定数据,作为 URI,而使用**。setType( )** 方法调用仅使用 MIME 类型指定数据。
第三种方法叫做**。setDataAndType( )** 结合了这两种方法,并将数据指定为 URI 和 MIME 类型。可以使用意图对象读取 URI。getData( ) 方法和数据类型可以通过使用意图对象读取。getType( ) 方法。
意图对象附加:在意图对象中使用附加
意向对象还可以包括键值对形式的额外内容(数据)。这些用于传递附加信息,这些信息应该被包括在内,以便于对意图对象进行正确的组件处理。
Android Intent 类还定义了许多额外的常量,就像它为活动和广播接收器定义 ACTION_ constants,以及 CATEGORY_ constants 一样,正如我们在上一节中看到的。这些额外常量始终以单词 extra 开头,包括以下内容:
EXTRA_ALARM_COUNT
EXTRA_BCC
EXTRA_CC
EXTRA_CHANGED_COMPONENT_NAME
EXTRA_DATA_REMOVED
EXTRA_DOCK_STATE
EXTRA_DOCK_STATE_HE_DESK
EXTRA_DOCK_STATE_LE_DESK
EXTRA_DOCK_STATE_CAR
EXTRA_DOCK_STATE_DESK
EXTRA_DOCK_STATE_UNDOCKED
EXTRA_DONT_KILL_APP
EXTRA_EMAIL
EXTRA_INITIAL_INTENTS
EXTRA_INTENT
EXTRA_KEY_EVENT
EXTRA_ORIGINATING_URI
EXTRA_PHONE_NUMBER
EXTRA_REFERRER
EXTRA_REMOTE_INTENT_TOKEN
EXTRA_REPLACING
EXTRA_SHORTCUT_ICON
EXTRA_SHORTCUT_ICON_RESOURCE
EXTRA_SHORTCUT_INTENT
EXTRA_STREAM
EXTRA_SHORTCUT_NAME
EXTRA_SUBJECT
EXTRA_TEMPLATE
EXTRA_TEXT
EXTRA_TITLE
EXTRA_UID
我们已经看到,一些动作与某些类型的数据 URIs 成对出现;类似地,一些动作通常与特定类型的附加动作成对出现。
例如,ACTION_TIMEZONE_CHANGED Intent 对象操作参数具有标识新时区的额外“时区”, ACTION_HEADSET_PLUG 具有指示耳机现在是插入还是拔出的额外“状态”,以及耳机类型的额外“名称”。
如果你要发明一个 ACTION_SHOW_PLANET_STATUS 动作,那么行星状态值将使用 EXTRA_STATUS_PLANET 键值对来设置。
意向对象有一系列的**。putExtra( )** 插入各种类型的额外数据参数的方法和类似的一组**。getExtra( )** 读取额外数据参数的方法。有趣的是,这些 Java 方法也与捆绑对象中使用的方法类似。
值得注意的是,这些额外的键值对可以通过使用作为 Android Bundle 对象来安装和读取。方法和。getExtras( ) 方法。如果你使用了大量的额外功能,这可能是最有效的设置方式,毕竟这是 Bundle 对象最初的用途。
意图对象标志:对意图对象使用标志
可以包含在 Android Intent 对象中的最后一种参数称为标志参数。标志是布尔值,作为一名程序员,你可能非常熟悉。标志对于以高度数据紧凑的方式设置状态和开关非常有用。
就意图对象而言,大多数标志参数会指示 Android 系统如何以某种方式启动或处理该活动,或者可能在启动后如何处理该意图。然而,Intent object flag 参数是足够开放和灵活的,您可以以任何您认为适合您的应用的创造性方式来使用它们,所以发挥您的创造性吧!
显性与隐性意图:使用哪种意图类型
安卓意图对象有两种分类:显性意图和隐性意图。显式意图是两者中较容易在你的应用中使用的,而隐式意图要复杂得多,通过意图过滤器,你可以允许其他开发者通过他们的意图对象隐式地使用你的应用组件。
明确的意图
正如我前面提到的,显式 Intents 对象使用组件名参数指定目标应用组件。这之所以称为显式,是因为您的组件(类)命名模式一般不会提供给其他应用的开发人员,所以如果允许他们使用 Intent 对象访问您的代码,他们必须通过组件名参数获得要调用的类名。
因此,显式意图通常主要用于应用内部消息传递(或内部应用消息传递)。一个很好的例子是启动一个从属服务类的活动或者启动一个相关的活动,我们将在本章的后面看到。
Android 总是向组件名中指定的目标类的实例传递一个明确的意图。当您使用显式意图时,除了组件名称之外,意图对象中的任何内容对于确定哪个应用组件获得意图都无关紧要。
另一方面,隐式意图不命名目标(意图对象中的组件名称参数为空)。
隐含的意图
隐式意图通常用于激活其他应用中的组件或 Android 操作系统中更一般化的特性或功能,在这些应用中很容易推断出需要什么,例如通过应用用户界面而不是通过操作系统电话拨号实用程序为用户拨打或呼叫电话号码。
当接收到隐式意图对象时,完全需要遵循不同的方法。在没有明确指定目标的情况下,Android 操作系统必须确定处理该意图对象的最佳应用组件。
这被称为意图解析,因为 Android 操作系统正在为你解析你的意图对象。意向对象解析可能导致启动一个活动类来显示一个新的用户界面,或者启动一个服务类来执行所请求的动作,或者甚至激活一个 Android 广播接收器来响应您的广播通知。
意图解析可以通过几种不同的方式来执行。如果 Intent 的内容清楚地表明了需要做什么,比如说在 Intent 对象中有一个 action_MAIN 的 ACTION 参数和一个 category_HOME 的 CATEGORY 参数常量,那么主屏幕将在 Android 设备的屏幕上启动。另一种解决包含更多定制动作的意图的方法是使用一个意图过滤器,开发者可以定义它来帮助其他应用将意图发送到它们适当的组件进行处理。
隐式意图解析:引入意图过滤器
在缺少填充有 Android 指定意图常数的意图对象的情况下(我们在本章前面已经详细讨论过了),通常通过将任何提交的意图对象的全部内容与该应用的 AndroidManifest.xml 文件中的现有意图过滤器定义进行比较来执行隐式意图解析。
意图过滤器是使用 XML 标记标签/参数逻辑在 AndroidManifest 内部创建的复杂逻辑结构。它们与您的应用中可能接收意图的组件相关联。
意图过滤器在涉及意图对象时执行一些重要的功能。首先,它们概述了您的应用组件所体现的功能,其次,它们用于指定它可以处理的意图的特征和限制。
意图过滤器 XML 定义使您的应用组件更有可能成功接收指定类型的隐式意图。在下一节中,我们将讨论可用于为您的应用定义意图过滤器解析结构的标签。
请注意,如果您的应用组件没有在您的 AndroidManifest.xml 文件中定义任何意图过滤器,它将只能接收显式意图。您的应用当然可以继续使用我们之前提到的 Android 操作系统意图常量发送隐含意图,因为这些是使用操作系统的意图过滤器结构定义的。
定义了意图过滤器的组件可以接收显式和隐式意图。
针对意图过滤器测试意图对象时,会分析该意图对象的三个主要参数。它们是动作(或动作常量)、数据(URI 和 MIME)和类别。
如果你想一想,这三个主流意图参数中有相当多的信息,正如我们之前所了解的,所以有了这些和意图过滤器结构(定义)来告诉我们如何应用这些信息参数,操作系统应该能够成功地解析任何和所有意图对象。
有趣的是,Intent 对象中的 extras 和 flags 参数在确定哪个应用组件接收给定意图的意图解析过程中完全不起作用。
接下来,让我们仔细看看 Android****标签及其参数和子(嵌套)标签,以便我们可以看到如何在 AndroidManifest.xml 文件中指定意图过滤器。关于<意图过滤器>标签结构、参数和子标签的更多详细信息可以在 Android 开发者网站上找到,网址如下:
http://developer.android.com/guide/topics/manifest/intent-filter-element.html
创建意图过滤器:使用 XML 标签
虽然自定义意图过滤器结构的编码超出了这本介绍 Android 应用开发的入门书籍的范围,但我将在本节中概述这些<意图过滤器>标签和结构,以便您理解这些结构是如何实现的,在您的 AndroidManifest.xml 应用引导文件中使用 XML 标记。
利用一个标签结构或层次来指定活动、服务或广播接收器子类可以在应用中响应的意图对象的类型。
一个标签在其父组件标签容器的上下文中声明其功能,父组件标签容器将是一个 <活动> 标签、一个 <服务> 标签或一个 <接收者> 标签,因为这些是主要的应用组件子类,在这些子类下您可以设计您的定制意图过滤器层次结构。在接下来关于服务、广播接收机和内容供应器的三章中,我们将学习这些标签。
位于其父标签内部的标签定义了一个意图对象可以为那个或做什么,以及一个组件类可以处理哪些类型的广播。
标签结构定义了它所包含的组件标签,因此它知道它可以接收指定类型的意图,并允许操作系统过滤掉那些对特定组件 XML 定义没有意义的意图。
正如你在本书前几章的 XML 声明中已经看到的,你很快就会看到,使用和广播组件,所有核心 Android 组件的类别总是在 AndroidManifest.xml 文件中定义。
因此,正如您所看到的,这种方法与首先为您的 Android 应用设置和配置组件的方式是高效和无缝的。因此,如果您需要添加一个层次结构,您只需对其进行模块化编码,将其嵌套在您的一个组件标签内,就在现有组件标签的子标签和参数之下。
标签的大部分内容由其子标签描述,包括 <动作> 标签、 <类别> 标签、 <数据> 标签元素。正如你可能想象的那样,< intent-filter >标签的这些子标签包含了在任何给定的 intent 对象的每个参数槽中寻找什么的定义。
这三个子标签中唯一一个在 <动作> 标签中绝对需要的子标签,它将定义采取什么动作来解析(完成)意图对象的消息或任务。< intent-filter >标签本身也可以有三个基本参数;一个用于图标图形,一个用于文本标签,一个给它一个优先级值。
接下来,让我们在我们的 Hello_World Android 应用中实际实现一个 Intent 对象,看看所有这些如何在我们现有的应用中协同工作。
在 Hello World 中使用意图对象启动活动
让我们创建一个名为 TimePlanet 的新活动子类,我们可以使用 ConfigPlanet 活动子类中的 Intent 对象调用它。我们将创建一个新的 Activity 子类,为它创建一个新的 XML 用户界面屏幕布局,将其添加到我们的 AndroidManifest.xml 文件中,以便 Android OS 知道它的存在,将用户界面元素添加到我们的 Configure a Planet Activity 中,以调用新的 TimePlanet Activity,然后编写实现 Intent 对象的 Java 代码,并使用它来调用(启动)TimePlanet 活动及其原子钟用户界面。
打开 Eclipse,然后打开你的 Hello World 项目文件夹,右键点击 chapter.two.hello_world 包名文件夹,最后选择新建类菜单序列如图图 16-1 所示。
图 16-1 。在 Eclipse 中创建新的类来保存我们的 TimePlanet.java 活动子类 Java 代码
这将打开新的 Java 类对话框,如图 16-2 所示,我们可以填写所有必要的参数来精确地创建我们希望 Eclipse 为我们生成的 Java 类的类型,在这种情况下,它是一个活动子类,将保存我们的行星时间用户界面元素。
图 16-2 。命名我们的 TimePlanet.java 类,选择一个 android.app.Activity 超类和公共修饰符
**源文件夹:**字段中应该已经有了您的 Hello_World/src 项目源代码文件夹。因为您右键单击包文件夹来调用这个新的 Java 类对话框,所以 Eclipse 会为您推断出文件夹信息。同样的,包:字段也应该自动填入你的包名。
接下来将您的 TimePlanet 类名放入 Name: 字段,然后单击**浏览。。。按钮位于超类:**字段的右侧,这样我们可以选择我们想要从中派生出新类的类的类型,在本例中,我们想要使用 Activity 类。
当图 16-2 右侧所示的超类选择对话框打开后,在选择类型:字段中输入一个一个字符,选择 Activity - android.app 选择选项后,点击 OK 按钮。一旦你回到新 Java 类对话框,点击完成按钮。
一旦你完成了这个新的 Java 类工作过程,Eclipse ADT 应该已经在 IDE 编辑窗格的中心部分为你打开了一个新的编辑标签,名为 TimePlanet.java(参见图 16-3 )。
图 16-3 。在 Eclipse 中打开一个新创建的 TimePlanet.java 活动子类,显示导入语句
应该已经为您编写了四行 Java 代码,如下所示:
package chapter.two.hello_world;
import android.app.Activity;
public class TimePlanet extends Activity {
}
这些声明了 chapter.two.hello_world 包名和一个 import 语句,导入一个 android.app.Activity 包及其相关类,供您在即将编写的 TimePlanet.java Activity 子类中使用。
您的名为 TimePlanet 的公共类也应该为您声明,并且应该使用 extends 关键字对 Activity 类进行子类化,并为您准备好开始编写 Java 代码的基础结构。
让我们编写标准代码来创建活动屏幕,并将其内容视图设置为 XML 用户界面设计文档,这是我们接下来要编写的。
首先,我们使用名为 savedInstanceState 的 Android Bundle 对象编写受保护的类 onCreate(),在该类中,我们使用 super 关键字从父 Activity 超类调用 onCreate()方法,并在 onCreate()方法调用中传递 savedInstanceState 变量,使用以下 Java 代码行:
super.onCreate(savedInstanceState);
接下来,我们将使用**setContentView()**Activity 类方法将我们的内容视图设置为一个 XML 文件,我们接下来将创建该文件,它将保存使用 XML 标记的用户界面定义。
Java 代码的 setContentView()方法调用行如下所示:
setContentView(R.layout.activity_time);
注意在图 16-4 中,Eclipse IDE 错误标记了 activity_time.xml 文件引用;这是因为我们还没有创建这个文件,而且因为我们接下来要这样做,所以我们现在可以忽略这个突出显示的错误。
图 16-4 。编写 onCreate()方法和 setContentView()方法来访问 activity_time.xml UI 布局
接下来,我们将创建我们的 XML 用户界面,这样我们就可以解决这个错误,然后一旦完成,我们将返回到这个编辑选项卡,并编写实现我们的意图对象的代码。
为我们的 TimePlanet.java 活动创建 linear layout XML
接下来我们需要做的是右击项目资源文件夹中现在熟悉的 /res/layout 文件夹,然后选择新建 Android XML 文件菜单命令序列来启动如图图 16-5 所示的新建 Android XML 文件对话框。
图 16-5 。创建一个新的 Android XML 文件和 LinearLayout 根元素
选择一个资源类型:布局的和**项目:**名称 Hello_World 并将文件命名为 activity_time 以匹配我们的活动 XML 文件命名约定。
接下来,选择根元素: LinearLayout 的类型,然后单击 Finish 按钮,这将创建我们的 activity_time.xml 文件,以及父 LinearLayout 标签。一旦你点击 Finish,Eclipse 会自动在 IDE 的中间部分打开一个编辑窗格,显示它为你编写的 XML 标记,如图 16-6 所示。
图 16-6 。在 Eclipse 中打开我们的 activity_time.xml 布局 xml 文件,并编辑< LinearLayout >标签
现在,我们可以添加我们的 LinearLayout 参数,以及文本标题元素、模拟时钟元素和按钮用户界面元素的几个子标签,当我们使用完 Planet Time 活动时,它会将我们返回到 Configure a Planet 活动。接下来,让我们单击 IDE 底部的图形布局编辑器选项卡,以便添加一个 AnalogClock UI 小部件。
打开图形布局编辑器后,点击屏幕左侧控件面板中的时间&日期抽屉,打开那个装满控件图标的抽屉,就可以看到模拟时钟控件,如图图 16-7 所示。我还在截图中展示了当您将模拟时钟小部件拖放到 Eclipse 中央工作区显示的空白应用屏幕模拟上时,您的屏幕看起来是什么样子。
图 16-7 。使用 Eclipse 中的图形布局编辑器选项卡在 UI 屏幕上拖放一个 AnalogClock 小部件
接下来,我们需要为行星时间屏幕创建两个字符串常量数据值,一个标题(标题文本)以及模拟时钟小部件下方按钮上的文本标签,如图图 16-8 所示。这两个<字符串>常量的 XML 标记应该如下所示:
<string name="time_caption_value">Home Planet Earth Time</string>
<string name="time_button_value">Return to Planet Configuration</string>
图 16-8 。将 activity_time.xml 文件所需的两个字符串常量添加到我们的 strings.xml 文件中
在我们使用完行星的原子钟功能后,我们将使用按钮返回到“配置行星”活动。
现在,我们准备在 activity_time.xml 用户界面 xml 规范中配置 LinearLayout 父标记和子标记。首先,我们需要向 LinearLayout 标签本身添加两个参数,这将允许我们从 Java 代码(android:id)控制它,并为它提供我们在本书前面创建的壮观的背景图像(android:background)。
给 LinearLayout 添加以下两个参数,如图图 16-9 所示:
android:id="@+id/timePlanetScreen"
android:background="@drawable/trans_stars_galaxy"
图 16-9 。为 TextView、AnalogClock、Button 和 LinearLayout 标签添加布局和文本参数
接下来我们需要添加格式化参数到 < AnalogClock > 标签,我们使用图形布局编辑器创建了这个标签,如图 16-7 中所示。让我们给它一个地球的背景图像,顶部 20 度倾斜,在屏幕上居中,确保我们的布局宽度和高度设置为 wrap_content 。我们可以使用以下 XML 参数完成所有这些事情:
android:id="@+id/analogClock1"
android:background="@drawable/earth"
android:layout_marginTop="20dp"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
接下来,让我们添加我们的 < TextView > 用户界面元素,在 GLE 或 XML 编辑器中,并将其配置为匹配我们的等离子背景的颜色,具有 25sp 的大文本大小和 40°的顶部倾斜边距,还将其居中,并确保它也使用 wrap_content 值,使用以下标记:
android:text="@string/time_caption_value"
android:textColor="#FFCCAA"
android:textSize="25sp"
android:layout_gravity="center"
android:layout_marginTop="40dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
最后,我们需要添加一个 <按钮> 用户界面元素,要么在 GLE 中,要么在 XML 编辑器中,并配置它以匹配我们的等离子体背景的颜色,上边距倾斜 20 °,并将其居中,确保它也使用标准的 wrap_content 布局值,使用这个标记:
android:id="@+id/timeButton"
android:textColor="#FFCCAA"
android:text="@string/time_button_value"
android:layout_marginTop="20dp"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
我们将颜色、间距和大小值与其他活动用户界面屏幕设计相匹配,以保持 Hello World 应用中所有用户界面屏幕的一致外观。整个用户界面设计 XML 标记可以在图 16-9 中看到。
接下来,我们将看看需要在我们的 AndroidManifest.xml 文件中做些什么来准备我们的应用以支持这个新的活动,并使用 label 参数在活动屏幕的顶部放置一个定制的屏幕标题。
为我们的 TimePlanet 活动配置 AndroidManifest.xml
在我们开始向 AndroidManifest.xml 文件添加标记之前,我们需要创建一个名为activity _ title _ time _ planet的字符串常量值,如图图 16-10 所示。
图 16-10 。为 TimePlanet.java 活动的屏幕标签添加一个<字符串>标签和参数
在 Eclipse 的编辑选项卡中打开 strings.xml 文件,添加一个新的 < string > 标签,其名称值和文本数据值如下:
<string name="activity_title_time_planet">Hello World - Planet Earth Time</string>
接下来,我们将把 TimePlanet 活动子类的标签添加到应用的 AndroidManifest.xml 文件中。右键点击图 16-11 中所示的 Android Manifest 文件,并从菜单中选择打开,这样我们就可以在编辑窗格中编辑文件的内容,也显示在图 16-11 的中间部分。
图 16-11 。为新的 TimePlanet.java 活动子类添加一个<活动>标签和参数
添加一个带有指定活动类名的 android:name 和引用我们刚刚在 strings.xml 文件中创建的字符串常量的 android:label 参数的标签。标签和参数 XML 标记应该如下所示:
<activity android:name="chapter.two.hello_world.TimePlanet"
android:label="@string/activity_title_time_planet" />
既然我们已经在应用清单中告诉了 Android OS 我们的新活动,我们需要在我们的 Configure a Planet Activity 屏幕中添加一个按钮用户界面元素。一旦我们将这个 XML 添加到 activity_config.xml 用户界面屏幕定义中,我们就可以添加声明和实例化这个按钮元素的 Java 代码。完成后,我们可以添加事件处理方法,该方法包含我们需要发送到 TimePlanet.java 活动的意图对象,以便在应用用户忙于配置他们的行星特征时,在他们希望查看他们的行星地球原子钟的任何时候启动该活动。
向 activity_config.xml 文件添加一个原子钟按钮标签
我们需要做的第一件事是为添加到 activity_config.xml UI XML 定义中的原子钟按钮用户界面元素创建我们的常量值。我们将字符串常量命名为 button_name_time ,然后将其值设置为原子钟,如图图 16-12 所示。现在我们可以在我们的 activity_config.xml 文件中添加引用这个字符串常量的 XML 按钮 UI 元素标记。
图 16-12 。将名为 button_name_time 的原子钟按钮标签字符串常量添加到 strings.xml 文件中
因为我们希望原子钟 UI 按钮元素位于文本数据输入字段之下(这样就不会与配置用户界面按钮混淆),我们将把它放在第二个 LinearLayout 容器标签中,如图图 16-13 所示。这样我们就有更少的参数需要把它放到合适的位置,只有一个 MarginTop 和一个 MarginLeft 和一个 textColor 来确保它匹配我们的空间等离子体和屏幕文本标题颜色。我们还引用我们的字符串常量,并将 ID 设置为时间按钮。
图 16-13 。在 activity_config.xml 的第二个 LinearLayout 容器的底部添加一个按钮标签
timeButton 按钮元素的 XML 标记应该如下所示:
<Button android:id="@+id/timeButton
android:text="@string/button_name_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:layout_marginTop="11dp"
android:layout_marginLeft="11dp" />
既然我们已经在 Configure a Planet 活动中添加了一个原子钟 UI 按钮对象,现在是时候进入 Java 代码,实例化该按钮对象,这样我们就可以添加事件处理代码,允许我们将一个 Intent 对象发送到我们的 TimePlanet.java 活动类并启动它!
用 Java 为我们的 ConfigPlanet.java 活动编码一个意图对象
让我们折叠其他按钮对象代码块,如图 16-14 中的加号所示,并实例化一个新的 timeButton 对象,并使用下面一行 Java 代码将其引用到我们的 XML:
Button timeButton = (Button) findViewById(R.id.timeButton);
图 16-14 。在 ConfigPlanet 活动的 timeButton 事件处理程序中编写 callTimeIntent Intent 对象
接下来,我们需要使用将事件监听器附加到按钮对象。setOnClickListener( ) 方法,并创建一个新视图。OnClickListener( ) 事件处理方法使用的四行代码如图图 16-14 所示:
timeButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Intent callTimeIntent = new Intent(view.getContext(), TimePlanet.class);
startActivityForResult(callTimeIntent, 0); } });
正如您所看到的,我们在 onClick()事件处理方法中构造了 Intent 对象,使用了下面一行 Java 代码:
Intent callTimeIntent = new Intent(view.getContext(), TimePlanet.class);
这声明了 Intent ,将其命名为 callTimeIntent ,并使用 new 关键字来构造 Intent,将它传递给两个必需的参数:当前上下文,由 view.getContext( ) 方法表示,以及 Intent 的目标, TimePlanet.class 活动专有名称。
下一行代码使用我们刚刚创建的 callTimeIntent 对象启动 TimePlanet 活动,代码如下:
startActivityForResult(callTimeIntent, 0);
第一个参数是 Intent 对象,第二个是代码,其中零表示没有代码,任何非零正数都是在中返回的代码。活动退出时调用的 onActivityResult( ) 方法。
用 Java 为我们的 TimePlanet.java 活动编写一个意图对象
接下来,我们需要将我们的意图处理代码添加到我们的 TimePlanet.java 活动子类中,以便我们可以在使用返回星球配置按钮退出活动时返回一个意图结果。我们需要添加的 Java 代码是
Button returnFromTimeButton = (Button) findViewById(R.id.timeButton);
returnFromTimeButton.setOnClickListener(new view.OnClickListener() {
public void onClick(View view) {
Intent returnIntent = new Intent();
setResult(RESULT_OK, returnIntent);
finish();
}
});
首先,我们需要实例化 returnFromTimeButton 按钮对象,然后使用。setOnClickListener()方法。这将调用新视图。OnClickListener()方法,然后包含我们的 onClick()事件处理程序,最后包含我们的 Intent 对象声明和 setResult()方法调用,最后是我们的 finish()方法调用,将我们返回到 ConfigPlanet,如图图 16-15 所示。
图 16-15 。在我们的 TimePlanet 活动的 returnFromTimeButton 处理程序中编写 returnIntent 意图对象
现在,我们唯一需要做的就是为我们在 activity_time.xml 文件中指定的 tran_stars_galaxy drawable XML 文件添加 wow factor 背景图像 fade Java 代码,如图 16-16 所示。
图 16-16 。为我们的 TransitionDrawable 用户界面屏幕背景淡出效果添加 Java 代码
我们这样做是因为我们已经在前一章中完成了设置特殊效果的所有工作,所以为什么不在原子钟屏幕上利用它来增加特殊效果的剂量呢!
让我们从我们的 NewPlanet.java 活动中复制 TransitionDrawable trans 对象和相关代码,并将其粘贴到我们的 TimePlanet.java 类中的 setContentView()方法下,如图图 16-16 所示。
不要忘记 trans.startTransition()方法代码,当我们进入用户界面屏幕时,它将启动转换动画。最终的 Java 代码块应该如下所示:
final TransitionDrawable trans = (TransitionDrawable)getResources().getDrawable(R.drawable.tran_stars_galaxy);
LinearLayout timePlanetScreen = (LinearLayout)findViewById(R.id.timePlanetScreen);
timePlanetScreen.setBackground(trans);
trans.startTransition(5000);
现在,我们终于准备好利用作为 Android 应用运行的工作流程,并测试我们新的 TimePlanet 活动和意图处理代码。
一旦 Nexus S 模拟器启动,点击菜单按钮,从菜单中选择配置行星选项,并启动 ConfigPlanet.java 活动。然后点击 UI 屏幕右下角的原子钟按钮,如图图 16-17 所示。
图 16-17 。在 Eclipse 的 Nexus 模拟器中测试我们的 TimePlanet.java 活动和意图对象调用
正如你所看到的,我们新的行星地球时间屏幕平稳启动,从恒星背景到等离子体空间背景的动画过渡平稳地发生在我们用行星地球制作的令人印象深刻的模拟时钟后面,如图 16-17 右侧所示。
在行星地球时间活动屏幕的底部是你的返回到行星配置按钮,点击这个,它将返回到配置一个行星活动。
为了测试我们的意图对象和它们的有效性,多点击几次原子钟和返回行星配置按钮,以绝对确保我们可以在这两个活动屏幕之间来回切换,次数不限。恭喜,你现在已经使用了一个 Intent 对象在两个应用活动屏幕之间切换了!
摘要
在这一章中,我们仔细研究了 Android 意图对象以及 Android 意图过滤器。我们研究了它们是如何工作的,以及不同类型的意图,这样我们就可以在接下来的几章中使用它们来启动服务和调用广播接收器。
我们了解了 Intent 对象中包含的信息,Android 操作系统中指定的动作、数据、类别、附加内容及其常量,以及通常在 Intent 中指定的标志、数据类型(MIME)和组件名称属性。
我们了解了指定组件名称的显式意图之间的区别,这样,意图对象的目标是已知的,不需要进行推断,我们还了解了组件名称未知而目标通过动作、数据和类别进行推断的隐式意图。
我们了解了如何在您的应用和 AndroidManifest.xml 文件中创建您自己的意图过滤器,从而为解析隐含的意图对象(那些没有指定组件名称的对象)提供推理引擎,以及在什么条件下需要利用这些意图过滤器为通过意图使用您的应用组件的其他开发人员提供其他应用兼容性。
最后,我们在 Hello World 应用中添加了一个原子钟 TimePlanet.java 活动,这样我们就可以展示如何使用 Intent 对象在应用中的活动之间进行切换。我们将在本书的下两章中学习如何在服务和广播接收器中使用 Intent 对象,所以我们不在本章中讨论这些例子。
在下一章中,我们将仔细研究 Android 服务,了解已启动和已绑定的服务,并了解它们如何帮助我们的应用更加流畅和高效地工作。
十七、Android 服务:使用后台处理
在这一章中,我们将深入研究 Android 服务、,开发人员利用这些服务来执行后台异步操作,这些服务可以自行处理数据流或计算,而无需将与应用用户界面设计同步,或者以任何方式与应用内容(持续的用户体验)同步。
服务通常被用来处理需要在你的应用用户体验的背景中进行的事情,与安卓用户对你的应用的实时使用并行**,但不与该应用的用户体验设计直接同步或实时连接。**
Android 服务的使用示例包括:在用户使用您的应用时播放长格式数字音频(比如专辑音乐曲目),在后台与某种服务器或数据库对话,下载数据,管理文件输入输出流,流式传输新媒体内容,如数字视频流或数字音频流,处理网络(SMTP 或 HTTP)协议交易,处理支付网关交易,GPS 数据的实时处理,以及类似的复杂任务。
通常委托给 Android 服务类的任务是那些不应该与用户界面和用户体验联系在一起的任务,因为强制并发(同步)处理可能会导致用户体验变得不自然或不平稳(即,不能描绘平滑的用户界面响应,从而不能描绘平滑和愉快的用户体验)。
委派给 Android 服务的任务也是处理器密集型的,所以在开发处理器密集型应用时,要考虑终端用户的电池寿命。正如你可能猜到的那样,Android 电池的两个主要功耗是长时间处理和长时间保持显示屏亮着(我们在前面的视频章节中提到过)。
在这一章中,我们将仔细研究 Android 服务类,Android 服务的各种特性,以及这些特性、功能、设置、常量和类似的特性是如何在您的 Android 应用中声明使用的。您可能已经猜到了,声明要使用的服务是在一个 AndroidManifest.xml 文件中完成的。
这是 Android 中比较复杂的主题之一,因为它本质上涉及绑定、同步、进程、处理器周期、线程、访问控制、权限以及类似的高级操作系统主题。
Android 服务基础:规则和特征
服务被定义为可以在后台执行处理密集型功能的 Android 应用组件,而不需要任何用户界面设计或任何活动显示屏幕,并且不需要用户与需要完成的处理进行任何交互。
Android 应用组件可以使用 Intent 对象启动服务类,服务将继续在后台处理,即使该 Android 设备用户切换到不同的 Android 应用。
一个 Android 应用组件可以将绑定到一个服务上与之交互,甚至执行进程间通信,你也可能知道这就是 IPC 。在对 Android 服务的概述之后,我们将在本章的下一节更仔细地研究进程和线程。
绑定是一种高级编程概念,涉及在两个独立的应用组件进程之间建立实时连接,当发生变化以及需要在它们的逻辑绑定连接之间进行更新时,这些进程会相互提醒。
一个 Android 服务通常采用两种格式之一,绑定或开始。当一个应用组件(比如一个活动)通过调用来启动服务时,一个 Android 服务就变成了启动的**。startService( )** 方法。
一旦启动,服务可以无限期地在后台运行,即使在启动该服务的组件随后被应用逻辑或 Android 操作系统破坏的情况下。
一个启动的服务执行一个单独的操作,并且不向调用实体返回结果,这很像一个被声明为 void 的方法。
例如,已启动的服务可能会通过网络下载或上传数据文件。最佳实践表明,当启动的服务操作完成时,该服务应该自动停止,以帮助优化 Android 操作系统资源,如处理器周期或内存使用。
当一个 Android 应用组件将绑定到一个服务时,一个绑定的服务被创建。这是通过调用来完成的。bindService( ) 方法。绑定服务提供了一个客户端-服务器接口,该接口允许组件通过使用进程间通信(IPC)与绑定服务进行交互、发送请求、获取结果,甚至跨进程进行这些操作。
绑定服务只在任何其他 Android 应用组件绑定到它时存在于 Android 系统内存中。多个应用组件可以同时绑定到该服务,但是,当所有这些解除绑定时,该服务就会被销毁(从系统内存中移除)。
我们将看看这两种类型的服务格式,以及一种混合方法,其中您的服务可以同时以这两种方式工作。这意味着您可以启动您的服务(这样它就是一个由启动的服务,并且可以无限期运行)并且允许绑定。
Android 服务是被指定为启动的服务还是被指定为绑定的服务取决于您是否实现了一些更有用的服务类回调方法。例如,服务类**。onStartCommand( )** 方法允许组件启动一个服务,而**。onBind( )** 方法允许绑定到那个服务。我们将在本章的后面部分详细介绍服务类的方法。
无论应用的服务是已启动、已绑定还是既已启动又已绑定,任何其他应用组件都可以使用服务,即使是来自单独的应用。这类似于任何应用组件都可以通过有目的地启动来启动活动。我们在前一章中详细介绍了使用意图对象,我们将在本章中介绍如何将意图对象用于服务。
值得注意的是,服务运行的优先级比不活动的活动优先级高,因此 Android 操作系统终止服务类的可能性比终止活动类的可能性小。
同样需要注意的是,您可以在清单 XML 文件中将您的服务声明为 private ,并阻止来自其他应用的访问,这通常是单个开发人员对他们的应用所做的事情。
默认情况下,服务将总是在主机应用的主进程的主线程中运行。在应用的这个主要进程内部运行的服务通常被称为本地服务。
程序员中一个常见的误解是,Android 服务总是运行在它自己独立的线程上。虽然如果你这样设置的话,这当然是可能的,但是默认情况下,服务会而不是创建自己的线程,因此除非你另外指定,否则服务不会在单独的线程中运行。我们将在本章的下一节讨论进程和线程,因为这是一个非常密切相关的主题。
这意味着,如果您的服务要执行任何 CPU 密集型工作(例如实时解码流数据)或阻塞操作(例如通过繁忙的网络协议进行实时网络访问),您应该在服务中额外创建一个新线程来执行这种类型的处理。
值得注意的是,除了服务类所在的线程(已经使用),您可能不需要为服务类使用另一个线程,例如,在本章的示例中,我们使用服务中的 MediaPlayer 播放音乐文件,而不需要生成另一个线程。
真正确定是否需要这样做的唯一方法是,首先尝试使用一个服务类进行后台处理,然后,如果它影响您的用户体验,则考虑在需要时实现一个线程类和对象。
进程或线程:有价值的基础信息
当你的 Android 应用的一个组件,比如说你的 MainActivity 类启动时,并且你的应用当前没有任何组件在运行,Android 操作系统将为你的应用启动一个全新的 Linux 进程,使用一个执行的线程,称为 UI 线程。一个进程可以生成或启动(或衍生)多个线程。
通常,所有 Android 应用组件都将在相同的初始进程和线程中运行。这通常被称为主线程。
如果您的一个 Android 应用组件启动,并且 Android 发现您的应用已经存在一个进程,由于您的应用中的另一个组件已经存在,那么该组件也将在相同的应用进程中启动,并且也将使用相同的线程。因此,本质上,要启动自己的线程,必须在 Java 代码中明确地这样做。
但是,您可以安排应用中的不同组件在单独的进程中运行,并且可以为任何进程创建额外的线程。正如我们将看到的,这是 Android 服务通常会做的事情。
如何指定流程:使用 android:process XML 参数
作为 Android 操作系统的默认设置,所有的应用组件都将在相同的进程中运行,大多数基本的 Android 应用不需要改变这种设置,除非有非常令人信服的理由。
对于高级应用(我们没有在本书中讨论,但是我们将在这里讨论这个概念,以彻底了解 Android 进程),如果您发现自己处于绝对需要控制某个应用组件属于哪个 Android 进程的情况,您可以在您的 AndroidManifest.xml 文件中指定这一点。
您的 AndroidManifest.xml 组件标签针对每种主要类型的应用组件,无论是活动标签、服务标签、广播接收器标签还是内容提供者标签,都将包含一个可选的 android:process 参数。
这个进程参数可用于指定应用组件需要运行的进程。您可以设置 process 参数,使每个应用组件都在自己的进程中运行,或者混合和匹配,使一些应用组件共享一个进程,而其他组件不共享该进程。
如果您想变得非常复杂,您还可以设置这些 android:process 参数,以便来自完全不同的 android 应用的组件可以在同一个 Android 进程中一起执行。
只有当这些特定的应用共享相同的 Linux 用户 ID,并且使用相同的证书签名时,才能实现这一点。
有趣的是,您的 AndroidManifest XML 文件中的全局 <应用> 标记也将接受 android:process 参数。
在您的标签中使用 android:process 参数将为您的应用设置一个默认的进程值,该值随后将应用于您的 XML 应用组件定义(嵌套)层次结构中的所有应用组件。当然,这不包括那些没有利用 android:process 参数为特定应用组件指定不同进程的应用组件,而不是您通过您的标记中的 android:process 参数设置为应用使用的默认进程。
值得注意的是,Android 可以选择在任何时候关闭一个进程,例如,当内存不足时,或者如果您的进程所使用的内存被其他具有更高优先级的进程所需要,或者从最终用户那里获得了更多的使用(注意)。
在进程内部运行的应用组件在终止后会被销毁,或者从内存中删除。不要担心,因为这些进程中的任何一个都可以在以后重新启动,用于那些需要为用户完成某些事情的应用组件。
当决定终止哪些进程时,Android 系统会权衡它们对用户的相对重要性。例如,与托管可见活动的进程相比,它更容易关闭托管屏幕上不再可见的活动的进程。因此,决定是否终止一个进程取决于该进程中运行的组件的状态。接下来将讨论用于决定终止哪些进程的规则。
Android 进程寿命:如何让你的进程保持活力
Android 试图尽可能长时间地将应用进程保存在系统内存中,但有时需要销毁运行在操作系统中的旧进程。这样做是为了为更新或更高优先级的进程回收系统内存资源。
毕竟,今天大多数 Android 设备只配备了 1 或 2gb 的主系统内存,当用户玩游戏、启动应用、阅读电子书、播放音乐、打电话等时,这可能会很快填满。
即使设备开始配备 3gb 的主内存,您仍然会遇到内存管理问题,而使用进程和线程是这些内存管理问题的核心,因此我们了解 Android 操作系统中如何处理进程非常重要。
Android 操作系统通过优先级层次来决定保留哪个进程和终止哪个进程。Android 将每个正在运行的进程放入这个优先级层次结构中,这是基于进程队列中运行的每个组件,以及当前状态(运行、空闲、停止等。)的那些组件。
从 Android 设备中清除内存的方式是,首先终止优先级(重要性)最低的进程,然后终止下一个优先级最低的进程,以此类推,直到较高优先级进程所需的系统资源被回收使用。
在这个优先级层次结构中有五个流程优先级。一旦您看到它们是什么,您将意识到这个流程优先级层次结构是如何逻辑地建立的,并且您还将很好地了解服务(异步处理或重载)和活动(用户界面屏幕)如何适应这个整体流程优先级模式,这对于理解非常重要。准备好一些啊哈时刻吧!
最高优先级的进程级别是前台进程,它是当前正在运行(处理)的主进程,因此是用户当前参与的应用任务所需要的。
如果一个流程包含用户当前正在交互的活动(用户界面屏幕),或者如果它托管当前绑定到用户正在交互的活动的服务,则该流程被认为处于前台。
如果一个进程当前正在执行一个在前台运行的服务,它也被认为是一个前台进程,这意味着服务对象已经调用了**。startForeground( )** 方法。
如果一个服务当前正在执行它的 onCreate()、onDestroy()或 onStart( ) 服务生命周期回调,我们将在本章中学习,或者当前正在广播一个 BroadcastReceiver 对象,它恰好调用它的**【on receive()**方法,它也将被 Android 操作系统赋予一个前台进程优先级状态。
在一个最佳的 Android 操作场景中,在任何给定的时间都只有少数前台进程在运行。这些进程只有在万不得已的情况下才会终止,例如,如果系统内存不足,以致操作系统或其应用无法继续有效运行。
下一个最高优先级的进程是可见进程,该进程不包含任何前台进程组件,但仍会影响用户在设备显示屏上看到的内容。
如果一个流程包含一个不在前台的活动,但在用户的显示屏上仍然可见,则该流程被认为是可见的,例如一个活动的**。onPause( )** 方法已被调用。
一个很好的例子是前台流程活动启动了一个允许在后台看到调用活动的对话框。
包含已绑定到可见活动的服务类的流程也将获得可见流程优先级。可见进程被认为几乎和前台进程一样重要,因此它们不会被终止,除非绝对需要保持所有前台进程在系统内存中运行。
五个级别中的中间优先级进程级别是服务进程,它是包含已经使用启动的服务的进程。startService( ) 方法,但是 Android 没有将它归类到两个最高进程优先级类别中的任何一个。
因为服务进程没有用户界面屏幕,并且在后台进程中异步运行,所以与用户在显示器上看到的任何内容都没有直接联系。但是,服务仍在执行最终用户希望继续执行的任务(例如在后台播放音乐专辑或通过网络下载数据)。出于这个原因,Android 让它们继续处理,除非没有足够的内存来支持它们以及前台和可见的进程。
第二个最低优先级的流程级别是后台流程,该流程包含一个终端用户当前不可见的活动,例如活动**。**【onStop()】方法被调用。
因为这些后台进程对用户体验没有可察觉的影响,每当有必要为更高优先级(前台、可见或服务)的进程回收系统内存时,Android 就会终止它们。
经常有相当多的后台进程在运行,Android 将后台进程保存在一个被称为 LRU(最近最少使用)的列表中。这用于保证具有用户最近使用的活动的进程是最后终止的进程。
值得注意的是,如果您的活动正确地实现了它们的生命周期方法,并保存了它们的当前状态,那么终止该活动的过程将不会对您的最终用户体验产生任何影响。
这是因为当您的用户导航回活动的用户界面屏幕时,活动会恢复其所有可见状态(记住您的 Bundle savedInstanceState 代码)。
最低优先级的进程级别是空进程,它不包含任何当前活动的应用组件。如果您想知道为什么一个空进程会被保存在系统内存中,那么让一个空进程保持活动状态的战略原因是为了 缓存优化,这将在下一次组件需要在该进程中运行时缩短启动时间。
Android 操作系统经常终止这些空进程,试图在各种进程缓存之间以及与其底层 Linux 内核缓存之间平衡整体系统内存资源。
最后,由于另一个进程依赖于某个进程,因此该进程的优先级可能会增加。任何当前服务于另一个进程的 Android 进程都不会比它当前服务的进程排名更低。
假设进程 01 中包含的内容提供者(数据库或数据存储)正忙于服务进程 02 中的用户界面活动,或者,如果进程 01 中的服务被绑定到进程 02 中的应用组件,则进程 01 总是被认为至少与进程 02 一样重要。
接下来,我们将看看线程,它是更低级的线程,用于在进程中调度处理器密集型和用户界面任务。
关于线程的一些警告:不要干扰 UI 线程
在 Android 操作系统通过你的 AndroidManifest.xml 文件启动你的应用后,它的操作系统会产生一个执行的线程,通常被称为主线程**。主线程负责操作系统和用户界面小部件之间的调度和管理事件,这一点我们在本书的前一章已经了解过。**
主线程还控制绘制您的图形、视频和动画(可绘制)素材到活动显示屏,因此它立即执行大量繁重的工作,这就是为什么您可能需要生成自己的线程,如果您想对您的 Android 应用执行的某些操作可能会使主(或主要)线程上已经繁重的工作负载过载,主线程实际上运行您的整个应用。
主线程通常也被称为 UI 线程,或用户界面线程,因为它是应用组件与 Android UI 工具包中的组件进行交互的线程。Android UI Toolkit 包括来自 android.widget 和 android.view 包的所有组件(类),我们在本书的前三部分已经广泛了解了这些组件。
在主进程中运行的所有 Android UI 工具包组件都在这个 UI 线程中实例化,并且对每个所需组件的操作系统调用都从这个 UI 线程中分派。
因此,响应系统回调的方法,比如**。onKeyDown( )** 事件处理程序,用于报告用户界面交互,或生命周期回调方法之一,如**。开始()法,或者说是一个。pause( )** 方法,甚至是一个**。destroy( )** 方法,总是在 Android 应用的主进程中包含的 UI 线程内运行。
当应用为响应用户界面交互而分派密集型处理时,单线程模型会导致用户体验性能降低,这就是为什么您必须正确利用线程的原因。
原因是显而易见的;如果 UI 线程中正在进行大量的处理,那么执行冗长的操作,如网络访问、复杂的计算或 SQL 数据库查询,将会阻塞整个用户界面的响应,因为这些处理周期和基本上阻塞了UI 相关事件的顺利(快速)处理。
当一个线程以这种方式被阻塞时,UI 事件不能被调度处理,这包括将图形(drawable)元素绘制到屏幕上。从用户体验的角度来看,您的应用似乎“挂起”或暂停了一段不期望的时间。
需要注意的是,如果你的应用阻塞 UI 线程超过几秒钟(超过五秒钟),你的用户将会看到一个对话框,其中包含非常不希望的(至少从用户体验的角度来看)“应用没有响应”(或 ANR)对话框。
同样需要注意的是,Android UI 工具包目前并不是所谓的“线程安全的”由于这个原因,任何时候你都不能从工作线程中操纵你的应用用户界面元素。
工作线程是任何非 UI 线程,通常也称为后台线程。换句话说,它是您在应用 Java 代码中产生的一个线程,用来卸载密集的“工人”后台处理,以便您的 UI 将继续平稳运行。
所以请记住,Android 线程处理的第一个关键规则是,您必须从 UI 线程内部对用户界面元素进行所有操作,记住 UI 线程是您的 Android 应用的主要线程。
第二条规则更一般化,就是不要因为任何原因在任何时候阻塞 UI 线程。这就是为什么要有工作线程的原因,这样,如果您需要做一些会导致 UI 线程被阻塞的事情,您可以在代码中生成一个工作线程来执行处理、流式传输、数据库访问或其他需要大量使用处理器的任务,也可能是高级应用编程。
我的 Android 应用应该使用服务还是线程?
Android 服务只是一个可以在后台运行的组件,即使用户没有与你的应用交互。如果你需要在主 UI 线程之外执行工作,但只是在用户与你的应用的用户界面交互时,那么你应该在你的应用的那个类中创建一个新的 Android 线程对象,使用一个 HandlerThread 对象或一个 AsyncTask 对象,而不是麻烦地编码(并在清单中声明)一个完整的 Android 服务子类。
假设您想在活动运行时从音乐服务中流式传输一些音乐。你想要做的是使用来创建一个线程。onCreate( ) 方法,使用启动它运行。onStart( ) 方法,最后使用一个将其停止。onStop( ) 方法。
正如我之前提到的,至少在你成为一名更高级的 Android 程序员之前,你可能会想要使用更精细的 Android Thread 子类,即名为 AsyncTask 和(or) HandlerThread 而不是更通用的 Thread 类。
所以,你可能想知道,什么时候会希望使用服务子类而不是在现有类中生成线程对象。如果您还记得上一节,包含服务子类的 Android 进程总是比利用后台处理活动(线程)的进程优先级更高。
如果您的应用要承担大量的处理、访问或流操作,您可能希望为此操作启动一个服务组件(类),而不是简单地创建一个工作线程。
如果后台功能很可能比您的活动持续时间长,这是一个特别重要的考虑因素。例如,将您使用 Android Camera 类创建的视频上传到 web 服务器的活动会希望利用服务类来执行此上传,以便此上传过程能够在后台继续,即使您的用户离开当前活动。
因此,您希望使用服务类而不是线程对象的原因是,使用服务组件将保证您的处理操作至少有一个服务流程优先级,而不管您的 Activity 子类发生了什么。
接下来,让我们学习如何编写我们的服务子类,以及如何使用 Intent 对象调用它。我们将使用 TimePlanet.java 活动子类来实现这一点,它是我们在前一章意图中创建的。
我们将通过创建一个名为 MusicService.java 的音乐播放器后台服务组件来实现我们的 Android 服务类生命周期。这将是一个服务子类,它将利用类声明中的 extends 关键字来子类化 Android 服务类。
在我们设置了访问这些服务生命周期方法所需的新用户界面元素后,我们将使用 Java 代码编写服务类生命周期方法,包括 onCreate( ) 、 onStart( ) 和 onDestroy( ) 方法。我们甚至将利用其中一个 Android Intent 对象来启动我们的后台服务,该服务将为我们的星球时间原子钟活动播放背景音乐,该活动是我们在本书涵盖 Intent 的前一章中创建的。
最后,我们还将了解如何将 <服务> 标签添加到我们的 AndroidManifest.xml 文件中,并且我们将在我们的 Nexus S 模拟器中测试我们的背景音乐服务子类,只是为了绝对确保一切都按照预期的方式工作。
在我们的 TimePlanet 活动中实现音乐服务
我们需要做的第一件事是创建启动音乐服务和停止音乐服务用户界面按钮对象,它们将控制我们的音乐服务。这些放在我们的 TimePlanet.java 活动类和 XML 定义文件中。为此,我们需要设置的第一件事是按钮 UI 元素标签所需的字符串常量,因此,通过使用以下 xml 标记,在/res/values 文件夹的项目的 strings.xml 文件中添加两个<字符串>常量定义:
<string name="start_button_value">Start Music Service</string>
<string name="stop_button_value">Stop Music Service</string>
如图 17-1 中的所示。现在我们准备添加两个按钮标签,它们将两个用户界面元素添加到我们现有的 UI 设计中。这些控制我们的音乐服务背景音乐组件。
图 17-1。将 start_button_value 和 stop_button_value 字符串常量添加到 strings.xml 文件中
在 Eclipse 编辑区域打开您的 activity_time.xml 文件,并复制我们在上一章中创建的 timeButton 用户界面组件,在其下复制两次。将第三个(底部)按钮标签保留为 timeButton,将第一个按钮 android:id 参数改为 startServiceButton ,将第二个按钮 android:id 参数改为 stopServiceButton ,如图图 17-2 所示。
图 17-2。向 activity_time.xml 添加 startServiceButton 和 stopServiceButton 按钮用户界面元素
接下来,让我们将这两个新按钮对象的 android:textColor 参数更改为 #FFAAAA ,或者一种漂亮的明亮的黄色颜色,以区分音乐服务按钮元素和返回配置按钮元素,后者是一种浅橙色,以匹配等离子背景。
接下来,更改 android:text 参数,使其指向我们在图 17-1 中设置的正确的字符串常量值;因此,对于第一个按钮标记,将 time_button_value 更改为 start_button_value,对于第二个按钮标记,将 stop_button_value。我们几乎完成了新按钮的参数化!
接下来,让我们更改两个按钮标签中的 android:marginTop 参数,使它们相互靠近,并与其他 UI 元素分开。
为此,我们将 startServiceButton marginTop 参数设置为 24 DIP (24dp ),该参数将启动音乐服务按钮元素从 AnalogClock UI 元素推开。然后将 stopServiceButton UI 元素 marginTop 参数设置为 6 DIP (6dp ),将停止音乐服务按钮元素放置在开始音乐服务按钮元素的正下方。
最后,因为我们的 startServiceButton 在 AnalogClock 下面 24 DIP,所以将 timeButton UI 元素的 marginTop 参数设置为相同的精确值。这导致音乐服务按钮 UI 元素在我们在前一章创建的现有用户界面设计中引人注目地居中。最终的标记如图 17-2 所示。
<Button android:id="@+id/startServiceButton"
android:textColor="#FFFFAA"
android:text="@string/start_button_value"
android:layout_marginTop="24dp"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button android:id="@+id/stopServiceButton"
android:textColor="#FFFFAA"
android:text="@string/stop_button_value"
android:layout_marginTop="6dp"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
为了更好地了解这个新的 TimePlanet.java 活动屏幕用户界面设计的外观,单击 XML 编辑窗格左下方的图形布局编辑器选项卡。如您所见,屏幕 UI 设计均匀分布,音乐服务按钮按功能分组。
要查看用户界面设计的真实外观,您需要使用 Run As Android Application 工作流程,因为我们从过去的经验中知道,Eclipse 中的图形布局编辑器工具并不总是以 Android 设备屏幕上呈现的方式显示边距参数。
既然我们的 Planet Time 用户界面屏幕上有了我们的音乐服务用户界面元素,那么是时候编辑我们的 AndroidManifest.xml 文件了。
配置我们的 AndroidManifest 文件以添加一个组件
当您将 Android 活动、服务或广播接收器组件添加到您的 Android 应用中时,您必须在您的 AndroidManifest XML 文件中声明它,该文件用于启动您的应用。
现在让我们通过在 Eclipse 的中央编辑窗格中打开您的 AndroidManifest.xml 文件来做到这一点。在现有 XML 标记的底部,添加一个 <服务> 标签,该标签位于结束</应用>标签之前,但位于我们在上一章中添加的时间表的最后一个<活动>标签之后。
这个标签应该实现一个 android:enabled="true" 参数,这将使这个服务组件能够在您的应用内部使用,以及一个 android:name= "。MusicService" 参数,用来引用 MusicService.java 类名。我们将在本章的下一节创建这个服务类。标签标记应该如下所示:
<service android:enabled="true" android:name=".MusicService" />
完成的 AndroidManifest.xml 文件和标记如图 17-3 所示。
图 17-3。将我们的<服务>标签和参数添加到我们的音乐服务类的 AndroidManifest.xml 文件中
现在我们准备编写 Java 代码,实现我们在本章前一节中创建的用户界面设计元素。
在我们的 TimePlanet 活动中编写 Java 代码来启动服务
在 Eclipse 的中央编辑窗格中打开TimePlanet.java活动类,并在其自身下再复制两次 returnFromTimeButton 按钮对象的按钮实例化和事件处理方法 Java 代码结构。我们这样做是为了不需要从头开始重新编写所有的 Java 代码,因为我们实现了两个非常相似的按钮对象,以及它们的事件处理基础设施。
命名(重命名)第一个复制的按钮对象startmusicebutton,引用其 ID 为 startButton 。删除 onClick(View view)方法调用内部的 Java 代码语句,这样我们就可以添加我们的新服务类相关的方法调用了。
接下来,重命名第二个复制的按钮对象:stopmusicebutton,引用其 ID 为: stopButton 。删除 onClick(View view)方法调用内部的 Java 代码语句,这样我们就可以添加我们的新服务类相关的方法调用了。
在我们的 startMusicServiceButton onClick()事件处理程序方法中,让我们添加一个 startService( ) 方法调用,以使用一个 Intent 对象启动我们的音乐服务组件,该对象引用我们当前的类上下文,代码如下:
startService(new Intent(this, MusicService.class));
这个方法调用使用一个 Intent 对象启动我们的服务,该对象是我们在 startService()方法调用中使用 Java new 关键字创建的,该关键字使用当前上下文和我们的 MusicService 类名引用作为参数来构造一个新的 Intent 对象,如前面一行 Java 代码所示。
接下来,在我们的 stopMusicServiceButton onClick()事件处理程序方法中,让我们添加 stopService( ) 方法调用,该方法调用使用一个 Intent 对象来停止和销毁我们的 MusicService 组件,该对象使用以下代码引用我们的当前类上下文:
stopService(new Intent(this, MusicService.class));
这个方法调用使用一个 Intent 对象来破坏我们的服务,这个 Intent 对象是我们使用 Java new 关键字在 stopService()方法调用本身内部创建的,这个关键字使用当前上下文和我们的 MusicService 类名引用作为参数来构造一个新的 Intent 对象,如前面一行 Java 代码所示。
请注意,在 Eclipse 编辑器中,我们放在两个 onClick()事件处理方法中的这两个 MusicService 子类方法调用都使用红色波浪下划线突出显示进行了错误标记,如图 17-4 所示。
图 17-4。将呼叫添加到。startService()和。stopService()和 TimePlanet.java 的新意向对象
红色波浪下划线的原因是因为我们还没有创建我们的 MusicService.java 服务子类,正如您所看到的,它在一个 Intent 对象参数中被引用,作为这个 Intent 对象需要传递到的类。
现在让我们创建 MusicService 服务子类,这样我们就可以在 Eclipse 中消除这个错误,更重要的是,因为这是我们实现这个服务组件的工作流程中的下一步!
为我们的 MusicService.java 类创建新的服务子类
让我们来看看在 Eclipse 中创建新 Java 类的另一种方法,将鼠标放在我们当前编辑窗格中看到的 TimePlanet.java 活动子类的红色波浪下划线上,然后选择弹出的助手对话框中显示的选项:**Create Class " music service "**在 Eclipse 中启动新 Java 类对话框。
这个助手对话框如图 17-5 所示,是我们调用新 Java 类对话框和超类选择对话框的另一种方式。
图 17-5。使用 Eclipse 错误对话框来调用一个新的>类对话框,这样我们就可以创建音乐服务类
我们看到的完成相同工作过程的另一种方法是右键单击 Eclipse Package Explorer 窗格中的 package name 子文件夹,然后选择我们现在熟悉的 New Class 菜单序列,它将为我们访问这些相同的新 Java 类创建对话框。
接下来,让我们填写新的 Java 类和超类选择对话框,将名为 MusicService.java 的新服务子类指定为 Hello_World/src 源代码文件夹中的 chapter.two.hello_world 包中的公共类。
正如你在图 17-6 中看到的,前五个字段已经为我们填写好了,所以只需点击浏览按钮打开一个超类选择对话框,键入一个“s”字符并选择 android.app.Service 类。
图 17-6。使用新的 Java 类对话框和超类选择对话框来指定我们的音乐服务类
在这两个对话框中点击 OK 和 Finish 按钮后,你将会看到 Eclipse 为你编写的公共类 MusicService extends Service (子类)Java 代码,如下面的代码和图 17-7 所示。
图 17-7。使用 IBinder()方法的新服务子类 MusicService】和为我们编码的导入语句
public class MusicService extends Service {
@Override
public IBinder onBind(Intent arg0) {
// TO DO: Auto-generated method stub
return null;
}
}
现在,我们所要做的就是添加我们的服务类生命周期方法调用,以实现我们基于 MediaPlayer 的音乐播放服务,我们将准备好在 Nexus S 模拟器中测试我们的应用。
用 Java 编写我们的音乐服务类服务生命周期方法
既然我们的 MusicService.java 类已经创建并打开以供编辑,让我们从在类的顶部添加一个名为 musicPlayer 的 MediaPlayer 对象开始,我们可以在我们的三个生命周期回调方法中使用它,接下来我们将编写代码。这将涉及下面一行 Java 代码:
MediaPlayer musicPlayer;
我们编写的第一个方法是调用服务类时第一个被访问的方法,那就是 onCreate( ) 方法。该方法创建一个 MediaPlayer 对象,并设置其使用,以及设置任何参数,在本例中为循环参数,如图图 17-8 所示。
图 17-8。编写 onCreate()、onStart()和 onDestroy 服务生命周期方法来控制 MediaPlayer
我们还使用一个 Toast 对象和一个**。makeText( )** 方法调用向我们展示了操作系统在为我们创建服务方面所做的工作。onCreate()方法将被声明为 public,因此任何类都可以访问它,并且因为它不返回值而无效,我们需要将方法声明和三行 Java 代码放入该方法中,以完成 MediaPlayer 的设置和配置,如下所示:
@Override
public void onCreate( ) {
Toast.makeText(this, "Music Service has been Created", Toast.LENGTH_SHORT).show( );
musicPlayer = MediaPlayer.create(this, R.raw.music);
musicPlayer.setLooping(true);
}
接下来,我们将编写我们的 onStart( ) 方法,因为当我们的服务启动时,这将是生命周期中的下一个方法。请记住,当调用服务时,会调用 onCreate()方法来创建和设置服务,然后调用 onStart()方法来启动它的运行。因此,我们将利用 onStart()方法启动 MediaPlayer 对象来播放音乐,同样,还包括另一条 Toast 消息,让我们了解服务流程的具体情况。因此,onStart()方法的代码如下所示:
@Override
public void onStart( ) {
Toast.makeText(this, "Music Service is Started", Toast.LENGTH_SHORT).show( );
musicPlayer.start( );
}
最后,我们使用我们的 onDestroy( ) 方法来停止 MediaPlayer 对象,并且还包含一个 Toast 消息,让我们了解关于服务的情况。因此,我们的 onDestroy()方法的代码如下所示:
@Override
public void onDestroy( ) {
Toast.makeText(this, "Music Service has been Stopped", Toast.LENGTH_SHORT).show( );
musicPlayer.stop( );
}
既然我们的 MusicService.java 服务子类已经被编码了,让我们通过点击 Eclipse 顶部标签为 TimePlanet.java 的标签回到我们的 TimePlanet.java 活动子类,如图图 17-8 所示。
使用 TimePlanet.this 细化我们的 TimePlanet 类上下文引用
当您进入 TimePlanet.java 编辑选项卡时,您会注意到您的 startService()和 stopService()方法调用仍然使用 Eclipse 错误级别突出显示元素以红色波浪下划线标出。这是因为 Intent 对象的第一个(上下文)参数需要引用 TimePlanet 类上下文,并且它当前引用的是它所在的视图类,而不是位于我们当前类食物链顶端的活动类。
因此,我们需要修改这段代码,以允许这个上下文一直“看到”我们类的顶部。为此,我们需要在 startService()和 stopService()方法中修改 Intent 对象中的第一个 this 参数,并将该参数改为: TimePlanet.this ,以便 Intent 对象引用 TimePlanet 类的当前上下文。正如你在图 17-9 中看到的,这消除了我们 Java 代码中的所有错误,我们现在准备在 Nexus S 模拟器中编译和运行我们的服务组件智能应用,这样我们就可以测试它,看看它工作得如何。
图 17-9。一旦音乐播放器类就位且添加了 TimePlanet.this reference,无错误 TimePlanet 代码
右键点击你的项目文件夹,使用 Run As Android Application 工作流程启动 Nexus S 模拟器,当应用启动后,点击菜单按钮,选择配置行星活动,一旦出现在屏幕上,点击原子钟按钮,进入行星地球时间活动屏幕,如图图 17-10 所示。
图 17-10。带有音乐服务按钮的 Nexus S 模拟器中运行的 TimePlanet 活动
测试音乐服务组件
现在是时候测试我们的新 MusicService 组件了,它继承了 Android Service 类,看看它的工作效果如何。点击开始音乐服务按钮,无缝聆听美妙的音乐回放。屏幕上会出现一条提示消息,告诉您服务是何时创建的以及何时启动的。
在任何时候,点按“停止音乐服务”按钮,您会注意到音乐停止播放,并且屏幕上会出现一条提示消息,告诉您音乐服务已经停止。继续点击每个按钮几次,以确保应用没有错误,并且服务和 MediaPlayer 对象可以随时启动和停止。
摘要
在本章中,我们仔细研究了一些更复杂的 Android 操作系统特性和概念,包括服务、进程和线程。我们了解了它们之间的关系,它们之间的区别,以及在我们的应用中何时使用它们。
首先我们看了一下 Android 服务及其基本形式和规则,包括启动的服务和绑定的服务之间的区别。我们查看了一些关键方法,如 startService()、stopService()和 bindService(),稍后我们在自己的服务类中实现了这些方法。我们了解了服务类生命周期及其运作方式。
然后,我们仔细研究了 Android 进程和线程,因为这个主题与服务密切相关,对于 Android 开发人员来说,理解这个主题非常重要。我们研究了如何在 Android Manifest XML 文件中指定一个流程,以及流程的生命周期。
我们了解了不同类型的流程,以及 Android 操作系统如何按照重要性对它们进行排序,我们还了解了不同类型的 Android 组件(如活动和服务)如何适应 Android 流程使用的优先级排序系统。
我们研究了在现有类中使用线程和创建一个新的服务子类来进行后台处理之间的权衡。我们还学习了 HandlerThread 和 AsyncTask 类,如果您决定在 Android 编程中获得更高的水平,并在应用组件中利用线程,您可以使用这些类。
最后,我们从零开始编写了自己的服务子类,名为 MusicService.java。我们将用户界面元素添加到 TimePlanet 活动中,这样我们就可以从我们的原子钟显示屏上控制音乐服务,无论如何它都需要一些背景音乐。
我们学习了如何在 AndroidManifest.xml 文件中添加一个 XML 标签来声明我们要使用的服务组件,然后编写 Java 代码来实现服务类生命周期方法,这些方法是实现我们的 MediaPlayer 对象及其生命周期方法所需要的,我们使用这些方法来控制我们的背景音乐播放引擎。
在下一章中,我们将学习所有关于广播接收器的知识,它可以用来向您的 Android 应用组件以及其他组件发送重要的应用和系统相关消息。
十八、广播接收器:Android 应用间通信
在这一章中,我们 将仔细看看 Android 的广播接收器类。这个类专门用于 Android 组件之间的通信,每个组件都是主要 Android 类(Activity、Service、BroadcastReceiver 等)的子类。)正如我们在本书的 Java 编码经验中所看到的。
这可以包括您自己的应用组件之间的通信,但更广泛地用于不相关的应用组件之间的通信。这意味着与其他应用的通信。事实上,它更常用于在您的应用和 Android 操作系统中包含的组件之间进行通信。
想想看,任何人的 Android 手机上最常用的组件(应用)都是手机自带的组件,因此是 Android 操作系统不可或缺的一部分。没有人可以否认,人们在日常使用他们的 Android 设备时会在很大程度上使用他们的 Android 设备的电话拨号器、日历、闹钟、定时器、电子邮件客户端、浏览器、屏幕保护程序、壁纸、铃声等等,无论是智能手机、平板电脑、电子阅读器、手表、机顶盒还是 iTV 电视机。
在这一章中,我将尝试向您展示在您的 Android 应用中实现广播接收器(通常简称为接收器)的各种方法,在本例中是在我们的 Hello World 应用中。也就是说,这个特定的 Android 主题在某种程度上超出了所包含的系统(Eclipse IDE 及其仿真器),我们需要保持在该系统中,以确保我们的每个读者都可以同步跟进。
这是因为我们的每个读者都有不同的 Android 设备硬件,一旦我们开始在不同的 Android 设备功能和不同的外部应用之间进行广播(使用 BroadcastReceivers ),他们就会与不同的开发人员和应用合作。
出于这个原因,我必须非常小心地为本章选择我的 Java 代码示例,因为它们需要支持市场上每种类型和型号的 Android 设备都保证支持的设备特性。
幸运的是,这是一本关于 Android 的介绍性书籍,所以我可以在本章中介绍 BroadcastReceiver 类的理论和规则。
我们将学习如何实现广播接收器方法(Java)和标签(XML ),这样您将大致了解如何在自己的 Android 应用中实现广播接收器,以及从哪里开始。
Android 广播接收机:基本概念和类型
Android 的 BroadcastReceiver 类是 java.lang.Object 的直接子类,这意味着它位于 Android 类层次结构的顶部,这可能是主要操作系统功能所期望的。
它是 android.content 包的一部分,所以它在 import 语句中的完整使用路径是Android . content . broadcast receiver,我们将在本章后面的 Java 代码中看到。
就像有两种不同类型的 Android 服务(启动和绑定)一样,也有不同类型的可以接收的广播:正常广播和有序广播。
正常的广播是异步的,因此是自由浮动的,不与操作系统环境中的任何东西绑定(同步)。订阅普通广播的任何接收器方法都可以以任何未定义的顺序自由运行。
因为 Android 是一个多线程的多任务操作系统,这也意味着正常的广播可以通过它们的接收器方法在完全相同的时间(并行)被处理。
这意味着正常的广播本质上更有效,因为它们不基于任何其他系统事件,并且操作系统具有以最佳方式处理它们的自由。
然而,这也意味着正常的广播不能利用任何返回的结果(任何类型的返回值),或者终止任何 API 或组件。
正常的广播是通过使用 Context.sendBroadcast( ) 方法发送的,我们将在本章的后面看到,当我们开始为添加到 Hello World 应用基础结构中的广播接收器编写 Java 代码时。
另一方面,有序广播按顺序传送**,一次传送给一个接收器。当每个接收器执行有序广播时,它可以将结果传播给下一个接收器。或者,它也可以选择在任何给定的接收器对象上中止广播,这样广播就不会传递到任何其他接收器。作为程序员,你可能会看到事件是如何在处理链中“冒泡”的。**
可以使用 AndroidManifest.xml 中的< receiver >标签内的<意图过滤器>的 android:priority 属性来控制广播接收器的处理顺序,您将在该标签中定义广播接收器。在这一点上,您可能不会感到惊讶,因为大多数组件关键的应用基础设施将在您的 AndroidManifest 中定义,以便 Android OS 可以在启动您的应用时设置这些进程和内存空间。
值得注意的是,指定完全相同优先级的标签和标签将以任意顺序运行。
通过使用**context . sendorderedbroadcast()**方法发送有序广播。
值得注意的是,即使在某些涉及正常广播的场景中,Android 操作系统也可能会恢复为一次一个接收器地传送您的正常广播,就像它是一个有序的广播一样。如果 Android 操作系统认为这种广播方法将为当前操作环境配置提供更多的处理或内存优化结果,则可能会发生这种情况。
另一个重要的考虑因素是可能需要创建进程的广播接收器。一次只应运行这些广播接收器中的一个,以便操作系统可以避免新进程使操作系统过载,每个新进程都会占用内存和处理资源。
在这种情况下,那些无序的(正常接收器)广播语义将总是成立的;这些创建进程的广播接收器将不能返回任何结果,也不能中止它们的广播组件。
总之,当你的 BroadcastReceiver 类通过你的 AndroidManifest.xml 标签作为一个应用组件启动后,它将成为你的 Android 应用整个生命周期的重要组成部分。
如果您需要随时查看 Android 应用的基本生命周期和相关信息(当您正在学习 Android 操作系统及其运行方式时,偶尔这样做是个好主意),您可以在 Android 开发者网站上找到这些信息,网址如下:
http://developer.android.com/guide/components/fundamentals.html
接下来,我们将回顾为什么我们需要将我们的 Activity Intent 对象和我们的 BroadcastReceiver Intent 对象分开,然后,我们将探讨 Android 操作系统基础架构中的安全性、广播接收器生命周期和广播接收器处理(流程)等问题。一旦我们了解了所有这些事情,我们就可以开始编码了!
广播你的意图:活动与广播接收者意图
我们使用 Intent 类和 Intent 对象来发送和接收这些广播。正如我在关于意图的第十六章中提到的,意图广播接收器引擎完全独立于使用 Context.startActivity()方法启动活动的意图。
因此,广播接收机无法处理与一起使用的意图。startActivity( ) 方法。事实上,你的广播接收机甚至没有察觉到一个活动意图的存在!类似地,当您广播您的 BroadcastReceiver 意图时,那些意图对象将永远不会遇到任何 Activity 子类,因此永远无法启动任何 Activity 子类。
Android 操作系统需要将这些类型的意图分开的主要原因是因为这两种类型的组件和它们调用的操作利用了两种非常不同类型的 Android 进程。
正如我们所知,启动一个有意图的活动是一个前台流程操作,它发生在主要的 UI 流程和线程中。这种类型的 Android 进程直接修改用户当前实时交互的内容。
另一方面,接收意向的广播是一个后台进程操作,用户并不知道,因此在我们在前一章中了解的进程优先级排序中,它没有那么高的优先级。正如你所看到的,我有一个很好的理由进入所有关于进程和线程的技术信息,因为它不仅仅适用于利用 Android 中的服务。
安全广播:广播接收机安全考虑
您可能已经注意到,广播接收器是通过 Android 上下文类 API 使用的,例如,要调用广播接收器,您可以使用它们的方法调用上下文对象/类,如下所示: Context.sendBroadcast( ) 或Context . sendorderedbroadcast()。
因此,广播接收机的核心访问是一个跨应用的实现,因此,您应该非常明智地考虑您自己的应用之外的其他应用如何滥用您的 Android 广播接收机的实现。本节有效地概述了在 Android 应用中使用广播接收器时,您可能需要记住的一些主要问题。
首先,Android Intent 命名空间是全局的。出于这个原因,您需要确保您的 Intent 动作名称以及其他字符串常量都封装在您自己的名称空间中。如果不遵守这条规则,可能会无意中与其他应用发生冲突。
每当实现. register receiver(BroadcastReceiver,IntentFilter)方法时,请注意任何其他 Android 应用都可以向该注册的 broadcast receiver 发送广播。事实上,通过使用 BroadcastReceiver 权限,您可以准确地控制谁可以向您注册的 Receiver 对象发送广播,我们将很快介绍这一点。
当您在您的应用 AndroidManifest.xml 文件定义中发布一个,然后在其中额外指定结构时,您需要认识到任何其他 Android 应用都可以将广播发送到该结构中,而不管您可能指定的结构如何。
有一种方法可以防止其他应用向您的应用的标签中的结构发送广播。做到这一点的方法是通过在你的<接收器>标签中使用一个 android:exported="false" 参数来使你的广播接收器对他们不可用。注意,该参数也可以在<服务>和<活动>标签中使用。
如果你决定使用**。sendBroadcast(Intent)** 方法或其相关方法,请注意任何其他应用都可以接收这些广播。
您可以通过使用权限来控制谁可以接收广播。如果您使用的是 Android 4(冰激凌三明治)或更高版本,您还可以通过使用**intent . set package()**方法调用将您的广播限制到任何单个应用。
这里需要注意的是,当您使用 LocalBroadcastManager 时,这些安全问题都不存在,我们将在本章的下一节中讨论这个问题,因为使用这个类的意图广播永远不会超出您当前的进程。
可以在广播的发送方或接收方实施广播访问权限。在等式的发送端实施权限的方法是使用提供一个非空权限参数**。sendBroadcast(Intent,String)** 或者,如果您使用的是有序广播,请使用以下命令:。sendOrderedBroadcast(Intent,String,BroadcastReceiver,android.os.Handler,int,String,Bundle) 方法调用。
只有通过在其 AndroidManifest.xml 文件中的 < uses-permission > 标签请求获得该许可常数的广播接收机,才能接收您的安全许可广播。
在接收广播时实施权限的方法是,在注册接收者时,再次提供一个非空权限。
这是在调用 Java 时完成的。register receiver(broadcast receiver,IntentFilter,String,android.os.Handler) 方法,或者,在 AndroidManifest.xml 文件中的静态 < receiver > 标记中。
只有先前被授予此权限的 BroadcastReceivers 才能向该 receiver 对象发送 Intent 对象。可以通过使用 Android 应用的 AndroidManifest.xml 文件中的 < uses-permission > 标记选项请求那些权限来授予权限。
广播接收机生命周期:规则和条例
BroadcastReceiver 对象只在接收者调用期间有效。【上下文,意图】on receive方法。一旦 Java 代码从这里返回。在 Receive()方法功能上,操作系统将认为该对象已经完成,它将不再是活动的。
这个广播接收器处理周期对于您在中究竟能做什么有着重要的意义。onReceive(Context,Intent) 方法调用实现。
您编写的任何需要异步操作的 Java 代码都是不允许的。这是因为您需要从函数返回来处理异步操作。但是,此时,您的 BroadcastReceiver 将不再是活动的,因此系统可以在异步操作完成之前终止该进程。
此外,您将无法显示任何对话框,也无法从 BroadcastReceiver 中绑定到任何服务类。
如果你需要在这种情况下显示一个对话框,你仍然可以通过使用 android.app 包中的 Androidnotification manager类 来实现这个目标。Android . app . notification manager 是 java.lang.Object 的子类,这个类通知您的用户后台发生的事件。通知可以有三种不同的格式:一种是存在于状态栏中的持久图标,可以通过你的启动器访问;第二种方法是打开或闪烁用户 Android 设备上的 LED 灯;最后,通过闪烁背光、播放声音甚至振动设备来提醒用户。有关 Android 通知管理器类的更多信息可以在以下 URL 找到:
http://developer.android.com/reference/android/app/NotificationManager.html
如果您需要启动一个服务类,正如我们在上一章中看到的,您必须利用 Context.startService( ) 方法调用向服务子类发送一个命令。
处理广播:广播如何影响 Android 进程
当前正在执行 BroadcastReceiver 对象的 Android 进程将运行 BroadcastReceiver 对象内部的 Java 代码。onReceive(上下文,意图)方法。Android 操作系统认为这是一个高优先级的前台进程,因此它将由操作系统保持活动和处理,除非可能在内存资源极度短缺的情况下。
一旦您的 Java 代码完成。调用 Receive()方法时,该广播接收器不再是活动的,它的宿主进程等级被重新校准,以便它与该进程中运行的其他应用组件一样重要,但不是更重要。
这一点尤其值得注意,因为如果该进程只是为了托管 BroadcastReceiver 而创建的(通常情况下),对于用户从未交互过或者甚至最近没有交互过的应用,那么当从。在 Receive()方法执行时,操作系统会将该进程视为一个空的进程优先级。
正如我们在前一章中了解到的空进程优先级,这意味着 Android 操作系统很可能会主动终止该进程,以便操作系统资源可用于其他更重要的进程。
这意味着,对于长时间运行的操作,您应该经常将服务与 BroadcastReceiver 结合使用,以在函数操作的整个过程中保持包含该服务的进程处于活动状态。
应用内部的广播:LocalBroadcastManager
如果您不需要在两个不同的 Android 应用之间发送 BroadcastReceiver,那么您最好使用 LocalBroadcastManager 类来利用 broadcast receiver 函数,而不是使用前面章节中描述的全局方法。请注意,如果您需要支持 3.0 之前的 Android 操作系统(例如最初的 Amazon Kindle Fire 的 2.3.7),这些操作系统不支持 LocalBroadcastManager 类。
LocalBroadcastManager 类提供了更有效的本地广播实现,因为不需要进程间通信。这也让你不用考虑其他 Android 应用接收或发送你的广播的各种安全问题。
这样做的原因是,通过使用这个 LocalBroadcastManager 类,您将知道您在应用中广播的数据不会离开应用的范围,因此,您不需要担心任何私人数据的泄漏。
第二,通过使用 LocalBroadcastManager,其他应用就不可能向你的应用发送任何广播,所以你也不必担心你的应用中有任何安全窗口会被其他程序员利用。
最后,就内存和处理而言,使用 LocalBroadcastManager 类比在整个 Android 操作系统中发送全局广播要高效得多。
注册广播接收器:动态与静态注册
有两种完全不同的方法来注册您的 Android BroadcastReceiver 对象,以便在您的 Android 应用中使用。
一种注册形式称为广播接收器注册的静态方法,这种方法是您最熟悉的格式,它涉及到使用 <接收器> 标签在您的 AndroidManifest.xml 文件中“预先”注册广播接收器以供使用(这就是为什么它被称为静态注册)。
另一种方法叫做动态广播接收器注册,这是使用 Java 代码而不是 XML 标记完成的。之所以称之为动态,是因为它是在 Java 代码中完成的,与此同时,您正在执行与 BroadcastReceiver 类相关的所有操作,而不是预先(静态)在 AndroidManifest.xml 应用引导文件中完成。
如果您想利用动态 BroadcastReceiver 注册,动态注册 BroadcastReceiver 类实例的方法是利用**context . register receiver()**方法调用。
需要注意的是,如果你在你的 Activity.onResume( ) 方法代码中注册了一个 BroadcastReceiver,你也应该记得在你的 Activity.onPause( ) 方法代码中取消对的注册。
这样做的原因是为了减少系统资源的浪费,因为您不希望在活动暂停时接收任何 Intent 对象,所以您添加了一个 onPause()方法并注销了接收者,这样您就不会让操作系统尝试将 Intent 对象发送到一个当前没有使用的活动。
确保不要犯在 Activity.onSaveInstanceState()方法中注销 BroadcastReceivers 的常见错误,因为如果用户在历史堆栈中向后移动,将不会调用此方法。
现在是时候尝试在我们自己的 Hello World Android 应用中实现 BroadcastReceiver 类和方法了。
在我们的应用中实现广播接收器
我们需要做的第一件事是为在 Hello World 应用中实现 BroadcastReceiver 打下一些基础,例如:创建字符串常量、XML 用户界面设计、AndroidManifest 条目等等。
当我们将添加到 TimePlanet.java 活动中的警报功能被触发并且警报响起时,我们的 BroadcastReceiver 将发送一条消息。因为计时器是与时间相关的,我们将把这个功能添加到 UI 屏幕上,在那里添加它是最符合逻辑的—我们的星球时间 UI 屏幕!
现在让我们添加新的字符串常量,这样我们就可以标记将启动闹钟倒计时的按钮 UI 元素和文本编辑字段,这样它就会有一个提示,告诉我们的最终用户我们希望他们在这个数据字段中输入什么类型的信息。
我们将为按钮对象贴上标签:启动计时器倒计时,并通过编写以下 XML 标记,使我们的提示为:输入秒数:
<string name="timer_hint_value">Enter Number of Seconds</string>
<string name="timer_button_value">Start Timer Countdown</string>
这两个新的字符串常量进入我们的 strings.xml 文件,该文件在我们的项目文件夹下的/res/values 文件夹中,如图 18-1 所示。
图 18-1 。添加我们的按钮 UI 元素标签字符串常量和编辑文本 UI 元素提示字符串常量
既然我们可以在 TimePlanet.java 活动的新 UI 设计中引用这些字符串常量,那么是时候将这些新 UI 元素添加到 activity_time.xml 文件中了。
使用 XML 设计我们的警报广播接收器用户界面
在 Eclipse 中心区域的编辑窗格中打开 activity_time.xml 文件,右键单击/res/layout 文件夹中的文件名,并选择 Open(如果愿意,也可以使用 F3 功能键)。
我们将把我们的 setAlarm EditText 用户界面小部件放在我们的模拟时钟的正下方,这样屏幕上所有的按钮 UI 元素将保持组合在一起。在< AnalogClock >标签下添加一个 < EditText > 标签,并添加参数,这些参数将对其进行配置,类似于我们为 activity_config.xml 用户界面屏幕创建的 EditText 字段。
让我们使用透明度为 0.75 的 alpha 值, **12 ems 的文本大小,**白色或 #FFFFFF,的背景颜色和粗体字体的文本样式。
使用**Android:layout _ gravity = " center "在 UI 设计中将数据输入字段居中,使用Android:layout _ margin top = " 10dp "**将我们的数据字段与 AnalogClock UI 元素隔开一点。
最后,让我们将我们的 android:typeface 参数指定为等宽,并将 numberDecimal 的 android:inputType 指定为计时器倒计时的秒数。
最终的 EditText XML 标记应该包含以下参数:
<EditText android:id="@+id/setAlarm"
android:hint="@string/timer_hint_value"
android:inputType="numberDecimal"
android:ems="12"
android:alpha="0.75"
android:layout_marginTop="10dp"
android:background="#FFFFFF"
android:textStyle="bold"
android:layout_gravity="center"
android:typeface="monospace"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
在 Eclipse XML 编辑器中,我们在每行放置两到三个参数以节省空间,这样我们可以在一个屏幕上看到我们用户界面屏幕定义的所有 XML 代码,如图 18-2 所示。
图 18-2 。添加我们的按钮和编辑文本标签,并配置它们的参数用于用户界面设计
接下来,我们需要在 setAlarm EditText 元素下添加我们的 startCounter Button 对象,这样,一旦用户添加了持续时间值,用户单击以启动计时器运行的按钮元素就在它的正下方。
在< EditText >标签下添加一个 <按钮> 标签,并添加将配置该按钮的参数,类似于我们已经在当前用户界面屏幕下创建的三个按钮字段。
让我们将 android:id 参数设置为我们将在 Java 代码中引用的 startCounter ,并将 android:text 参数设置为引用我们之前创建的名为 timer_button_value 的字符串常量。
让我们使用黄色或 #FFFFAA 的背景颜色和**Android:layout _ gravity = " center "**的居中参数来将 UI 设计中的数据输入字段居中,并确保所需的 android:layout_width 和 android:layout_height 参数被包括在内并被设置为 wrap_content 。
最终的按钮标签 XML 标记应包含以下参数:
<Button android:id="@+id/startCounter"
android:text="@string/timer_button_value"
android:textColor="#FFFFAA"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
既然我们已经为计时器用户界面元素实现了 XML 标记,那么让我们也实现 XML 标记,我们将需要这些标记来声明我们的 BroadcastReceiver 组件子类,以便在我们的应用中使用。如您所知,这是在 AndroidManifest.xml 文件中完成的,所以现在让我们开始吧。
添加我们的 alarm receiver broadcast receiver Android 清单 XML
在 Eclipse 中心区域的编辑窗格中打开您的 Androidmanifest.xml 文件,方法是右键单击 Hello_World 项目文件夹底部的文件名,并选择打开菜单选项(如果您愿意,也可以使用 F3 功能键)。
让我们在清单底部的标签下添加一行,正好在 XML 标记的应用组件定义块的结束标签上方。
为我们新的 BroadcastReceiver 子类添加一个 < receiver > 标记,我们接下来将用 Java 对其进行编码,并使用一个 name 参数引用应用组件名称的完整路径名,使用以下标记:
<receiver name="chapter.two.hello_world.AlarmReceiver" />
完整的 AndroidManifest.xml 文件声明了我们最新的 Hello World 应用组件,包括六个 Activity 组件,以及一个服务和一个 BroadcastReceiver 组件,如图 18-3 所示。
图 18-3 。为我们的 AlarmReceiver BroadcastReceiver 子类添加一个< receiver >标签到 AndroidManifest.xml
现在,我们已经设计了警报用户界面元素,并在清单中声明了广播接收器,是时候使用 Java 代码在我们的 TimePlanet.java 活动子类中编写警报控制用户界面元素和方法了,它们将在该子类中显示。
使用 Java 编写 startTimerButton 和 startTimer()方法的代码
让我们走一条程序员的捷径,用最简单的方法实现我们的startTimerButtonButton UI 对象!复制并粘贴 startMusicServiceButton 代码行(全部六行)到它们下面。
接下来将 startTimerButton 名称改为 startTimerButton,UI XML ID 引用由 R.id.startServiceButton 改为 R.id.startCounter,最后将 startService(new Intent(this,class))方法调用改为**startTimer(view);**方法调用。
现在,Eclipse 将红色错误突出显示我们正在使用的这个新方法名,至少直到我们对这个新方法进行编码,这是我们下一步要做的,在我们活动的底部。startTimerButton 按钮用户界面元素的新事件处理代码如图 18-4 所示,还有**public void start timer(View View)**方法 Java 代码,我们接下来将详细讨论这些代码。startTimerButton Java 代码块应如下所示:
startTimerButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
startTimer(view);
}
}
图 18-4 。在 TimePlanet.java 活动子类中编写用户界面按钮和 startTimer()方法
我们将使用 public 访问来声明我们的 startTimer()方法,因此它可供公共使用,并使用 void 返回值,因为它不向调用实体返回任何内容,在本例中,单击 startTimerButton UI 对象。
编写我们的 startTimer( ) Java 方法
接下来,我们将声明我们的 EditText 用户界面元素以供使用,我们将它命名为 alarmText ,并使用 findViewById( ) 方法来引用它,以通过 R.id.setAlarm 指向我们的 setAlarm EditText 标签 XML 定义。
请注意,如果您还没有导入 EditText 类来使用,这段代码将会有红色下划线突出显示,您可以将鼠标悬停在它上面,让 Eclipse 为您编写这段 import 语句 Java 代码。
接下来,我们声明一个名为 i 的整型变量,并将其设置为一个整型对象,我们称之为**。parseInt( )** 方法。这个方法通过调用解析一个整数值。toString( ) 转换方法,关闭**。getText( )** 方法,从 alarmText EditText 对象中调用该方法,以检索用户输入到数据字段中的文本值。所有这些都是在一行 Java 代码中完成的,如下所示:
int i = Integer.parseInt(alarmText.getText().toString());
接下来,我们声明一个名为 intent 的 intent 对象,并构造一个新的 Intent 对象,使用 new 关键字,使用当前上下文 this 和 AlarmReceiver.class 的一个目标组件,我们将在下一节中对其进行编码。现在,Eclipse 红色下划线突出显示了这个引用,因为我们还没有创建和编码这个 BroadcastReceiver 子类。
接下来,我们将创建一个 Android 挂起内容对象,命名为告警内容,并通过调用来加载 Android 告警功能。getBroadcast( ) 方法 off ofofpending Intent类使用参数配置我们的 alarmIntent 与当前上下文,一个报警请求代码,我们的 intent Intent 对象,我们刚刚在前面的代码行中创建,以及一个零值用于标志参数,因为此时我们没有将标志值传递给 getBroadcast( ) 方法调用。
请注意,如果您还没有导入 PendingIntent 类来使用,这段代码将会有红色下划线突出显示,您应该将鼠标悬停在它上面,让 Eclipse 为您编写导入语句 Java 代码。如果您想更详细地研究这个 PendingIntent 类,可以在 Android 开发者网站上找到信息,网址是:
http://developer.android.com/reference/android/app/PendingIntent.html
接下来,我们将创建一个 Android AlarmManager 对象,我们将其命名为 alarmManager ,并使用 getSystemService( ) 方法加载该对象,该方法从 AlarmManager 类中调用,并将 ALARM_SERVICE 常量值作为参数传递。这为我们设置了一个警报功能,这是我们可以调用的许多 Android 操作系统功能之一,在这种情况下,通过使用 AlarmManager API。
请注意,如果您还没有导入 AlarmManager 类来使用,这段代码将会有红色下划线突出显示,您应该将鼠标悬停在它上面,让 Eclipse 为您编写导入语句 Java 代码。如果您想更详细地研究这个 AlarmManager 类,可以在 Android 开发者网站上找到信息,网址如下:
http://developer.android.com/reference/android/app/AlarmManager.html
接下来,我们需要使用 alarmManager 类的来配置我们刚刚创建的 alarmManager 对象。方法设置()。我们将通过。set()方法需要三个参数,一个整数(在这种情况下是系统常数),它表示报警的类型,一个以毫秒为单位的触发时间,它由一个长值表示,以及一个挂起内容操作,在我们的情况下是我们在此之前创建的两行 Java 代码的告警内容。
AlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (I * 1000), alarmIntent);
注意,我们在。set()方法调用,使用system . current time millis()方法,该方法获得以毫秒为单位的当前系统时间,并将其与用户在我们的 EditText UI 元素中输入的秒数相加,乘以 1000,将该秒值转换为毫秒,从而得到触发时间!
startTimer()方法中的最后一行 Java 代码使用一个 Android Toast 对象和一个. makeText()方法调用,通过以下 Java 代码将我们的警报设置确认消息发布到活动屏幕:
Toast.makeText(this, "Alarm set in " + i + "seconds", Toast.LENGTH_SHORT).show();
同样,我们使用字符串值和我们的整数值 I 连接来显示一条 Toast 消息,告诉用户在 X 秒内用他们输入的值设置警报,然后在最后使用方法链接将. show()方法调用附加到。makeText()方法调用,使整个 Toast 对象构造只使用一行 Java 代码。
我们已经准备好创建并编写我们的 AlarmReceiver 类,它将实现我们的**on receive()**broadcast receiver 方法,以使一切协同工作。
创建 AlarmReceiver BroadcastReceiver 子类
将鼠标放在图 18-4 中红色波浪下划线突出显示的地方,然后选择创建 AlarmReceiver 类选项,让 Eclipse 创建如图图 18-5 所示的新 Java 类对话框。
图 18-5。让 Eclipse 使用超类选择对话框创建一个名为 AlarmReceiver 的新 Java 类
因为 Source folder:,Package:,and Name: data 字段已经为我们填写好了,只需点击 Browse 按钮,在中输入 a " b ,选择位于超类选择对话框顶部的字段,然后向下滚动到broadcast receiver-Android . content选择,在对话框的**匹配项:**部分找到,并选择它,如图所示
最后,点击 OK 按钮,返回新建 Java 类对话框,然后点击完成按钮,创建你的 AlarmReceiver BroadcastReceiver 子类,如图图 18-6 所示。
图 18-6。我们的 AlarmReceiver.java broadcast receiver 子类基础设施展示了一个 onReceive()方法
请注意,已经为我们创建了**on receive()**BroadcastReceiver 方法,其中包含了适当的访问控制和参数,我们可以编写 Java 代码来实现我们的 broadcast receiver 方法的警报相关功能。
接下来,我们将用我们的代码替换图 18-6 中显示的 TODO 自动生成方法存根,以便在接收到我们的广播时向屏幕发送消息。
编码我们的 alarmreceiver broadcastreceiver 子类
让我们使用 Android Toast 类在调用 onReceive()方法并接收到广播给它的 Intent 对象时向屏幕发送一条消息。
使用烤面包片**。makeText( )** ,方法使用传入 onReceive()方法的上下文对象作为其第一个参数,然后指定文本消息警报通知,最后是 Toast。LENGTH_SHORT 为方法参数的持续时间常数,然后链一个**。show( )** 方法调用在 java 代码语句的末尾使用下面单行 Java 代码:
Toast.makeText(arg0, "ALARM NOTIFICATION", Toast.LENGTH_SHORT).show();
完整的 AlarmReceiver BroadcastReceiver 子类及其 onReceive()方法如图图 18-7 所示。
图 18-7。编写我们的 onReceive()方法,在 AlarmReceiver.java 类中显示我们的警报消息
AlarmReceiver BroadcastReceiver 子类的 Java 代码如下所示:
package chapter.two.hello_world;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context arg0, Intent arg1) {
Toast.makeText(arg0, "ALARM NOTIFICATION", Toast.LENGTH_SHORT).show();
}
}
现在是时候在 Android Nexus S 模拟器中测试我们的广播接收器实现了,看看我们在本章中编写的所有 XML 和 Java 代码是否能正常工作。
右键单击 Hello_World 项目文件夹,选择 Run As Android Application 菜单序列启动 Nexus S 模拟器,这样我们就可以测试我们最新的应用版本。
当主屏幕出现时,单击菜单按钮以启动选项菜单,然后选择配置行星菜单选项以启动配置行星活动屏幕。
在屏幕的右下角,单击原子钟按钮启动您的 TimePlanet.java 活动用户界面屏幕,并在行星地球模拟时钟用户界面元素下的文本数据字段中输入以秒为单位的计时器持续时间值。
接下来,单击位于输入秒数数据输入字段下方的启动计时器倒计时按钮,发送广播并启动 AlarmManager 对象。该屏幕的用户界面如图图 18-8 所示。
图 18-8。测试运行在 Nexus S 仿真器上的 AlarmReceiver BroadcastReceiver 子类
正如您所看到的,我们现在实现的 BroadcastReceiver 应用组件运行良好,在指定倒计时秒数的 Toast 消息出现后,我们的警报通知 Toast 消息随后出现在屏幕上,当然,这是在我们为警报功能指定的秒数过后。
恭喜你!您已经成功实现了三种主要类型的 Android 组件:活动、服务和广播接收器!酷!休息一下,喝杯冷饮,放松一下!
现在,我们已经成功地在我们的 Android 应用中实现了一个广播接收器,我们需要学习如何实现的唯一组件类型是一个内容提供者,我们将在下一章向我们的 Hello World Android 应用添加内容提供者功能时了解它!
摘要
在本章中,我们仔细观察了 Androidbroadcast receiver类及其 onReceive( ) 方法。
我们研究了两种不同类型的广播接收器广播,即正常广播和有序广播,了解了它们之间的区别,以及何时需要实施每种类型的广播。
接下来,我们研究了广播接收器的安全性考虑,这些考虑源于广播接收器被发送到您的应用之外,并且也允许其他开发人员的代码进入您的应用。
然后我们看了 BroadcastReceiver 生命周期,以及关于如何允许您使用 onReceive()方法调用和 Java 代码函数来处理各种类型的 Java 编程目标的基本规则和规定。
接下来,我们仔细研究了 Android 操作系统如何处理广播接收器,以及您对它们的使用将如何影响您的应用处理优先级,我们在第十七章中了解到了这一点。
我们仔细研究了用于封闭应用环境中本地化广播的 LocalBroadcastManager 类,以及如何在 Android 操作系统中动态(在 Java 方法调用中)和静态(在我们的 Android Manifest XML 标记中)注册广播。
最后,我们冒险在自己的 Hello World 应用中实现了一个广播接收器,以便在 TimePlanet 活动中实现一个计时器功能。我们在 activity_time.xml 文件中添加了 UI 元素,在 AndroidMainfest.xml 文件中添加了一个新的标记,最后实现了一个 AlarmReceiver BroadcastReceiver 子类和一个 onReceive()方法。
在下一章中,我们将学习 Android 中所有的内容提供者,以及 SQLite 数据库引擎。我们把最复杂的主题留到了最后,这样在我们深入到像使用 Android 的内容提供者 API 构建 DBMS 引擎这样复杂的事情之前,您就可以最大限度地体验 Android 了。抓紧你的帽子!开始了。
十九、Android 内容供应器:访问数据存储
在这一章中,我们将学习关于 Android 操作系统的一个更高级的话题:内容供应器。内容提供者是指:数据存储。我们已经熟悉了 android.content 包,之前已经使用过它的类,我们将会更加熟悉 android.database 包,以及 android.provider 包。
Android 操作系统中最丰富的内容提供者之一是数据库 APISQLite,它是 Android 不可或缺的一部分。因此,本章还将涵盖 SQLite 数据库引擎的内在高级主题、设计原则和 Android 内容供应器 API 实现概述。
本章的主题比我们在前面章节中学习的其他主题更高级的原因是,本章包含两个不同高级主题的信息。
第一个是 SQL 和 SQLite 数据库设计概念,Android 的内部 ContentProvider 基于此,第二个涵盖了 Android 中当前可用的所有不同类型的内容提供者。
首先,我们将在一个相当高的层次上介绍数据库设计概念,这样你就有了关于我们将在本章的其余部分做什么的基础知识。
然后,我们将讨论不同类型的 Android 内容供应器,以及它们的概念和技术,以及如何通过统一资源标识符(URI)路径构造来访问它们。
接下来,我们将看看 SQL 数据库引擎和 Android SQLite 数据库包和 API。最后,我们将在 Hello World 应用中实现一个 Android 内容提供者,看看它们是如何协同工作的。
数据库基础:概念和术语
正如大多数程序员已经知道的那样,数据库管理系统,或 DBMS ,是一个高度结构化的数据存储系统,或“引擎”,它可以以表格格式存储有价值的数据,可以很容易地访问和更新。世界上最流行的开源 DBMS 数据库编程语言之一叫做 SQL ,代表结构化查询语言。
结构化部分来自于数据库被安排成表格格式的事实,而查询部分来自于这些数据表被设计成对某个数据值进行搜索的事实。语言部分来自于这样一个事实:SQL 已经发展成为一种如此复杂的数据库编程语言,以至于关于这个永恒的 RDBMS(关系数据库管理系统)主题的书籍可能比 Android 操作系统上的书籍要多得多。
对于那些从未使用过数据库技术的读者,我将在这里回顾一下基础知识,这样我们就可以使用相同的知识库。您可能熟悉的流行数据库软件包包括 Access 和 FileMaker,并且许多人可能使用过 Excel 以表格格式存储数据,这与数据库非常相似,只是更加直观。
MySQL 等关系数据库中的数据使用表存储,这些表支持数据的行和列。这类似于 Excel 这样的电子表格,除了数据库通常不会像在电子表格中一样一次全部可见,尽管一旦我们学习了所有相关的编程,如果您愿意,您可以生成报告来实现这一最终结果!
每个关系数据库表列在你的数据库记录结构中总是包含相似的数据类型和数据分类,这通常被称为数据库字段。
这意味着相反,数据库表中的每一个行将代表一个完整的数据记录。因此,一般来说,当您写入数据库记录时,当您第一次添加该记录时,您将写入一整行或一条完整的数据记录,但是当您搜索数据库信息时,您通常是通过某一列或字段来查找特定的数据或信息。
如果您的数据库表中有大量的数据字段(列),那么在数据库设计方法中,您可能希望有不止一个数据库(数据表)。
在现实世界的数据库设计中,其理论很大程度上超出了入门书籍的范围,出于访问(搜索)性能和组织的原因,您将希望有多个单一的数据库结构。事实上,Android 操作系统使用不止一种数据库结构来存储和访问终端用户的信息,这一点我们将在本章后面很快看到。
拥有多个数据库的方法是让每个数据库(表)中的每个记录都有一个唯一的键(唯一索引)。这样,使用该键,单个数据记录的信息可以跨越多个数据库表。在 Android 中,这个键被称为 ID ,并且总是通过 Android 的 SQLite 数据库中的常量 "_ID" 来指定。例如,如果您的 key 或 _ID 值为 137,您的电子邮件信息和电话信息可能在两个不同的表中,但存储在相同的 key (index)值下,因此与您的 Android 用户帐户准确关联。
MySQL 和 SQLite:开源数据库引擎
MySQL 是目前世界上最流行的开源关系数据库管理系统(RDBMS)引擎之一。如果你拥有自己的服务器硬件,你可以使用MySQL.com网站下载并安装 MySQL,然后你就可以用很少的软件购买费用托管大量的信息数据库。
SQLite 是 MySQL RDBMS 引擎的一个小得多的版本,设计用于嵌入式硬件,如平板电脑、智能手机、电子书阅读器、iTV 电视机、手表、汽车仪表盘、机顶盒、家庭媒体中心和其他通常称为互联网 2.0 的消费电子设备。有趣的是,所有的 HTML5 浏览器中都有 SQLite。
SQLite 代表结构化查询语言 Lite ,是作为 Android 操作系统一部分的开源数据库引擎。Android 中有一个 SQLite API(包),它包含了实现 SQLite API 所需的所有 RDBMS 函数。这些包含在 android.database.sqlite 包中的一系列类和方法中。
SQLite 是专门为嵌入式系统设计的,类似于 JavaME (Micro Edition),因此只有 256KB 的内存,用于托管关系数据库引擎实现。
SQLite 支持最小的、标准的关系数据库功能和特性集,如通用 SQL 语法、数据库事务和预处理语句,这足以为 Android 提供强大的数据库支持。
SQLite 支持三种不同的数据类型:TEXT(Java 中称为字符串值)、INTEGER(Java 中称为 long 值)和REAL(Java 中称为 double 值)数据类型。
使用 SQLite 时,所有其他数据类型在保存到数据库字段之前必须转换为这些兼容的数据类型之一。
值得注意的是,SQLite 本身并不验证任何可能写入其字段(表列)的数据类型是否实际上是已定义的数据类型。这意味着您可以将一个整数写入字符串列,反之亦然。如果您想更详细地研究 SQLite,可以访问 SQLite 站点,该站点位于以下 URL:
http://www.sqlite.org/
要在 Android 中使用 SQLite,您需要构建 SQLite 语句来创建和更新数据库,然后由 Android 为您管理。当您的应用创建数据库时,数据库将保存在一个目录中,该目录将始终使用以下 Android OS 数据库路径地址:
DATA/data/YOUR_APPLICATION_NAME/databases/YOUR_DATABASE_FILE_NAME
接下来,我们将看看许多不同类型的 Android 内容供应器,以及如何通过 Android 操作系统及其 android.content 包及其类和方法来访问它们。
Android 内容供应器和内容解析器:简介
Android 内容提供者对象管理应用的数据访问。如果你想在不同的 Android 应用之间共享数据,Android 内容供应器是你想要使用的。
这通常是在系统内存中的数据库或文件中,或者在设备的 SD 卡数据存储设备上,或者在偏好设置(名称-值对)中,甚至在外部网络服务器上的某种结构化数据集。
Android 内容供应器的一般目的是以标准化的方式封装数据,同时为 Android 开发者提供某种机制来加强他们的数据安全性。
内容提供者是标准的 Android 接口,它将一个系统进程中的数据与另一个进程中运行的 Java 代码连接起来。如果您想访问内容提供者内部的数据,您可以使用当前应用上下文中的 ContentResolver 对象作为数据库客户机与该内容提供者通信。
ContentResolver 类是从 java.lang.Object 子类化而来,它是 android.content 包的一部分。如果您想研究关于这个类及其常数、构造函数和方法的更详细的信息,您可以在 Android 开发人员网站的以下 URL 找到专门介绍这些信息的整个网页:
http://developer.android.com/reference/android/content/ContentResolver.html
Android ContentResolver 对象与 ContentProvider 对象进行通信,正如我们所知,它是实现 Android 的 Content Provider 超类的一个类的实例。
内容提供者对象有点像 Android,它有自己的自定义数据库引擎,从应用组件客户端接收数据请求,然后在自己的进程中执行请求的数据解析操作,如果可以找到请求的数据,则返回结果。
值得注意的是,如果开发人员不需要在他们的应用之外共享他们的数据,他们就不需要编写他们自己的内容提供者子类,大多数应用都不需要这样做。
您需要创建自己的内容提供者的子类的场景包括在应用中提供自定义搜索建议,或者如果您需要在您的应用和其他 Android 应用之间复制或粘贴复杂的数据。
Android 包括特定于应用的操作系统内容供应器,这些供应器管理常见类型的新媒体数据,如音频、视频、图像以及文本数据,如个人联系信息。
你可以看看开发者网页上的 Android 预定义内容提供者类,它提供了 android.provider 包的参考文档。它位于以下 URL:
http://developer.android.com/reference/android/provider/package-summary.html
Android 操作系统预装了这些内容供应器数据库。这些通过存储日常数据来帮助 Android 用户,例如最终用户的联系信息、日历、电话号码和多媒体文件。
这些特定于应用的内容提供程序类为开发人员提供了预构建的方法,用于向这些定制的内容提供程序写入数据值或从中读取数据值。
与往常一样,AndroidManifest.xml 文件中列出了一些限制,Android 应用开发人员可以轻松访问这些预构建的内容提供者类,以便在他们的应用特性或功能中使用。
寻址内容供应器:使用内容 URI
如果你想告诉 Android 操作系统你想访问哪个内容供应器,理解内容 URI 的概念是很重要的。我们以前使用过 URI 对象,所以你应该熟悉它们在 Android 应用中准确引用数据(内容)路径的功能。内容提供者有一种特殊的路径格式,就像 HTTP 有一种特殊的格式 HTTP://内容也有一种非常相似的特殊格式(因此我们很容易记住),这就是:
content://
Android 内容供应器的完整 URI 遵循以下格式:
content://Authority/Path/ID
作为一个例子,这里有一个(假想的)Hello World 内容 URI:
content://com.helloworld.universedatabase/planets/earth/1337
在这个 URI 中,com . hello world . universe database是数据权威,行星/地球/ 是数据路径, 1337 是数据记录的 ID。
一个内容 URI 总是包含四个必要的部分:要使用的模式,在本例中是**Content://;**权威;数据的可选路径;以及您想要访问的数据记录的 ID。
内容提供者的模式总是单词" content ",冒号和双正斜杠" :// "总是附加在 URI 的前面,用于将数据模式与数据授权机构分开。
URI 的下一部分被称为内容供应器的权限。如您所料,每个内容供应器的授权必须是唯一的。权威命名约定通常遵循 Java 包命名约定。
许多组织选择使用他们组织的反向. com 域名,加上您可能发布的每个内容供应器的数据限定符,因此我们前面的示例假设我们拥有helloworld.com域名,当然,我们并不拥有。
因为 Android 开发人员文档建议您使用 ContentProvider 子类的完全限定类名,如果我们遵循这个示例内容 URI,那么我们可以将 ContentProvider 子类命名为 UniverseDatabase.java。
URI 标准的第三部分是数据的路径,虽然是可选的,但出于组织的目的,使用它是一种相当标准的做法。我们不会将数据放在服务器的根文件夹中,而是放在一个行星文件夹中,为每个行星数据库使用子文件夹。在我们的例子中,一个子文件夹是地球。
例如,Android 的 MediaStore 数据库(我们接下来将会看到)的内容供应器使用不同的路径名来确保音频、图像和视频文件保存在不同的数据类型位置。
通过使用不同的路径名,单个内容供应器可以容纳许多不同类型的以某种方式相关的数据,例如保存在 MediaStore 内容供应器中的新媒体内容类型。
对于完全不相关的数据类型,标准的编程实践是希望为每个数据库使用不同的内容提供者子类,以及不同的数据权限(和路径)。
最后一个 URI 参考规范组件是 ID ,您可能已经猜到了,它需要是数字。这个 ID,或 Android 中的 _ID ,在您想要访问单个数据库记录时会用到。如您所见,URI 参考规范从最通用或最高级(content://)的规范开始,经过授权机构(服务器名),向下经过数据路径(目录路径),最终到达数据记录本身(ID)。这是首先建立任何数据路径的逻辑方法,所以我不认为您在理解 URI 参考规范及其构造方面有任何问题。
Android 操作系统内容供应器:作为操作系统一部分的数据库
android 提供了一个 android.provider 包,其中包含了 Android 操作系统中标准的所有主要数据库类型的 Java 接口。
这些包括 Android 用户最常使用的数据库,如联系人数据库、日历数据库和媒体商店数据库。我们将在这一节中讨论这些及其组成部分。
这些功能用于个人管理、时间管理和多媒体管理,这是 Android 设备上最常访问的三项任务,无论是智能手机、平板电脑、电子书阅读器还是独立电视设备。
正如我们在数据库设计一节中了解到的,这些数据库被分割成多个逻辑子数据库,并通过使用一个键或索引 _ID 值来引用,就像它们是一个单独的数据存储一样。我们知道,这样做是为了系统性能(内存使用),以及数据访问速度和数据库访问的方便性的原因。
Android MediaStore 数据库
MediaStore 数据库包含 9 个不同的新媒体素材数据库, CalendarContract 数据库包含 11 个不同的日历组件数据库, ContactsContract 数据库包含的数据库最多,有 21 个功能数据库。
媒体商店数据库包括五个音频数据相关数据库以及一个图像和一个视频相关数据库。表 19-1 显示了媒体商店数据提供者接口,以及它们访问的数据类型(参考)。
表 19-1 。Android 供应器包中的 MediaStore 数据库及其包含的数据类型
| 数据库ˌ资料库 | 描述 |
|---|---|
| 媒体商店。音频.白蛋白列 | 代表相册的数据库列 |
| MediaStore。音频.艺术专栏 | 代表艺术家的数据库列 |
| 媒体商店。音频。音频列 | 代表跨越多个数据库的音频文件的数据库列 |
| 媒体商店。Audio.GenresColumns | 代表流派的数据库列 |
| 媒体商店。音频.播放列表栏 | 代表播放列表的数据库列 |
| 媒体商店。图像。图像列 | 代表图像的数据库列 |
| MediaStore。视频,视频专栏 | 代表视频的数据库列 |
| 媒体商店。文件.文件列 | 所有媒体的主表的列 |
| MediaStore。MediaColumns | MediaProvider 表的公共列 |
Android 日历合同数据库
日历合同数据库包括 11 个日历相关数据库,每个数据库支持各种日历功能,例如事件、与会者、提醒、提醒以及其他类似的日历相关数据支持功能。
android 操作系统通过其 android.provider 包为 Android 日历数据库访问提供预建支持的原因是,对于希望访问这些日历功能的应用来说,能够向现有的 Android 日历功能集添加很酷的新功能是合乎逻辑的。
表 19-2 显示了 CalendarContract 数据提供程序接口,以及它们访问的不同类型的日历函数数据(因此,它们将允许您使用内容提供程序直接引用这些数据)。
表 19-2 。Android Provider 包中的 CalendarContract 数据库及其包含的数据类型
| 数据库ˌ资料库 | 描述 |
|---|---|
| 日历合同。日历栏栏 | 用于日历提醒功能的数据 |
| CalendarContract。CalendarCacheColumns | 用于日历缓存功能的数据 |
| CalendarContract。日历栏 | 其他 URI 可以查询的日历列 |
| CalendarContract。CalendarSyncColumns | 供同步适配器使用的通用列 |
| CalendarContract。颜色列 | 用于日历颜色功能的数据 |
| CalendarContract。EventDaysColumns 列 | 用于日历事件日功能的数据 |
| 日历合同。事件列 | 事件数据库中的列(联接) |
| 日历合同。扩展属性列 | 日历扩展属性中使用的数据 |
| CalendarContract。提醒栏 | 用于日历提醒功能的数据 |
| CalendarContract。同步列 | 其他数据库使用的同步信息列 |
接下来,我们将看看新的 Android Contacts 数据库结构,从 Android 2.1(éclair)操作系统版本开始,它现在通过 ContactsContract 名字引用(而不仅仅是使用 Contacts)。
Android 联系人联系数据库
contacts Contact数据库包括多达 21 个与联系人数据相关的数据库表,这并不令人惊讶,因为如今联系人管理包括大量信息,如姓名、电话号码、电子邮件地址、社交媒体存在、状态、显示名称等。表 19-3 显示了 ContactsContract 数据提供者接口,以及它们访问的数据类型(以及它们将引用的数据类型)。
表 19-3 。联系人联系 Android 供应商包中的数据库以及它们包含的数据类型
| 数据库ˌ资料库 | 描述 |
|---|---|
| 联系人联系人。CommonDataKinds.BaseTypes | 支持所有类型化数据类型 |
| 联系人联系人。CommonDataKinds.CommonColumns | 跨特定类型的公共列 |
| 联系我们。ContactNameColumns(联系名称列) | 原始联系人数据库中的联系人姓名和联系人姓名元数据列 |
| 联系我们。ContactOptionsColumns(联系人选项列) | ContactsContract 列。跟踪用户对联系人的偏好或与联系人的交互的联系人 |
| 联系我们。联系我们 | ContactsContract 列。接触是指固有的接触属性 |
| 联系我们。ContactStatusColumns(联系状态列) | 用于联系人状态信息的数据 |
| 联系人联系人。数据列 | 数据表中的列(连接) |
| 联系我们。DataColumnsWithJoins(数据列移动联接) | 组合 ContactsContract 返回的所有联接列。数据表查询 |
| 联系人联系人。显示名称来源 | 用于产生显示名称的数据类型 |
| 联络人协定。FullNameStyle | 用于组合成全名的常数 |
| 联系人联系人。组列 | 用于联系人分组信息的数据 |
| 联系我们。PhoneLookupColumns(语音查找列) | 用于联系人电话查找的数据 |
| 联系人联系人。PhoneticNameStyle | 姓名发音的常数 |
| 联系我们。PresenceColumns(存在列) | 附加数据链接返回 ID 条目 |
| 联系我们。拉瓦连络我们 | 用于 RawContact 数据库的数据 |
| 联系人合同。设置列 | 用于联系人操作系统设置的数据 |
| 联络人协定. status column | 用于社会地位更新的数据 |
| 联系我们。streamitemphotoscolumns | StreamItemPhotos 表中的列 |
| 联系人联系人。StreamItemsColumns | StreamItems 表中的数据列 |
| 联系人联系人。同步列 | 当表的每一行属于特定帐户时出现的列 |
接下来,我们将了解弃用的概念,因为它与 Android 数据库有关,因为你们中的一些人可能需要使用旧的 Contacts 数据结构来支持 Android 2.0,甚至 1.5 和 1.6 的古代用户。
过时的内容提供者:过时的数据库结构
作为一名 Android(以及任何其他类型,就此而言)开发者,你需要高度意识到弃用的概念。正如您现在可能已经知道的,当编程语言(以及操作系统)中的特性(包括类、方法、接口、常量,或者在这种情况下的数据库)已经停止使用,以支持新的特性和编程环境的特性集(以及随后的开发能力)的长期发展时,就会发生弃用。
Contacts 数据库是弃用的一个很好的例子,所以我将在它自己的部分中讨论这个主题,只是为了确保您理解在您的编程工作过程中容纳弃用的特性是多么重要。
如果您在上一节中查看了 Android Provider 包的链接,那么在查看所有这些信息时,您可能已经注意到,在 API Level 5 之后,原始的 Android Contacts 数据库结构被彻底修改了。
在 API 5 之后,组成原始 Android Contacts 数据库结构的九个数据库被替换为一个新的数据库结构,称为 ContactsContract ,我们在上一节中已经介绍过。
请注意,如果您的 Android 应用操作系统支持足够早,您仍然可以使用这些数据库,因为 API Level 5 相当于 Android 2。在 Android 开发者网站上有一个非常有用的部分,叫做仪表盘。这向您显示了的 API 级别、的操作系统版本,甚至每个版本的当前市场份额的百分比,所以请务必在有空时查看一下,它位于以下 URL:
http://developer.android.com/about/dashboards/index.html
这一切意味着,如果您支持任何早于 Android 2.1 的 Android 设备,即任何 Android 2.0、1.6 或 1.5 设备,那么您将需要自动检测您的最终用户的操作系统版本,并提供使用 9 个联系人数据库而不是 21 个联系人数据库的代码。
如果您查看前一个链接中的仪表板,您还会注意到这种支持只占当前市场份额的 0 . 2 %,这很可能不值得花费编码时间和精力!
内容提供者访问:在清单中添加权限
其余的 Android 内容供应器信息我们将在 Hello World 项目的上下文中学习,因为这更有趣、更高效,并且让我们在 Eclipse 中工作,而不仅仅是阅读一本书。
在 Eclipse 中打开你的 Hello_World Android 项目,右键单击并打开 AndroidManifest.xml 文件,该文件位于你的包浏览器窗格的底部附近,如图 19-1 中突出显示的所示。在中央 XML 编辑区点击底部的权限标签,打开 Android 清单权限可视化编辑器,如图图 19-1 所示,然后我们添加权限。
图 19-1 。使用 Eclipse 权限编辑器的选项卡来访问 Android 清单权限编辑器
点击 Add… 按钮打开如图图 19-2 所示的对话框,允许您选择要添加到 AndroidManifest.xml 文件中的权限标签的类型。选择使用权限选项,点击确定按钮关闭对话框,返回权限编辑器。
图 19-2 。权限类型为的添加权限对话框
现在我们已经有了显示在权限栏中的使用权限标签,如图图 19-3 所示,我们想使用右侧使用权限栏的属性来配置使用权限标签。找到名称下拉菜单,点击向下箭头,找到并选择Android . permission . read _ CONTACTS常量,如图图 19-3 所示。
图 19-3 。通过下拉菜单添加 READ_CONTACTS 使用权限属性到使用权限标签
一旦您选择了Android . permission . read _ CONTACTS常量,您将会看到屏幕左侧权限栏中的条目将会发生变化,以反映 <用途-权限> 标签的新名称参数。通过单击中央 xml 编辑窗格底部的 AndroidManifest.xml 选项卡,您可以随时看到为您编写的 XML 代码。
我们刚刚添加的 READ_CONTACTS 权限允许我们对 Android CONTACTS 数据库执行读取访问,您可能已经猜到了。
对数据库表的读操作在业内被称为非破坏性数据库访问操作。这是因为读操作不会改变数据库中的任何数据,它只是“查看”数据(读取数据),并可能获取读取的数据并将其显示在用户界面中,这取决于所涉及的 Java 代码。
接下来,我们想要添加一个 WRITE_CONTACTS 数据库权限,所以再次点击 Add… 按钮,并从我们在图 19-2 中看到的同一个对话框中选择另一个****标签。一旦你点击 OK 按钮,你会看到一个使用权限标签出现在权限栏中,就在 Android . Permission . read _ CONTACTS 的正下方,如图图 19-4 所示。
图 19-4 。通过名称下拉菜单添加第二个 WRITE_CONTACTS 使用权限属性
接下来下拉名称菜单,找到一个Android . permission . write _ CONTACTS参数,选择它作为你的第二个< uses-permission >标签。要查看为你编写的 XML 代码,点击如图图 19-5 所示的 AndroidManifest.xml 选项卡。
图 19-5 。AndroidManifest.xml 文件标签显示了两个使用许可标签的 xml 标记
需要注意的是,要使权限列从使用权限图标更新到 WRITE_CONTACTS 参数,您必须点击添加…按钮(如果您正在添加更多权限),或者点击 AndroidManifest.xml 选项卡,然后再次点击权限选项卡以刷新权限编辑器可视编辑器,并在左侧的权限列中显示正确的条目,如图图 19-6 所示。
图 19-6 。Eclipse Android 清单权限可视化编辑器显示 twp 数据库访问权限
现在我们已经设置了对联系人数据库的读写权限,我们可以创建我们的 AlienContact.java 活动子类,它将保存我们的用户界面设计和我们的 Make Alien Contact 功能。
内容提供者活动:创建 AlienContact 类
让我们在 Hello_World Android 应用中创建我们的第十个(也是最后一个) Java 类,右键单击项目源(/src)文件夹,选择 New Class 菜单序列,在 Eclipse 中调出熟悉的 New Java Class helper 对话框,如图图 19-7 所示。
图 19-7 。创建一个名为 AlienContact.java 的新 Java 类,其超类类型为 android.app.Activity
使用您当前的 Android 项目信息,源文件夹和包数据字段应该已经为您填写好了,所以让我们将这个新的 Java 类命名为 AlienContact ,因为它将使用 Contacts 数据库来允许我们在我们遇到的任何新世界中跟踪我们的外星人联系人。
接下来单击超类字段旁边的 Browse… 按钮,这样我们就可以为我们的 AlienContact.java 活动子类指定一个活动超类类型。一旦超类选择对话框出现,你可以在顶部的选择类型字段中键入一个一个字符,然后在对话框的匹配项部分找到 Activity - android.app 条目。
一旦你点击 OK 按钮,你的超类字段应该被设置为 android.app.Activity 包类,它在你的新 Java 类中实现了所有这些 Android Activity 特性。设置完成后,您可以单击完成按钮在 Eclipse 中生成代码。
接下来我们将添加我们的。 onCreate( ) 方法创建活动的基本代码,并使用**。setContentView( )** 方法来定义一个新的 activity_contact.xml 用户界面 xml 定义我们将编写来定义我们的活动屏幕。
我们还将在我们的 AndroidManifest.xml 文件中定义我们的活动,并给它一个 android:label 参数,以便它有一个活动屏幕标题。
内容提供者活动:准备 AlienContact 类
添加您常用的**受保护的 void onCreate(Bundle savedInstanceState)方法,并调用super . onCreate(savedInstanceState);**正如我们在前面章节中所做的那样,然后还添加一个引用 activity_contact.xml 文件的 setContentView()方法,我们接下来将创建该文件。代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contact);
}
新的 AlienContact.java 活动子类和 onCreate()方法如图图 19-8 所示。请注意,对我们的 activity_contact.xml UI 定义的引用(我们接下来将创建它)以红色错误突出显示。因为我们知道这个引用很快就会被定义并放置到位,所以我们现在可以安全地忽略 Eclipse 中 Java 代码中的这个错误。
图 19-8 。创建 onCreate()方法并引用 activity_contact.xml UI 布局容器定义
接下来,我们需要在我们的 Android Manifest XML 文件中定义我们的 AlienContact 活动,使用一个 < activity > 标签和 android:label 参数,就像我们之前做的那样给我们的 UI 屏幕一个标题,如图图 19-9 所示。下面是我们将添加到 AndroidManifest.xml 文件底部的 XML 标记:
<activity android:name="chapter.two.hello_world.AlienContact"
android:label="@string/activity_title_alien_contact" />
图 19-9 。添加 XML 标记,向我们的 AndroidManifest.xml 添加一个新的 AlienContact 活动子类
现在,我们准备创建我们的活动用户界面屏幕 XML 文件,我们将按照 Android 惯例将其命名为 activity_contact.xml 。
内容供应器用户界面:创建 activity_contact.xml
在 Eclipse 的 Package Explorer 窗格中右键单击项目层次结构中的/res/layout 文件夹,并选择 New Android XML File 菜单序列。这将启动新的 Android XML 文件助手对话框,如图图 19-10 所示。
图 19-10 。使用一个新的 Android XML 文件对话框来创建我们的 activity_contact.xml 文件
选择布局的资源类型和 Hello_World 的项目设置,然后将文件命名为 activity_contact ,最后为你的用户界面设计容器选择根元素类型 LinearLayout 。
接下来我们需要做的是使用标签向 strings.xml 文件添加一些字符串常量。首先,让我们使用以下 XML 标记添加活动屏幕标题(标签)字符串标记:
<string name="activity_title_alien_contact">Hello World - Alien Contacts</string>
接下来,让我们添加三个外来的联系人 UI 按钮标签,我们需要在我们接下来要编写的 XML UI 标记中引用它们。这些将按如下方式编写,并显示在图 19-11 的顶部。
<string name="find_alien_button_value">List Aliens in this Galaxy</string>
<string name="add_spock_button_value">Add Spock to my Alliance</string>
<string name="add_worf_button_value">Add Worf to my Alliance</string>
图 19-11 。在 strings.xml 文件中添加活动屏幕标题和用户界面按钮元素 <字符串>标签
接下来,我们需要在父 LinearLayout 容器标记中添加 Button 标记元素,并向 LinearLayout 添加参数以添加背景空间图像,这样我们的用户界面看起来更专业,并与我们迄今为止使用的应用设计的其余部分相匹配。
第一个用户界面按钮标签利用了一个设置为 findAliens 的 android:id 参数,其文本颜色为黄色或 #FFFFAA ,并引用了我们之前创建的字符串常量。
此外,我们利用一个设置为 40dp 的 android:layout_marginTop 参数来将我们的 UI 元素从 UI 屏幕设计的顶部向下推一点,并利用一个设置为 center 的 android:layout_gravity 参数来使我们的 UI 元素很好地位于屏幕设计的中间。
最后,确保将默认的 android:layout_width 和 height 参数设置为 wrap_content 。您的按钮 XML 如下所示:
<Button android:id="@+id/findAliens"
android:text="@string/find_alien_button_value"
android:textColor="#FFFFAA"
android:layout_marginTop="40dp"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
第一个用户界面按钮元素,以及接下来的三个,我们将使用复制和粘贴操作来生成,以快速创建该用户界面设计的 XML 标记的剩余部分,如图图 19-12 所示。一旦我们复制了这个 XML 定义,并在它下面粘贴了三次,我们就可以修改参数,为我们的按钮创建新的 UI 元素标签,这些标签将添加外星人并退出活动。
图 19-12 。编写 XML 标记,将四个按钮用户界面元素添加到我们的 LinearLayout 容器中
让我们现在就这样做;将 findAliens (ID)按钮标签再复制粘贴三次到其下,并将 android:id 参数从 findAliens 分别更改为 addSpock 、 addWorf 和 returnHome 。
接下来,将 android:layout_marginTop 参数更改为用于 addSpock 和 addWorf 按钮标签的 25dp ,用于 returnHome 按钮的 60dp 。
我们将把 android:textColor 参数设置为 #FFFFAA 或黄色,将 android:layout_gravity 参数设置为 center ,最后,将 android:layout_width 和 height 参数设置为 wrap_content ,这样我们所有的按钮用户界面元素都被统一格式化。
接下来,让我们使用 XML 编辑窗格底部的图形布局编辑器选项卡,看看我们的新用户界面设计是什么样子的。因为我们编辑的最后一个按钮元素是 returnHome 按钮,所以它应该在视图中被选中,因为它是 XML 文本编辑选项卡中光标所在的位置。
视觉效果可以在图 19-13 中看到,我们的设计看起来相当专业,并且与我们的其他五个活动屏幕设计匹配良好。这意味着我们已经完成了 XML 用户界面设计标记,我们可以继续编写 Java 代码来实现用户界面设计的功能。我们将使用按钮对象及其事件处理方法,这些方法最终将包含我们的 Java 代码,这些代码将访问存储在 Android 操作系统中的联系人数据库结构。
图 19-13 。在 Eclipse 图形布局编辑器中预览我们的 activity_contact.xml 用户界面设计
接下来,让我们回到 Eclipse 中的 AlienContacts.java 编辑选项卡,添加代码行来实例化每个按钮对象,然后将事件处理逻辑附加到它,这样我们就有了一个框架来触发我们的数据库操作。
在 AlienContact 类中编码用户界面元素
首先,我们需要为对象创建和事件处理编写第一个 aliensButton Button 对象 Java 代码,然后我们可以在其下复制三次,并创建其他三个用户界面按钮。
完成此任务的 Java 代码如图 19-14 所示,并已被复制,以使用以下 Java 代码块为我们的 aliensButton、spockButton、worfButton 和 homeButton 用户界面元素创建按钮对象:
Button aliensButton = (Button)findViewById(R.id.findAliens);
aliensButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
}
图 19-14 。为我们的按钮 UI 元素及其 onClick()事件处理方法添加 Java 代码
我们在 onClick()事件处理代码块中使用了 finish()方法调用,这样每个按钮都有一个功能(返回到主屏幕),直到我们在本章后面用数据库相关代码替换它。这使得测试 Java 代码变得更加容易,并且不会出错。
如果您愿意,您可以使用您的运行 Android 应用工作流程来启动 Nexus S 模拟器,并查看其用户界面屏幕,点击每个按钮返回应用主屏幕。
接下来,我们将编写 aliensButton 对象的代码,以便它调用一个 listAliens( ) 方法,该方法使用 Toast 类列出我们星系中的所有外星人,以便所有外星人的名字都广播到我们的用户界面屏幕上。
使用 ContentResolver:编写 listAliens()方法
我们需要做的第一件事是用 listAliens()方法调用替换 aliensButton 事件处理代码块中的 finish()方法调用。当你这样做时,Eclipse 会使用红色波浪下划线来突出这个方法不存在的事实,如图 19-15 所示。
图 19-15 。在 type 'AlienContact' helper 选项中添加 listAliens()方法调用和 Create 方法' list aliens()'
图 19-15 中还显示了当你的鼠标停留在这个突出显示的错误上时出现的帮助器对话框,以及当你选择第二个名为**的选项时 Eclipse 为你编写的空方法,在类型‘alien contact’**中创建方法‘find aliens()’,瞧!看看你的编辑屏幕的底部:空的 listAliens( ) 方法出现了!
现在是时候编写第一个与数据库相关的 Java 代码来访问 Contacts 数据库了,我们已经请求允许在 Android Manifest XML 文件中使用该数据库。在这种情况下,我们最初将使用 READ_CONTACTS 权限,因为我们将读取联系人数据库,然后在用户界面屏幕上列出我们银河联盟中的所有外星人。
首先,我们使用 Android Cursor 类,用于在数据库表中导航,并创建一个名为 AlienCur 的游标对象(我很想将该对象命名为 alienCurse,但没有这样做)。在同一行代码中,我们可以使用下面一行 Java 代码,通过使用 getContentResolver( ) 方法,用我们想要遍历的数据库表加载这个游标对象:
Cursor alienCur = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
这调用了 ContentResolver 类 getContentResolver()。query( ) 方法,该方法接受您想要查询的数据库 URI 的参数,以及投影、选择、参数和排序顺序。因为我们只是通读整个 Contacts 数据库,所以我们不打算使用任何其他数据库访问说明符参数,因此我们在该参数槽中使用了空值和空值(作为一个未使用的指示器)。
现在我们已经创建了 alienCur 游标对象,并加载了我们希望它遍历和读取的数据库内容,我们可以构造一个 Java while 语句来读取这个数据库表中的所有记录(字段)。因为我们希望光标指向**。moveToNext( )** 或者在 while 循环仍然有效时移动到下一条记录,我们从以下内容开始:
while (alienCur.moveToNext()) { our while loop processing Java code goes in here }
在 while 循环内部,我们有两个 Java 语句,它们在 while 循环运行期间处理数据库内容(有效)。这是由 while 循环的条件定义的,该条件指定 while 循环的内容应该在该循环仍然可以移动到 Next 时执行(此时数据库表中还有另一个记录要读取)。
第一行代码创建一个名为 alienName 的字符串对象来保存我们将要读取的数据,然后将它设置为使用我们在第一行代码中创建的游标对象从数据库表中实际读取数据的操作。完成此任务的 Java 代码如下:
String alienName =
alienCur.getString(alienCur.getColumnIndex (ContactsContract.Contacts.DISPLAY_NAME_PRIMARY));
我们将我们的 alienName String 对象设置为来自的结果。getString( ) 方法,在 alienCur 光标对象上调用,该对象从获取其结果。在 alienCur 游标对象上调用的 getColumnIndex( ) 方法。
如您所见,赋予光标对象的参数。getColumnIndex()方法调用指定主显示名称数据库记录中保存的数据列(字段)。这通过使用 DISPLAY_NAME_PRIMARY 常量来引用,该常量本身通过Contacts contact数据库来引用,并且位于 Contacts 表中。如图 19-16 中的所示。
图 19-16 。编写使用 getContentResolver()访问 ContactContracts 数据库的 listAliens()方法
现在我们有了想要显示的数据,以我们需要的字符串格式,我们可以简单地调用 Toast 对象,通过它的。makeText()方法,并将数据显示到新的用户界面屏幕上。
我们将通过使用下面一行熟悉的 Java 代码来做到这一点,这些代码链接了和**。makeText( )** 和**。show( )** 方法一个接一个地关闭 Toast 对象。像这样:
Toast.makeText(this, alienName, Toast.LENGTH_SHORT).show();
进入结束 while 循环定义的右括号后,我们最终需要关闭(从系统内存中移除光标对象)alienCur 光标对象,它当前保存着我们的数据库数据。
顺便说一下,这个 Cursor 对象可能会占用大量的系统内存,所以当我们完成时,使用 Cursor 类的非常重要。close( ) 方法将我们已经定义的光标对象清除出系统内存。
这是通过调用 alienCur 对象的一个简单方法来完成的。这是使用 Java 点符号完成的,如以下 Java 代码所示:
alienCur.close();
现在,我们准备将外星人添加到我们的 Android 模拟器的联系人数据库中,这样我们就可以测试我们刚刚在前面几节中编写的权限、Java 代码和 XML 标记。
向 ContactsContract 数据库添加外来联系人
假设您仍然打开 Eclipse,通过使用窗口 Android 虚拟设备管理器菜单序列启动 Android 模拟器。该工作流程启动 Nexus S 模拟器,而不需要模拟器自动加载和运行 Hello World 应用。
这里还需要注意的是,如果由于某种原因一个作为 Android 应用运行的工作进程由于某种原因没有启动 Android 的仿真器功能,那这就是用的方式硬启动Android 仿真器。
当 Android 虚拟设备管理器对话框出现时,从我们在本书前面设置的模拟器列表中选择 Nexus S 模拟器选项,并点击开始… 按钮。
当启动选项对话框出现时,取消从快照启动,以及保存到快照选项,点击启动按钮。
这将打开启动 Android 模拟器对话框以及一个进度条,显示模拟器正在加载和启动。一旦它启动,你就可以关闭 Android 虚拟设备管理器对话框。
当 Nexus S 模拟器启动时,您将看到一个智能手机开始屏幕模拟器,如截图左侧的图 19-17 所示。
图 19-17 。使用联系人数据库图标(左下方第二个图标)创建新联系人
管理联系人数据库的工具非常重要,它就在智能手机开始屏幕的左下角,由蓝色联系人表图标模拟中的一个小对话头指示。
点击此图标启动仿真器的联系人管理实用程序,该程序显示在图 19-17 的右侧。
点击顶部按钮在您的智能手机(模拟器)联系人数据库中创建一个新的联系人。这将弹出一个对话框,通知您由于您使用的是模拟器,您将要输入的新联系信息将不会被备份,因为您使用的不是真正的电话。
如果你想使用模拟器创建真实的联系人数据库信息,可以通过真实的 Android 智能手机帐户访问,对话框会询问你是否要添加一个帐户来备份你在模拟器上在线输入的联系人。
因为我们只是将它用作本章中学习的联系人数据库代码的测试应用,所以我们将选择左边的按钮选项“ Keep Local,”,这样我们输入的信息就存储在我们工作站的硬盘驱动器上,而不是像以前那样“在云中”。
该保持本地对话框如图 19-18 所示,一旦我们选择保持本地选项,我们将看到联系人管理用户界面。
图 19-18 。保持我们的联系人数据库在本地,并在数据库中添加一个名为 Goran Agar 的新外星人
一旦联系人管理界面出现,如图 19-18 右侧所示,我们将输入我们的第一个外星人名字,戈兰·阿加尔,杰姆·哈达尔种族之一,出现在流行的星际迷航系列中。
一旦在位于用户界面屏幕顶部的深蓝色线上输入 Goran Agar 的名称,点击位于文本输入字段右上方和左侧的浅蓝色 Done 按钮,如图图 19-18 所示。
要查看您刚刚在联系人数据库中输入的条目,单击屏幕顶部的向左箭头,您将被带到联系人数据库列表屏幕,如左侧的图 19-19 所示。
图 19-19 。查看添加的外星人联系人,并重复添加库达克·艾坦和雷玛塔·柯兰的工作过程
在联系人数据库列表屏幕的右下角,在这个时间点上,只显示了我们的外星朋友 Goran Agar,如果你仔细看,你会看到一个方形的灰色头像图标,旁边有一个+符号。这允许你添加另一个联系人到你的联系人数据库,所以现在点击这个图标,我们将添加更多的外星人到我们的银河联盟。
添加另外两个来自 Jem Hadar 种族的外星人,名为 Remata Klan 和 Kudak Etan,使用与前面概述的相同的工作流程。一旦你这样做了,联系人数据库列表屏幕应该看起来像在图 19-19 中的截图的右边。
现在,我们的外星人联系人数据库中有足够多的外星人来测试我们的应用!
在测试我们的 AlienContact.java 活动之前,我们需要能够从我们的主屏幕选项菜单访问它,所以让我们接下来这样做,以便我们可以测试我们的 listAliens()方法,现在我们已经有了(Alien)联系人数据库中的数据来这样做。
将 AlienContact 活动添加到主屏幕菜单
要添加新的选项菜单项,我们需要做的第一件事是在 strings.xml 文件中为菜单标签创建一个常量。
按照我们其他菜单字符串常量的格式,让我们创建一个名为< string >标签的 menu_contact_planet ,菜单标签值为 Make Alien Contact ,并将其放在我们其他菜单字符串常量下的 strings.xml 文件中,如图图 19-20 所示。您的<字符串>标记的 XML 标记应该如下所示:
<string name="menu_contact_planet">Make Alien Contact</string>
图 19-20 。将名为 menu_contact_planet 的第五个菜单项标签 Make Alien Contact 添加到我们的 strings.xml 中
完成后,我们将准备好更改 activity_main.xml 文件中的占位符第五个菜单项,该文件位于 /res/menu 子文件夹中,带有“创建外国人联系人”菜单选项,该选项将调用我们的 AlienContact.java 活动子类。记住我们的应用有两个 activity_main.xml 文件,一个在用于 UI 的**/布局中,一个在用于菜单设计的/菜单**中。
右键单击 /res/menu 文件夹中的 activity_main.xml 文件,选择打开选项(或者在包浏览器中选择文件名,然后点击 F3 )。编辑文件中最后一个占位符项,使 android:id 设置为 menu_contact ,编辑 android:title 参数引用**@ string/menu _ contact _ planet**,如图图 19-21 所示。
图 19-21 。将我们的第五个菜单<项目>占位符标签更改为 menu_contact 以访问创建外星人联系人
因为我们的 onCreateOptionsMenu( ) 方法已经扩展了我们的菜单 XML 文件定义,所以我们接下来逻辑上需要做的是修改我们的 onOptionsItemsSelected( ) 方法,并添加一个 case 语句,它处理 menu_contact ID 并启动我们的AlienContact.java活动子类。
在 Eclipse central 编辑窗格中打开您的 MainActivity.java 活动,并关闭除 onOptionsItemSelected()之外的所有方法,如图 19-22 所示。然后,添加一个 case R.id.menu_contact: 语句,创建一个 intent_contact Intent 对象,并将其设置为调用新的 AlienContact 类。最后,调用**。startActivity( )** 方法,使用这个 Intent 对象,然后使用**break;**退出本节的 case 语句。
图 19-22 。在 switch()方法 case 语句中添加 menu_contact 条目,打开 AlienContact 活动
第五个菜单项 case 语句代码应如下所示:
case R.id.menu_contact:
Intent intent_contact = new Intent(this, AlienContact.class);
this.startActivity(intent_contact);
break;
既然我们已经使我们的用户(和我们自己)能够启动 AlienContact.java Activity 子类,我们可以测试我们的 activity_contact.xml 用户界面设计和实现它的 Java 代码,以及将读取我们在上一节中刚刚创建的 Alien Contacts 数据库条目的 ContentResolver。
右键单击你的 Hello_World 项目文件夹,然后选择 Run As Android Application 菜单序列,或者,如果你喜欢,利用窗口 Android 虚拟设备管理器工作进程,我们在本章前面使用它来启动模拟器。
测试 AlienContact 活动中的 listAliens()方法
一旦 Nexus S 模拟器启动,找到 Hello_World 图标(土星)并启动应用。如果您使用了“作为 Android 应用运行”菜单序列,这将为您自动完成。
一旦 Hello_World 主屏幕出现,点击模拟器右上角的菜单按钮,调出我们刚刚创建的选项菜单,如图 19-23 左侧所示。
图 19-23 。选择创建外国人联系人菜单项,启动外国人联系人活动测试返回主页按钮
选择菜单底部的创建外来联系人菜单项,打开图 19-23 右侧显示的外来联系人活动用户界面屏幕。
单击“返回主页”按钮以及“添加 Spock”和“添加 Worf”按钮,并确保它们调用 finish()方法,将您带回 Hello_World 应用的主页屏幕。
一旦你完成了测试,再次点击模拟器中的菜单按钮,进入 AlienContact.java 活动子类,这样我们就可以测试列出这个星系中的外星人按钮,并确保它遍历我们的外星人联系数据库,使用光标对象,并列出精英杰姆·哈达尔星系间种族的所有外星人联系。
当你点击图 19-24 左侧所示的“列出这个星系中的外星人”按钮时,你会看到在用户界面屏幕的底部出现 Toast 消息,列出了你的外星人联系数据库中的所有三个 Jem Hadar 种族成员。
图 19-24 。使用列出银河系中的外星人按钮及其 Toast 消息测试列出外星人方法
Goran Agar 的 Toast 消息显示在图 19-24 中截图的右下方,因此我们的 ContentResolver Java 代码运行良好。接下来,让我们编写另一个名为 addToAlliance( ) 的 Java 方法,它向我们展示了如何使用 Android ContentValues 类,通过使用我们自己的定制 Java 代码,而不是必须使用 Android 操作系统的联系人管理实用程序,来使我们能够向我们的联盟数据库添加新的外星人联系人。
Android ContentValues:编写一个 addToAlliance()方法
打开 Eclipse 中的 AlienContact.java 编辑选项卡,用对我们将要编写的新 addToAlliance( ) 方法的方法调用替换 onClick()事件处理程序中对 spockButton 和 worfButton 代码块的 finish()方法调用。该方法调用的格式传递一个类似于 addToAlliance(字符串值)的字符串参数,因此 Java 代码行如下所示:
addToAlliance("Spock");
一旦为 Spock 和 Worf 添加了 addToAlliance()方法调用,我们就需要编写**protected void addto alliance(String new alien)**方法。
我们在 addToAlliance()方法中做的第一件事是使用 Java new 关键字构造一个名为 alienContact 的 ContentValues 对象,使用下面一行 Java 代码:
ContentValues alienContact = new ContentValues();
之后,我们可以用传递到方法中的 newAlien 参数字符串数据加载 alienContact ContentValues 对象。我们通过使用来做到这一点。put( ) 方法,从 alienContact ContentValues 对象调用,第一个参数指示我们希望将数据放在哪里,第二个参数是要放入该字段的数据本身。这显示在屏幕底部附近的图 19-25 中。
图 19-25 。编写我们的 addToAlliance()方法来访问 ContentValues 类,通过它来添加外星人。放( )
如您所见,这是通过使用下面两行 Java 代码为我们希望在 ContentValues 对象中指定的数据库表 RawContacts 中的 ACCOUNT_NAME 和 ACCOUNT_TYPE 列(数据字段)完成的:
alienContact.put(RawContacts.ACCOUNT_NAME, newAlien);
alienContact.put(RawContacts.ACCOUNT_TYPE, newAlien);
然后我们创建一个名为 addUri 的 Uri 对象,并将其设置为等于一个getcontentresolver . insert()方法调用的结果,使用数据库要写入的参数(插入数据)和要写入的数据。这是 RawContacts 数据库,使用 RawContacts 指定。CONTENT_URI 常量和alien contactCONTENT values 对象,现在加载了我们的 newAlien 字符串数据。这是使用以下单行 Java 代码完成的:
Uri addUri = getContentResolver().insert(RawContacts.CONTENT_URI, alienContact);
接下来我们创建一个名为 rawContactId 的 long 变量,并将其设置为等于一个content URIs . parseid(addUri)方法调用的结果,这将把 addUri Uri 对象转换为与兼容的 long 值格式。put( ) 我们即将调用的方法。这是使用以下单行 Java 代码完成的:
long rawContactId = ContentUris.parseId(addUri);
请记住,当您键入这个新的数据库写操作 Java 代码时,您调用或使用的任何 Android 类,比如尚未导入的 ContentValues、ContentUris 或 ContentResolver,也就是说,在类的顶部为它编写的 import 语句(正如您已经看到的,Eclipse 将很容易为您完成)将在有问题的方法调用下面用红色波浪线突出显示。
既然我们在变量 rawContactId 中有了正确(长)数字格式的数据库 ID,我们现在可以使用**。clear( )** 方法来清除我们的 alienContact ContentValues 对象,以便我们可以再次使用它,将更多的数据值写入与 Android Contacts 数据库相关的其他数据库中。这一行 Java 代码应该如下所示:
alienContact.clear();
这就是 Android 中数据库编码难的原因;正如我在本章开始时提到的,与用于访问(或写入)数据的 Java 代码相比,它更多地涉及如何建立数据库层次结构的复杂性。您将在这里看到这一点,因为我们将讨论如何使用 rawContactId 将您写入 RawContacts 数据库表的相同数据写入相关的 data 和 StructuredName 数据库表。
首先,我们需要将 rawContactId 值放入数据中。RAW_CONTACT_ID 数据库表,使用。put()方法,只需一行 Java 代码:
alienContact.put(Data.RAW_CONTACT_ID, rawContactId);
然后,我们需要使用下面一行 Java 代码,将 StructuredName 数据库表的 CONTENT_ITEM_TYPE 数据列中的数据值放入 data 数据库表的 MIME_TYPE 数据列中:
alienContact.put(Data.MIME_TYPE, StructuredName.CONTENT_ITEM_TYPE);
然后我们需要将我们的 newAlien String 对象(别名)放入 StructuredName 数据库表的 DISPLAY_NAME 数据列中,如下所示:
alienContact.put(StructuredName.DISPLAY_NAME, newAlien);
最后,我们可以使用我们在过去几行代码中构建的 CONTENT_URI 和alien contactCONTENT values 对象,将所有这些与我们已经建立的 RawContacts 数据库相关的数据插入到数据数据库表中。我们将再次通过使用我们可信的 getContentResolver()来实现这一点。insert( ) 方法调用,使用下面的 Java 代码:
getContentResolver.insert(Data.CONTENT_URI, alienContact);
现在,我们准备向最终用户敬酒,并告诉他们我们刚刚做了什么,以便他们知道 newAlien 已经作为新联盟成员添加到 RawContacts(和 Data)数据库表中。这是使用下面一行代码完成的,它将两个方法链接在一起:
Toast.makeText(this, "New Alliance Member: " + newAlien, Toast.LENGTH_SHORT).show();
让我们通过 Android Nexus S 模拟器中的 AlienContact.java 活动来测试我们的新 addToAlliance( ) Java 方法,看看我们的新联盟成员 Spock 和 Worf 是否被添加到外星人联系人数据库中。
启动 Nexus S 模拟器,当 Hello_World 主屏幕出现时,点击菜单按钮,选择与外星人联系菜单选项。
一旦活动屏幕出现,点击你的添加史巴克到我的联盟和添加武夫到我的联盟按钮,并确保吐司消息出现在用户界面屏幕的底部。目前看起来很棒!
现在,我们剩下要做的就是确认外星人的联系人数据确实已经正确地添加到 Android 联系人数据库中,就像我们已经使用之前使用的联系人管理实用程序完成的一样。
如果我们正确编写了数据库代码,那么当我们下一次输入 Android 联系人管理实用程序时,我们的新外星人联系人数据将会出现在该实用程序中,就像我们使用该实用程序直接添加它一样。如果我们没有正确地编写数据库结构,数据就不会出现在那里。
退出 Hello_World 应用,返回到 Smartphone 模拟器主屏幕,并像我们在本章前面所做的那样,单击屏幕左下角的联系人管理图标。当你的(外星人)联系人列表出现时,当你向下滚动时,你会看到斯波克和沃夫现在出现在你的联系人数据库列表中,并且按字母顺序排列正确!
请注意,如果您愿意,您也可以使用应用中的“列出外星人”按钮进行测试,以确保这两个外星人也已添加。
摘要
在这最后一章中,我们仔细研究了 Android 操作系统中最复杂和详细的领域之一:数据访问、内容提供者和 SQL 数据库管理系统。这是一个确实需要单独的书来讨论的话题,所以,为你出色完成的学习工作而自我表扬一下吧。
我们首先看了一下数据库术语和基础知识,这样我们就在同一页上了。我们学习了 SQL 和数据库表、数据行(记录)和数据列(字段),以及如何通过 ID、键或索引访问数据。
然后我们看了一下开源 MySQL 数据库技术,以及这个开源 DBMS 引擎的内存高效版本,它是 Android 操作系统 SQLite 的一部分。我们学习了 SQLite 数据类型以及在 Android 操作系统中使用 SQLite 的注意事项。
接下来,我们概述了 Android 内容供应器和 Android 内容解析器,以及 Android 操作系统可以访问存储数据的不同位置,包括首选项、内存、SD 卡、内部文件系统、内部 SQLite 数据库和远程数据服务器。
然后,我们看了看如何使用内容 URI 访问数据库表中的数据。我们查看了内容 URI 结构的不同部分,将一些样本 URIs 放在一起,最后看一下在 Android 操作系统中使用内容 URI 的约定。
接下来,我们看了一下最著名的内容供应器 Java 接口,这些接口是 Android 操作系统的一部分,用于用户经常使用的联系人、日历和媒体商店功能。我们列出了这些数据库的各种子表,然后查看了 Android 2.1 之前使用的废弃数据库。
最后,是时候为我们自己的 Hello World Android 应用添加内容供应器功能了。我们使用 Eclipse 中的可视化权限编辑器向 AndroidManifest.xml 文件添加了权限,然后创建了一个 AlienContact.java 活动和所有字符串常量、用户界面设计、菜单设计和方法,这些都是实现对内置 Android Contacts 数据库结构进行读写所需要的。
最后,我们学习了 Android 联系人管理实用程序,这样我们就可以测试我们的数据库代码,并创建读取我们的外星人联系人数据库并将新的外星人写入我们的外星人联系人数据库的方法。
恭喜你,你已经完成了对 Android 操作系统及其许多关键特性、包、方法和类的全面介绍。现在,请确保并实践您在这里学到的知识,并通过查看 Android 开发者网站上的所有最新信息来建立这些知识。