安卓-5-编程示例-全-
安卓 5 编程示例(全)
原文:
zh.annas-archive.org/md5/40c894f611f940eae635275b8cae4f95译者:飞龙
前言
欢迎阅读 Android 5 编程实例指南,这是一本逐步指导如何为 Lollipop 平台开发 Android 移动应用程序的指南。本书将带您了解开发环境的安装和配置,包括设计、编码和测试过程,以及如何发布和盈利您的移动应用程序。
在此过程中,我们将开发一系列小型、实用的应用程序,旨在介绍所有熟悉的移动应用程序功能,如地图和触摸屏事件。我们将详细讲解 Android 5 及以上版本的新特性,特别是新的设计语言 Material Design。
为了实现所有这些,您将使用 Android Studio 集成开发环境(IDE)和最新的软件开发工具包(SDK)。为了测试和调试应用程序,您将能够使用连接到计算机的实体设备,并创建模拟 Android 设备的虚拟设备。所有这些软件都是开源的,可以免费下载和使用。
Android 平台不再仅限于手机和平板电脑,本书后面将简要介绍 Android Wear、TV 和 Auto,学习如何适应和扩展现有应用程序,并创建专门为这些形态设计的应用程序。
从逻辑上讲,本书以如何注册为 Google 开发者并发布您的应用程序的详细说明作为结尾。为了达到尽可能多的用户,您将了解如何使 Android 5 应用程序向后兼容,并在旧版本中包含 Lollipop 功能。最后,还将介绍如何在应用程序中包含内购和广告,到那时,您将完全准备好开发并分发您自己的应用程序。
本书涵盖内容
第一章, 设置开发环境,带您了解 SDK、Android Studio 和设备模拟器的安装和配置。
第二章, 构建用户界面,帮助您将 Material 主题应用到纵向和横向用户界面。
第三章, 活动和碎片,带您了解如何创建活动和协同工作的布局。
第四章, 管理 RecyclerView 及其数据,帮助您使用 Material Design 的 CardView 和 RecyclerView 来显示数据。
第五章, 检测触摸屏手势,让您能够应用触摸监听器和手势监听器来检测触摸屏事件。
第六章, 通知和操作栏,让您能够使用 Android 最新的锁屏功能发布推送通知。
第七章, 地图、位置和谷歌服务,涵盖地图并帮助您检测设备位置。
第八章,电视、汽车和可穿戴设备应用,帮助您使用专为这三种新形式因素设计的 API 开发应用。
第九章,相机、视频和多媒体,让您可以使用原生应用和媒体录制小部件和类来捕获、存储和播放图像、视频和声音。
第十章,发布和营销,涵盖了在 Google Play Store 上发布应用以及通过应用内购买和广告进行货币化的流程。
您需要这本书的内容
您需要这本书的所有内容是一个至少有 4 GB RAM 的 PC(尽管 8 GB 更佳),以及互联网连接。本书所需的所有软件都是开源的,并且可以免费下载。
这本书面向的对象
如果您有一个关于移动应用的伟大想法,并且对诸如 Java 之类的程序设计语言有一定的了解,这本书就是为您准备的。不需要实际开发应用或甚至使用 IDE 的经验。
术语
在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“从实际的角度来看,SDK 为我们提供了两种材料主题版本(浅色和深色)以及两个小部件,CardView用于简单内容,RecyclerView用于列表。”
代码块设置如下:
ArrayList<MainDataDef> mainData = new ArrayList<MainDataDef>();
for (int i = 0; i < MainData.nameArray.length; i++) {
mainData.add(new MainDataDef(
MainData.imageArray[i],
MainData.infoArray[i]
));
})
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
<activity
android:name=".DetailActivity"
android:label="@string/title_activity_detail">
</activity>
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“选择空白活动并保持其他一切不变,或者选择您自己的值。”
注意
警告或重要注意事项以如下框中的形式出现。
小贴士
小技巧和窍门看起来像这样。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
既然您已经自豪地拥有了一本 Packt 图书,我们有许多事情可以帮助您充分利用您的购买。
下载示例代码
您可以从www.packtpub.com下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上对版权材料的盗版是所有媒体中持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。
第一章. 设置开发环境
Android 5 是目前可用的最重要的更新,因为它的平台是在 2009 年创建的。它引入了一个全新的用户界面和数千个新的 API,包括全新的相机 API。Android 5 还集成了令人兴奋的新节能技术和改进的应用性能。
在本章中,我们将安装和配置构建 Android 应用所需的所有开发工具。这些工具将包括 Android Studio 和 Android SDK,各种平台特定工具和系统镜像,以及一个 Android 虚拟设备。一旦我们的环境设置妥当,我们将创建一个简单的“Hello World”项目,并在移动设备和模拟器上对其进行测试。这个练习将为我们提供一个很好的机会,熟悉我们开发环境中最广泛使用的元素,同时也提供了一个快速但实用的演示,说明 Android Studio 项目是如何构建的。
在本章中,我们将:
-
了解 Android 5 的新功能和不同之处
-
下载并安装 Android Studio 和 SDK
-
安装最新的 SDK 工具、平台工具和构建工具
-
安装 Lollipop 平台和系统镜像
-
创建一个基本的“Hello World”项目
-
在手机上运行应用
-
配置 Android 虚拟设备 (AVD)
-
在 AVD 模拟器上运行应用
在我们开始之前,仔细了解一下 Android 5 本身是很有价值的,看看它与其他 Android 版本有什么不同,以及它能为开发者提供什么。
Android 5 是什么?
Android 5,或称 Lollipop,代表了迄今为止对 Android 操作系统的最革命性升级。它为用户引入了许多令人兴奋的新功能,同时为开发者提供了一大批新的 API 和访问前沿技术的途径。最显著和明显的变化无疑是新的 Material Design UI 以及能够在可穿戴设备、电视和我们的汽车上部署 Lollipop 的能力。
在探索它对我们作为开发者意味着什么之前,快速看看 Android 5 对用户来说看起来是什么样子,并不是一个坏主意。
从用户的角度看 Lollipop
除了扩展和改进的通知栏以及更功能性的锁屏之外,任何 Android 5 用户首先会注意到的是新的视觉语言——Material Design。他们会注意到几乎他们接触或与之交互的每一件事都会有一个动画响应。这些简单的屏幕行为旨在为用户提供清晰直观的视觉反馈。另一个有趣的变化是新的概览功能,它取代了最近应用,允许单个文档和整个应用都可用。

从用户的角度来看,Lollipop 与之前版本最有趣的区别之一是,他们现在将在电视、汽车以及可穿戴设备,如手表和眼镜上遇到它。那些拥有这些可穿戴设备的人无疑会想要这些利用 Lollipop 引入的两个新传感器(如心率传感器和倾斜传感器)的应用程序。
从开发者的角度来看,Lollipop
从开发者的角度来看,Android 5 提供的不仅仅是更漂亮的 UI、改进的电池寿命和更好的锁屏。对于我们来说,超过 5,000 个新 API、全新的设计语言以及数十个新功能和技术的出现,Android 5 为我们提供了迄今为止最强大的工具集。Android 现在不仅更强大,而且编程起来也比以前更容易。有了真正有帮助的 IDE 和为易用性而设计的 API,开发应用程序从未如此简单或令人畏惧。如果你想把你的想法变成现实,那么 Android 5 就是你的选择。现在比以往任何时候都更真实,我们受限于的只有我们想象力的力量。
材料设计
材料设计 UI 范式远不止是一个吸引人且易于理解的界面。它是一种严肃的设计语言,其中包含一些关于我们如何与设备互动的重要观点。受到对未来材料可能行为想法的启发,这种材料可以被视为一个动态且响应式的纸张片段,它可以移动、改变形状和大小、分裂、合并,并且存在于三维空间中。正是这个额外的维度,加上实时可编程的阴影,赋予了材料设计其深度感。在材料上显示内容的方式也是动态的,谷歌建议我们将其视为“智能墨水”。在构建使用材料设计的应用程序时,需要考虑一些设计规则,我们将在后续章节中讨论这些规则。从实际的角度来看,SDK 为我们提供了两种材料主题版本(浅色和深色)和两个小部件:CardView用于简单内容,RecyclerView用于列表。我们还可以定义和自定义应用程序使用的阴影、动画和可绘制元素。
其他设备
Lollipop 为我们开发者提供的最激动人心的机会之一是能够为除手机和平板电脑以外的设备创建应用程序。Android 5 使得编写适用于从手表大小的屏幕到家庭影院大小的屏幕的应用程序成为可能,包括任何介于两者之间的尺寸。
电视
Android 5 使得为电视编码与为手机编码非常相似。主要区别在于尺寸、观看距离以及电视应用程序通常使用遥控器和 D-pad 进行导航的方式。Android 5 SDK 配备了专门设计的主题和布局,这使得我们能够轻松地将为平板电脑构建的应用程序部署到电视上,反之亦然。
可穿戴
当涉及到为可穿戴设备设计应用时,功耗和屏幕尺寸受限等问题成为一些更为重要的决定因素。因此,Android 5 对可穿戴应用实施了严格的时间超时策略。由于所有 Android 可穿戴应用都需要首先在手机上安装,我们有在可穿戴设备或父设备上展示内容的机会。尽管存在这些限制,并且并非所有功能都可在可穿戴设备上使用(例如网页浏览),但新增的心率传感器 API 为那些对创建基于健康和健身的应用感兴趣的开发者提供了新的和令人兴奋的机会。
自动
在我们的汽车中使用的 Android 提供了 Lollipop 引入的另一个新领域。这里的重点完全在于安全:车内应用只允许运行消息和音频功能。这意味着在为汽车开发应用时,我们需要考虑哪些功能会因为安全原因而被禁用。
这就是我们现在需要的所有理论和背景知识。现在是时候开始工作并设置我们的工作空间了。
安装和配置开发环境
在我们甚至开始使用 Android SDK 之前,我们需要确保已经安装了最新的 Java 开发工具包(JDK)。你很可能已经安装了它,但如果你不确定,请在命令行中输入java -version,希望你会看到类似以下的内容:

注意
注意,Android 5 需要 Java 1.7 或更高版本。
安装 SDK
如果你只有 Java 运行时环境(JRE),你可以从www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html下载 JDK。你还需要记录 JDK 在你的计算机上的安装位置。它可能类似于C:\Program Files\Java\jdk1.8.0_25。让我们开始安装,按照以下步骤操作:
-
下载并安装 Android Studio 和 SDK。它们可以在
developer.android.com/sdk/index.html找到,并且应该作为一个单独的可执行文件捆绑在一起。 -
运行可执行文件,按照向导操作,确保安装所有组件,如下面的截图所示:
![安装 SDK]()
-
在运行 Android Studio 之前,我们需要设置一个环境变量,使其指向我们的 JDK。从控制面板的系统属性窗口中,选择高级选项卡,然后选择环境变量...按钮。添加一个名为
JAVA_HOME的新用户变量,并将它粘贴到之前提到的 JDK 路径中:![安装 SDK]()
管理 SDK 工具
在我们开始构建和测试任何应用之前,还需要一两个工具。SDK 将工具、平台和设备系统镜像分开,使我们能够只下载我们需要的包或那些针对我们项目的特定包。
Android SDK 管理器是我们用来做这件事的程序。它可以从 Android Studio 运行,或者作为 SDK 根目录下的独立应用程序运行,通过执行SDK Manager.exe文件。现在,我们将从 Android Studio 环境中运行它。这可以通过在工具栏中点击 SDK 管理器图标在打开的项目中完成,或者从配置页面的快速启动屏幕中完成。

-
打开 SDK 管理器;我们可以看到有三个部分:一个工具文件夹,一个从 Android 1.5 开始的历史 API 平台文件夹列表,以及一个** Extras**文件夹。
-
打开工具文件夹。在顶部,你会看到Android SDK Tools和Android SDK Platform-tools。这些将代表可用的最新工具,并且两者都必须安装。
在此之下,你会看到一个构建工具列表,由于本书专注于 Lollipop,我们只需要安装最新版本。
![管理 SDK 工具]()
-
接下来是平台列表,我们只需要选择适用于我们的 Lollipop 开发者的平台,这意味着任何 API 级别为 21 或更高的平台。
在某个时候,你可能想要在更早的平台测试你的应用以覆盖尽可能广泛的市场,但由于本书专注于 Android 5,安装最新的平台就足够了。选择此文件夹中的所有项目。
Extras文件夹中有几个实用的工具。特别是Android Support Library和Google Play services,它们提供了广泛的额外 API。如果你计划在连接到 PC 的实体设备上测试你的应用,那么你需要Google USB Driver,如果你有较新的英特尔处理器,你将需要硬件加速器来运行模拟器。选择下面显示的包,并安装它们。
![管理 SDK 工具]()
我们现在准备好创建我们的第一个应用了,但首先,关于硬件加速的一个注意事项。模拟器加速器需要手动执行,可以在 SDK 文件夹下的extras\intel\Hardware_Accelerated_Execution_Manager\intelhaxm-android.exe中找到。根据你的系统,你可能还需要在 BIOS 中启用虚拟化。
Android 软件定期更新,定期检查 SDK 管理器以获取更新是非常有价值的,尤其是在开始新项目之前。
小贴士
很遗憾,由于 Android 系统处于持续开发中,安装过程并不总是像这里显示的那么简单。谷歌会尽力解决出现的问题,如果遇到任何问题,可以访问网页 tools.android.com/knownissues 获取非常有用的信息。
创建“Hello World”应用程序
最后,是时候构建我们的第一个应用程序了。它几乎什么都不会做,但将让我们详细了解 Android Studio 如何构建应用程序。我们将看到哪些文件和代码会自动为我们生成,并通过以下步骤掌握项目的目录结构:
-
启动 Android Studio,从起始屏幕选择创建新的 Android Studio 项目。
-
按照向导操作,接受建议的值,确保您选择了手机和平板的设备类型以及最低 SDK 级别,不低于 API 21。
-
选择空白活动模板,并接受向导在最终屏幕上建议的字段值,然后点击完成。短暂的暂停后,IDE 将打开。![创建“Hello World”应用程序]
初看起来,IDE 可能显得令人畏惧,但如果我们逐个元素地接近它,实际上是非常直接的。看看左侧的面板——它显示了项目的目录结构。有两个主要分支,app和Gradle Scripts。当涉及到打包我们的应用程序时,我们将回到 Gradle,但现在,展开app分支。

这里有三个主要部分,manifests、java和res(代表资源)。除了这个目录结构,Android Studio 还会为我们自动生成几个代码文件,因此当我们第一次创建项目时,包括以下内容:
-
manifest文件声明了我们的应用程序的许多更广泛的属性,例如用户所需的权限。可以通过双击AndroidManifest.xml节点来打开、查看和编辑它。 -
Android 项目将布局数据和过程代码分开。我们创建的单一活动的布局信息可以在名为
activity_main.xml的 XML 文件中找到。有两种方式查看此文件:设计视图,它显示小部件列表和手机图像;以及文本视图,可以通过面板底部的文本选项卡访问。我们可能需要的任何可绘制内容、字符串和菜单定义也都存放在res目录中。 -
当我们创建项目时,IDE 也会创建一个名为
MainActivity的 Java 类。一开始,它只包含一些空的回调函数,这些函数在 Activity 首次创建、菜单创建以及菜单项被选中时被调用。
Android Studio 和模拟器在运行过程中都会生成大量的临时文件。这可能导致你的防病毒软件出现性能问题。如果你愿意,为 Android 目录设置排除规则可以显著加快软件的速度。
在物理设备上测试应用
现在是时候看看我们的应用在实际设备和模拟器上的样子了。要在实际手机或平板上运行项目,你首先必须从 SDK 管理器下载 Google USB 驱动程序并在手机设置中启用 USB 调试。连接你的手机并点击 IDE 主工具栏上的运行按钮,或者从菜单中选择运行。确认你使用的设备后,应用将被安装并启动。
设置虚拟设备
我们可能无法总是访问我们希望应用运行的所有硬件,但有了 Android 虚拟设备,我们可以创建几乎任何我们想象中的形态,并在这些设备上测试我们的应用。让我们通过以下步骤设置一个虚拟设备:
-
从主工具栏打开 AVD 管理器:
![设置虚拟设备]()
-
我们可以简单地选择一个预配置的设备,但要更好地理解 AVD 的工作原理,请点击创建虚拟设备...按钮。
-
从类别列表中选择手机。
-
选择一个展示的手机。我选择了Nexus 6,但这并不重要。
-
点击克隆设备...按钮,以保持原始设备不变。
-
以下页面允许我们编辑设备的硬件配置文件。在这里,我们可以选择屏幕大小、内存容量和传感器等特性。保持一切不变,但快速查看一下可用的选项,以备后用。
-
点击完成。
-
你将返回到硬件选择屏幕。和之前一样,从类别列表中选择手机,现在你会找到我们刚刚克隆的 AVD。选择它并点击下一步。
-
选择一个 API 级别为 21 或更高且目标为 5 或更高的系统镜像,然后点击下一步。
小贴士
如果你启用了英特尔硬件加速,则选择x86或x86_64作为 ABI。否则,你将不得不使用 ARM。在本演示中,我将假设你正在使用 ARM ABI。
-
以下页面主要用于验证我们的设置,但也给了我们调整缩放和提升模拟器性能的机会。是否调整缩放将取决于你正在工作的屏幕和模拟设备的分辨率。同样,通过利用你自己的计算机 GPU 加速模拟器性能或通过快照加快启动时间的选择将取决于你的硬件和项目目的。
-
点击完成。
在虚拟设备上运行应用与在实际手机上运行没有区别,唯一的区别是选择哪个设备。

如你所想,AVD 的触摸屏可以用鼠标操作。可以通过按下左Ctrl + F12来旋转设备。此外,可以通过F7键来按下电源按钮,可以通过Ctrl + F5和Ctrl + F6来调节音量大小。
这基本上结束了我们对 Lollipop、Android Studio 和 SDK 的快速浏览。在我们开始实际编程之前,还有一个值得一看的最后工具,那就是Android 设备监控器。
监控设备
再次运行应用,无论是在设备上还是在模拟器上,但首先打开 Android 设备监控器。它可以在主工具栏上找到,位于 SDK 管理器右侧。设备监控器提供了对我们设备内部实际发生情况的非常有用的视图,以及一些有趣的操控方式。通过在编辑器左下角的图标中选择 Android,可以直接从 IDE 中获取设备监控器的简化版本。

除了大量有用的调试信息外,设备监控器还直接提供了对数据的访问,通过文件浏览器和允许我们发送调用、短信消息并将其设置为虚假位置的工具。
小贴士
设备工具栏中的相机图标可以用来截取全尺寸屏幕截图,即使屏幕上的设备被缩放。
摘要
对于一个入门章节来说,我们已经涵盖了相当多的内容,但我们已经遇到了我们以后需要的几乎所有基本工具。到目前为止,你将拥有一个功能齐全且最新的 IDE,并且知道如何在 AVD 上创建和测试应用。你已经看到了在创建 Android 项目时生成的一些重要文件和资源,现在你应该理解这些文件在项目中的位置。
在浏览了我们开发环境中最显著的功能之后,我们现在可以深入挖掘,看看我们如何构建复杂和动态的布局,以及如何通过 Java 代码使这些设计栩栩如生。
第二章:构建 UI
Material Design UI 范式为 Android 平台带来了全新的外观和感觉。这种新的方法旨在为 Android 应用程序提供一个干净、简单的外观,具有直观的控件和动画。谷歌谈论虚拟纸张和虚拟墨水,这个概念在新屏幕组件(或小部件)卡片(或CardView)中最为明显,它不同于之前的 Android 小部件,可以投射阴影并具有圆角。

在我们将第一个CardView小部件放置到布局中之前,我们可以开始利用 Material Design,通过应用和自定义一个材质主题。这些主题允许我们定义一些基本颜色和属性,然后自动应用于我们的应用程序,赋予它一个品牌身份,帮助用户轻松识别我们的应用程序。
在创建完我们的布局后,我们可以看到 Java 是如何用来提供功能的。在这里,我们将使用一个按钮来启动一个简单的 Material Design 动画,然后将其应用到我们的布局中,以处理屏幕旋转,并为有视觉障碍的用户提供图像的文本上下文。
在本章中,我们将涵盖以下主题:
-
将材质主题应用到我们的应用程序中
-
应用您的品牌颜色
-
理解 Material Design 颜色指南
-
向相对布局添加新的小部件
-
编写一些 Java 代码来检测按钮点击
-
编写代码以生成动画
-
使用 Gradle 控制台观察构建过程
-
为图像应用可访问性选项
-
创建适用于不同屏幕方向的布局
在本章中,我们将继续开发在前一章中开始创建的Hello World应用程序,并使用它来演示一个简单的动画。代码可以从 Packt Publishing 网站下载,名称为Hello World - Chapter 2。
应用 Material Design 主题
Android 主题控制着应用程序的一般外观,如默认背景颜色、文本颜色和大小。在 Android 5 之前,Holo 主题是最广泛使用的内置主题,您可以通过在设计选项卡中查看activity_main.xml文件顶部点击App Theme按钮来预览它。
小贴士
注意,当在手机或模拟器上运行应用程序时,预览主题不会对应用程序产生影响,因为这必须在代码中实现。
所有 Android 主题都高度可配置,没有比材质主题更可配置的,它允许我们仅用几行代码就设置一个应用于整个应用程序的颜色方案,并且与它的前辈不同,还可以更改工具栏和导航栏的颜色。以下练习详细说明了如何将这种品牌应用到我们在上一章中设置的项目中:
-
从上一章打开
Hello World项目。 -
如果它还没有打开,请从菜单中使用视图 | 工具窗口 | 项目打开项目工具窗口。
-
定位到
res/values文件夹,并右键单击它。 -
从菜单中选择新建 | XML | 值 XML 文件,并将文件命名为
colors。 -
按照以下方式填写
colors.xml文件:<?xml version="1.0" encoding="utf-8"?> <resources> <color name="primary">#FF9800</color> <color name="primary_dark">#F57C00</color> <color name="accent">#03A9F4</color> <color name="text_primary">#DF000000</color> <color name="text_secondary">#8A000000</color>> </resources> -
打开
res/values/styles/styles.xml (v21)文件,并按照以下方式完成它:<?xml version="1.0" encoding="utf-8"?> <resources> <style name="AppTheme" parent="android:Theme.Material.Light"> <item name="android:colorPrimary">@color/primary</item> <item name="android:colorPrimaryDark">@color/primary_dark</item> <item name="android:colorAccent">@color/accent</item> <item name="android:textColorPrimary">@color/text_primary</item> <item name="android:textColor">@color/text_secondary</item> <item name="android:navigationBarColor">@color/primary_dark</item> </style> </resources> -
现在,您可以在手机或模拟器上运行应用,以查看我们的品牌颜色是如何应用的:
![应用 Material Design 主题]()
小贴士
注意,并非所有材料主题颜色都会在标准的 Android AVD 上显示,特别是状态栏和导航栏。要查看更改,您需要一个真实设备或第三方模拟器。
能够将我们自己的颜色方案应用到之前不可编辑的 UI 元素,如状态栏和导航栏,这是一个巨大的优势。这不仅让我们能够控制整个屏幕的外观,而且让我们的应用具有可识别和独特的风格。
Android 提供了colorPrimaryDark和navigationBarColor等字段,作为在应用中应用我们的颜色方案的便捷方式。通常建议导航栏保持黑色,这里仅通过演示进行了着色。我们没有使用所有可能使用的颜色属性;如果我们想使用,我们可以使用windowBackground和statusBarColor设置窗口背景颜色,这将覆盖默认的colorPrimaryDark设置。
小贴士
注意,colorAccent在此演示中不可见。它用于开关、滑块和可编辑的文本视图等。它包含在这里,因为我们将在整本书中使用这个主题(或您选择的颜色),随着我们的进展,colorAccent的包含将变得明显。
在 IDE 的帮助下,为我们的主题选择颜色变得非常容易,正如你所看到的,我们定义的颜色在侧边栏中显示:

从colors.xml文件中,我们可以点击这些颜色以生成一个动态的颜色轮,供我们选择。虽然我们可以自由地为我们主题选择任何喜欢的颜色,但 Google 设计指南建议从推荐的色调中选择颜色,完整的列表可以在www.google.com/design/spec/style/color.html找到。此外,Google 还建议主要颜色应具有值 500,较深的版本应为 700。
在colors.xml文件中,可以看到文字颜色是用 alpha 通道定义的。
Google 建议我们使用透明度来产生各种文字的阴影。特别是,他们建议我们的主要文字的透明度约为 87%,次要文字的透明度约为 54%。当处理深色背景上的白色文字时,应使用 100% 和 70% 的透明度值。文本提示的透明度应约为 28%,无论是背景还是前景。
你会注意到有两个styles.xml文件,一个是我们在v21版本中使用的,另一个与同一个名字。这个其他样式文件用于在需要使我们的应用向后兼容时提供替代主题。我们将在适当的时候讨论这个问题,但现在可以安全地忽略其他样式文件。
材料设计指南不必严格遵循,尤其是当你设计一个全屏应用,比如游戏时。它们的存在是为了帮助开发者构建在整个平台上提供一致体验的应用,而你如何严格遵循这些指南完全取决于你。
看过如何轻松地将个性化主题应用到我们的应用后,我们现在可以开始向布局添加更多视觉组件,并查看它们如何随后用 Java 编程控制。我们将继续使用Hello World项目,并使其变得更有趣,我们将添加一些简单的 Android 5 动画。
添加动画小部件
就像许多编程语言一样,设计和功能基本上是分开处理的。我们使用 XML 来设计我们的布局,使用 Java 来提供它们的功能。在这里,我们将看到这两者是如何完成的,并将分别处理每个。
设计 XML 布局
我们将使用图形设计视图来构建这个 UI,但值得在每个步骤后从底部的标签检查文本视图,看看我们做出的更改是如何应用到 XML 中的。
-
打开
Hello World项目,然后是activity_main.xml文件。 -
点击底部的设计标签以查看设备预览。
-
将
TextView控件拖动到屏幕中间,就像这样:![设计 XML 布局]()
-
从左侧的调色板中拖放一个按钮小部件到屏幕的底部中央。
-
从位于TextView下方的调色板中拖动一个ImageView控件。顶部的提示应该读作:
centreHorizontal below=<generated> -
在选择ImageView的情况下,或者通过在组件树面板中选择它,在下面的属性面板中找到
src并点击....按钮以打开此对话框:![设计 XML 布局]()
-
选择Mip Map | ic_launcher并点击确定。
-
在预览窗口中选择ImageView并按Ctrl + C,然后按Ctrl + V。
-
将ImageView副本放置在我们刚刚创建的一个右侧。
-
重复此过程,将第三个ImageView放置在左侧,这样布局的下半部分看起来就像这样:
![设计 XML 布局]()
-
现在,使用底部的标签打开我们布局的文本视图。
-
定位到按钮节点,并点击显示
android:text="New Button"的行。一个琥珀色的快速修复灯泡会出现,同时会显示有关硬编码字符串的警告。![设计 XML 布局]()
-
点击快速修复下拉菜单并选择提取字符串资源。
-
在结果对话框中,将资源名称设置为
button_text并点击确定。 -
以文本视图打开文件,并将此行添加到
RelativeLayout元素中:android:id="@+id/view_group" -
然后,将此行添加到TextView:
android:textAppearance="?android:attr/textAppearanceMedium"就设计布局而言,这就完成了!
小贴士
您可以使用Ctrl + Alt + L来自动格式化任何代码。Ctrl + Alt + Shift + L将打开重排对话框。
所有 Android 布局设计在其根部都有一个名为ViewGroup的容器对象,其中放置了所有其他图形对象。这包括其他 ViewGroup,尽管具有许多嵌套 ViewGroup 的复杂布局可能会对性能产生负面影响。
我们在这里使用的 ViewGroup 小部件容器是RelativeLayout。还有几种其他类型的布局,每种布局都适合特定的目的,我们将在整本书中遇到这些布局。我们在这里使用的 RelativeLayout 小部件容器允许我们定义小部件相对于其他小部件的位置。例如:
android:layout_below="@+id/textView
当涉及到设计将在不同尺寸和比例的屏幕上运行的布局时,这非常方便。
在生成布局文件方面,我们有三种工具可供使用。我们有(几乎)所见即所得的设备预览窗口,允许我们定位和调整小部件的大小,属性面板允许我们设置特定的值,也许最强大的是文本编辑窗口,它提供了对我们设计每个方面的控制。
我们使用android:attr/textAppearanceMedium设置了文本的大小。我们本可以使用类似android:textSize="42sp"的东西来精确设置大小,但使用textAppearanceMedium、textAppearanceLarge或textAppearanceSmall会考虑用户在手机上配置的文本设置。
我们刚刚所做的一切中,最重要的方面是每个小部件都有一个标识符,形式为android:id="@+id/some_unique_identifier"。这些 ID 是我们从 Java 代码中在运行时引用和控制小部件的方式。
我们出于方便使用了内置的应用程序图标作为我们的ImageView控件,但我们也可以提供自己的图像,将其存储在res(ources)/drawable文件夹中,并使用其文件名(不带扩展名)作为其 ID。我们将在后面做很多这样的事情,所以在这里不必担心。如果您查看mipmap文件夹,您将看到有四个ic_launcher图标,针对不同的屏幕密度。为了为所有可用的屏幕密度提供高质量图标,您需要提供所有四个图标。
当我们在屏幕上创建按钮时,IDE 为我们提供了文本新按钮。尽管像这样的硬编码字符串可以完美工作,但并不推荐这样做,因为您将无法提供其他语言的翻译。
我们的布局已经就绪,现在我们可以继续让它执行一些操作。在这里,我们将应用一些新的 Android 5 动画,这些动画在按钮被点击时生效。
使用 Java 控制小部件行为
Android 5 引入了一种新的、更简单的屏幕元素动画方式。这些动画在从一个屏幕切换到另一个屏幕时非常有用,并且可以直观地向用户展示应用正在做什么。这个应用只有一个屏幕(Activity),所以我们将只让我们的小部件飞离屏幕,然后返回。但在开始之前,我们需要配置 IDE 以自动导入应用将使用的 Java 库。按照以下步骤查看如何完成这两个任务:
-
从 文件菜单 选择 设置 | 编辑器 | 通用 | 自动导入,并勾选以下所有复选框:
![使用 Java 控制小部件行为]()
-
打开
MainActivity.java文件。 -
在类顶部添加以下字段:
public class MainActivity extends Activity { private ViewGroup viewGroup; private TextView textView; private ImageView imageView, imageView2, imageView3; private Button button; -
在
onCreate()方法中,在setContentView(R.layout.activity_main);行下方添加以下代码:viewGroup = (ViewGroup) findViewById(R.id.view_group); textView = (TextView) findViewById(R.id.textView); textView.setText("Animation demo"); imageView1 = (ImageView) findViewById(R.id.imageView); imageView2 = (ImageView) findViewById(R.id.imageView2); imageView3 = (ImageView) findViewById(R.id.imageView3); -
在此之下,添加
Button控制的代码如下:button = (Button) findViewById(R.id.button); button.setText("OK"); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { TransitionManager.beginDelayedTransition(viewGroup, new Explode()); toggle(textView, imageView, imageView2, imageView3); } }); -
创建一个名为
toggle()的新方法,并按照以下方式完成它:private static void toggle(View... views) { for (View v : views) { boolean isVisible = v.getVisibility() == View.VISIBLE; v.setVisibility(isVisible ? View.INVISIBLE : View.VISIBLE); } } -
应用现在可以在模拟器或连接的手机上进行测试。在 IDE 工具栏上点击运行图标:
![使用 Java 控制小部件行为]()
虽然代码易于理解,但它涵盖了非常关键的一些点。首先,是 onCreate() 方法。该方法在 Activity 启动时立即被调用,通常是通过应用程序图标,并将成为你将创建的几乎所有 Android 应用的起点。我们为我们创建的前三条线填充了布局。然后我们使用 findViewById() 将我们的布局小部件与 Java 实例关联起来。
注意
注意,为了方便,我们在这里使用了编辑器建议的名称。将来,我们将使用 textView 形式声明 Java 实例,以及 text_view 用于 XML 对应物。
我们还使用 setText() 方法更改了我们两个小部件内的文本。我们也可以在 XML 中这样做,但了解如何使用 Java 动态完成此操作非常有用。
我们附加到按钮上的 OnClickListener() 接口为我们提供了 onClick() 方法,使我们能够控制在小部件被点击时执行哪些操作。在这个 Activity 中只有一个按钮,所以我们为它创建了一个特定的 OnClickListener()。通常,我们的应用将拥有多个按钮或可点击控件,正如我们将在下一章中看到的,Activity 本身可以实现一个点击监听器,然后有一个 onClick() 方法来处理所有按钮。
动画本身是通过TransitionManager类配置和触发的,我们将在本书稍后回到这个话题。现在,值得将术语Explode()更改为Fade()或Slide()。这些更改产生的影响不会让你感到惊讶,但了解它们是可用的很有用。大多数时候,当我们将动画应用到我们的应用中时,它们是为了演示从一个 Activity 到另一个 Activity 的过渡,而不仅仅是作为装饰,就像我们在这里所做的那样。
一旦你点击运行图标,构建过程可能会相当慢,尤其是在较旧的机器上。然而,有一些方便的工具可以帮助我们观察这个过程。将鼠标悬停在 IDE 左下角的小图标上,并选择Gradle 控制台。

不必理解 Gradle 控制台输出的内容,但看到在较长的构建过程中过程没有停止是令人放心的。以相同的方式可以访问的其他两个非常有用的窗口是Android和运行窗口。这些也可以通过键盘上的Alt + 4和Alt + 6分别打开。
如果你花了一些时间实验上述应用,你会注意到当模拟器或设备旋转 90 度时,有一两件事情并没有按我们希望的方式工作。首先,每次设备旋转时动画都会重置。这是因为这,以及任何其他 Activity,都会在方向改变和onCreate()方法被重新调用时重新加载。然而,还有几个其他回调方法允许我们拦截这个过程。我们将在稍后更详细地查看 Activity 生命周期,但现在我们将探索第二个问题,即系统在横屏视图中定位我们的文本和图像的方式。
小贴士
可以通过按Ctrl + F12将 AVD 旋转 90 度。按主页键将 AVD 返回到其主页,而Esc键与按设备后退按钮相同。
创建替代布局
当运行我们应用之一的设备旋转到横屏模式时,它引用的 XML 文件与在竖屏模式下的相同。通常这工作得非常好,但设置一个更适合横屏屏幕形状的替代布局非常简单。按照以下步骤创建一个用于横屏查看的替代布局文件。
-
在设计视图中打开
activity_main.xml文件。 -
点击左上角的图标,并选择创建横屏变体。
![创建替代布局]()
-
拖动并重新排列屏幕上的小部件,以形成更令人愉悦的空间使用。
-
选择一个
ImageView控件。然后点击琥珀色的快速修复图标。它会通知你图片缺少contentDescription。点击这条消息并完成如下所示的对话框:![创建替代布局]()
-
最后,在设备或模拟器上运行应用程序并检查其旋转时的行为。
![创建替代布局]()
包含一个用于横屏方向的布局文件就像在res/layout-land目录中放置一个具有相同名称和控件 ID 的文件,而不是在res/layout目录中。然后我们可以以任何我们喜欢的方式编辑此文件,并且当屏幕旋转到横屏方向时,它将自动展开。
由于创建替代布局文件是一项如此快速且简单的任务,我们还探讨了如何为有视觉障碍的用户提供替代输出,形式为图像的内容描述。当有视觉障碍的用户设置了辅助功能选项,并且我们提供了适当的文本替代品时,此描述将被读给用户听。为了简洁起见,我们不会在本书的每个练习中都添加此描述,但建议在打算发布的任何应用程序中都包含此类属性。
小贴士
下载示例代码
您可以从您在www.packtpub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册以直接将文件通过电子邮件发送给您。
摘要
这就结束了我们对 XML 布局与 Java 代码之间关系的介绍。我们看到了如何为各种屏幕尺寸和方向生成布局,以及如何将这些 XML 定义连接到动态的 Java 代码,该代码在运行时控制我们应用程序的行为。值得注意的是,我们看到了onCreate()方法是如何用来设置我们的应用程序的,以及它何时被调用以响应设备的旋转。我们利用这一事实,通过创建一个专门为旋转屏幕设计的替代布局来利用这一事实。
在下一章中,我们将探讨如何实现两个最新的 Android 小部件,即CardView,它是一个方便且时尚的容器,可以显示我们希望展示的任何信息,以及RecyclerView,它以内存高效的方式管理CardView控件或其他视图的列表。
第三章。活动和片段
在仅使用单个屏幕或仅使用单个 Activity 的应用程序中,有用的应用程序非常少;我们需要一种方法来在各个 Activity 之间切换,并将信息从一个传递到另一个。一般来说,每个新的 Activity 都需要自己的布局文件,但这并不总是如此;有时我们想要相同的布局,但数据资源不同。在本书的后面部分,我们将构建一个应用程序,作为世界上一些最著名和最受欢迎的旅游景点的导游。我们将从这个旅程开始,构建一个仅针对一个地点的简单示例,其中包括对 Android 5 中引入的CardView小部件的介绍,并学习如何从一个 Activity 启动另一个 Activity。
然后,我们将查看片段,它允许我们以模块化的方式构建布局。片段的行为有点像迷你 Activity,可以在运行时动态地添加到 Activity 中,或者可以像其他 ViewGroup 一样在布局文件中定义。我们将构建一个小型计时应用程序,使用片段在数字时钟面和模拟时钟面之间切换。
接下来,我们将在我们的应用程序中包含一个选项菜单,允许用户更改设备上的时间和区域相关设置。我们将在菜单中添加操作图标,以便它可以在 Lollipop 之前称为操作栏的地方显示。最后,我们将利用操作栏的替代品,即工具栏,对其进行自定义,以便它可以放置在屏幕上的任何位置,并包含比其前身更多的功能。
在本章中,我们将:
-
添加
CardView -
为
CardView设置布局 -
添加图片
-
创建第二个 Activity 和布局
-
使用 XML 定义
onClick行为 -
编程两个 Activity 协同工作
-
使用片段动态更改布局
-
探索翻译编辑器
-
使用 XML 添加静态片段
-
包含一个选项菜单
-
使用意图访问用户设置
-
将菜单图标添加到操作栏
-
用自定义工具栏替换操作栏
添加 CardView 小部件
与我们之前遇到的视图不同,CardView并不包含在标准 SDK 库中,而是作为(Lollipop 特定的)V7 支持库的一部分,并且不可从图形布局设计模式中获取;因此,在布局中应用它需要更多的工作。
-
开始一个新的 Android 项目。
-
设置应用程序名称为
Stonehenge Guide,尽管你可以称它为任何你喜欢的名字。 -
从下一页选择手机和平板的设备类型和空白 Activity。
-
保持其他选项不变,等待项目构建。
-
在设计视图中打开
activity_main.xml文件,并删除我们在创建项目时自动生成的"Hello World!"文本视图。 -
为了完整性,还请在
res/value/strings.xml文件中删除"Hello World!"字符串资源。 -
编辑
styles.xml(v21) 文件并创建一个colors.xml文件以实现 Material Design 颜色方案,就像我们在上一章中所做的那样。 -
从项目资源管理器打开
Gradle Scripts/build.gradle (Module: app)文件:![添加 CardView 小部件]()
-
编辑
dependencies部分以匹配以下片段:dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:cardview-v7:22.0.+' } -
通过此处显示的工具栏图标同步项目:
![添加 CardView 小部件]()
-
打开
app/res/values/dimens.xml并添加以下三个新尺寸资源:<dimen name="card_height">200dp</dimen> <dimen name="card_corner_radius">4dp</dimen> <dimen name="card_elevation">3dp</dimen> -
从
app/res/layout打开activity_main.xml文件并添加以下CardView代码,以便完成的布局看起来像这样:<RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="@dimen/card_height" android:layout_gravity="center" card_view:cardCornerRadius="@dimen/card_corner_radius" card_view:cardElevation="@dimen/card_elevation"> </android.support.v7.widget.CardView> </RelativeLayout>
我们以与上次相同的方式创建了此项目,并且拥有为我们创建的初始活动和布局文件非常有用;这可以在设置大多数新项目时为我们节省大量时间。
如前所述,CardView 不是标准库的一部分,这就是为什么我们不得不将其包含在构建文件中,当我们应用其他新的 Lollipop 小部件 RecyclerView 时,我们也将不得不做同样的事情。正因为如此,我们不得不再次同步项目,以便构建引擎知道要加载哪些库,这与一个人可能导入 Java 库的方式非常相似。在这种情况下,我们只需从 构建 | 重新构建项目 菜单项重新构建项目即可,但这并不总是情况,养成全面同步的习惯是个好主意,因为这不仅会重新构建我们的项目,还会检查其他可能的错误,例如缺少资源定义。
注意
注意,当我们向 dimens.xml 文件添加值时,还有一个 dimens.xml (w820dp) 文件。当为宽度超过 820 像素的平板电脑和设备设计布局时,我们会使用此文件,因为我们为较小设备设置的边距和填充可能看起来不正确。
通过检查 CardView 的 XML 代码,可以立即明显看出,它的实现方式与我们迄今为止处理的小部件截然不同。像这样包含外部库中的元素非常简单,尽管我们不会在本书中介绍它,但了解有多个第三方库可用,这些库包含许多在其他情况下通过标准 SDK 不可用的功能是有用的。
CardView 拥有两个仅限于 Lollipop 的独特属性,我们之前还没有遇到过:cardCornerRadius 和 cardElevation。这些属性的目的很明显,但值得注意的是,更改它们的效果不会在预览窗格中显示,并且增加高度只会影响小部件的阴影,而不会影响其大小。

在创建了一个 CardView 作为容器之后,现在是为它提供一些内容的时候了。
将图像和文本添加到布局中
我们现在将使用 CardView 来显示一些基本信息,即一张照片、一个标题和一段简短的文本。为此,你需要找到工作室存储你的项目文件的位置。这个目录将被称为 AndroidStudioProjects,并且很可能位于你的主目录或指定的保存位置。
-
定位你的
AndroidStudioProjects目录,并打开\StonehengeGuide\app\src\main\res\drawable文件夹。这可以通过在项目资源管理器中右键单击 drawable 并从菜单中选择 Show in Explorer 来完成。 -
找到一个图像并将其保存到
drawable目录中。任何图像都可以;我这里使用的是名为stonehenge.png的图像,大约是 640 x 480 像素。 -
打开你的
res/values/strings.xml文件并添加以下字符串:<string name="title_text">Stonehenge</string> <string name="detail_text">One of the most famous sites in the world, Stonehenge is a prehistoric monument located in Wiltshire, England, about 2 miles west of Amesbury and 8 miles north of Salisbury.</string> -
打开
res/values/dimens.xml文件并添加以下尺寸:<dimen name="frame_width">160dp</dimen> <dimen name="card_padding">8dp</dimen> -
我们需要在
CardView内部放置一个RelativeLayout。这在图形设计模式下是不可能的,所以请将一个RelativeLayout拖动到屏幕上的任何位置,然后编辑 XML 代码,使卡片看起来像这样:<android.support.v7.widget.CardView android:id="@+id/main_card_view" android:layout_width="match_parent" android:layout_height="200dp" android:layout_gravity="center" card_view:cardCornerRadius="3dp" card_view:cardElevation="4dp"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="@dimen/card_padding"> </RelativeLayout> </android.support.v7.widget.CardView> -
接下来,我们将在这个
RelativeLayout中填充一个包含ImageView和两个TextView的FrameLayout,使其看起来像这样:![向布局中添加图像和文本]()
最好的方式是通过 CardView 内部 RelativeLayout 的完整代码来展示其他设置和属性:
<RelativeLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:padding="@dimen/card_padding">
<FrameLayout
android:id="@+id/frameLayout"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_height="match_parent"
android:layout_width="@dimen/frame_width">
<ImageView
android:clickable="false"
android:id="@+id/imageView"
android:layout_gravity="left|center_vertical"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="img/stonehenge" />
</FrameLayout>
<TextView
android:id="@+id/title_text_view"
android:layout_alignParentTop="true"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/card_padding"
android:layout_toEndOf="@+id/frameLayout"
android:layout_width="wrap_content"
android:text="@string/title_text"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/my_text_view"
android:layout_below="@+id/title_text_view"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/card_padding"
android:layout_toEndOf="@+id/frameLayout"
android:layout_width="wrap_content"
android:text="@string/detail_text"
android:textAppearance="?android:attr/textAppearanceSmall" />
</RelativeLayout>
正如我们所看到的,任何放置在我们项目中的 drawable 目录下的图像文件,都以与其他资源相同的方式对我们变得可访问。放置在这个文件夹中的图像将可供我们的应用程序使用,无论它们运行在哪种设备上。你可能会注意到还有四个其他的 drawable 目录,例如 drawable-xxhdpi 文件夹。这些目录在构建适用于广泛屏幕密度的应用程序时特别有用,原因有两个。首先,它们允许我们为支持此类屏幕的用户包含高质量图像,其次,它们可以在屏幕密度较低的设备上节省内存,因为 Android 只加载可以由每个特定物理屏幕支持的图像。
我们在这里遇到的大多数布局功能我们在前面的章节中已经遇到过,没有太多要解释的,除了可能提到的 ImageView。值得注意的是,除了使用 android:src 将我们的照片与 ImageView 关联外,我们还可以使用 android:background,它执行一个非常类似的功能,尽管它不尊重图像的原始宽高比。创建完布局后,我们现在可以继续添加另一个 Activity。
创建第二个 Activity
到目前为止,我们的应用程序除了显示信息之外什么也不做。所以,接下来我们将通过使其在用户点击图像时在另一个 Activity 中显示图片的较大版本来添加一些功能。正如你将看到的,使用 Android Studio 创建新的 Activity 非常简单。
-
右键单击项目资源管理器中的 Java 节点,选择 新建 | 活动 | 空白活动。
![创建第二个活动]()
-
在生成的向导中,将
ImageActivity作为 活动名称,将activity_image作为 布局名称,将image_menu作为 菜单资源名称,并将 标题 保持不变。 -
打开
activity_image.xml文件,并在布局中放置一个单独的ImageView,如下所示:<ImageView android:id="@+id/large_image_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:src="img/stonehenge" android:onClick="returnToMainActivity"/> -
添加以下字符串资源:
<string name="title_activity_image">Stonehenge</string> -
打开
MainActivity的 Java 文件,并在onCreate()方法中的setContentView(R.layout.activity_main);行下添加以下代码:ImageView mainImageView; mainImageView = (ImageView) findViewById(R.id.imageView); mainImageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(getApplicationContext(), ImageActivity.class)); } }); -
打开
ImageActivity的 Java 文件,并将以下公共方法添加到类中:public void returnToMainActivity(View v) { startActivity(new Intent(getApplicationContext(), MainActivity.class)); } -
在设备或模拟器上运行应用。
新的活动向导方便地为我们创建了一个 Java 活动和一个布局 XML 文件,但并不是每个活动都需要关联一个 Layout 文件。通常,我们可以为许多活动使用相同的布局,前提是每个 Java 活动都有一种选择要访问和显示的数据的方法。
在我们没有意识到的情况下,当我们创建新的活动时,向导还修改了清单文件以包含新的活动。这值得一看,因为有时你不会使用向导来创建活动,在这种情况下,将需要手动修改 AndroidManifest.xml 文件。
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ImageActivity"
android:label="@string/title_activity_image" >
</activity>
我们的第二个活动布局文件只包含一个视图,但与之前探索的视图相比有一个显著的不同,那就是使用了 android:onClick 属性。之前我们使用 View.OnClickListener() 来控制小部件被点击时的行为。在 XML 中声明这为我们提供了另一种实现方式,尽管它缺乏 Java 版本的某些灵活性,但它简单且易于使用。我们只需声明当小部件被点击时调用的方法,然后在 Java 中添加该方法,这里我们称之为 returnToMainActivity()。
注意
在前面的章节中,我们让用户点击一个 Button 小部件,尽管这似乎是一个明显的选择,但值得注意的是,几乎任何视图或小部件都可以响应点击事件。
StartActivity() 方法接受一个 Intent 作为其参数。这是任何 Android 应用中的一个重要对象,值得快速查看,因为它不仅在与活动一起工作时是必不可少的,而且在处理服务和广播时也是必不可少的,它们是大多数应用的其他两个主要组件。
注意
服务 类似于线程,在后台运行,而 广播 是系统范围内的消息,任何应用都可能接收并对其做出响应。
意图基本上是我们希望我们的应用执行的操作的描述。它们由两部分组成,一个动作和要执行的动作的数据。有几个Intent()构造函数,这里我们使用了Intent(String action, Uri data)。从一个活动启动另一个活动在许多情况下都是有用的,这里我们通过一个被点击的方法来启动它。当然,还有一个在几乎所有移动应用中都有的熟悉输入功能,即菜单,这是我们接下来要探讨的内容。
应用片段
使用两个或多个活动来创建单独的屏幕是包括我们应用中多个页面的直接方法。然而,这并不是唯一的方法,系统还提供了片段类。片段类似于 ViewGroups;它们作为活动的一部分存在,但它们创建和销毁的方式使它们更像迷你活动。与活动不同,我们也可以在屏幕上拥有多个片段。
在 Android 应用中部署片段有两种方式。首先,它们可以直接添加到我们的布局 XML 文件中,使用<fragment>标签,并且它们也可以在运行时动态添加和删除。虽然我们现在将探讨这两种技术,但第二种,动态方法使片段变得非常灵活和有用。
除了通常的主活动布局和代码之外,每个片段还有一个 XML 和一个 Java 组件,这使得编码比单独使用活动要复杂一些。在接下来的练习中,我们将创建一个简单的应用,允许我们在运行时添加和替换片段。
-
开始一个新的 Android Studio 项目。
-
选择空白活动模板(不是带有片段的空白活动),将项目命名为
Fragment Example或类似名称,然后打开activity_main.xml文件。 -
通过直接编辑代码将布局从
RelativeLayout更改为Linear布局。编辑器应该根据您的输入更改关闭标签。 -
使用此行设置布局的垂直方向:
android:orientation="vertical" -
将
TextView替换为这两个按钮:<Button android:id="@+id/button_analog" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/button_analog_text" /> <Button android:id="@+id/button_digital" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/button_digital_text" /> -
点击行
android:text="Analog";,在侧边栏会出现一个琥珀色的快速修复,如下所示:![应用片段]()
-
点击它选择提取字符串资源,在结果对话框中命名字符串为
button_analog_text。 -
对其他按钮也做同样的操作,将其命名为
button_digital_text,在这些按钮下方添加这个FrameLayout:<FrameLayout android:id="@+id/fragment_container" android:layout_width="match_parent" android:layout_height="match_parent" /> -
在项目资源管理器中的
layout文件夹上右键单击,选择新建 | 新建资源文件。将其命名为fragment_analog,并给它一个RelativeLayout根元素,如下所示:![应用片段]()
-
打开文件并在根元素内插入这个
AnalogClock:<AnalogClock android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" /> -
在项目资源管理器中选择
fragment_analog.xml,使用Ctrl + C和Ctrl + V创建一个副本,并将其命名为fragment_digital.xml。 -
在这个新文件中,将
AnalogClock替换为这个TextClock:<TextClock android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:textSize="48sp" /> -
在项目资源管理器中定位并选择包含你的
MainActivity.java文件文件夹。它将具有与你的包相同的名称。 -
在其上下文菜单中,选择 新建 | Java 类 并将其命名为
FragmentAnalog。填写类如下:public class FragmentAnalog extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_analog, container, false); } } -
将此文件复制一份,并将其命名为
FragmentDigital.java。 -
仅更改返回语句中的布局引用:
return inflater.inflate(R.layout.fragment_digital, container, false); -
打开
MainActivity.java文件并更改类声明,使其实现一个点击监听器,如下所示:public class MainActivity extends Activity implements View.OnClickListener -
这将生成一个错误和一个红色快速修复。选择 实现方法 以将
onClick()方法添加到类中。 -
将这些
Button字段添加到类中:private Button analogButton, digitalButton; -
在
onCreate()方法的末尾包含以下四行:analogButton = (Button) findViewById(R.id.button_analog); analogButton.setOnClickListener(this); digitalButton = (Button) findViewById(R.id.button_digital); digitalButton.setOnClickListener(this) -
按照以下方式完成
onClick()方法:@Override public void onClick(View v) { Fragment fragment; if (v == analogButton) { fragment = new AnalogFragment(); } else { fragment = new DigitalFragment(); } replaceFragment(fragment); } -
按照以下方式实现
replaceFragment()方法:public void replaceFragment(Fragment fragment) { FragmentManager manager = getFragmentManager(); FragmentTransaction transaction = manager.beginTransaction(); transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); transaction.replace(R.id.fragment_container,fragment); transaction.addToBackStack(null); transaction.commit(); } -
你现在可以在设备或模拟器上运行应用。
![应用片段]()
我们通过创建一个简单的布局开始这个练习,但我们还使用了一个方便的快捷方式来避免在我们的布局中硬编码字符串。这样做使得创建我们应用的翻译版本变得非常简单,并在创建替代布局时节省了大量工作。对于本书中的此类练习,没有必要遵循此做法,为了节省时间,我们不会进一步关注它。
提示
可以通过在项目资源管理器中右键单击 res/values/strings.xml 文件并选择 打开翻译编辑器(预览)来打开翻译编辑器。这个编辑器使得翻译 Android 应用变得非常简单。
我们还添加了一个空的 FrameLayout 作为我们片段的容器,虽然我们可以使用任何 ViewGroup,但 FrameLayout 是最简单的。片段就像活动一样,它们既有 XML 组件也有 Java 组件,在这里我们创建了两个非常简单的片段,只是为了看看它们如何动态地包含和替换。片段当然可以包含许多小部件和视图,所有这些都可以用通常的方式交互,并通过它们各自的 Java 文件中的代码进行控制。片段,就像活动一样,有一个生命周期和相关的回调,如 onCreate() 方法,在这里我们使用了 onCreateView(),当尝试填充片段时会被调用。需要注意的是,尽管片段可以,并且通常确实包含所有类型的代码,但它们不应该直接相互通信。这应该通过包含它们的活动来完成。
你会注意到,我们在这里实现的 OnClickListener 与我们在上一章中实现的方式不同,在那里我们直接在要点击的视图中实现它。在这里,OnClickListener 是整个类的一部分。这种方法通常更受欢迎,尽管计算哪个小部件被点击需要额外的工作,但它通常是一个更整洁的解决方案,特别是对于非常复杂和交互式布局。
FragmentManager和FragmentTransaction是我们用来直接操作 Fragment 的工具。定义这些工具的前两行设置了正在进行的交易,尽管在调用commit()之前没有采取任何行动。我们调用replace(layout, fragment)在显示之间切换,但也可以使用具有相同参数的add()或仅使用 Fragment 的remove()。
使用addToBackStack()非常重要,因为没有它,用户在设备上按下返回按钮将返回到上一个 Activity 而不是上一个 Fragment,而这通常是我们想要的。另一个有趣的注意点是,管理器和交易命令可以链式调用,如果我们想的话,我们可以用这一行替换所有六行:
getFragmentManager().beginTransaction()
.transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.replace(R.id.fragment_container, fragment)
.addToBackStack(null)
.commit();
淡入淡出过渡并不是严格必要的,因为系统通常智能地处理 Activity 和 Fragment 之间的过渡,但这里将没有过渡。FragmentManager 和 FragmentTransaction 还有很多可以做的,完整的文档可以在developer.android.com/reference/android/app/FragmentManager.html和developer.android.com/reference/android/app/FragmentTransaction.html找到。接下来,我们需要看看在应用中应用 Fragment 的另一种方式。
添加静态 Fragment
我们不能不快速看一下他们可以实现的另一种方式,即在 XML 文件中定义为<fragment>标签的静态布局。尽管这些 Fragment 缺乏我们刚刚遇到的动态排序的灵活性,但它们仍然非常有用,尤其是在复杂的多面板应用中,不同的 Fragment 执行非常不同的功能。这不仅有助于保持我们的代码组织,而且比复杂的嵌套 ViewGroups 更节省资源。
要最好地了解如何实现,请使用带有 Fragment 的空白 Activity模板启动一个项目,并查看activity_main.xml文件。
<fragment
android:id="@+id/fragment"
android:name="com.example.kyle.staticfragmentexample.MainActivityFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_main" />
这演示了如何将静态的 Fragment 包含在标准布局文件中。它显示了使用tools:layout在其中展开哪个布局,以及哪个 Java 类通过android:name来控制它。可以通过这种方式组合多个 Fragment,以及 ViewGroups 和 Views 来创建复杂的 Activity。
值得一看的是这个模板中的 Java 类和其他 XML 文件,以了解其他组件是如何工作的。现在你将熟悉其中大部分内容,因为我们刚刚完成了这项工作。现在不要关闭项目,因为我们将要使用它来查看如何添加菜单。
添加菜单和工具栏
几乎所有移动应用都包含某种形式的全局菜单,该菜单提供对整个应用所需功能的访问。在 Android 应用中,菜单可以通过多种方式打开,但最常用的方法是选项菜单,它可以通过工具栏或操作栏访问。选项菜单项也可以作为文本或图形出现在工具栏上。
首先,我们将添加一个基本的下拉工具栏菜单来实现两个按钮当前提供的功能。从片段部分打开项目,并按照以下步骤操作:
-
打开
res/menu/menu_main.xml文件。 -
将现有的
<item>标签替换为以下三个:<item android:id="@+id/menu_date" android:orderInCategory="100" android:showAsAction="never" android:title="Date and Time" /> <item android:id="@+id/menu_location" android:orderInCategory="100" android:showAsAction="never" android:title="Location" /> <item android:id="@+id/menu_sleep" android:orderInCategory="100" android:showAsAction="never" android:title="Sleep" /> -
可以通过打开预览窗格以与布局相同的方式预览菜单:
![添加菜单和工具栏]()
-
打开
MainActivity文件,找到onOptionsItemSelected()方法。 -
重写如下:
@Override public boolean onOptionsItemSelected(MenuItem item) { Fragment fragment; int id = item.getItemId(); switch (id) { case R.id.menu_date: startActivity(new Intent(android.provider.Settings.ACTION_DATE_SETTINGS)); break; case R.id.menu_location: startActivity(new Intent(android.provider.Settings.ACTION_LOCALE_SETTINGS)); break; case R.id.menu_sleep: startActivity(new Intent(android.provider.Settings.ACTION_SOUND_SETTINGS)); break; } return super.onOptionsItemSelected(item); } -
在设备上运行应用程序,并使用菜单打开日期、地区和音量设置。
将菜单项添加到菜单 XML 文件中
每个项目都需要一个标题和一个 ID。我们可以使用 orderInCategory 属性来改变项目出现的顺序,该属性使用升序整数值对项目进行排序,从上到下(以及在工具栏上从左到右)排序。菜单项可以通过嵌套在 <group> 标签内来分别进行分类和排序。
小贴士
子菜单可以通过嵌入一个 <menu> 标签来创建,其中包含自己的项目,这些项目位于 <item> 标签内。
如我们很快将看到的,菜单项可以在工具栏上以与 API 21 以下版本的 Android 中 Action bar 上相同的方式显示。这可以通过 showAsAction 属性来完成;如果您仍然打开了项目,将此值设置为 always 和 ifRoom 再次运行应用程序是值得的,以查看这种设置的效果。这样做只是将我们的菜单选项移动到栏上,但我们也可以使用图标来表示我们的选项;这就是我们将要探索的 Android 5 工具栏的新特性。
配置工具栏
如前所述,Android 5 引入了许多应用屏幕顶部 Action bar 的替代品:工具栏。工具栏确实执行了其前身所有的相同功能,例如显示菜单选项和其他频繁执行的操作,但可定制性更高。最有趣的是,工具栏现在可以放置在屏幕的任何位置。
在接下来的练习中,我们将把我们的菜单添加到工具栏上作为图标;在工具栏上包含导航、标志和标题;然后将它放置在屏幕底部。
工具栏图标需要遵守一些特定的材料设计指南,完整的指南可以在www.google.com/design/spec/style/icons.html找到。基本上,它们需要在透明背景上的简单、单色符号。起初,你可以从www.google.com/design/icons/下载为特定屏幕密度专门设计的系统图标。以下是本项目下载的三个图标,以及在下一次练习中使用的名称。

下载或找到适合你正在为的设备的像素密度的类似图片。然后执行以下步骤:
-
打开我们刚刚正在工作的项目,并将你的图标复制到
drawable文件夹中。 -
打开
menu_main.xml文件,并在menu_date项中添加以下行:android:icon="@drawable/time" -
在各自的项标签中为位置和睡眠图标做同样的操作。
-
将所有三个项的
android:showAsAction属性从"never"更改为"ifRoom"。 -
如果你想要看看你的品牌颜色与操作图标对比的效果,就像我们之前做的那样,应用一个材料主题,并运行应用来查看图标的外观。
![配置工具栏]()
-
现在打开
main_activity.xml文件。 -
在两个按钮上方插入这个
Toolbar:<Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?android:attr/colorPrimary" android:elevation="4dp" android:minHeight="?android:attr/actionBarSize" /> -
打开
res/values/styles/styles.xml (v21)文件,并像这样编辑AppTheme定义:name="AppTheme" parent="android:Theme.Material.Light.NoActionBar" -
你需要为这里的标志准备另一张图片。这张图片不必简单朴素,但不应该包含文字,并且大小约为 96 x 96 px,具体取决于你设备的像素密度。如果你想节省时间,可以在第 11 步的
setLogo()命令中使用其他图片之一。 -
打开
MainActivity文件并添加以下字段:Private Toolbar toolbar; -
然后,将以下行添加到
onCreate()方法中:toolbar = (Toolbar) findViewById(R.id.toolbar); setActionBar(toolbar); getActionBar().setTitle("Clock"); toolbar.setSubtitle("tells the time"); toolbar.setLogo(R.drawable.clock_logo); -
在
activity_main.xml文件中,从根布局中移除填充。 -
就这样。你现在可以运行应用,输出结果可能如下所示:
![配置工具栏]()
将选项菜单项添加到操作栏的操作很简单,只需将showAsAction设置为ifRoom或always,并且我们可以通过withText同时包含文本和图标。这为向用户展示选项提供了一个方便的方法,但一旦我们可以在布局中定义它并从 Java 中引用它,它就变成了一个功能更强大的工具。这意味着我们可以在屏幕上的任何位置放置它,并在其中放置任何内容。我们可以通过在 XML 中的Toolbar元素内添加该元素来添加ImageView或Button。一旦我们在 Java 中有了引用,我们就可以添加点击监听器或任何其他方法,就像我们处理其他组件一样。
我们需要通过将主题更改为Material.Light.NoActionBar来移除原始的工具栏,尽管我们可以保留它并添加一个工具栏,当然,我们也可以有多个工具栏,甚至可能包含在一个Fragment中,相互替换以创建一个更动态的界面。我们还必须从父布局中移除填充,以便它紧贴边缘,就像传统的工具栏一样,并设置其高度,使其看起来像我们习惯的工具栏。
摘要
在本章中,我们涵盖了大量的内容,从介绍CardView小部件以及 Android 5 中引入的新特性开始,例如提升它和其他视图,使它们看起来像是浮在屏幕之上。我们看到了如何向项目中添加图像文件,以及如何优化这些图像以高效地匹配用户设备的屏幕密度。我们还看到了如何包含新的活动,以及如何使用 XML(以及 Java)来控制小部件和视图在被点击时的行为。本章还涵盖了活动之间相互通信的许多方法之一。
我们继续探索使用Fragment类来增加我们应用灵活性的另一种方法,这些类具有许多活动功能,但可以组合在一个布局中,并像 ViewGroups 一样处理。
最后,我们研究了选项菜单和工具栏之间的关系,并看到了 Android 5 中引入的工具栏如何被视为我们活动的一部分,而不是固定在顶部的独立小部件。
在掌握了 Android 编程的一些重要基础知识之后,我们现在可以进一步考虑如何构建更复杂的应用程序,这些应用程序包含更大的数据集,以及如何利用今天移动设备中找到的更多技术。
第四章. 管理 RecyclerView 和其数据
在上一章中,我们看到了如何处理多个 Activity,但显示在两者中的数据都是静态的。为了进步,我们需要一种将数据选择应用于预定义布局的方法,并理想情况下将其作为列表的一部分展示。Android 5 引入了 RecyclerView——这是之前使用的 ListView 的更高效和灵活的版本。要实现 RecyclerView,我们需要一个 LayoutManager、一个 Adapter 和一些数据来工作。在本章中,我们将启动一个新应用程序,创建一个 CardView 列表,每个都显示相关数据,并作为获取更详细信息链接。
在本章中,我们将涵盖以下主题:
-
创建 RecyclerView
-
设计 CardView 布局
-
包含 LayoutManager
-
创建数据和适配器
-
添加 ViewHolder
-
响应 RecyclerView 的选择
-
将视图连接到网页
创建 RecyclerView
我们新项目的 MainActivity 将完全由嵌套在根布局中的 RecyclerView 组成,构建起来非常快且简单。然而,与 CardView 类似,它是 Android 5 的新特性,因此,它是 V7 支持库的一部分,并且还需要修改一个 Gradle 构建文件,以便它能够工作。以下步骤演示了如何创建 RecyclerView:
-
在 Android Studio 中启动一个新项目。给它一个应用程序名称为
Ancient Britain;勾选手机和平板电脑复选框并选择空白活动。 -
打开 Gradle 脚本文件,
build.gradle(模块 app)并添加以下两个依赖项:dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:cardview-v7:21.0.+' compile 'com.android.support:recyclerview-v7:21.0.+' } -
在
res/layout中的activity_main.xml文件中,将TextView标签替换为以下代码:<android.support.v7.widget.RecyclerView android:id="@+id/main_recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" /> -
以我们之前章节中的方式创建一个颜色主题。
-
重新构建项目。
就这么简单!我们的 RecyclerView 现在填充了屏幕,除了自动设置为 Google 推荐设计标准的边距之外。在 Gradle 构建脚本中添加依赖项是我们已经熟悉的事情,无需解释。text_secondary 颜色名称将设置较小的文本为深灰色。
添加带有布局的 CardView
我们的 Recycler 列表将由 CardViews 组成,每个都符合我们接下来要设计的布局。在本节结束时,我们的主 Activity 将看起来像这样:

我们需要做的第一件事是设计我们的 CardView。
-
在项目资源管理器中的 layout 上右键单击,然后从菜单中选择 新建 | 布局资源文件。
-
命名为
card_main并确保其根元素是一个水平的LinearLayout。 -
设计一个类似于上图中的卡片布局,具有以下组件结构和 ID。
![添加带有布局的 CardView]()
小贴士
可以通过拖放直接从组件树中移动和重新排序布局组件。
-
将两个内部的
LinearLayout和ImageView实例定位如下:![添加带有布局的 CardView]()
-
在本节的剩余部分,根据您的喜好设置边距和/或填充。当
layout_width和layout_height没有提及时,使用wrap_content。 -
根据您的喜好设置圆角半径和高度,但请确保以下属性包含在
CardView元素中:android:id="@+id/card_view" android:layout_width="match_parent" android:layout_height="100dp" -
确保以下属性包含在
ImageView中:android:id="@+id/card_image" android:layout_width="0dp" android:layout_weight="2" android:src="img/li>Set these properties in the inner (vertical) `LinearLayout`:android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="5"
android:orientation="vertical"
Include these settings in the upper of the two `TextView` instances:android:id="@+id/card_name"
android:textAppearance="?android:attr/textAppearanceLarge"
Finally, set these properties in the lower `TextView`:android:id="@+id/card_info"
android:textAppearance="?android:attr/textAppearanceSmall"
在这里设置的属性大多数是我们熟悉的:最重要的是 id 属性,它允许我们通过 Java 访问。然而,一两个东西可能需要解释。
LinearLayout 允许我们根据每个视图的 layout_weight 属性分配成比例的屏幕空间。使用 layout_weight 而不是告诉视图尽可能多地占用空间(match_parent),或者尽可能少地占用空间(wrap_content),允许我们分配父视图组的特定比例。两个权重为 1 的视图将各自占用 50% 的空间,权重为 1 和 4 的视图将分别占用 20% 和 80%。在这里,我们使用了权重 2 和 5,这意味着 2/7 的可用空间分配给了图像,5/7 分配给了垂直布局。我们将布局宽度设置为 0dp 以避免干扰权重。
注意
可绘制资源 ic_launcher.png 仅被用作占位符,以方便设计布局;它将在代码中稍后替换。
我们使用了 textAppearanceSmall 和 textAppearanceLarge 来设置我们的文本大小。这通常比使用特定的 dp 数量更可取,因为这些设置会自动调整以适应用户屏幕的大小。
注意
尽管使用权重在我们的布局中具有明显的优势,但需要指出的是,这可能会对性能产生不利影响,因为系统需要更频繁地重新计算其/他们的位置。
现在我们有了 RecyclerView 和 CardView 布局,我们可以继续使用 LayoutManager 将两者结合起来。
添加布局管理器
通过 RecyclerView.LayoutManager 将视图定位在 RecyclerView 中,它反过来与 RecyclerView.Adapter 通信,并将我们的数据绑定到视图。首先,让我们设置一个 LayoutManager。
-
打开
MainActivity.Java文件。 -
在类的顶部声明以下字段:
private RecyclerView recyclerView; public static int currentItem; -
在
onCreate()方法内部,添加以下这些行:recyclerView = (RecyclerView) findViewById(R.id.main_recycler_view); recyclerView.setHasFixedSize(true); RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this); recyclerView.setLayoutManager(layoutManager);
到现在为止,findViewById()的使用应该对我们来说已经很熟悉了。setFixedSize()方法的使用非常有用。如果你知道你的列表在运行时将保持相同的长度,那么将其设置为 true 将提高你应用程序的性能,因为它在项目不可见时巧妙地回收项目,这就是为什么它被称为RecyclerView。
给 RecyclerView 一个 LayoutManager 就像声明一个新的LinearLayoutManager,传递当前上下文,并使用RecyclerView.setLayoutManager()来设置连接。可以将LayoutManager视为属于 RecyclerView 的,它是视图与数据适配器通信的方式,而数据适配器反过来访问数据集,如这张图所示:

在我们继续创建我们的Adapter类之前,我们将为它设置一些数据来工作。
添加数据集
Android 系统利用 SQLite 来处理大型和复杂的数据集,我们将在本书的后续章节中回到这一点。对于这个项目,我们将使用 Java 创建我们所需的数据数组。以下是这样做的方法:
-
通过在项目资源管理器中右键单击MainActivity并从菜单中选择New | Java Class来创建一个新的 Java 类。将其命名为
MainDataDef。 -
按照以下内容填写它:
public class MainDataDef { int image; String name; String info; public MainDataDef(int image, String name, String info) { this.image = image; this.name = name; this.info = info; } public int getImage() { return image; } public String getName() { return name; } public String getInfo() { return info; } } -
如果你已经从 Packt 网站下载了项目文件,那么数据集将包含十个记录。然而,应用程序将能够很好地处理任何数量的记录,这里只包括前五个。创建另一个名为
MainData的 Java 类,并按照以下内容完成它:public class MainData { static Integer[] imageArray = {R.drawable.henge_icon, R.drawable.horse_icon, R.drawable.wall_icon, R.drawable.skara_brae_icon, R.drawable.tower_icon}; static String[] nameArray = {"Stonehenge", "Uffington White Horse", "Hadrian's Wall", "Skara Brae", "Tower of London"}; static String[] infoArray = {"Aylsbury, Wiltshire", "Uffington, Oxfordshire", "Cumbria - Durham", "Mainland, Orkney", "Tower Hamlets, London"}; static Integer[] detailImageArray = {R.drawable.henge_large, R.drawable.horse_large, R.drawable.wall_large, R.drawable.skara_brae_large, R.drawable.tower_large}; static Integer[] detailTextArray = {R.string.detail_text_henge, R.string.detail_text_horse, R.string.detail_text_wall, R.string.detail_text_skara, R.string.detail_text_tower}; static String[] detailWebLink = {"https://en.wikipedia.org/wiki/Stonehenge", "https://en.wikipedia.org/wiki/Uffington_White_Horse", "https://en.wikipedia.org/wiki/Hadrian%27s_Wall", "https://en.wikipedia.org/wiki/Skara_Brae", "https://en.wikipedia.org/wiki/Tower_of_London"}; } -
如果你已经下载了项目文件,你会看到
drawable目录包含图像:如果没有,你需要添加自己的。如果你正在使用上面看到的数据集,那么你需要十张图像,名称如下所示。确保*_icon.png文件大约是 160 x 160 px,而*_large.png文件大约是 640 x 480 px。![添加数据集]()
-
数据的最后一部分是由相当长的字符串组成。如果你已经下载了项目文件,这些可以在
strings.xml资源文件中找到,如果没有,那么你需要大约 100 个单词的五个字符串,名称如下:<string name="detail_text_henge"> <string name="detail_text_horse"> <string name="detail_text_wall"> <string name="detail_text_skara"> <string name="detail_text_tower">
将我们的数据数组组合起来相当直接,但值得注意的是我们如何使用整数来引用图像和字符串,利用自动生成的R类,该类将每个单独的资源与一个静态整数关联。这可以通过在app | package name下选择Packages来找到。它不能被编辑,但查看其工作原理是有帮助的。
我们使用strings.xml文件来存储长字符串。对于长文本来说,这并不实用,我们通常会把这些资源作为文本文件存储在原始资源文件夹中,这一点我们将在本书的后续章节中介绍。
Android TextView 能够处理基本的标记格式化标签,如 <b></b> 和 <i></i>。以下是一些可用的格式化标签列表:
<big></big>
<b></b>
<i></i>
<small></small>
<strike></strike>
<sup></sup>
<sub></sub>
<tt></tt>
<u></u>
在数据就绪后,我们现在可以着手创建我们的数据适配器。
创建一个 Adapter
RecyclerView.Adapter 负责将我们的数据绑定到我们的视图上。我们通过另一个 RecyclerView 子类,即 ViewHolder 来控制这一过程,我们将在 Adapter 内部创建它。这可以通过以下步骤实现:
-
在我们的项目中创建一个名为
MainAdapter的新 Java 类,与其它类并列。 -
将类声明更改为以下内容:
public class MainAdapter extends RecyclerView.Adapter<MainAdapter.MainViewHolder> { -
直接在此之下,键入:
private ArrayList<MainDataDef> mainData; public MainAdapter(ArrayList<MainDataDef> a) { this.mainData = a; } public static class MainViewHolder extends RecyclerView.ViewHolder { -
这最后一行将生成一个错误,由红色下划线指示。将光标放在类声明上的某个位置并按 Alt + Enter。
-
从下拉列表中选择 Implement Methods 并然后选择这里显示的所有三个方法:
![创建一个 Adapter]()
-
在此之下,添加以下类:
public static class MainViewHolder extends RecyclerView.ViewHolder { ImageView imageIcon; TextView textName; TextView textInfo; public MainViewHolder(View v) { super(v); this.imageIcon = (ImageView) v.findViewById(R.id.card_image); this.textName = (TextView) v.findViewById(R.id.card_name); this.textInfo = (TextView) v.findViewById(R.id.card_info); } } -
完成以下
onCreateViewHolder()方法:@Override public MainViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.card_main, parent, false); v.setOnClickListener(MainActivity.mainOnClickListener); return new MainViewHolder(v); } -
然后,
onBindViewHolder()方法:@Override public void onBindViewHolder(final MainViewHolder holder, final int position) { ImageView imageIcon = holder.imageIcon; TextView textName = holder.textName; TextView textInfo = holder.textInfo; imageIcon.setImageResource(mainData.get(position).getImage()); textName.setText(mainData.get(position).getName()); textInfo.setText(mainData.get(position).getInfo()); } -
然后,
getItemCount()方法:@Override public int getItemCount() { return mainData.length(); } -
打开
MainActivity文件。 -
在
onCreate()方法的底部添加以下代码:ArrayList<MainDataDef> mainData = new ArrayList<MainDataDef>(); for (int i = 0; i < MainData.nameArray.length; i++) { mainData.add(new MainDataDef( MainData.imageArray[i], MainData.nameArray[i], MainData.infoArray[i] )); } RecylerView.Adapter adapter = new MainAdapter(mainData); recyclerView.setAdapter(adapter); -
您现在可以在模拟器或手机上测试项目。
Adapter 的大部分工作由 ViewHolder 执行。这个类负责,正如其名称所暗示的,保存关于我们 RecyclerView 中每个视图的信息,包括视图的元数据和它在列表中的位置。我们使用类定义和构造函数来定义与我们的卡片布局相关联的三个视图。ViewHolder 需要三个回调方法,onCreateViewHolder(),它膨胀 CardView 并执行任何其他操作,如添加 onClickListener、onBindViewHolder()。这个方法接受 ViewHolder 的卡片内部视图版本并将它们连接到我们的数据,以及 getItemCount(),它返回我们列表的长度。
最后,为了将数据连接到 RecyclerView,我们构建我们的 ArrayList mainData,然后从 mainData 设置一个新的 MainAdapter 并使用 setAdapter() 方法将 RecyclerView 连接到其 Adapter。
在 RecyclerView 就位并连接到我们的数据集与 Adapter 后,我们准备添加一个点击监听器和第二个 Activity。
响应 RecyclerView 选择
成功创建了一个 RecyclerView,并用包含我们数据的 CardViews 填充后,我们需要能够选择单个项目并对它们进行操作。接下来,我们将为 RecyclerView 提供一个 点击监听器 并添加一个新的 Activity,该 Activity 将在单独的屏幕上以更详细的方式展示我们的记录。
创建 OnClickListener
在我们创建新的 Activity 之前,我们需要一个 OnClickListener,它可以告诉我们哪个 CardView 被点击了。以下是实现方式:
-
打开
MainActivity类。 -
添加以下类成员:
static View.OnClickListener mainOnClickListener; -
在
onCreate()方法中分配监听器,如下所示:mainOnClickListener = new MainOnClickListener(this); -
创建以下类:
private class MainOnClickListener implements View.OnClickListener { private final Context context; private MainOnClickListener(Context c) { this.context = c; } @Override public void onClick(View v) { currentItem = recyclerView.getChildPosition(v); startActivity(new Intent(getApplicationContext(), DetailActivity.class)); } }
这与我们之前实现 OnClickListener 的方式非常相似,只是在这里,我们不是将其分配给单个视图,而是使其对整个类可用。这意味着我们必须以某种方式构建监听器,以便传递调用视图的 Context 对象。在 onClick() 方法中,我们可以使用 RecyclerView 的 getChildPosition() 方法记录哪个视图被点击。完成此操作后,现在只需创建新的 Activity。
添加新的 Activity
我们最后输入的方法将生成一个错误消息,因为类 DetailActivity 尚未存在。该 Activity 将显示有关我们古代遗址的更多详细信息,并为每个遗址的维基百科页面提供网页链接。
创建肖像布局
如常,创建 Android Activity 有两个部分,在我们编写代码之前,我们需要定义其布局。为此,请按照以下步骤操作:
-
在
layout目录中创建一个新的垂直LinearLayoutXML 文件,并将其命名为activity_detail。 -
在设计模式下,创建一个与以下组件树匹配的布局,并为视图提供 ID 和内容,如下所示:
![创建肖像布局]()
-
如果您尚未下载项目文件,找到一个或创建一个适合用作网页图标的小图像,大小约为 48 x 48 px。将其命名为
web_icon.png并将其放置在您的drawable目录中。 -
调整视图的属性以匹配此处显示的结构:
![创建肖像布局]()
-
在 ImageView
detail_image上设置以下属性:android:layout_height="0dp" android:layout_gravity="center_horizontal" android:layout_weight="3" -
将
android:textAppearance="?android:attr/textAppearanceLarge"添加到detail_nameTextView: -
TextView
detail_distance应设置android:textAppearance="?android:attr/textAppearanceMedium"。 -
detail_textTextView 需要以下属性:android:layout_height="0dp" android:layout_weight="2" android:textAppearance="?android:attr/textAppearanceSmall" android:maxLines = "100" android:scrollbars = "vertical" -
使用
detail_web_iconImageView,将layout_gravity设置为"right"。
在此过程中,我们没有遇到之前未遇到的内容,除了 detail_text TextView 中的两个属性 maxLines 和 scrollbars,这些属性是自解释的。我们分配给此视图的文本可能对于许多较小的屏幕来说可能太长,因此我们已设置此视图为可滚动。我们还需要在 Java 中添加一行代码以完全实现此功能,但我们将很快介绍这一点。
使用 layout_weight 定义我们的布局意味着,即使我们旋转屏幕,所有视图都将保持可见。尽管如此,如果您在设备上测试此布局,您会发现这并不是一个吸引人或者空间效率高的布局。接下来,我们将重新设计横幅布局,使其更适合适应方向。
创建横幅布局
我们设计的垂直线性布局并不适合横屏屏幕。为了最佳地填充空间,仅仅重新排列视图是不够的,我们需要将根布局更改为水平方向,并插入一个垂直布局来放置文本和网页图标。按照以下步骤创建布局:
-
如果尚未打开,请以设计模式或文本模式(预览窗格打开)打开
activity_detail.xml文件。 -
从预览下拉菜单中选择 创建横屏变体,如下所示:
![创建横屏布局]()
-
以文本模式打开
detail_activity.xml(横屏)文件。 -
在
detail_imageImageView 中,修改以下行:android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="center_vertical" -
插入以下垂直线性布局,使其包含三个 TextView 和其他 ImageView:
<LinearLayout android:orientation="vertical" android:layout_width="0dp" android:layout_weight="3" android:layout_height="fill_parent"> < TextView ... /> < TextView ... /> < TextView ... /> < ImageView ... /> </LinearLayout> -
修改
detail_textTextView 中的这些行:android:layout_height="0dp" android:layout_weight="2" android:layout_gravity="left" -
然后,添加以下内容:
android:layout_marginLeft="8dp"
现在的横屏布局应该看起来像这样:

这里没有我们之前没有遇到的内容。然而,值得注意的是,虽然这将花费更多时间,但我们本可以使用 RelativeLayout 来实现这个目的,如果我们那样做了,我们就不需要内部的垂直 LinearLayout。ViewGroup,如布局,需要相当多的内存,尽管对于像这样的小应用程序来说这并不重要,但将视图容器的数量保持在最低限度是一个好的做法。将 activity_detail.xml(横屏)重构成 RelativeLayout 作为读者的练习。
现在只剩下创建 Java Activity 类 ActivityDetail,它将根据点击的卡片选择并显示其内容。
连接视图到网页
ActivityDetail 类并不复杂,尽管我们确实需要添加代码的 Java 组件,这将使我们的 TextView 可滚动,并且我们还可以看到如何使用 Intent 对象来启动浏览器并打开特定的网页。按照以下步骤完成 Activity:
-
在项目资源管理器中,通过右键单击我们已构建的任何类,通过选择 新建 | Activity | 空白 Activity 创建一个新的 Activity,将文件命名为
DetailActivity并接受对话框中建议的其他值。 -
打开
DetailActivityJava 代码。 -
将
onCreate()方法中的这一行setContentView(R.layout.activity_detail2);改为以下内容:setContentView(R.layout.activity_detail); -
当你创建 Activity 时,IDE 将创建一个匹配的布局文件
activity_detail2。通过在项目资源管理器中选择该文件并按 Delete 键来删除此文件。 -
添加以下全局字段:
private ImageView detailImage; private ArrayList<MainDataDef> detailData; -
按照以下方式分配它们:
detailImage = (ImageView) findViewById(R.id.detail_image); TextView detailName = (TextView) findViewById(R.id.detail_name); TextView detailDistance = (TextView) findViewById(R.id.detail_distance); TextView detailText = (TextView) findViewById(R.id.detail_text); detailText.setMovementMethod(new ScrollingMovementMethod()); ImageView detailWebLink = (ImageView) findViewById(R.id.detail_web_link); -
仍然在
onCreate()中,添加以下行以将我们的视图链接到我们的数据:int i = MainActivity.currentItem; Random n = new Random(); int m = n.nextInt((600 - 20) + 1) + 20; setTitle(getString(R.string.app_name) + " - " + MainData.nameArray[i]); detailImage.setImageResource(MainData.detailImageArray[i]); detailName.setText(MainData.nameArray[i]); detailDistance.setText(String.valueOf(m) + " miles"); detailText.setText(MainData.detailTextArray[i]); -
在此之下,添加以下行以编程方式设置网页链接:
detailWebLink.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.addCategory(Intent.CATEGORY_BROWSABLE); intent.setData(Uri.parse(MainData.detailWebLink[MainActivity.currentItem])); startActivity(intent); } }); -
在模拟器或手机上运行应用程序。希望第二个 Activity 将看起来像这样:
![连接视图到网页]()
活动向导自动为我们创建了一个布局文件,即使我们之前已经创建了一个,这也是为什么我们必须编辑 Activity 的 ContentView 并删除不需要的 XML 文件。当然,我们可以从头开始构建我们的类并避免这样做,但活动向导为我们构建了一个节省时间的类模板,包括所有必需的成员。
我们视图的命名和分配不需要解释。然而,这段代码还通过调用其 setMovementMethod(new ScrollingMovementMethod()) 方法提供了我们可滚动 TextView 的最后一部分。
我们使用公共字段 currentItem 来访问我们的数据,我们为了方便将其保存在同一个数组列表中。我们现在用随机数填充距离字段,但稍后当我们到达本书中的地理位置时,我们将返回并使这个功能正确运行。
将视图链接到网页是新的功能,需要解释说明。这是布局中唯一可以点击的视图,因此为它提供自己的 OnClickListener 是一件简单的事情。在这里,我们可以更详细地看到 Intent 对象,以及它有多么有用。setAction() 方法告诉 Intent 要执行哪个操作,形式为一个静态的、最终的 String,在这里是 ACTION_VIEW,这是最广泛使用的 Intent 操作,并且它告诉 Intent 以最合适的方式显示数据。这意味着,在大多数情况下,系统将选择正确的方式来显示数据,例如在 ImageView 中显示图片,在联系人应用中显示联系人,以及在默认浏览器中显示网页。
注意
注意,当我们需要知道已设置到 Intent 中的操作时,有一个相应的 Intent.getAction() 方法。
addCategory(Intent.CATEGORY_BROWSABLE) 调用并非绝对必要,在大多数情况下,即使没有它应用程序也能正常运行。Intent 类别使得 Intent 只考虑那些带有该类别的应用程序(或更准确地说,活动)来解析操作。要将应用程序放入一个或多个类别中,请将它们添加到清单文件中的 <intent-filter> 标签,如下所示:<category android:name="android.intent.category.CATEGORY"/>。Intent 对象定义了一系列类别常量,例如,APP_MUSIC, APP_MESSAGING, 和 APP_CALENDAR。
在放置了第二个 Activity 及其布局文件之后,我们现在可以显示有关我们古代遗址的详细信息,并提供一个链接到包含更多信息的网页。完成这些后,我们就完成了这个练习。
摘要
在本章中,我们构建了一个小但完整的应用程序。其结构并非由主题内容决定,而是可以应用于任何数量的应用程序。我们利用了 Android 最新的小部件,并生成了一个 CardView 的 RecyclerView 列表,展示了简单的数据集。在这个过程中,我们看到了如何通过适配器将我们的数据连接到我们的视图,以及适配器如何反过来使用 ViewHolder 来维护我们的单个视图和布局。添加OnClickListener允许用户选择数据项,并通过链接到网站的新屏幕导航到更多信息。
接下来,我们将更深入地探讨 RecyclerView,看看如何移动列表中的项目上下移动。为此,我们首先需要学习如何使用OnTouchListener检测用户的滑动手势。
第五章. 检测触摸屏手势
到目前为止,我们创建的应用程序都使用了 OnClickListeners 来检测用户输入。然而,Android 手机能够处理复杂的触摸屏手势。这些输入通过 OnTouchListener 捕获,然后由 GestureDetector 管理。这些检测器和它们自己的监听器能够识别几种最简单和最常用的手势,例如 长按、双击 和 滑动。所有触摸屏事件的核心是 MotionEvent 类,它处理手势的各个元素,例如手指何时何地放置或从屏幕或视图中移除。这个类提供了查询这些事件的众多类,因此可以构建我们自己的自定义手势。
为了了解如何在古代英国应用程序中实现手势,我们将添加一个功能,允许用户通过在视图中滑动来查看同一 ImageView 中的小图库。
在本章中,我们将学习如何:
-
将
GestureDetector添加到视图中 -
添加
OnTouchListener和OnGestureListener -
检测和细化滑动手势
-
使用 DDMS Logcat 来观察
MotionEvent类 -
编辑 Logcat 过滤器配置
-
使用
SimpleOnGestureListener简化代码 -
将
GestureDetector添加到 Activity 中 -
编辑 Manifest 来控制启动行为
-
隐藏 UI 元素
-
创建启动画面
-
锁定屏幕方向
将 GestureDetector 添加到视图中
一起,view.GestureDetector 和 view.View.OnTouchListener 就是我们为 ImageView 提供手势功能所需的所有内容。监听器包含一个 onTouch() 回调,它将每个 MotionEvent 传递给检测器。我们将编程大型的 ImageView 以便它能够显示一个小图库,用户可以通过在图像上左右滑动来访问这些图片。
这个任务有两个步骤,因为在实现我们的手势检测器之前,我们需要提供它工作的数据。
添加图库数据
由于这个应用程序是用于演示和学习目的,因此我们可以尽可能快地进步,所以我们只为项目中的一个或两个古代遗址提供额外的图片。以下是操作方法:
-
打开古代英国项目。
-
打开
MainData.java文件。 -
添加以下数组:
static Integer[] hengeArray = {R.drawable.henge_large, R.drawable.henge_2, R.drawable.henge_3, R.drawable.henge_4}; static Integer[] horseArray = {}; static Integer[] wallArray = {R.drawable.wall_large, R.drawable.wall_2}; static Integer[] skaraArray = {}; static Integer[] towerArray = {}; static Integer[][] galleryArray = {hengeArray, horseArray, wallArray, skaraArray, towerArray}; -
要么从 Packt 网站下载项目文件,要么找到四张你自己的图片(大约 640 x 480 像素)。将它们命名为
henge_2、henge_3、henge_4和wall_2并将它们放置在res/drawable目录中。
这一切都非常直接,伴随的代码允许你拥有任意长度的单个数组。这是我们添加到图库数据中所需的所有内容。现在,我们需要编写我们的 GestureDetector 和 OnTouchListener。
添加 GestureDetector
除了我们将为ImageView定义的OnTouchListener之外,GestureDetector 还有它自己的监听器。在这里,我们将使用GestureDetector.OnGestureListener来检测滑动手势并收集描述它的MotionEvent。
按照以下步骤编程ImageView以响应滑动手势:
-
打开
DetailActivity.java文件。 -
声明以下类字段:
private static final int MIN_DISTANCE = 150; private static final int OFF_PATH = 100; private static final int VELOCITY_THRESHOLD = 75; private GestureDetector detector; View.OnTouchListener listener; private int ImageIndex; -
在
onCreate()方法中,像这样分配detector和listener:detector = new GestureDetector(this, new GalleryGestureDetector()); listener = new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return detector.onTouchEvent(event); } }; -
在此之下,添加以下行:
ImageIndex = 0; -
在
detailImage = (ImageView) findViewById(R.id.detail_image);行之下,添加以下行:detailImage.setOnTouchListener(listener); -
创建以下内部类:
class GalleryGestureDetector implements GestureDetector.OnGestureListener { } -
在处理由此产生的错误之前,向类中添加以下字段:
private int item; { item = MainActivity.currentItem; } -
在注册错误的行上点击任何位置,然后按Alt + Enter。然后选择实现方法,确保已勾选复制 JavaDoc和插入 @Override框。
![添加 GestureDetector]()
-
按照以下方式完成
onDown()方法:@Override public boolean onDown(MotionEvent e) { return true; } -
填写
onShowPress()方法:@Override public void onShowPress(MotionEvent e) { detailImage.setElevation(4); } -
然后填写
onFling()方法:@Override public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { if (Math.abs(event1.getY() - event2.getY()) > OFF_PATH) return false; if (MainData.galleryArray[item].length != 0) { // Swipe left if (event1.getX() - event2.getX() > MIN_DISTANCE && Math.abs(velocityX) > VELOCITY_THRESHOLD) { ImageIndex++; if (ImageIndex == MainData.galleryArray[item].length) ImageIndex = 0; detailImage.setImageResource(MainData.galleryArray[item][ImageIndex]); } else { // Swipe right if (event2.getX() - event1.getX() > MIN_DISTANCE && Math.abs(velocityX) > VELOCITY_THRESHOLD) { ImageIndex--; if (ImageIndex < 0) ImageIndex = MainData.galleryArray[item].length - 1; detailImage.setImageResource(MainData.galleryArray[item][ImageIndex]); } } } detailImage.setElevation(0); return true; } -
在模拟器或手机上测试项目。
上述代码中手势检测的过程开始于OnTouchListener监听器的onTouch()方法被调用。然后它将那个MotionEvent传递给我们的手势检测类GalleryGestureDetector,该类监控运动事件,有时将它们串联起来并计时,直到检测到已识别的手势。在此点,我们可以输入自己的代码来控制我们的应用如何响应,就像这里使用onDown()、onShowPress()和onFling()回调那样。依次查看这些方法是有益的。
初看之下,onDown()方法可能显得多余;毕竟,我们试图捕捉的是滑动手势。实际上,覆盖onDown()方法并从中返回true对于所有手势检测都是必要的,因为所有手势都以一个onDown()事件开始。
onShowPress()方法的目的也可能不清楚,因为它似乎比onDown()做得多一点。正如JavaDoc所述,此方法对于向用户添加某种形式的反馈,确认他们的触摸已被接收很有用。Material Design 指南强烈建议这种反馈,并且在这里我们稍微提高了视图的抬升。
在不包括我们自己的代码的情况下,onFling() 方法将几乎识别任何在边界视图内结束于用户手指抬起的行为,无论方向或速度如何。我们不希望非常小或非常慢的动作导致动作发生;此外,我们还想能够区分垂直和水平移动以及左右滑动。MIN_DISTANCE 和 OFF_PATH 常量以像素为单位,VELOCITY_THRESHOLD 以像素每秒为单位。这些值需要根据目标设备和个人偏好进行调整。onFling() 中的第一个 MotionEvent 参数指的是前面的 onDown() 事件,就像任何 MotionEvent 一样,其坐标可以通过其 getX() 和 getY() 方法获得。
注意
MotionEvent 类包含数十个用于查询各种事件属性的实用类——例如,getDownTime(),它返回自当前 onDown() 事件以来的毫秒数。
在这个例子中,我们使用了 GestureDetector.OnGestureListener 来捕获我们的手势。然而,GestureDetector 有三个这样的嵌套类,其他两个是 SimpleOnGestureListener 和 OnDoubleTapListener。SimpleOnGestureListener 提供了一种更方便的方式来检测手势,因为我们只需要实现那些与我们感兴趣捕获的手势相关的方法。我们将很快编辑我们的 Activity,使其实现 SimpleOnGestureListener,这样我们就可以整理我们的代码并移除我们不需要的四个回调。之所以选择这种绕路的方式,而不是一开始就应用简单的监听器,是为了看到通过手势监听器我们可以获得的所有手势,并展示 JavaDoc 注释有多么有用,尤其是如果我们对框架还不熟悉的话。例如,看看下面的截图:

另一个非常实用的工具是 Dalvik Debug Monitor Server (DDMS),它允许我们在应用运行时查看应用内部的情况。我们的手势监听器的工作原理是一个很好的观察点,因为它的许多方法都是不可见的。
使用 DDMS 查看手势活动
要使用 DDMS 查看 OnGestureListener 的工作原理,我们首先需要创建一个标签来识别我们的消息,然后创建一个过滤器来查看它们。以下步骤演示了如何进行操作:
-
打开
DetailActivity.java文件。 -
声明以下常量:
private static final String DEBUG_TAG = "tag"; -
在
onDown()方法内添加以下行:Log.d(DEBUG_TAG, "onDown"); -
在
onShowPress()方法中添加行Log.d(DEBUG_TAG, "onShowPress");,并为我们的每个OnGestureDetector方法做同样的事情。 -
在
onFling()的适当子句中添加以下行:Log.d(DEBUG_TAG, "left"); Log.d(DEBUG_TAG, "right");从窗口底部的 Android 选项卡打开 Android DDMS 窗格,或者按 Alt + 6。
![使用 DDMS 查看手势活动]()
-
如果logcat不可见,可以通过右上角下拉菜单右侧的图标打开。
-
点击此下拉菜单并选择编辑过滤器配置。
-
按照以下截图完成对话框:
![使用 DDMS 查看手势活动]()
-
你现在可以在手机或模拟器上运行项目,并在 Logcat 中查看哪些手势被触发以及如何。你的输出应该类似于这里:
02-17 14:39:00.990 1430-1430/com.example.kyle.ancientbritain D/tag﹕ onDown 02-17 14:39:01.039 1430-1430/com.example.kyle.ancientbritain D/tag﹕ onSingleTapUp 02-17 14:39:03.503 1430-1430/com.example.kyle.ancientbritain D/tag﹕ onDown 02-17 14:39:03.601 1430-1430/com.example.kyle.ancientbritain D/tag﹕ onShowPress 02-17 14:39:04.101 1430-1430/com.example.kyle.ancientbritain D/tag﹕ onLongPress 02-17 14:39:10.484 1430-1430/com.example.kyle.ancientbritain D/tag﹕ onDown 02-17 14:39:10.541 1430-1430/com.example.kyle.ancientbritain D/tag﹕ onScroll 02-17 14:39:11.091 1430-1430/com.example.kyle.ancientbritain D/tag﹕ onScroll 02-17 14:39:11.232 1430-1430/com.example.kyle.ancientbritain D/tag﹕ onFling 02-17 14:39:11.680 1430-1430/com.example.kyle.ancientbritain D/tag﹕ right
DDMS是我们调试应用和查看底层发生情况时不可或缺的工具。一旦在代码中定义了日志标签,我们就可以为其创建一个过滤器,以便我们只看到我们感兴趣的消息。Log类包含几个方法,可以根据其重要级别报告信息。我们使用了Log.d,代表调试。所有这些方法都使用相同的两个参数:Log.method。这些方法的完整列表如下:
-
Log.v: 详细 -
Log.d: 调试 -
Log.i: 信息 -
Log.w: 警告 -
Log.e: 错误 -
Log.wtf: 非预期错误
注意
值得注意的是,在打包分发过程中,大多数调试信息将被忽略,除了详细消息;因此,在最终构建之前删除它们是至关重要的。
在了解了更多关于我们的手势检测器和监听器的内部工作原理之后,我们现在可以通过实现GestureDetector.SimpleOnGestureListener来从我们的代码中删除未使用的方法。
实现简单的 OnGestureListener
将我们的手势检测器从一类监听器转换为另一类非常简单。我们只需要更改类声明并删除不需要的方法。为此,请执行以下步骤:
-
打开
DetailActivity文件。 -
将我们的手势检测器类的类声明更改为以下内容:
class GalleryGestureDetector extends GestureDetector.SimpleOnGestureListener { -
删除
onShowPress()、onSingleTapUp()、onScroll()和onLongPress()方法。
这就是切换到SimpleOnGestureListener所需的所有操作。我们现在已经成功构建并编辑了一个手势检测器,允许用户浏览一系列图片。
注意
你可能已经注意到在手势监听器中没有onDoubleTap()方法。实际上,双击可以通过第三个GestureDetector监听器OnDoubleTapListener来处理,它的工作方式与另外两个非常相似。然而,谷歌在其 UI 指南中建议,在可能的情况下,应使用长按代替。
在继续处理多指事件之前,我们将看看如何通过向我们的项目中添加启动画面来将GestureDetector监听器附加到整个 Activity。在这个过程中,我们还将了解如何创建全屏 Activity 以及如何编辑Maniftest文件,以便我们的应用在启动时显示启动画面。
将 GestureDetector 添加到 Activity 中
我们迄今为止采用的方法允许我们将GestureDetector监听器附加到任何视图或视图组,当然,这也适用于ViewGroups,如Layouts。有时我们可能想要检测整个屏幕上的手势。为此,我们将创建一个可以通过长按来关闭的启动屏幕。
在实现手势检测器之前,我们需要做两件事:创建布局和编辑清单文件,以便应用能够使用我们的启动屏幕启动。
设计启动屏幕布局
整个活动处理手势与单个小部件处理手势之间的主要区别是,我们不需要OnTouchListener,因为我们可以覆盖活动自己的onTouchEvent()。以下是实现方法:
-
从项目资源管理器上下文菜单中创建一个新的空白活动,命名为
SplashActivity.java。 -
活动向导应该创建了一个相关的 XML 布局,名为
activity_splash.xml。打开它并使用文本选项卡查看。 -
从根布局中移除所有填充属性,使其看起来类似于以下内容:
<RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.kyle.ancientbritain.SplashActivity">在这里,我们需要一个图像作为启动屏幕的背景。如果您尚未从 Packt 网站下载项目文件,找到一个图像,大致与目标设备屏幕的大小和宽高比相似,将其上传到项目 drawable 文件夹,并命名为
splash。我使用的文件大小是 480 x 800 像素。:
![设计启动屏幕布局]()
-
移除向布局内放置的
TextView,并用此ImageView替换:<ImageView android:id="@+id/splash_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="img/splash"/> -
在此之下创建一个
TextView,例如以下内容:<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="40dp" android:gravity="center_horizontal" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="#fffcfcbd"/> -
添加以下文本属性:
android:text="Welcome to <b>Ancient Britain</b>\npress and hold\nanywhere on the screen\nto start"小贴士
为了节省时间向
strings.xml文件添加字符串资源,输入一个硬编码的字符串,例如前面的一个,并注意编辑器的警告,以便将字符串提取如下:![设计启动屏幕布局]()
在这个布局中,我们没有遇到任何之前没有遇到过的事情。我们移除了所有填充,以便我们的启动图像能够填充布局;然而,您将从预览中看到这似乎并不是情况。我们将在 Java 代码中的下一个步骤中处理这个问题,但我们需要首先编辑我们的清单,以便应用能够使用我们的SplashActivity启动。
编辑清单文件
配置AndroidManifest文件以便应用能够启动我们选择的任何活动非常简单;它是通过一个意图来实现的。当我们编辑清单时,我们还将配置显示以填充屏幕。只需按照以下步骤操作:
-
打开
res/values-v21/styles.xml文件并添加以下样式:<style name="SplashTheme" parent="android:Theme.Material.NoActionBar.Fullscreen"> </style> -
打开
AndroidManifest.xml文件。 -
将
MainActivity中的<intent-filter>元素剪切并粘贴到SplashActivity中。 -
包含以下属性,以便整个
<activity>节点看起来类似于以下内容:<activity android:name=".SplashActivity" android:theme="@style/SplashTheme" android:screenOrientation="portrait" android:configChanges="orientation|screenSize" android:label="Old UK" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
我们之前已经遇到过主题和样式,在这里,我们利用了一个为全屏活动设计的内置主题。在许多情况下,我们可能会在这里设计一个横幅布局,但正如启动屏幕通常所做的那样,我们使用android:screenOrientation属性锁定方向。
android:configChanges这一行实际上在这里并不需要,但包括它是有用的,因为了解它。配置任何这样的属性都可以防止系统在设备旋转或屏幕尺寸改变时自动重新加载 Activity。而不是 Activity 重新启动,onConfigurationChanged()方法会被调用。这里并不需要,因为屏幕尺寸和方向已经在之前的代码行中处理好了,这一行只是作为一个参考点。
最后,我们更改了android:label的值。你可能已经注意到,根据你使用的设备的屏幕尺寸,我们的应用程序名称在主屏幕或应用程序抽屉中可能无法完整显示。在这种情况下,当你想为你的应用程序使用简短名称时,它可以在这里插入。
在所有其他设置就绪的情况下,我们可以继续添加我们的手势检测器。这并不与之前我们做的方式完全不同,但这次,我们将检测器应用于整个屏幕,并将监听长按事件,而不是滑动事件。
添加 GestureDetector
除了在这里为整个 Activity 实现手势检测器之外,我们还将完成配置启动屏幕的最终步骤,以便图像填充屏幕,同时保持其宽高比。按照以下步骤完成应用程序的启动屏幕。
-
打开
SplashActivity文件。 -
声明一个
GestureDetector,就像我们在之前的练习中所做的那样:private GestureDetector detector; -
在
onCreate()方法中,按照以下方式分配和配置我们的启动图像和手势检测器:ImageView imageView = (ImageView) findViewById(R.id.splash_image); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); detector = new GestureDetector(this, new SplashListener()); -
现在,像这样覆盖 Activity 的
onTouchEvent()方法:@Override public boolean onTouchEvent(MotionEvent event) { this.detector.onTouchEvent(event); return super.onTouchEvent(event); } -
创建以下
SimpleOnGestureListener类:private class SplashListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(MotionEvent e) { return true; } @Override public void onLongPress(MotionEvent e) { startActivity(new Intent(getApplicationContext(), MainActivity.class)); } } -
在你的手机或模拟器上构建并运行应用程序。
到目前为止,整个 Activity 中手势检测器的实现方式应该已经熟悉,同样,长按事件的捕获也应该熟悉。ImageView.setScaleType(ImageView.ScaleType)方法在这里是必不可少的;这是一个非常实用的方法。CENTER_CROP常量将图像缩放以填充视图,同时保持宽高比,必要时裁剪边缘。

有几种类似的ScaleTypes,例如CENTER_INSIDE,它将图像缩放到最大可能的大小而不裁剪它,以及CENTER,它根本不缩放图像。CENTER_CROP的优点在于这意味着我们不需要为我们的应用最终将在其上运行的众多设备上的每一个可能的宽高比设计一个单独的图像。只要我们通过不将重要信息放置得太靠近边缘来为非常宽或非常窄的屏幕留出空间,我们只需要提供一些不同像素密度的图像,就可以在大分辨率设备上保持图像质量。
小贴士
ImageView的缩放类型可以通过 XML 设置,例如使用android:scaleType="centerCrop"。
你可能想知道为什么我们没有使用向导提供的内置全屏活动;我们本可以轻松做到这一点。向导为全屏活动创建的模板代码提供了比我们这个练习所需多得多的功能。尽管如此,这个模板仍然值得一看,尤其是如果你想要一个当用户与活动交互时将状态栏和其他组件显示出来的全屏。
这就带我们结束了本章的内容。我们不仅看到了如何使我们的应用与触摸事件和手势交互,还看到了如何向 IDE 发送调试信息以及如何创建全屏活动。
概述
我们在本章的开头通过向我们的项目中添加一个GestureDetector来开始。然后我们编辑它,以便能够过滤出有意义的触摸事件(在这个例子中是左右滑动)。接着我们看到了当只对捕获已识别手势的子集感兴趣时,SimpleOnGestureListener如何能为我们节省大量时间。我们还看到了如何使用 DDMS 在运行时传递调试信息,以及如何通过 XML 和 Java 的组合来隐藏状态栏和操作栏,使整个屏幕只填充一个视图或视图组。
接下来,我们将探索我们的应用如何通过框架提供的各种通知形式与用户通信。我们还将看到如何允许用户将他们自己的数据添加到应用中并配置一些设置。
第六章:通知和操作栏
在前几章中,我们一直在处理的垂直滚动RecyclerView是一个处理长列表的绝佳工具,其中每个项目中的数据不多,或者每个项目只显示整个信息的一小部分。当然,有时我们希望显示全屏信息,同时仍然将其作为列表的一部分。这就是ViewPager发挥作用的地方,因为它允许我们将称为Fragment的完整页面、迷你 Activity 链接在一起,这样我们可以通过简单的、直观的水平滑动手势在它们之间导航。
除了利用我们可用的各种视图和小部件外,我们常常希望在用户没有专注于我们的应用时,通知他们某些事件。智能手机通常提供某种形式的提示区域,而 Android 也不例外,其通知栏位于屏幕顶部。向用户发布通知是一个非常简单的过程,Android 在 Lollipop 中增加了两个新功能,现在可以在锁屏上添加显示通知,并生成浮动、抬头通知。
为了展示这些功能,我们将构建一个小型天气预报应用(使用假数据),当天气恶劣时,将以通知的形式向用户发出警告。
在本章中,你将学习如何:
-
使用
ViewPager构建屏幕滑动 -
使用 Fragment 而不是 Activity
-
创建
ViewPager和PagerAdapter -
向操作栏添加标签
-
向用户发布通知
-
管理返回栈
-
设计通知栏图标
-
生成抬头通知
-
创建扩展通知
-
配置锁屏通知
-
设置通知优先级和可见性
构建 ViewPager
ViewPager及其变体是从ViewGroup类扩展出来的,可以将其视为一种布局管理器,负责放置和导航页面。ViewPager与PagerAdapters协同工作,为每个页面填充适当的数据。
我们ViewPager页面的代码包含在一个新类中,即Fragment。Fragment 在结构和目的上与 Activity 非常相似,但可以被包含和组合在 Activity 中,从所有目的来看,可以将其视为子 Activity。
正如往常一样,在我们开始编程之前,我们需要考虑我们应用的布局和外观。在这里,我们需要为ViewPager和Fragment创建布局。以下章节中的步骤展示了如何设置我们的布局和资源文件。
创建布局
创建项目后,该项目将需要两个布局文件,并且为了利用一些 Material Design 功能,我们将编辑样式资源。以下步骤解释了如何完成此操作:
-
在 Android Studio 中创建新项目
-
命名为
天气预报并使用空白活动模板开始它。 -
打开
build.gradle (Module: app)文件。 -
添加以下依赖项:
compile 'com.android.support:support-v4:21.0.+' compile 'com.android.support:cardview-v7:21.0.+' -
如果您尚未下载项目文件,请找到五个大约 400 x 400 px 的合适图像,名称如下,并将它们放置在项目的
drawable目录中:![创建布局]()
-
打开
res/values-v21/styles.xml文件。 -
在
<style>标签内添加以下颜色项:<item name="android:colorPrimary">#2b2</item> <item name="android:colorPrimaryDark">#080</item> <item name="android:colorAccent">#8f8</item> <item name="android:textColorPrimary">#004</item> <item name="android:textColorSecondary">#448</item> -
打开
activity_main.xml文件。 -
用以下代码替换其内容:
<android.support.v4.view.ViewPager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent"/> -
创建一个名为
fragment_layout.xml的新布局文件。 -
创建一个与这里显示的相匹配的
ViewGroup层次结构:![创建布局]()
-
使用
drawable文件夹中的图像和文本作为占位符,重新创建布局(突出显示内部两个布局),如图所示,选择合适的边距和其他视觉属性:![创建布局]()
-
CardView元素应如下所示:<android.support.v7.widget.CardView android:id="@+id/card_view" android:layout_width="match_parent" android:layout_height="wrap_content" card_view:cardCornerRadius="4dp" card_view:cardElevation="4dp"> -
为了实现正确的文本着色,将
android:textAppearance设置为应用我们在styles.xml文件中设置的textColors,其中textColorPrimary应用于视图,并且将textAppearance设置为"?android:attr/textAppearanceLarge"。
我们在我们的 gradle 脚本中添加了一个支持库(v4)以及CardView支持,我们将在以后使用。支持库提供了标准 SDK 中不可用的功能,并为许多其他功能提供了向后兼容性。
这就是我们需要做的所有布局和资源工作。通过在样式文件中应用自定义颜色,我们允许系统像往常一样决定将哪些颜色应用到哪些屏幕组件上;然而,在这里,它应用了我们自己的个人方案。这些颜色在应用中显示;不仅这种方法是确保我们的应用具有独特性的好方法,而且它也使它们在我们的努力下保持一致感。
小贴士
当处理大型布局文件时,可以使用<include layout="@layout/some_layout"/>来整理过程,其中some_layout将在膨胀时插入到 include 标签的位置。
在设置好布局、主题和样式后,我们现在可以继续编写一些代码。
添加 ViewPager 和 FragmentPagerAdapter
如前所述,ViewPager是一个水平滚动屏幕的布局管理器。填充这些页面的工作由PagerAdapter执行,或者更具体地说,在这个案例中是FragmentPagerAdapter,它执行相同的函数,但使用 Fragment 而不是 Activity。
包含一个小数据集,我们将需要总共四个类来实现我们的ViewPager。以下是实现方法:
-
打开
MainActivity类。 -
包含以下导入:
import android.support.v4.app.FragmentActivity; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.support.v4.view.ViewPager; -
按照以下方式扩展类:
public class MainActivity extends FragmentActivity{ -
添加以下类字段:
private ViewPager viewPager; -
在
onCreate()方法中添加以下三行以启动我们的ViewPager:viewPager = (ViewPager) findViewById(R.id.pager); FragmentAdapter adapter = new FragmentAdapter(getSupportFragmentManager()); viewPager.setAdapter(adapter); -
创建一个名为
FragmentAdapter.java的新类。 -
包含以下导入:
import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; -
按照以下所示完成类:
public class FragmentAdapter extends FragmentPagerAdapter { public FragmentAdapter(FragmentManager m) { super(m); } @Override public Fragment getItem(int index) { Fragment fragment = new WeatherFragment(); Bundle args = new Bundle(); args.putInt("day", index); fragment.setArguments(args); return fragment; } @Override public int getCount() { return 5; } } -
创建一个名为
WeatherFragment.java的新类。 -
添加以下导入:
import android.support.v4.app.Fragment; -
按照以下方式填写类:
public class WeatherFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_layout, container, false); Bundle args = getArguments(); int i = args.getInt("day"); TextView textOutlook = ((TextView) rootView.findViewById(R.id.text_outlook)); ImageView symbolView = ((ImageView) rootView.findViewById(R.id.image_symbol)); TextView tempsView = ((TextView) rootView.findViewById(R.id.text_temp)); TextView windView = ((TextView) rootView.findViewById(R.id.text_min)); TextView realFeelView = ((TextView) rootView.findViewById(R.id.text_real_feel)); textOutlook.setText(WeatherData.outlookArray[i]); symbolView.setImageResource(WeatherData.symbolArray[i]); tempsView.setText(WeatherData.tempsArray[i] + "°c"); windView.setText(("Min " + WeatherData.minArray[i] + "°c")); realFeelView.setText("Real feel " + WeatherData.realFeelArray[i] + "°c"); return rootView; } } -
创建一个名为
WeatherData.java的最后一个类,并按照以下方式完成它:public class WeatherData { static String[] outlookArray = {"Developing snow storms", "Partly sunny and breezy", "Mostly sunny", "Afternoon storms", "Increasing cloudiness"}; static Integer[] symbolArray = {R.drawable.snowy, R.drawable.partly_sunny, R.drawable.sunny, R.drawable.stormy, R.drawable.cloudy}; static Integer[] tempsArray = {0, 1, 3, 2, 4}; static Integer[] minArray = {-5,-3,-2,0,2}; static Integer[] realFeelArray = {-1, 2, 0, 1, 3}; } -
你现在可以在手机或模拟器上运行和测试应用程序。
你可能想知道为什么在本节中包含了关于导入哪些库的说明,因为你可能已经在设置中配置了自动导入。这个功能在几乎所有情况下都工作得很好,并且可以节省大量时间。如果你还没有启用它,可以在 文件 | 设置... | IDE 设置 | 编辑器 | 自动导入 中找到。它仅在支持库包含与标准库同名导入时才有用。如果我们没有首先导入这些库,当我们尝试输入代码时,我们会遇到以下截图所示的情况:

当这种情况发生时,仍然可以通过简单地从编辑器提供的列表中选择 v4 版本来导入正确的库。
MainActivity 代码非常简短,因为大部分工作都在其他地方完成。ViewPagers 需要一个 ID,这个 ID 是在 activity_main.xml 文件中设置的 XML 定义的 ViewPager。PagerAdapter 在下一个类中实现;这里我们只是将两者连接起来。
FragmentAdapter 类也非常简单易懂。我们只需要将所选页面的索引传递给我们的 Fragment 类,我们通过 Bundle 对象来完成这个操作。然后片段负责使用适当的数据布局视图。重要的是要指出,我们没有直接使用 PagerAdapter,而是使用其变体之一,即 FragmentPagerAdapter,它旨在与片段一起工作。还有一个 FragmentSatePagerAdapter,它执行相同的工作,但更适合较长的列表。这是因为框架处理那些对用户不可直接访问的页面的方式。对于像我们这样的短列表,最佳性能可以通过 FragmentPagerAdapter 实现。
我们创建的 Fragment 类也非常简单易懂,因为它所做的只是扩展我们的 fragment_layout,以我们现在已经非常熟悉的方式关联和设置我们的视图。片段与活动非常相似,因为它们各自都有自己的生命周期,可以在不同的点通过回调方法(如 onCreate() 和 onPause())进行拦截,就像我们对活动所做的那样。在这里,我们选择使用 onCreateView() 方法,因为它提供了访问我们需要的实例来扩展片段,以及由适配器存储在 Bundle 中的页面索引,并且当视图创建时也会被调用。
ViewPagers提供了许多功能,却几乎不需要付出任何努力。一旦连接到适配器,我们就可以创建屏幕滑动,无需担心实现触摸监听器,也不必过分关注在任何给定时间选中的是哪一页。
就目前而言,我们的应用做得非常少,我们需要提供更多功能,这包括首先在操作栏中添加标签,以便我们可以将每个页面与实际日期关联起来,其次是通过编程一个通知构建器来发布系统通知区域中的天气警报。我们将从添加带日期的标签开始。
添加标签和日期
如果用户不知道何时可以期待预测的天气,那么天气预报应用就没有什么用处。我们很容易在我们的片段布局中添加另一个视图,但我们将我们的页面附加到操作栏的标签上,并访问日历和日期格式类来填充它们。
要拦截页面之间的变化,我们需要一个OnPageChangeListener,它在页面变化时被调用,并重新定义类声明,使其实现ActionBar.TabListener。我们将使用 Java 自带的Calendar和SimpleDateFormat对象来计算和格式化我们的日期。
我们要添加到操作栏中的标签的所有操作都可以通过在MainActivity类中遵循以下步骤来完成:
-
打开
MainActivity.java文件。 -
按照以下方式编辑声明:
public class MainActivity extends FragmentActivity implements ActionBar.TabListener -
选择这个错误产生的错误,遵循编辑器的建议,并实现建议的三个方法。
-
编辑
onTabSelected()方法以匹配以下内容:@Override public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) { viewPager.setCurrentItem(tab.getPosition()); } -
包含以下类字段:
private ActionBar actionBar; -
将以下行添加到
onCreate()方法中:Calendar calendar = Calendar.getInstance(); String weekDay; SimpleDateFormat dayFormat; dayFormat = new SimpleDateFormat("EEEE", Locale.getDefault()); actionBar = getActionBar(); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); actionBar.addTab(actionBar.newTab().setText("Today").setTabListener(this)); for (int i = 1; i < 5; i++) { calendar.add(Calendar.DAY_OF_WEEK, 1); weekDay = dayFormat.format(calendar.getTime()); actionBar.addTab(actionBar.newTab().setText(weekDay).setTabListener(this)); } -
仍然在
onCreate()中,包括以下监听器以检测页面变化:viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int i, float v, int i2) { } @Override public void onPageSelected(int position) { actionBar.setSelectedNavigationItem(position); } @Override public void onPageScrollStateChanged(int i) { } }); -
现在,你可以在设备或 AVD 上运行项目了。
标签式操作栏在许多安卓应用中都很常见,而ActionBar.TabListener是一个接口,每当标签被选中、添加或移除时都会被调用。在这里,我们使用它的选择来通知我们的ViewPager变化。ViewPager布局也提供了一个很好的机会来查看我们之前定义的颜色是如何应用到 UI 的各种组件上的,例如当用户尝试滚动到列表的任一端之外时出现的彩色提示。

Calendar 和 SimpleDateFormat 类并不难理解。日期格式化遵循 Unicode 技术标准(UTS)#35,其详细信息可以在 www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns 找到。在这里,我们应用了一个独立的星期格式,使用 "cccc"。我们本来可以更有创意,使用类似 "c LLL d" 的格式来得到类似 Tue Mar 15 的结果,或者甚至利用系统内置的格式化功能,通过更改 dayFormat 的赋值来 dayFormat = (SimpleDateFormat) new SimpleDateFormat().getDateInstance();,如下所示:

最后一个拼图部分是通过 ViewPager.OnPageChangeListener 实现的,我们使用它来通知操作栏当通过滑动而不是标签选择新页面时。
在两种导航形式都到位并连接后,我们现在可以继续编程我们的应用以发布通知。
编程通知
Android 5 提供了一个比之前版本更灵活的通知框架。除了能够向通知栏发布标准通知外,我们现在还可以将通知扩展到比以前包含更多细节,并且能够通过在屏幕上浮动的 heads-up 通知来通知用户,当他们在使用全屏应用或屏幕锁定时。
在使用通知时,重要的是要敏感地关注用户的需求,不要发布太多通知或给予它们过高的重要性。正因为如此,通知可以分配 优先级和可见性 设置。话虽如此,现在是我们向应用添加通知的时候了。
添加标准通知和图标
假设我们希望发送的通知既不重要也不私密,我们只想通知用户某个事件并给他们提供打开我们应用的机会,我们需要发布一个标准通知,以便在通知栏上出现一个小图标,当抽屉打开时,会出现一个带有一些简短文本和图标的卡片。点击它将打开我们的应用。通知需要在通知栏上显示特定类型的图标。我们将从这里开始,然后继续实现通知。按照以下步骤在我们的天气应用上设置此功能:
-
如果您尚未下载项目文件,您需要一个单色图标,背景为透明(就像您在这里看到的那样),名为
small_icon.png,并存储在drawable目录中:![添加标准通知和图标]()
-
打开
MainActivityJava 文件。 -
添加此类字段:
private static int notificationId; -
在
onCreate()方法中,添加以下语句:if (notificationId == 0) { postAlert(0); } -
创建
postAlert()方法并完成如下:private void postAlert(int i) { NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setContentTitle("Weather Alert!") .setContentText(WeatherData.outlookArray[i]) .setSmallIcon(R.drawable.small_icon) .setAutoCancel(true) .setTicker("Wrap up warm!"); Intent intent = new Intent(this, MainActivity.class); TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); stackBuilder.addParentStack(MainActivity.class).addNextIntent(intent); PendingIntent pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(pendingIntent); NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(notificationId, builder.build()); notificationId++; } -
应用现在可以在模拟器或设备上测试,打开通知抽屉应该产生以下结果:
![添加标准通知和图标]()
这里有很多事情在进行。首先,有通知图标。这是构建者所必需的,并且必须遵循特定的格式。除了要小之外,图标还需要在透明背景上的简单设计。图片的颜色并不重要,因为系统只考虑 alpha 通道。因此,强烈不建议使用具有中间 alpha 级别的颜色。
在到达代码的其余部分之前,指出使用通知下拉中的小图标的一个替代方案是不妥的,尽管它适合条上的最小空间,但并不总是看起来很好。在大视图左侧的圆形图标中。将以下设置添加到我们的构建器中:
.setLargeIcon(BitmapFactory.decodeResource(getResources(), WeatherData.symbolArray[i]))
现在运行应用会产生如下图标:

我们使用NotificationCompat来构建我们的构建器。NotificationCompat是我们之前导入的支持库提供的一个辅助类。Builder本身有三个必需的参数:ContentTitle、ContentText和SmallIcon。还有很多其他的参数,我们将在后面讨论,但只有这三个是必需的。我还将AutoCancel设置为 true,因为这非常有用,因为它会在通知被从抽屉中选中后自动关闭。设置Builder.Ticker的目的在应用运行时就会变得明显。
由于它们可用的位置,通知可以在其他应用有焦点时查看和触发。当用户按下返回键时,这可能会导致用户产生困惑的结果。我们希望导航键的行为就像应用以通常的方式启动一样。这就是TaskStackBuilder的功能,它在这个案例中接受一个Intent来启动我们的MainActivity,并将其放置在最近的活动回退栈上,而PendingIntent使其在我们的应用外部可用。
实际的通知是通过调用NotificationManager.notify()来实现的。在这里使用 ID 并不是绝对必要的,因为我们的应用一次只发出一个通知。然而,这不仅是一种跟踪多个通知的有用方式,还可以像我们在这里做的那样,确保只发出一个通知。
扩展通知
NotificationCompat类为我们提供了三种内置样式。在这里,我们将使用BigPictureStyle。在我们开始之前,你需要一个用作“大图片”的图片。找到大约 640 x 480 像素的合适图片,将其放在你的drawable文件夹中,并命名为snow_scene或更改以下代码中的引用。一旦你的图片就位,按照以下简短的步骤进行:
-
打开
MainActivity.java类。 -
在
onCreate()方法中,在我们的 Intent 声明之前添加以下代码:NotificationCompat.BigPictureStyle bigStyle = new NotificationCompat.BigPictureStyle(); bigStyle.bigPicture(BitmapFactory.decodeResource(getResources(), R.drawable.snow_scene)); builder.setStyle(bigStyle); -
就这样!在你的手机或模拟器上运行应用并打开通知抽屉。
![展开通知]()
NotificationCompat.Style 对象非常方便为我们添加通知的详细信息,并且易于理解和使用。除了 BigPictureStyle,还有 BigTextStyle 用于标题类型的通知,以及 InboxStyle 用于列表。
这就留下了我们另外两个新的通知功能:弹窗通知和锁屏通知。这两个功能都不需要额外的编码,但它们是通过调整现有通知的某些隐私和优先级设置来触发的。本章的下一节和最后一节将演示如何实现这一点。
发布弹窗和锁屏通知
弹窗和锁屏通知的创建方式与标准通知相同。区别在于我们的 NotificationCompat.Builder 类的 VISIBILTY 和 PRIORITY 属性。以下是如何做到这一点的步骤:
-
打开
MainActivity类。 -
给我们的构建器实例以下属性:
.setPriority(Notification.PRIORITY_HIGH) .setVisibility(Notification.VISIBILITY_PUBLIC) .setVibrate(new long[]{100, 50, 100, 50, 100}) .setCategory(Notification.CATEGORY_ALARM) -
当应用运行时,它现在将显示弹窗和锁屏通知。
小贴士
可以通过按 F7 键在模拟器上锁定屏幕。
在这里,setPriority() 方法是我们决定一个通知是否重要到足以考虑使用可能会干扰他们正在执行的其他任务的弹窗消息,而不是标准版本。优先级必须设置为 PRIORITY_HIGH 或 PRIORITY_MAX,并且通知必须设置为触发振动,以便以这种方式显示通知。其他优先级是 MIN、LOW 和 DEFAULT。
小贴士
如果你不想让你的通知触发振动,但仍然以弹窗的形式出现,你可以使用以下行代替:
builder.setVibrate(0);
尽管有更复杂的方式来配置振动,但这里使用的构造函数将满足大多数目的。长整型数组代表交替的时间(以毫秒为单位),代表振动的脉冲和静默交替,因此在这个例子中,设备会嗡嗡作响三次,每次 100 毫秒,中间有 50 毫秒的暂停。
与其他通知不同,用户可以通过设置选择是否在设备上显示锁屏消息。然而,作为开发者,我们可以决定在用户的锁屏上显示多少,如果有的话,信息。
将通知的可见性设置为 PUBLIC 将导致内容标题和内容文本都显示出来,设置为 PRIVATE 将只显示标题,而 SECRET 则完全不显示。
摘要
在本章中,我们探讨了使用ViewPager作为垂直滚动RecyclerView的替代方案。我们使用了 Fragment 而不是 Activity,并了解了一些我们可以使用 Action Bar 完成的事情。我们学习了如何发布各种通知以及如何管理返回栈,以便为用户提供一致的导航回应用程序。在大多数情况下,当我们的应用程序甚至没有积极运行时,也会发送通知;为此,我们需要使用服务,这是一种后台 Activity。
在下一章中,我们将探讨如何将 Google Maps 集成到我们的应用程序中,并使它们具有位置感知能力。这涉及到注册我们的应用程序以获取 API 密钥,并使用LocationListener来确保我们的应用程序能够更新当前位置数据;由于这可能需要大量资源,我们还将了解如何优化此过程。
第七章。地图、位置和 Google 服务
我们至今使用的标准 SDK API 提供了一套强大的工具,用于开发各种应用程序。然而,Google 还提供了一系列移动服务,如 Gmail、翻译和地图。作为开发者,我们都可以使用这些服务,以及 Google 提供的 API,以便与它们交互并将它们集成到我们的应用程序中。
小贴士
所有 Google 服务 API 的完整和最新列表可以在以下位置找到:developers.google.com/apis-explorer/#p/。
由于连接到 Google 服务的应用程序使用 Google 的数据和服务器,因此需要一个简单的身份验证过程,这被称为 API 密钥。在本章中,我们将通过构建一个简单的基于地图的应用程序来展示如何进行此操作,该应用程序显示我们选择的位置。完成此操作后,我们将使用 LocationListener 来跟踪用户移动时的应用程序。
在本章中,您将:
-
获取 API 密钥以访问 Android 的 Google Maps
-
理解权限及其应用方法
-
使用
GoogleApiClient访问LocationServices -
获取设备最后已知的位置
-
使用
LocationListener更新位置 -
优化位置更新间隔
-
添加 Google Maps UI 功能
-
设置模拟位置
-
使用
MapClickListener获取位置
使用 Google Maps 构建位置感知应用程序
要在我们的应用程序中添加一个非常基本的 Google 地图,需要两个不同的步骤。首先,我们需要将我们的应用程序注册到 Google 并获取一个 API 密钥以唯一标识我们的应用程序;一旦地图运行起来,我们可以使用 GPS 定位我们的位置,然后放大到该位置或任何其他位置。我们首先从这些步骤中的第一个开始。
获取 API 密钥
没有任何东西可以阻止我们立即开始,尽管您需要首先检查,当我们很久以前在 第一章 中安装 SDK 的所有组件时,设置开发环境,我们包括了以下包:

在我们开始之前需要注意的另一件事是,如果您打算使用模拟器测试此应用程序,那么您需要构建一个新的模拟器,其中系统镜像目标是 Google APIs (Google Inc.) – google_apis [Google APIs],而不是 Android 5.x。第三方虚拟设备可能需要自己的配置才能运行 Play Services。完成这些后,我们就准备好创建基于位置的应用程序了:
-
开始一个新的 Android Studio 项目。
-
在适当的屏幕上选择 Google Maps Activity,之前我们选择了 Blank Activity。
-
将其他所有内容按照向导的建议进行设置。
-
编辑器应打开
google_maps_api.xml文件;如果没有,请从res/values目录中打开它。 -
检查代码。Google 将提供一个以
https://console.developers.google.com/flows/开头并以您的包名结尾的链接,例如com.example.kyle.distancefinder。 -
点击此链接,您将被带到 Google 开发者控制台。获取 API 密钥
-
如有需要,请注册。
-
您将被提示创建一个新项目。您可以随意命名,因为您将能够再次使用它来创建其他应用。
-
在 项目仪表板 下的 APIs & auth 侧边栏中,选择 APIs。
-
启用 Google Maps Android API v2 API。
-
再次,在 仪表板 下的 APIs & auth 中,选择凭证并点击 创建新密钥 按钮。
-
在结果屏幕上,复制 API 密钥并将其粘贴到
google_maps_api.xml文件中,在YOUR KEY HERE处,确保两端没有多余的空格。 -
现在在手机或 Google APIs 模拟器上测试应用。结果将类似于以下截图:获取 API 密钥
在我们的应用中集成基本地图非常简单。然而,通过使用 Maps Activity 向导,我们已经为我们做了很多基本工作。而且,在能够在应用中选择的位置包含地图之前,理解这一点至关重要。
查看一下 build.gradle 文件,注意依赖关系是如何为我们修改的:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.android.gms:play-services:6.5.87'
compile 'com.android.support:appcompat-v7:22.0.0'
}
当在其他项目中包含地图时,我们始终需要在此构建的 gms:play-services 库。support:appcompat 库可能不那么明显。它用于使应用向后兼容。在这里它不是必需的,我们将在最后一章介绍如何吸引最多用户时再回到它。
现在打开项目的 Manifest 文件。您会注意到与之前项目的一些不同,首先是以下几行:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
任何下载过 Android 应用的用户都会熟悉用户在安装前必须授予使用各种设备功能(如网络访问)权限的方式。这些标签在清单中就是这样实现的,每次您包含需要用户权限的功能时都需要应用它们。幸运的是,它们都有非常直观的参考,完整列表可以在 developer.android.com/reference/android/Manifest.permission.html 找到。
在未来的项目中还需要在清单中包含的其他元素是以下应用程序元素的两个元数据子元素。第一个会自动使我们的应用运行最新的 play 服务版本,第二个是之前获取的 API 密钥应用的地方:
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="@string/google_maps_key" />
总体来说,设置 API 密钥非常简单。我们只需要注册一次,个人项目就可以为需要类似功能的 app 重复使用;说到功能,我们可能该给我们的 app 添加一些功能了。最广泛使用,并且可以说是最有用的 Google Map API 是位置服务,它允许用户使用 GPS、WiFi 和网络信号强度来定位他们的设备地理位置。
获取最后已知位置
将位置感知技术集成到我们的 app 中的第一步是确定用户的最后已知位置。这就像许多基于位置的工作一样,需要借助 GoogleApiClient 和一个作为这些服务主要入口点的接口来完成。
在添加 Java 代码之前,我们将编辑布局本身,这样我们就可以看到发生了什么。按照以下步骤获取我们设备的最后已知位置:
-
打开距离查找器项目。
-
打开
activity_maps.xml布局文件。 -
编辑内容如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="No last location" /> <fragment android:id="@+id/map" class="com.google.android.gms.maps.SupportMapFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MapsActivity" /> </LinearLayout> -
打开
MapsActivity.java文件。 -
除了为我们声明的
GoogleMap之外,还包括以下字段:private static final String DEBUG_TAG = "tag"; private TextView textView; private GoogleApiClient googleApiClient; -
将以下代码添加到
onCreate()方法中:textView = (TextView) findViewById(R.id.text_view); googleApiClient = new GoogleApiClient.Builder(this) .addApi(LocationServices.API) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); -
现在,更改类声明本身,使其实现以下接口:
public class MapsActivity extends FragmentActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { -
这将生成一个错误。使用快速修复来实现以下方法:
![获取最后已知位置]()
-
按照以下方式完成
onConnected()回调,以显示我们的位置:@Override public void onConnected(Bundle bundle) { Location loc = LocationServices.FusedLocationApi.getLastLocation(googleApiClient); Log.d(DEBUG_TAG, "Connected"); if (loc != null) { textView.setText(loc.toString()); } } -
要将连接状态报告给 LogCat,编辑另外两个新方法如下:
@Override public void onConnectionSuspended(int i) { Log.d(DEBUG_TAG, "Connection suspended"); } @Override public void onConnectionFailed(ConnectionResult connectionResult) { Log.d(DEBUG_TAG, "Connection failed"); } -
按照以下方式编辑
onResume()方法:@Override protected void onResume() { super.onResume(); setUpMapIfNeeded(); googleApiClient.connect(); Log.d(DEBUG_TAG, "onResume() called - connected"); } -
添加一个新的
onPause()方法并按照以下方式完成它:@Override protected void onPause() { super.onPause(); if (googleApiClient.isConnected()) { googleApiClient.disconnect(); Log.d(DEBUG_TAG, "Disconnected"); } } -
最后,按照以下方式重写
setUpMap():private void setUpMap() { mMap.addMarker(new MarkerOptions() .position(new LatLng(51.178844, -1.826189)) .title("Stonehenge")); } -
在手机或 AVD 上运行项目。除非你在你刚刚创建的模拟器上运行 app,否则你的位置将以以下格式显示在文本视图中:
位置[fused 51.507350,-0.127757 acc=4 et=+5m16s558ms alt=19.809023 vel=0.0]。
在这个练习的开始,我们稍微改变了布局以包括一个TextView。然而,我们仍然将片段的类设置为SupportMapFragment,并希望这能更加清晰。SupportMapFragment是我们可能在 app 中想要的所有地图容器的明显选择。它简单且几乎自动处理大多数地图过程。使用这个容器,我们几乎可以用GoogleApiClient完成所有其他需要的事情。从代码中可以看出,它允许我们设置监听器和回调来管理地图和连接活动。一旦 API 客户端连接了我们,找到我们设备的最后已知位置只需要调用单个函数getLastLocation()。
确保在我们用户可能不再需要我们使用的服务时,我们断开与任何服务的连接至关重要。未能这样做会导致应用程序无谓地消耗设备的电力和数据。通过使用 onPause() 回调在 Activity 失去焦点时断开客户端,然后使用 onResume() 重新连接,我们可以确保当 Activity 可见时我们的地图保持连接,同时当不可见时我们不会浪费用户的电池和数据。
getLastLocation() 方法非常有用;它不需要网络连接或 GPS,并且可以立即获取。然而,在许多情况下,我们需要在用户移动时更新我们应用程序的位置。这是通过 LocationListen3er 回调和 LocationRequest 对象来完成的,这些内容将在下一节中展开。
请求位置更新
LocationRequest 对象高度可配置,允许我们控制请求的频率和接收到的信息的准确性。这意味着我们可以设计出不需要更多资源的应用程序,同时当应用程序的目的需要时,仍然可以提供高度准确和频繁的位置数据。
在下一阶段,我们将实现一个 LocationListener 来跟踪我们应用程序的位置。我们还将添加一个或两个功能来展示地图 API 为我们提供的某些功能。这一部分只有几行代码,如下所示:
-
在我们的
FragmentActivity类声明中包含一个LocationListener,如下所示:public class MapsActivity extends FragmentActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener { -
这将生成一个错误。使用快速修复导入 Google 版本的
LocationListener,如下所示:![请求位置更新]()
-
创建以下类字段:
private LocationRequest locationRequest; -
在
onCreate()方法中,创建以下LocationRequest:locationRequest = LocationRequest.create() .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) .setInterval(30000) .setFastestInterval(5000); -
接下来完成
onLocationChanged()方法如下:@Override public void onLocationChanged(Location location) { LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this); textView.setText(location.toString()); } -
现在扩展
setUpMap()方法如下:private void setUpMap() { mMap.addMarker(new MarkerOptions() .position(new LatLng(51.178844, -1.826189)) .title("Stonehenge") .snippet("3000 BC") .icon(BitmapDescriptorFactory.fromResource(R.mipmap.ic_launcher))); mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE); mMap.setMyLocationEnabled(true); mMap.getUiSettings().setMyLocationButtonEnabled(true); mMap.getUiSettings().setZoomControlsEnabled(true); mMap.getUiSettings().setZoomGesturesEnabled(true); mMap.getUiSettings().setCompassEnabled(true); mMap.getUiSettings().setRotateGesturesEnabled(true); } -
在您的手机或模拟器上运行项目,进行短距离散步或使用 Android 设备监控器设置模拟位置:
![请求位置更新]()
就像其他地图编码方面一样,我们在这里所做的添加非常直接。尽管有 Android 版本的 LocationListener,但 Google 的版本更容易使用,功能更强大,并且被 Google 本身强烈推荐。我们在这里使用了内置图标,但当然任何图像都可以。我们不太可能使用我们在这里设置的所有的 UI 控件,它们更多地是为了演示目的而不是实际使用。
我们设置间隔的方式很有趣。单位是毫秒,因此我们已将应用程序设置为每 30 秒请求一次新的位置。然而,使用 setFastestInterval() 允许我们利用可能请求位置的其他应用程序,因此我们可以以每 5 秒的频率更新。
我们将精度设置为尽可能高,使用 PRIORITY_HIGH_ACCURACY 常量。这显然可能会消耗用户的电量。许多应用只需要精确到几百英尺,在这种情况下,人们会使用 PRIORITY_BALANCED_POWER_ACCURACY,或者如果城市级别足够,可以使用 PRIORITY_LOW_POWER。还有 PRIORITY_NO_POWER,它将尝试在不消耗额外电量的情况下提供最佳精度。
我们还将地图设置为卫星类型。我们也可以使用其他类型,例如 MAP_TYPE_TERRAIN、MAP_TYPE_HYBRID 或 MAP_TYPE_NORMAL,具体取决于我们应用的目的。
Google 提供了一种非常简单的方法,使用 MyLocationButton 来放大我们的位置。然而,可能会有时候我们想要放大到另一个位置,或者根本不放大,甚至缩小。接下来的部分将展示如何做到这一点,以及如何从地图上的点击确定位置。
移动和动画 Google 地图
本章的最后部分在编码方面要求很少。Google 为地图提供了一个专门的点击监听器以及我们非常熟悉的回调方法。在这里,我们将使用它来放大到地图上点击的任何点。
-
打开
MapsActivity文件。 -
在声明中,实现此接口:
GoogleMap.OnMapClickListener -
接下来,将以下行添加到
setUpMap()方法中:mMap.setOnMapClickListener(this); -
创建一个名为
onMapClick()的方法,并按照以下方式完成:public void onMapClick(LatLng latLng) { textView.setText("Clicked, position = " + latLng); CameraPosition position = new CameraPosition.Builder() .target(latLng) .zoom(6) .build(); mMap.animateCamera(CameraUpdateFactory.newCameraPosition(position)); } -
这就是全部内容。现在你可以运行应用了,它会在点击的任何点上放大。
![移动和动画 Google 地图]()
使用 GoogleMap.OnMapClickListener 大概是显而易见的,并且它以 LatLng 对象的形式提供位置,而无需我们做任何额外的工作。缩放级别是唯一需要解释的部分,这也非常简单。有 12 个级别,其中 12 表示街道级别,1 显示整个地球。以这种方式获取位置的方式为我们打开了在应用中包含各种有用和有趣功能的大门。
摘要
随着移动设备的日益普及,能够在我们的应用中包含地图变得至关重要。Google Maps API 使得这项任务变得非常简单,可能唯一复杂的部分就是获取 API 密钥和在清单文件中设置权限。
通过能够为真实和模拟设备设置模拟位置,以及包括用户已经习惯的 UI 组件,使用地图进行开发变得更加简单。
在下一章中,我们将深入探讨 Android 5 中可能最令人兴奋的方面之一:为可穿戴设备、电视和汽车开发的能力。
第八章. 适用于电视、汽车和可穿戴设备的 App
近期 Android 发展中最令人兴奋的新方向之一,是将平台从手机和平板电脑扩展到电视、汽车仪表盘以及如手表这样的可穿戴设备。这些新设备使我们能够为现有的应用提供附加功能,同时还能创建专门为这些新环境设计的全新应用。
我们已经掌握了开发此类应用所需的技能,而本章的主要内容是解释每个平台的独特之处以及谷歌希望我们遵循的指南。当涉及到开发人们在驾驶时使用的应用时,这一点尤为重要,因为安全必须是首要考虑的因素。在开发可穿戴应用时,还需要解决某些技术问题,例如将设备与手机配对以及完全不同的 UI 和使用方法。
在本章中,您将:
-
创建可穿戴 AVD
-
使用 adb 命令将可穿戴模拟器连接到手机
-
将可穿戴模拟器连接到手机模拟器
-
创建包含移动和可穿戴模块的项目
-
使用可穿戴 UI 库
-
创建形状感知布局
-
创建和自定义可穿戴设备的卡片
-
理解可穿戴设计原则
-
访问可穿戴传感器
-
使应用可在 Google TV 上使用
-
包含 Leanback 支持
-
理解 Android Auto 安全指南
-
配置 Auto 项目
-
安装 Google 模拟器
-
使用 Android 设备监控器发送短信
Android Wear
创建或修改适用于可穿戴设备的应用可能是本章中处理的三种形式因素中最复杂的一种,并且需要比其他项目更多的设置。然而,可穿戴设备通常使我们能够访问一些更有趣的新传感器,如心率监测器。通过了解它是如何工作的,我们还可以了解如何管理传感器。
如果您无法访问 Android 可穿戴设备,请不要担心,因为我们将会构建 AVD。如果您希望将实际 Android 5 手机与 AVD 配对,理想情况下您应该拥有这样一部手机。如果没有,仍然可以使用两个模拟器进行工作,但设置起来会稍微复杂一些。考虑到这一点,我们现在可以准备我们的第一个可穿戴应用。
构建和连接到可穿戴 AVD
在模拟器上独立开发和测试可穿戴应用是完全可能的,但如果我们想测试所有可穿戴功能,我们需要将其与手机或平板电脑配对。下一个练习假设您拥有实际设备。如果您没有,仍然完成任务 1 至 4,我们稍后会介绍如何使用模拟器完成剩余部分。
-
打开 Android Studio。在此阶段,您不需要启动项目。
![构建和连接到可穿戴 AVD]()
启动 SDK 管理器并确保已安装相关包。
-
打开 AVD 管理器。
-
创建两个新的 Android Wear AVD,一个圆形和一个方形,如下所示:
![构建和连接到可穿戴 AVD]()
-
确保在手机上选中了 USB 调试。
-
从 Play Store 在此 URL 安装 Android Wear 应用程序:
play.google.com/store/apps/details?id=com.google.android.wearable.app。将其连接到计算机并启动我们刚刚创建的其中一个 AVD。 -
定位并打开包含
adb.exe文件的文件夹。它可能类似于user\AppData\Local\Android\sdk\platform-tools\。 -
使用 Shift + 右键单击,选择 在此处打开命令窗口。
-
在命令窗口中,输入以下命令:
adb -d forward tcp:5601 tcp:5601
-
启动配套应用程序,按照说明将两个设备配对。
能够将真实世界设备连接到 AVD 是一种很好的开发形式,而不必拥有这些设备。可穿戴配套应用程序简化了连接两个设备的过程。如果你已经运行仿真器一段时间,你会注意到许多操作,如通知,会自动发送到可穿戴设备。这意味着我们的应用程序通常可以无缝地与可穿戴设备链接,而无需我们编写代码来预先处理这种情况。
adb.exe(Android 调试桥)是我们开发工具包的重要组成部分。大多数时候,Android Studio 会为我们管理它。然而,了解它的存在以及如何与之交互是有用的。我们在这里使用它来手动在我们的可穿戴 AVD 和手机之间打开一个端口。
有许多可以从命令提示符发出的 adb 命令,其中最有用的是 adb devices,它列出了所有当前可调试的设备和仿真器,当事情不正常时非常方便,可以查看是否需要重新启动仿真器。通过使用 adb kill-server 和 adb start-server 分别关闭和启动 ADB。使用 adb help 将列出所有可用的命令。
小贴士
在步骤 10 中使用的端口转发命令需要在手机从计算机断开连接时每次发出。
在不编写任何代码的情况下,我们已经看到了一些内置在 Android Wear 设备中的功能以及 Wear UI 与大多数其他 Android 设备的不同之处。
即使你通常使用最新的 Android 硬件进行开发,使用仿真器通常仍然是一个好主意,特别是用于测试最新的 SDK 更新和预发布版本。如果你没有真实设备,接下来的小节将展示如何将你的可穿戴 AVD 连接到手机 AVD。
将可穿戴 AVD 与另一个仿真器连接
配对两个仿真器与配对真实设备非常相似。主要区别是我们如何在没有访问 Play Store 的情况下安装配套应用程序。按照以下步骤查看如何操作:
-
启动一个 AVD。这需要针对 Google API,如这里所示:
![连接可穿戴 AVD 与其他模拟器]()
-
下载
com.google.android.wearable.app-2.apk。在网上有很多地方可以通过简单的搜索找到它,我使用了www.file-upload.net/download。 -
将文件放置在您的
sdk/platform-tools目录中。 -
在此文件夹中Shift + 右键单击,然后选择在此处打开命令窗口。
-
输入以下命令:
adb install com.google.android.wearable.app-2.apk. -
启动您的可穿戴 AVD。
-
在命令提示符中输入
adb devices,确保两个模拟器都可见,输出类似于以下内容:List of devices attached emulator-5554 device emulator-5555 device -
在命令提示符中输入
adb telnet localhost 5554,其中5554是手机模拟器。 -
接下来,输入
adb redir add tcp:5601:5601。 -
您现在可以使用手持 AVD 上的可穿戴应用程序连接到手表。
正如我们所看到的,设置可穿戴项目比我们之前执行的一些其他练习要花更长的时间。一旦设置完成,过程与为其他形式因素开发的过程非常相似,我们现在可以继续进行。
创建可穿戴项目
我们迄今为止开发的所有应用程序都只需要一个模块,这是有道理的,因为我们只为单个设备构建。在接下来的步骤中,我们将跨两个设备进行开发,因此需要两个模块。这非常简单,您将在接下来的步骤中看到。
-
在 Android Studio 中创建一个新的项目,并将其命名为类似
Wearable App的名称。 -
在目标 Android 设备屏幕上,选择手机和平板电脑以及可穿戴,如下所示:
![创建可穿戴项目]()
-
您将被要求添加两个活动。对于移动活动,选择空白活动,对于可穿戴设备,选择空白可穿戴活动。
-
其他所有内容都可以保持不变。
-
在圆形和方形虚拟设备上运行应用程序。
您首先会注意到有两个模块,移动和可穿戴。第一个模块与我们之前看到的相同,但与可穿戴模块有一些细微的差别,值得稍微看一下。最重要的差别是WatchViewStub类。它在可穿戴模块的activity_main.xml和MainActivity.java文件中的使用方式可以查看。
这个帧布局扩展专门为可穿戴设备设计,可以检测设备的形状,以便填充适当的布局。利用WatchViewStub并不像想象中那么简单,因为适当的布局只有在WatchViewStub完成其操作后才会被填充。这意味着,为了访问布局中的任何视图,我们需要使用一个特殊的监听器,该监听器在布局被填充后会被调用。如何通过打开可穿戴模块中的MainActivity.java文件并检查onCreate()方法来了解OnLayoutInflatedListener()的工作方式,该onCreate()方法看起来如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
@Override
public void onLayoutInflated(WatchViewStub stub) {
mTextView = (TextView) stub.findViewById(R.id.text);
}
});
}
除了可穿戴应用程序和设备在开发中的设置方式外,另一个显著的不同之处在于 UI。我们用于手机和平板电脑的小部件和布局在大多数情况下不适用于手表屏幕较小的尺寸。Android 提供了一套全新的 UI 组件,我们可以使用,这就是我们接下来要探讨的内容。
设计可穿戴设备的 UI
除了在设计布局时必须考虑可穿戴设备的小尺寸外,我们还有形状问题。为圆形屏幕设计会带来自己的挑战,但幸运的是,Wearable UI 库使这一点变得非常简单。除了我们在上一节中遇到的WatchViewStub,它能够填充正确的布局外,还有一种方法可以设计一个布局,使其既适用于方形屏幕也适用于圆形屏幕。
设计布局
项目设置向导自动在build.gradle (Module: wear)文件中将此库作为依赖项包含:
compile 'com.google.android.support:wearable:1.1.0'
以下步骤演示了如何使用BoxInsetLayout创建一个形状感知布局:
-
打开上一节中创建的项目。
-
您需要三个图像,必须放置在可穿戴模块的
drawable文件夹中:一个名为background_image的图像,大小约为 320 x 320 px,以及两个大小约为 50 x 50 px 的图像,分别命名为right_icon和left_icon。 -
在可穿戴模块中打开
activity_main.xml文件。 -
用以下代码替换其内容:
<android.support.wearable.view.BoxInsetLayout android:background="@drawable/background_image" android:layout_height="match_parent" android:layout_width="match_parent" android:padding="15dp"> </android.support.wearable.view.BoxInsetLayout> -
在
BoxInsetLayout内部添加以下FrameLayout:<FrameLayout android:id="@+id/wearable_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="5dp" app:layout_box="all"> </FrameLayout> -
在其中添加以下三个视图:
<TextView android:gravity="center" android:layout_height="wrap_content" android:layout_width="match_parent" android:text="Weather warning" android:textColor="@color/black" /> <ImageView android:layout_gravity="bottom|left" android:layout_height="60dp" android:layout_width="60dp" android:src="img/left_icon" /> <ImageView android:layout_gravity="bottom|right" android:layout_height="60dp" android:layout_width="60dp" android:src="img/right_icon" /> -
在可穿戴模块中打开
MainActivity.java文件。 -
在
onCreate()方法中,删除setContentView(R.layout.activity_main);行之后的所有行。 -
现在,在方形和圆形模拟器上运行应用程序。
![设计布局]()
如我们所见,BoxInsetLayout在无论屏幕形状如何的情况下都能很好地填充我们的布局。其工作原理非常简单。BoxInsetLayout创建一个正方形区域,其大小可以适应圆形屏幕的圆形。这是通过app:layout_box="all"指令设置的,该指令也可以用于定位组件,正如我们将在下一分钟看到的那样。
我们还将BoxInsetLayout的填充设置为 15 dp,而FrameLayout的填充设置为 5 dp。这会在圆形屏幕上产生 5 dp 的边距,在方形屏幕上产生 15 dp 的边距。
无论您是使用WatchViewStub并为每种屏幕形状创建单独的布局,还是使用BoxInsetLayout并仅创建一个布局文件,这完全取决于您的偏好以及您应用程序的目的和设计。无论您选择哪种方法,您无疑都希望将 Material Design 元素添加到您的可穿戴应用程序中,其中最常见且最灵活的是卡片。在下一节中,我们将探讨两种实现方式,即CardScrollView和CardFragment。
添加卡片
CardFragment 类提供了一个默认的卡片视图,包含两个文本视图和一个图像。它设置起来非常简单,具有所有 Material Design 特性,如圆角和阴影,几乎适用于所有用途。它可以进行定制,正如我们将看到的,尽管 CardScrollView 通常是一个更好的选择。首先,让我们看看如何为可穿戴设备实现默认卡片:
-
打开当前项目中 wear 模块中的
activity_main.xml文件。 -
删除或注释掉文本视图和两个图像视图。
-
打开 wear 模块中的
MainActivity.java文件。 -
在
onCreate()方法中,添加以下代码:FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); CardFragment cardFragment = CardFragment.create("Short title", "with a longer description"); fragmentTransaction.add(R.id.wearable_layout, cardFragment); fragmentTransaction.commit(); -
在可穿戴设备模拟器中运行应用,看看默认卡片的外观。
![添加卡片]()
我们在 第六章 中遇到了 FragmentManager,通知和动作栏,在这里它以非常相似的方式运行,并且需要很少的解释。我们创建 CardFragment 的方式也非常直接。我们在这里使用了两个字符串参数,但还有一个第三个参数,即可绘制参数,如果将行更改为 CardFragment cardFragment = CardFragment.create("TITLE", "with description and drawable", R.drawable.left_icon);,那么我们将得到以下输出:

对于可穿戴设备上的卡片,这个默认实现对于大多数用途来说都很好,并且可以通过重写其 onCreateContentView() 方法进行定制。然而,CardScrollView 是一个非常方便的替代方案,这是我们接下来要探讨的。
定制卡片
CardScrollView 在我们的布局内部定义,并且它能够检测屏幕形状并调整边距以适应每个形状。要了解这是如何实现的,请按照以下步骤操作:
-
打开 wear 模块中的
activity_main.xml文件。 -
删除或注释掉每个元素,除了根
BoxInsetLayout。 -
在
BoxInsetLayout内放置以下CardScrollView:<android.support.wearable.view.CardScrollView android:id="@+id/card_scroll_view" android:layout_height="match_parent" android:layout_width="match_parent" app:layout_box="bottom"> </android.support.wearable.view.CardScrollView> -
在其中添加以下
CardFrame:<android.support.wearable.view.CardFrame android:layout_width="match_parent" android:layout_height="wrap_content"> </android.support.wearable.view.CardFrame> -
在
CardFrame内部添加一个LinearLayout。 -
向其中添加一些视图,以便预览与这里的布局相似:
![定制卡片]()
-
打开
MainActivity.java文件。 -
将我们添加到
onCreate()方法中的代码替换为以下内容:CardScrollView cardScrollView = (CardScrollView) findViewById(R.id.card_scroll_view); cardScrollView.setCardGravity(Gravity.BOTTOM); -
现在,你可以在模拟器上测试应用,这将产生以下结果:
![定制卡片]()
小贴士
如前图所示,Android Studio 为可穿戴设备形状提供了预览屏幕。像一些其他预览一样,这些并不总是你将在设备上看到的样子,但它们允许我们通过拖放小部件来快速组合布局。
如我们所见,CardScrollView 和 CardFrame 的实现甚至比 CardFragment 更简单,而且更加灵活,因为我们几乎可以设计出任何我们想象的布局。我们在这里再次分配了 app:layout_box,但这次使用 bottom,使得卡片尽可能低地放置在屏幕上。
在为如此小的屏幕设计时,保持我们的布局尽可能干净和简单是非常重要的。谷歌的设计原则指出,可穿戴应用应该是可一目了然的。这意味着,就像传统的手表一样,用户应该能够快速查看我们的应用并立即获取信息,然后返回他们之前正在做的事情。
另一个谷歌的设计原则——零到低交互——用户只需要单次点击或滑动就能与我们的应用交互。带着这些原则,让我们创建一个小型应用,其中包含一些实际的功能。在下一节中,我们将利用许多可穿戴设备中找到的新心率传感器,并在显示屏上显示每分钟的当前心跳次数。
访问传感器数据
Android Wear 设备在用户手腕上的位置,使其成为健身应用的完美硬件,而且不出所料,这些应用非常受欢迎。与 SDK 的大多数功能一样,访问传感器非常简单,使用管理器和监听器类,并且只需要几行代码,正如您通过以下步骤将看到的那样:
-
打开本章中我们一直在工作的项目。
-
将背景图片替换为可能适合健身应用的图片。我使用了一个简单的爱心图片。
-
打开
activity_main.xml文件。 -
删除除根
BoxInsetLayout之外的所有内容。 -
将此
TextView放置在其中:<TextView android:id="@+id/text_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:gravity="center" android:text="BPM" android:textColor="@color/black" android:textSize="42sp" /> -
打开可穿戴模块中的 Manifest 文件。
-
在根 Manifest 节点内添加以下权限:
<uses-permission android:name="android.permission.BODY_SENSORS" /> -
打开可穿戴模块中的
MainActivity.java文件。 -
添加以下字段:
private TextView textView; private SensorManager sensorManager; private Sensor sensor; -
在 Activity 中实现
SensorEventListener:public class MainActivity extends Activity implements SensorEventListener { -
实现监听器所需的两个方法。
-
编辑
onCreate()方法,如下所示:@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text_view); sensorManager = ((SensorManager) getSystemService(SENSOR_SERVICE)); sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HEART_RATE); } -
添加这个
onResume()方法:protected void onResume() { super.onResume(); sensorManager.registerListener(this, this.sensor, 3); } -
以及这个
onPause()方法:@Override protected void onPause() { super.onPause(); sensorManager.unregisterListener(this); } -
编辑
onSensorChanged()回调,如下所示:@Override public void onSensorChanged(SensorEvent event) { textView.setText("" + (int) event.values[0]); } -
如果您无法访问真实设备,您可以从这里下载传感器模拟器:
-
应用现在已准备好测试。
![访问传感器数据]()
我们首先在 AndroidManifest.xml 文件中适当的模块中添加了一个权限;这是我们之前做过的事情,每次我们使用需要用户在安装前给予权限的功能时都需要这样做。
包含背景图片可能看起来是必要的,但合适的背景确实有助于提高可一目了然的特性,因为用户可以立即知道他们正在查看哪个应用。
从SensorManager和Sensor在onCreate()方法中的设置方式来看,很明显所有传感器都以相同的方式访问,并且可以通过不同的常量访问不同的传感器。在这里我们使用了TYPE_HEART_RATE,但任何其他传感器都可以使用适当的常量启动,所有传感器都可以使用我们在这里找到的相同的基本结构来管理,唯一的真正区别是每个传感器返回SensorEvent.values[]的方式。所有传感器的完整列表及其产生的值的描述可以在developer.android.com/reference/android/hardware/Sensor.html找到。
就像我们的应用在任何时候利用后台运行的功能一样,我们至关重要地需要在 Activity 的onPause()方法中注销我们的监听器,无论何时它们不再需要。在这里我们没有使用onAccuracyChanged()回调,但它的目的应该是清晰的,并且有许多可能的应用需要其使用。
这标志着我们对可穿戴应用及其构建方式的探索结束。这类设备继续变得越来越普遍,而且更多富有创意的使用方式的可能性是无限的。只要我们考虑人们为什么以及如何使用智能手表等设备,并通过编程实现需要最小交互性的可查看界面来利用这些设备的地理位置,Android Wear 似乎注定会越来越受欢迎和使用,开发者也将继续生产出更多创新的应用。
Android TV
与 Android Wear 相比,Android TV 位于大小谱的另一端。就像 Wear 一样,当我们为这种形态设计应用时,其大小至关重要。主要考虑因素是用户与屏幕的距离,这通常在 10 英尺左右。这意味着设计简单、干净的布局,并避免使用小而/或长的文本。
与 Wear 不同,我们为手机和平板电脑设计的许多应用也可以提供给电视。正如人们所想象的那样,这需要我们对清单进行一些调整,以便让我们的应用在 Google Play 商店中可见,供搜索特定电视应用的用户使用。此外,电视没有我们手机上的一些功能,如 GPS 和触摸屏,我们也需要考虑这一点。
Android Studio 中由项目向导生成的电视应用模板存在 bug,除非谷歌在你阅读此内容时已经修复了它,否则使用它来生成一个可工作的应用远非直截了当。尽管如此,它仍然值得一看,因为 Java 目录中包含十几个专为流媒体和电视节目等应用设计的类,这些类非常方便。
模板无法工作的事实,在我们的情况下实际上是一件好事,因为我们可以利用这个部分来了解如何从头开始构建一个兼容电视的应用程序,或者如果您愿意,如何将已经开发的应用程序转换为可以在 Android TV 上安装的形式。
以下练习可以使用空白活动模板进行,适用于手机和平板电脑或您已经开发的应用程序。
-
打开您项目的清单文件。
-
在根清单节点内部,添加以下权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> -
在同一节点中添加以下功能使用:
<uses-feature android:name="android.hardware.touchscreen" android:required="false" /> <uses-feature android:name="android.software.leanback" android:required="false" /> <uses-feature android:name="android.hardware.microphone" android:required="false" /> <uses-feature android:name="android.hardware.screen.portrait" android:required="false" /> -
找到一个或创建一个 320 x 180 px 的
xhdpi横幅图像来代表您的应用程序。理想情况下,它应该包含文本以及一个可识别的图像,如下所示:![Android TV]()
-
将图像放置在您的
drawable文件夹中,并命名为banner。 -
在应用程序节点内部添加以下行:
android:banner="@drawable/banner" -
打开您的
build.gradle文件并添加以下依赖项:compile 'com.android.support:leanback-v17:22.0.0' -
在清单文件中,将主题声明行更改为以下内容:
android:theme="@style/Theme.Leanback" -
创建一个电视 AVD 并测试应用程序。
虽然在这里我们并没有做很多,但还有很多东西需要解释。我们大部分的工作都是为了确保我们的应用程序在浏览 Play 商店的电视应用程序时是可见的。电视不支持纵向屏幕布局,因此如果需要此功能,它将简单地不会出现在 Play 商店中作为电视可用。我们希望包括支持此方向设备的特性,这就是我们如何做到这一点,同时仍然使我们的应用程序对电视用户可用。我们必须在所有电视应用程序中包含录制音频的权限,但电视通常不支持麦克风。
我们在第 7 步中添加的Leanback 支持库是开发电视应用程序的一个非常有用的工具。它提供了一个非常合适的主题Theme.Leanback,几个专为电视设计的实用小部件,并且以这种方式管理边距,以确保我们的布局不会被裁剪。如果没有它,我们可能需要设置大约 10%的宽边距来避免这种电视过度扫描。如果我们想专门为电视编写,我们也会更改主活动意图过滤器内的类别<category android:name="android.intent.category.LEANBACK_LAUNCHER" />。
对于 Android TV 的编程,尤其是广播和流媒体方面,还有很多内容我们在这里无法涵盖。不过,一般来说,开发电视应用程序所需的技能与手持设备编程相同,许多应用程序在两种格式上都能运行得很好。只要我们考虑到用户与屏幕的距离以及有限的输入方式,我们就可以编写出能够在所有平台上提供满意体验的程序。
Android 操作系统是一个非常灵活的系统,非常适合各种形态。我们已经看到它可以在半英寸到家庭影院大小的屏幕上运行。Android Lollipop 预示着另一个新的令人兴奋的平台,允许用户在驾驶汽车时运行 Android 应用程序。
Android Auto
Android Auto 应用是在连接到兼容的车载仪表盘时,在驾驶员仪表盘上运行某些受限内容的应用。当为汽车开发时,主要关注的是安全,任何不符合这些严格标准的 Auto 应用都不会在 Play 商店发布。许多 Android 应用在驾驶时过于分散注意力,不适合使用。实际上,Android Auto 真正支持的功能只有两个:音频播放和文本到语音消息。本书的范围不涉及这一方面的全面探讨,但这是一个很好的时机来了解这类应用的设置方式以及如何使用 SDK 中提供的媒体和消息模拟器。首先,我们需要看看谷歌对 Auto 应用坚持的安全规则:
-
在自动屏幕上不得有动画元素
-
只允许音频广告
-
应用必须支持语音控制
-
所有按钮和可点击控件必须在两秒内响应用户操作
-
文本必须超过 120 个字符,并且必须始终使用默认的 Roboto 字体
-
图标必须是白色,以便系统可以控制对比度
-
应用必须支持日间和夜间模式
-
应用必须支持语音命令
-
应用特定的按钮必须以不超过两秒的延迟响应用户操作
重要:这些以及其他一些规定,在发布前将由谷歌进行测试,因此您自己运行所有这些测试至关重要。完整列表可以在developer.android.com/distribute/essentials/quality/auto.html找到。
小贴士
设计适合日间和夜间模式的应用,并且可以通过系统自动在不同光照条件下保持可读性,这是一个相当详细的主题,谷歌为此提供了一份非常有用的指南,可以在commondatastorage.googleapis.com/androiddevelopers/shareables/auto/AndroidAuto-custom-colors.pdf找到。
尽管我们有这些限制,但 Auto 应用的开发方式与其他应用相同。不过有一个小差别,当为 Auto 开发时,我们需要定义我们的应用使用哪些车载功能。这是通过一个 XML 文件来完成的。按照以下简短的步骤,看看如何操作:
-
为手机和平板电脑启动一个新的 Android Studio 项目,或者打开一个现有的项目。
-
在
res目录内创建一个新的Android 资源目录,并命名为xml。![Android Auto]()
-
在这个
xml文件夹内,创建一个新的 Android 资源文件,并命名为类似auto_config.xml。 -
按照以下方式完成:
<?xml version="1.0" encoding="utf-8"?> <automotiveApp> <uses name="media" /> <uses name="notification" /> </automotiveApp> -
打开清单文件。
-
在
application节点内添加以下行:<meta-data android:name="com.google.android.gms.car.application" android:resource="@xml/auto_config"/> -
就这样,我们的应用现在将检测其宿主设备是否连接到车载仪表盘。
这里只需指出两点。这两个名称用途分别是用于运行音频播放应用和接收消息。只有当我们的应用被设计为同时具备这两个功能时,我们才需要两者。
Google 为测试媒体浏览和消息应用提供了模拟器,让我们可以在桌面上安全地测试项目。以下步骤演示了如何安装它们以及如何使用 telnet 连接发送模拟短信:
-
从 Android Studio 中打开 SDK 管理器。
-
确保你有最新的 Android Auto API 模拟器版本,它们位于
Extras文件夹中。 -
连接一个设备或模拟器。
-
前往你的
sdk/extras/google/simulators文件夹,并在此处使用 Shift + 右键点击打开命令窗口。 -
使用
adb devices检查你的设备是否已连接。 -
输入以下两个命令来安装这两个模拟器:
adb install media-browser-simulator.apk adb install messaging-simulator.apk小贴士
如果你使用的是第三方虚拟设备,例如 Genymotion,你将能够通过将它们拖放到模拟器屏幕上来安装这些应用。
使用 adb 命令安装模拟器非常简单,当然,任何 .apk 文件都可以通过这种方式安装到已连接的设备上。媒体浏览器模拟器可以用大多数媒体服务进行测试,例如 Play Music。

可以通过从 Emulator Control 选项卡启动 Android Device Monitor 来向消息模拟器发送文本。
这就是我们在 Android Auto 中能涵盖的所有内容。该平台提供了 Lollipop 提供的最激动人心的新可能性之一,毫无疑问,Android 将在未来越来越多地出现在车辆中。
摘要
Android Wear、TV 和 Auto 与传统形态有根本性的不同,彼此之间也完全不同。这意味着我们在这里必须涵盖很多不同的领域。
尽管它们的体积和功能相对较小,但可穿戴设备为我们提供了巨大的可能性。我们现在知道如何创建和连接可穿戴 AVD,以及如何轻松地为方形和圆形设备开发。然后我们探讨了设置 TV 应用所需的条件,如何将现有应用转换为可在电视上使用,以及 Leanback 支持提供的有用库和功能。我们最后探讨了在开发过程中必须遵守的严格安全规则,以及用于测试 Auto on 的工具。
最大的改进之一,尽管可能对用户来说不太明显,涉及相机 API。这些 API 对 Lollipop 来说是全新的,而且添加多媒体到我们的应用也是我们在下一章将要讨论的内容。
第九章。相机、视频和多媒体
近年来,移动多媒体技术取得了巨大进步,许多人不仅在他们的移动设备上听音乐和看电影,而且使用它们来制作自己高质量的多媒体内容。SDK 提供了 API,允许我们包含媒体播放以及媒体捕获,并且随着相机 API 的完全重写,现在是开发 Android 多媒体应用程序的最好时机。
许多多媒体功能可以通过简单地利用系统的原生应用程序(如相机)非常容易地集成到我们的应用程序中。或者,我们可以直接与 API 合作,开发处理所有照片和视频捕获过程的应用程序,尽管这不是一个简单的任务。然而,简单实现的一件事是在我们的应用程序中包含多媒体的录制和播放,包括音频。
在本章中,你将:
-
使用原生相机应用程序预览图像
-
自动重构代码
-
从原生相机保存图像到我们的应用程序
-
处理 IO 异常
-
创建一个唯一的文件名
-
将图像添加到设备图库
-
使图像私有
-
捕获和播放视频
-
添加视频控件
-
处理视频中断而不会丢失位置
-
将视频打包到应用程序中
-
从内存中播放视频
-
从网络流式传输视频
-
使用 MediaRecorder 记录音频文件
-
使用 MediaPlayer 播放音频文件
捕获图像
更多的时候,当在我们的应用程序中包含图像或视频捕获时,我们只需要利用系统已经为这些目的设计的应用程序,并且我们可以通过 Intent 调用它们,就像我们在自己的应用程序中调用 Activity 一样。我们甚至不需要知道调用的是哪个应用程序,因为系统会自动寻找最合适的,甚至在有选择的情况下为用户提供选择。
在这里,我们将在 Ancient Britain 应用程序中包含一个拍照功能,该功能利用原生相机应用程序捕获图像,将其显示在视图中并保存到特定目录。然后我们将我们的图像提供给设备图库和其他应用程序。这不是一个简短的练习,所以我们将它分成三个部分:准备和重构、预览相机拍摄,以及保存相机拍摄。
代码重构
为了节省时间,我们不会为我们的相机功能设置另一个按钮。相反,我们将重用当前用于将用户带到相关维基百科页面的 ImageView。我们还需要设置一些权限和功能使用,并添加一个新图形。按照以下步骤准备 Ancient Britain 应用程序,我们在第四章管理 RecyclerView 及其数据中开始,以包含对原生相机的调用:
-
在 Android Studio 中打开
Ancient Britain项目并打开清单文件。 -
包含以下标签:
<uses-feature android:name="android.hardware.camera" android:required="true" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> -
找到一个适合相机功能的图标大小的图片,例如以下所示:
![重构代码]()
-
在文件资源管理器视图中将其保存在
res/drawable目录中,并用您刚刚制作的文件替换web_icon.png文件。 -
打开
DetailActivity.java文件。 -
定位以下行,并在
detailWebLink上右键单击:ImageView detailWebLink = (ImageView) findViewById(R.id.detail_web_link); -
按Shift + F6重命名实例
detailCameraButton。 -
对于 XML 引用,也进行相同的操作,将其重命名为
detail_camera_button。 -
在项目资源管理器中的
drawable文件夹中选择web_icon并将其重命名为camera_icon。![重构代码]()
我们在这里做的第一件事是向清单中添加权限和功能,这里包含的功能是为了防止没有摄像头的设备在 Play 商店中找到它。
小贴士
如果您正在为 API 级别 17 或以下开发,您需要添加权限:android.permission.CAMERA。
我们接下来所做的重构并非绝对必要,但它使代码更容易理解,并展示了使用F6键重命名事物是多么简单。这些效果会在整个项目中传播,例如当我们在一个 Java 中的 XML 引用中重命名时,相应的布局文件也会相应地编辑,并且通过重构菜单有许多方便的重构工具可用。
预览相机输出
为了预览相机,我们需要触发一个调用本地相机的 intent,以及当相机返回到我们的应用程序时的响应方式。这三个步骤实现了这一点:
-
将以下字段添加到
detailActivity类中:private static final int PREVIEW_REQUEST_CODE = 1; private static final int SAVE_REQUEST_CODE = 2; private String photoPath; private File photoFile; -
将现在
detailCameraButton按钮的onClickListener中的onClick()方法替换为以下代码:@Override public void onClick(View v) { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (intent.resolveActivity(getPackageManager()) != null) { startActivityForResult(intent, PREVIEW_REQUEST_CODE); } } -
为该类提供以下
onActivityResult()方法:@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == PREVIEW_REQUEST_CODE && resultCode == RESULT_OK) { Bundle extras = data.getExtras(); Bitmap imageBitmap = (Bitmap) extras.get("data"); detailImage.setImageBitmap(imageBitmap); } else if (requestCode == SAVE_REQUEST_CODE && resultCode == RESULT_OK) { // To complete } }
现在可以运行应用程序了。点击相机图标将允许您拍照,照片将在布局中的其他ImageView中显示。

我们在这里创建的 intent 是在MediaStore类上调用的,使用一个打开本地相机的常量。注意相机活动是如何通过 intent 的resolveActivity()方法受到保护的。如果没有适合请求的应用程序在设备上,并且触发了 intent,那么应用程序将会崩溃。如果没有找到合适的应用程序,PacketManager()将不包含任何内容。
当控制权交回我们的应用程序时,会调用onActivityResult()方法。requestCode用于检查相机活动是从哪里被调用的,而resultCode用于测试它是否成功。我们使用数据值对data从相机返回的 Bundle 中提取位图。这个特定的图像只是一个缩略图。完整图像是可用的,接下来我们将看到如何将其存储在 SD 卡上。
保存相机输出
为了节省时间和额外的编码,我们将使用现有的小部件作为按钮来触发保存图片以及拍照的 Intent。我们将替换主图像视图的 onTouchListener 为 onClickListener 并从那里调用所需的函数。按照以下步骤查看如何操作:
-
在
detailActivity类中,将onCreate()方法中的detailImage.setOnTouchListener(listener);行替换为以下代码:detailImage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { takePhoto(); } }); -
创建
takePhoto()方法,如下所示:private void takePhoto() { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { File photoFile = null; try { photoFile = filename(); } catch (IOException ex) { Toast toast = Toast.makeText(getApplicationContext(), "No SD card", Toast.LENGTH_SHORT); toast.show(); } if (photoFile != null) { takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile)); startActivityForResult(takePictureIntent, SAVE_REQUEST_CODE); } } } -
包含
filename()方法,如下所示:private File filename() throws IOException { String time = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String file = MainData.nameArray[MainActivity.currentItem] + "_" + time + "_"; File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); File image = File.createTempFile(file, ".jpg", dir); photoPath = "file:" + image.getAbsolutePath(); return image; } -
在
onActivityResult()方法中,替换掉注释的// To complete line with this code:else if (requestCode == SAVE_REQUEST_CODE && resultCode == RESULT_OK) { Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); Uri contentUri = Uri.fromFile(photoFile); intent.setData(contentUri); this.sendBroadcast(intent); } -
您现在可以运行并测试应用。点击相机图标将替换主图像为刚刚拍摄的图像,点击图像本身将允许您将图片保存到设备的 SD 卡上的图片目录中。
![保存相机输出]()
显然,这个示例处理用户输入的方式有点笨拙。理想情况下,我们会添加新的按钮或甚至另一个 Activity 来处理预览和保存图片。我们采取这种方法是为了简洁并突出显示这些过程本身。这两个方法本身也值得仔细检查。
takePhoto() 方法触发与相机按钮的 onClick() 方法相同的 intent。使用不同的请求代码来展示我们如何调用相同的外部 Activity,但根据调用位置的不同而做出不同的响应。Android 通常管理异常相当好,但我们不能保证 SD 卡的存在,并且尝试捕获这个异常是有意义的。我们可以创建一个消息。如果 photoFile 文件的创建成功(这很少不成功),它可以通过以下行包含在我们的 Intent 中 takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile));。
当创建文件名时,我们需要小心不要与其他文件冲突。在没有太多预防性代码的情况下,可以通过设置一个唯一的文件名来实现,这里是通过附加时间戳来完成的。这个方法在我们尝试捕获系统异常时被调用,因此需要抛出 IOException 声明。
最后,我们在 onActivityResult() 方法的 else 子句中添加了一些代码,该子句在图片保存并控制返回到我们的应用后被调用。ACTION_MEDIA_SCANNER_SCAN_FILE Intent 是请求媒体扫描器在下次运行时将其添加到媒体数据库。这意味着我们的图片将出现在原生图库应用中,并且可供任何使用媒体数据库的其他应用使用,例如壁纸选择器。
如果你只想让你的图像在应用内部可用,仅仅省略这些行是不够的,因为图像仍然可以通过任何文件浏览软件访问。为了防止这种情况,使用Environment.getExternalFilesDir()而不是Environment.getExternalStoragePublicDirectory()。这也会在应用卸载时删除这些文件。
小贴士
媒体扫描器不一定在可预测的时间运行,在测试时,你可能需要重新启动你的设备或模拟器来强制它包含你的文件。
以这种方式控制平台的摄像头是一个非常方便的方法,可以在最少的编码下整合其功能。当然,从头开始重新创建一个摄像头或视频应用是完全可能的,我们很快就会看看如何做到这一点。首先,让我们看看如何以与这里使用摄像头相同的方式录制和播放视频。
捕获和播放视频
使用原生应用从我们自己的应用中捕获视频内容的方式几乎与我们刚刚应用的方式相同。主要区别在于,在处理视频内容时,许多功能都是由专门设计的控件VideoView提供的。我们还将添加带有MediaController的视频控制按钮,并看看如何在应用发送到后台时暂停视频。按照以下步骤构建一个简单的视频应用:
-
开始一个新的 Android Studio 项目。
-
将我们在上一个练习中包含的功能使用和权限添加到清单中。
-
打开
activity_main.xml文件,并用以下VideoView替换TextView:<VideoView android:id="@+id/video_view" android:layout_width="match_parent" android:layout_height="match_parent" /> -
打开
MainActivity.java并添加以下字段:private static final int VIDEO_REQUEST_CODE = 1; private android.widget.VideoView videoView; private int position = 0; private MediaController mediaController; -
在
onCreate()方法中包含以下代码:videoView = (VideoView) findViewById(R.id.video_view); if (mediaController == null) { mediaController = new MediaController(this); } videoView.setMediaController(mediaController); takeVideo();. Add the takeVideo() method, like so: private void takeVideo() { Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); if (takeVideoIntent.resolveActivity(getPackageManager()) != null) { startActivityForResult(takeVideoIntent, VIDEO_REQUEST_CODE); } } -
然后是
onActivityResult()方法:@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == VIDEO_REQUEST_CODE && resultCode == RESULT_OK) { Uri videoUri = data.getData(); videoView.setVideoURI(videoUri); } } -
如果你现在测试项目,你将能够录制和播放视频。然而,如果 Activity 失去焦点并重新启动,视频也将从头开始播放。
为了纠正这个问题,添加这两个方法:
@Override public void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); savedInstanceState.putInt("Position", videoView.getCurrentPosition()); videoView.pause(); } @Override public void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); position = savedInstanceState.getInt("Position"); videoView.seekTo(position); } -
如果你现在测试应用并使用另一个应用中断播放,然后返回到 Activity,它将从上次停止的地方继续播放。
![捕获和播放视频]()
到第 6 步之前,我们派发意图以捕获视频的方法几乎与用于静态图像的方法相同,唯一的区别是MediaController,它添加了我们所有人都与视频播放相关联的熟悉控件。在处理视频时,尤其是处理较长的视频时,用户可能希望暂停播放并与其他应用交互。为了确保用户返回时视频从上次停止的位置继续播放,我们不得不在应用发送到后台之前使用onSaveInstanceState()方法拦截 Activity 生命周期,并在它返回时再次使用onRestoreInstanceState()。我们在这里使用了VideoView.pause()和VideoView.seekTo()。以下是在VideoView中可用于控制视频播放的方法:
-
VideoView.start() -
VideoView.pause() -
VideoView.resume() -
VideoView.seekTo(position)
尽管能够在我们的应用中提供视频录制功能非常有用,但很多时候我们可能想要播放应用内打包的视频或从外部源(如设备 SD 卡或甚至从互联网流式传输)的视频。上面的示例只需要进行一些小的调整,下一节将展示如何将其调整为从除相机本身以外的其他来源播放视频。
从内存和互联网播放视频
我们可能希望在应用中包含视频内容或播放其他应用产生的视频的原因不计其数,在本节中,我们将看到如何打包视频到我们的应用中,以及如何从设备的存储和互联网上播放视频。以下练习将指导您如何完成这些操作:
-
打开我们刚刚工作的项目。
-
在
res目录下创建一个名为raw的新文件夹。![从内存和互联网播放视频]()
-
找到一个格式为以下之一、命名为
movie的短视频文件,并将其粘贴到res/raw文件夹中:.webm、.3gp、.mp4或.mkv。 -
打开
MainActivity文件。 -
在
onCreate()方法中,注释掉对takePhoto()的调用,并添加以下两行:videoView.setVideoURI(Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.movie)); videoView.start(); -
如果您想要在应用内播放视频,可以在这里停止并运行应用。
-
要播放存储在设备 SD 卡上的视频,将您刚才输入的行替换为以下内容:
videoView.setVideoPath("/sdcard/some_directory/some_movie.mp4"); videoView.start(); -
如果运行,应用现在将播放 SD 卡上的指定文件。要流式传输视频,请使用以下代码:
videoView.setVideoPath("http://www.your_site.com/movies/movie.mp4"); videoView.start();
就这些了。我们将应用内的视频存储在res/raw目录下。虽然未包含在内,但当我们创建项目时,raw是一个已识别的资源文件夹,可以用于存储我们不希望在项目构建和/或打包时编译的任何文件。
关于此代码的其他注意事项是,当从内部存储或互联网加载时,我们使用VideoView.setVideoPath()而不是VideoView.setVideoURI()。
调用其他应用,例如相机应用,是一种非常方便的方式,可以在不进行大量编码的情况下集成这些功能。当然,有时我们可能希望更深入地集成相机 API。这需要从头开始构建相机,但这超出了本章的范围。然而,Android 5 确实引入了一套全新的相机 API,即android.hardware.camera2,它取代了android.hardware.Camera API。Camera2允许一些令人兴奋的新特性,例如控制单个相机和改进的存储能力,尽管这里没有足够的空间从头开始构建 camera2 应用,但 SDK 中包含了一个非常有信息量的示例,我们将现在查看它。
探索 camera2 API
camera2 API 比它们的 predecessors 复杂得多,但它们也复杂得多。从头开始构建相机 app 远非简单。幸运的是,Android SDK 中包含了许多示例 app,并且有一个合适的 camera2 样本,我们可以查看。
样本可以直接从启动窗口的快速启动面板中加载到 Android Studio,在 IDE 内部选择文件 | 导入样本...。

大多数 camera2 过程都是从CameraManager开始的。这个类允许我们识别和连接到设备上连接的任何相机,以及确定它们的属性。在示例中,Camera2BasicFragment类是大多数有趣工作的地方,你可以看到如何在openCamera()方法中使用CameraManager打开相机,以及在setUpCameraOutputs()中使用CameraCharacteristics类获取相机 ID 以及它是否是前置相机。这个类在setUpCameraOutputs()方法中也可以看到,它被用来排除前置相机。
CameraDevice类用于在应用中表示单个相机,并用于设置CaptureRequest和CaptureRequestSession,以便进行拍照的实际过程。这也使我们能够控制诸如自动对焦和白平衡等功能。《CameraCaptureSession》是提供 camera2 功能的地方,例如能够连续拍摄多张图片。
探索和实验Camer2Basic样本是非常值得的,还有一个Camera2Video样本。如果你对使用 Android 5 功能从头开始构建相机 app 感兴趣,那么官方文档developer.android.com/reference/android/hardware/camera2/package-summary.html是值得查看的。
尽管 camera2 API 非常复杂,但它们有一个严重的缺点:它们是 Android 5 API 中唯一一组难以实现向后兼容的 API。任何主要依赖相机和视频功能的 app 都需要一定数量的替代代码来使其适用于旧平台。Jelly Bean 和 KitKat 目前占据了超过四分之三的市场份额,并且看起来很可能会在未来一段时间内占据你目标受众的很大一部分。除非你计划利用 camera2 特定的功能,例如以 RAW 格式捕获图像或连续拍摄多张照片,否则你应该认真考虑使用原始的 Camera API,尽管它们已经过时,但仍然完全可用。
录制和播放音频
在本章中,我们之前看到了如何使用原生应用和 VideoView 捕获和播放多媒体内容。还有一个非常实用的工具用于录制和播放媒体文件,特别是音频:MediaRecorder 类。MediaRecorder 允许我们简单地设置音频源、输出位置和格式,同时让我们控制播放和录制功能。在这个练习中,我们将开发一个小型应用,用于录制和播放设备内置麦克风捕获的音频:
-
开始一个新的 Android Studio 项目。
-
找到三个与按钮大小相似的媒体图像,如下所示,并将它们放置在您的
drawable文件夹中。![录制和播放音频]()
-
将它们命名为播放、录制和停止。
-
打开清单文件并包含这些权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> -
创建一个类似于下面的布局:
![录制和播放音频]()
-
使用
ImageViews作为按钮,并给它们分配 IDsrecord_button、stop_button和play_button。将TextView命名为text_view。 -
打开
MainActivity并包含这两个字段:private MediaRecorder recorder; private String filename; -
在
onCreate()方法中添加这个TextView:final TextView textView = (TextView) findViewById(R.id.text_view); -
添加这个文件路径:
filename = Environment.getExternalStorageDirectory().getAbsolutePath() + "/recording.3gp"; -
然后添加这些
MediaRecorder配置:recorder = new MediaRecorder(); recorder.setAudioSource(MediaRecorder.AudioSource.MIC); recorder.setAudioEncoder(MediaRecorder.OutputFormat.AMR_NB); recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); recorder.setOutputFile(filename); -
在布局中创建的每个
ImageView都需要在onCreate()方法中添加一个OnClickListener,如下所示:-
ImageView
recordButton:ImageView recordButton = (ImageView) findViewById(R.id.record_button); recordButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { try { recorder.prepare(); } catch (IOException e) { } recorder.start(); textView.setText("Recording..."); } }); -
ImageView
stopButton:ImageView stopButton = (ImageView) findViewById(R.id.stop_button); stopButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { recorder.stop(); recorder.release(); textView.setText("Recording complete"); } }); -
ImageView
playButton:ImageView playButton = (ImageView) findViewById(R.id.play_button); playButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { try { play(); } catch (IOException e) { } textView.setText("Playing..."); } });
-
-
最后,添加
play()方法,其外观如下:public void play() throws IllegalArgumentException, SecurityException, IllegalStateException, IOException { MediaPlayer player = new MediaPlayer(); player.setDataSource(filename); player.prepare(); player.start(); } -
您现在可以在手机上运行该应用(因为库存模拟器还没有麦克风功能),录制和播放音频。文件
recording.3gp可以在 SD 卡的根目录中找到。
MediaRecorder 类简化了音频录制工作,也可以用来录制视频。就像我们用来播放音频的 MediaPlayer 类一样,这个类同样易于使用。使用 MediaRecorder.release() 很重要,因为没有它,系统会继续使用资源。我们在这里只准备和播放了文件,但 MediaPlayer 可以做更多的事情,因此查看其文档是值得的,文档可以在 developer.android.com/reference/android/media/MediaPlayer.html 找到。
再次强调,我们使用了 Environment.getExternalStorageDirectory() 来自动选择用户首选的外部存储设备,尽管我们与本章早期管理多媒体的方式有所不同,但任何一种方法都可以应用于许多情况。MediaRecorder 和 MediaPlayer 一起提供了一个简单但强大的方法,将音频整合到我们的应用中。
摘要
多媒体,如音频和视频,已成为我们使用移动设备方式的一个不可或缺的部分。无论是创建还是消费多媒体内容,在我们的应用中加入多媒体功能都能使它们更具吸引力和实用性。在本章中,我们学习了如何将原生应用,如相机,集成到我们自己的应用中,从而在过程中节省了大量编码工作。我们看到了如何捕捉、录制和回放相机图像、视频,以及最终音频。
这不仅结束了我们对 Android 5 多媒体的探索,而且也差不多结束了本书的编程部分,因为最后一章探讨了如何将我们的成品推向世界,以及如何将我们的辛勤工作转化为经济收益。本章中有一两个练习,我们将探讨如何使我们的应用向后兼容,以吸引更多的潜在用户,并且我们将最后一次回到 Ancient Britain 应用,使用 Google AdMob 服务为其添加移动广告。
第十章。发布和营销
覆盖了本书的所有主题后,如果你还没有这样做,你现在可以创建、开发和营销自己设计的应用了。当然,关于 Android 5 以及 Android 本身还有很多东西要学习,但现在我们已经了解了如何应用一些最常用的结构和对象,进一步探索 SDK 就变得相当简单。一旦我们了解了监听器接口的实现方式,那么查找文档以了解何时需要引入新的接口就变得简单了。
开发 Android 应用的整个目的就是为了分发它。尽管有众多方式可以让我们的工作对他人可用,但最明显的选择是通过 Android Play Store。本章将一步步指导你如何做到这一点。在这个过程中,我们将了解如何使我们的应用与早期版本兼容,保留 Android 5 API 的大部分功能以及许多编程到我们的 Lollipop UI 中的 Material Design 特性。
在本章中,你将:
-
制作向后兼容的应用
-
了解如何为旧系统创建替代布局
-
将 Material 主题应用于旧版本
-
用 Material Design 工具栏替换 ActionBar
-
准备应用发布
-
创建数字证书和私钥
-
生成一个签名 APK 文件
-
准备促销媒体
-
完成商店列表
-
发布应用
-
学习如何通过电子邮件和网站分发应用
-
许可应用
-
提供产品或出版商的链接
-
添加官方品牌
-
构建一个用于应用内付费的模板项目
-
包含一个 AdMob 横幅广告
制作向后兼容的应用
在整本书中,我们一直专注于为 Android 5 开发,尽管运行这个平台设备的数量注定会大幅增加,但它们仍然只占所有活跃 Android 设备的一小部分。实际上,Jelly Bean和KitKat(API 16 至 19)仍然占据了访问 Google Play Store 的平台版本中的绝大多数。

小贴士
可以在developer.android.com/about/dashboards/index.html找到关于所有活跃设备上平台相对分布的最新报告。这个页面还包含了关于当前使用的屏幕尺寸和密度的类似信息,可以极大地帮助我们定位用户。
显然,我们希望我们的应用能够触及尽可能多的人,我们开发的许多应用只需做很少的调整就可以让运行早期版本的用户使用。幸运的是,Android 提供了支持库,如v7 AppCompat r21(或更高版本)来简化这个过程。
添加 v7 支持库
实际上,我们在开始开发之前应该仔细考虑我们希望我们的应用在哪些平台上可用;然而,为了演示目的,在接下来的简短练习中,我们将使我们在书中早期开发的应用对运行 API 16 及更高版本设备的设备可用。
-
打开我们之前开发的Ancient Britain应用。
-
打开
manifest文件。 -
在根节点内部,包括以下标签:
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="22" /> -
打开
build.gradle文件并添加以下依赖项:compile 'com.android.support:cardview-v7:22.0.+' compile 'com.android.support:recyclerview-v7:22.0.+' compile "com.android.support:appcompat-v7:22.0.+" -
编辑默认配置,如下所示:
defaultConfig { applicationId "com.example.kyle.ancientbritain" minSdkVersion 16 targetSdkVersion 22 versionCode 1 versionName "1.0" } -
准备一个针对 API 级别 16 的 AVD 或手机:
![添加 v7 支持库]()
-
在设备上运行应用。它看起来会正常工作,直到你尝试在 DetailActivity 屏幕上滑动图片,那时它会崩溃。
-
打开
DetailActivity.java文件。 -
在
onShowPress()和onFling()方法中,有一个调用detailImage.setElevation()。对每个方法应用条件语句,如下所示:if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { detailImage.setElevation(4); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { detailImage.setElevation(0); } -
再次运行应用以检查此修复是否已生效。
在清单中声明uses-sdk是必要的,因为这是 Play Store 决定哪些设备可以看到应用的方式。v7 AppCompat r21+库使得向后兼容成为可能。除此之外,它还提供了非常不错的 Material Design 小部件和其他 UI 组件。还有针对RecyclerView和CardView的库,尽管阴影不是动态的,但考虑到我们的应用现在可以触及的大量用户,这只是一个微不足道的代价。
更改最低 SDK 级别是我们使我们的应用对旧版本可用所需做的第一件事。正如我们所见,21 级或更高版本的 API 会在调用时导致应用崩溃,就像在这个任务中的setElevation()调用一样。通过能够在运行时查询设备的 API,我们有办法绕过这个限制,并且通常用户体验的质量损失很小。
另一种方便的方法来解决这个问题,是为不同的平台创建不同的布局。你可以为你的 Material Design 布局创建一个res/layout/v-21/目录,并在res/layout/中创建旧版替代方案。
要真正将Material Design的感觉带到旧平台上,我们可以利用这些库做更多的事情,比如使我们的自定义主题可用,这就是我们接下来要做的。
将 Material Design 应用于旧平台
通常在为 API 21 之前的平台开发应用时,我们在创建应用时将最低 SDK 设置为最低的目标级别,而不是像我们刚才做的那样逆向工程过程。在这里,我们将看到如何添加许多Material Design功能。以这种方式开发也是一个很好的方法来判断我们希望我们的应用向后兼容到什么程度,以及我们愿意放弃多少功能。
这个下一个练习演示了如何为 API 16 构建一个应用并应用 Material Design 到 UI。为此,请按照以下步骤操作:
-
在 Android Studio 中创建一个名为Material Jelly Bean的新项目。
-
不要在包名中使用
com.example。 -
选择手机和平板作为形态因子,并将API 16作为最小 SDK。注意支持你的应用设备数量。
![将 Material Design 应用于旧平台]()
-
选择空白活动并保持其他一切不变,或者选择你自己的值。
-
将以下内容添加到清单文件的根节点:
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="22" /> -
打开
res/values/styles.xml文件并按照以下方式填写:<resources> <style name="AppTheme" parent="Theme.AppCompat.Light"> <item name="colorPrimary">#ff7d00</item> <item name="colorPrimaryDark">#d96a00</item> <item name="colorAccent">#b25900</item> </style> </resources> -
打开
activity_main.xml文件。 -
将根布局从相对布局更改为线性布局,并通过添加以下内容设置其方向:
android:orientation="vertical" -
将
TextView转换为这个EditText:<EditText android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="Hello ?" android:inputType="text" /> -
在此点运行应用,我们的 Material 主题调色板将被应用:
![将 Material Design 应用于旧平台]()
-
通过在
styles.xml文件中设置主题来禁用 ActionBar:Theme.AppCompat.Light.NoActionBar -
在
EditText上方放置这个工具栏:<android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/colorPrimary"/> -
你可能希望调整
dimens.xml文件中布局的填充。 -
打开你的主活动文件,并将以下代码添加到
onCreate()方法中:Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); if (toolbar != null) { setSupportActionBar(toolbar); } -
这将生成一个导入错误。使用快速修复选择 v7 Toolbar。
-
在 API 16 设备或模拟器上运行应用,以查看 Material Design 风格的工具栏。
![将 Material Design 应用于旧平台]()
AppCompat主题系列提供了我们习惯的大多数 Material Design 功能。我们选择的代表我们应用的颜色仍然在应用中熟悉的地点出现,并着色各种小部件,使我们的应用具有一致和可识别的感觉。然而,某些元素仍然丢失,如果你在 Android 5 设备上运行应用,你会得到以下输出。

关于我们实现Material Toolbar的方式有两个重要的事情需要注意。首先,注意我们的MainActivity类扩展了ActionBarActivity,并且对于任何使用 AppCompat 构建的应用来说,如果它要有工具栏,这必须是这样。其次,注意我们通过setSupportActionBar()方式填充它,这与大多数视图不同。这是我们习惯于管理工具栏的方式之间的唯一两个真正差异;除此之外,其他一切都可以使用我们熟悉的类和方法来完成。
为了将 Material Design 引入早期版本,可以做一些其他的事情,但到目前为止,这已经足够让我们开始将我们的应用带给尽可能多的人。接下来,我们将转向一个更严肃的主题,即向世界发布我们的应用。
发布应用
不言而喻,你已经在各种手机和模拟器上彻底测试了你的应用,可能已经准备好了你的推广材料,并检查了Google Play 政策和协议。在发布之前有许多事情需要考虑,例如内容评级和国家分布。从编程的角度来看,在我们继续之前,我们只需要检查三件事情。
-
从项目中移除所有日志,例如:
private static final String DEBUG_TAG = "tag"; Log.d(DEBUG_TAG, "some info"); -
确保你在清单文件中声明了应用
标签和图标,例如:android:icon="@mipmap/my_app_icon" android:label="@string/my_app_name" -
确保你在清单文件中声明了所有必要的权限,例如:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
现在,我们只需再走三个步骤就能在 Google Play 商店看到我们的应用。我们所需做的就是生成一个签名发布 APK,注册为Google Play 开发者,最后将我们的应用上传到商店或在我们自己的网站上发布。还有一两种发布应用的方法,我们将在本节末尾看到它们是如何操作的。不过,首先,我们将开始生成一个准备好上传到 Google Play 商店的 APK。
生成签名 APK
所有发布的 Android 应用都需要一个数字签名的证书。这用于证明应用的真伪。与许多其他数字证书不同,这里没有权威机构,你持有签名密钥,这显然需要得到安全保护。为此,我们需要生成一个私钥,然后使用它来生成签名 APK。所有这些都可以在 Android Studio 的“生成签名 APK 向导”中完成。这些步骤将引导你完成。
-
打开你想要发布的应用。
-
从构建 | 生成签名 APK...菜单启动“生成签名 APK 向导”。
-
在第一个屏幕上选择创建新...。
-
在下一个屏幕上,为你的密钥库提供路径和名称,并设置一个强密码。
-
对于别名也做同样的操作。
-
选择一个大于 27 年的有效期,如下所示:
![生成签名 APK]()
-
至少填写一个证书字段。点击确定,你将被带回到向导界面。
-
选择发布作为构建变体,然后点击完成。
-
现在,你已经准备好了一个可用于发布的签名 APK。
密钥库(.jks文件)可以用来存储任意数量的密钥(别名)。通常,你会为每个发布的应用使用不同的密钥,并且在生成应用的更新时必须使用相同的密钥。Google 要求证书至少有效到 2033 年 10 月 22 日,任何超过这个日期的数字都适用。
小贴士
重要提示:至少保留一个密钥的安全备份。如果你丢失了它们,你将无法开发那些应用的未来版本。
大多数安卓应用都是以这种方式打包的,只有一个例外:谷歌穿戴。如果你正在发布一个穿戴应用,你首先需要访问 developer.android.com/training/wearables/apps/packaging.html。在我们的数字证书签发并就绪后,我们现在只需两个步骤就可以发布。如果你还没有这么做,现在是时候注册成为谷歌 Play 开发者了。
注册为开发者
与签署 APK 类似,注册开发者账号同样简单。请注意,谷歌会收取一次性费用 25 美元以及你应用可能产生的收入的 30%。以下说明假设你已经拥有一个谷歌账号。
-
在以下位置查看支持位置:
support.google.com/googleplay/android-developer/table/3541286?hl=en&rd=1 -
前往开发者 Play 控制台:
-
使用你的谷歌账号登录并输入以下信息:
![注册为开发者]()
-
阅读并接受谷歌 Play 开发者分发协议。
-
使用谷歌 Checkout 支付 25 美元,如果需要则创建一个账号,这样你就成为了一名注册的谷歌开发者。
如果你打算让应用在全球范围内可用,那么检查支持位置页面总是值得的,因为它会定期变化。剩下要做的就是上传我们的应用,我们现在就要这么做。
在谷歌 Play 商店发布应用
将我们的应用上传并发布到 Play 商店是通过开发者控制台完成的。正如你将看到的,在这个过程中,我们可以提供大量关于我们应用的信息和推广材料。如果你已经遵循了本章前面的步骤并且有一个准备发布的已签名的 .apk 文件,请完成以下说明以发布它。或者,你可能只是想看看目前涉及的内容以及推广材料的形式。在这种情况下,确保你拥有以下四张图像和一个已签名的 APK,并在最后选择保存草稿而不是发布应用。
-
至少两张应用截图。这些截图的任何一边都不能短于 320 像素或长于 3840 像素。
-
如果你希望你的应用在 Play 商店对搜索平板电脑应用的用户可见,那么你应该准备至少一张 7 英寸和一张 10 英寸的截图。
-
一张 512 x 512 像素的高分辨率图标图像。
-
一个 1024 x 500 像素的特色图形,例如;准备好这些图像和已签名的
.apk文件,我们就需要开始。决定你希望为应用收取多少费用(如果有的话),然后按照以下说明操作:-
打开你的开发者控制台。
-
提供一个标题并点击上传 APK按钮。
-
点击上传第一个生产版 APK。
-
定位您的已签名
app-release.apk文件。它将在AndroidStudioProjects\YourApp\app中。 -
将其拖放到建议的空间中。
-
当此操作完成后,您将被带到应用页面。
-
逐步完成前四个部分:
![在 Google Play 商店发布应用]()
-
完成所有必填字段,直到 发布应用 按钮变为可点击状态。
-
如果您需要帮助,按钮上方的 为什么我不能发布? 链接将列出未完成的必填字段。
-
当所有必填字段都填写完毕后,点击页面顶部的 发布应用(或保存草稿)按钮。
-
恭喜!您现在是一名已发布的 Android 开发者。
-
我们现在知道如何将我们的应用发布到 Play 商店。当然,还有许多其他的应用市场,它们都有自己的上传流程。然而,Google Play 提供了最广泛的受众群体,是发布应用的明显选择。
在我们继续之前,还有两种其他分发方法我们将要探讨:在网站上发布和通过电子邮件分发。
通过电子邮件和网站分发
这两种方法中的第一种与听起来一样简单。如果您将 APK 附件附加到电子邮件中,并在 Android 设备上打开,当附件打开时,用户将有机会安装应用。在较新的设备上,他们可以直接从电子邮件中点击安装按钮。
小贴士
对于这两种方法,您的用户必须在设备的设置中允许安装 未知来源。
从您的网站分发应用几乎与通过电子邮件发送一样简单。您需要做的只是将 APK 文件托管在您的网站上某个位置,并提供类似 <a href="download_button.jpg" download="your_apk"> 的下载链接。当从 Android 设备浏览您的网站时,点击您的链接将在他们的设备上安装您的应用。
小贴士
通过电子邮件分发无法提供对盗版的保护,因此应仅在此前提下使用。其他方法与我们期望的同样安全,但如果您想采取额外措施,Google 提供了可以在 developer.android.com/google/play/licensing 找到的 许可服务。
无论我们发布的是付费应用还是免费应用,我们都希望能够触及尽可能多的用户。Google 提供了几个工具来帮助我们做到这一点,以及我们将要看到的应用盈利方式。
推广和盈利应用
很少有应用在没有先进行良好推广的情况下取得成功。有无数种方法可以做到这一点,毫无疑问,您在如何推广您的产品方面将领先一步。为了帮助您触及更广泛的受众,Google 提供了一些实用的工具来协助推广。
在查看推广工具之后,我们将探讨两种从我们的应用中赚钱的方法:应用内支付和广告。
推广应用
Google 提供了两种非常简单的方法来帮助引导人们访问 Play Store 上的我们的产品;来自两个网站和我们的应用的链接以及 Google Play 徽章,它为我们提供了官方的品牌标识。
我们可以添加指向单个应用和我们的出版商页面的链接,在那里可以浏览我们所有的应用。我们可以在我们的应用和网站上包含这些链接。
-
要在 Play Store 中包含指向特定应用的页面的链接,请使用在 Manifest 中找到的完整包名,格式如下:
http://play.google.com/store/apps/details?id=com.full.package.name -
要在 Android 应用中包含此链接,请使用:
market://details?id= com.my.full.package.name -
如果您想要一个指向您的出版商页面以及所有产品的列表的链接,请使用:
http://play.google.com/store/search?q=pub:my publisher name -
当从应用中链接时,请进行与之前相同的更改:
Market://search?q=pub:my publisher name -
要链接到特定的搜索结果,请使用:
search?q=my search query&c=apps. -
要使用官方 Google 徽章作为链接,请将上述元素之一替换为以下突出显示的 HTML:
<a href="https://play.google.com/store/search?q=pub:my publisher name"> <img alt="Get it on Google Play" src="img/en_generic_rgb_wo_60.png" /> </a>
徽章有两种尺寸,60.png 和 45.png,以及两种样式,"Android app on Google Play" 和 "Get it on Google Play"。只需更改相关代码以选择最适合您目的的徽章。

在我们的应用发布并放置了指向 Play Store 页面的链接后,现在是时候考虑如何从不可避免的下载中获利了,因此我们来到了如何货币化我们的 Android 应用。
应用货币化
从应用中赚钱有很多方法,但最受欢迎和有效的方法有两种:内购(IAB)和广告。内购可能相当复杂,也许值得单独一章来介绍。在这里,我们将看到如何构建一个有效的模板,您可以用它作为您可能开发的应用内产品的基石。它将包括所有需要的库和包,以及一些非常有用的辅助类。
相比之下,在我们的应用中包含 Google AdMob 广告对我们来说现在是一个非常熟悉的过程。广告实际上只是另一个 View,可以像任何其他 Android 小部件一样被识别和引用。本章和本书的最终练习将是构建一个简单的 AdMob 示例。不过,首先,让我们看看内购。
内购
用户可以在应用内购买大量产品,从升级和可解锁内容到游戏内对象和货币。无论用户购买什么,Google 结账流程都确保他们将以与其他 Play Store 产品相同的方式支付。从开发者的角度来看,每次购买都将归结为对按钮点击的响应。我们需要安装 Google Play Billing Library,并在我们的项目中添加一个 AIDL 文件和一些辅助类。以下是方法:
-
开始一个新的 Android 项目或打开一个你想添加内购功能的项目。
-
打开 SDK 管理器。
-
在 Extras 下,确保已安装 Google Play Billing Library。
-
打开 Manifest 并应用以下权限:
<uses-permission android:name="com.android.vending.BILLING" /> -
在 Studio 的项目面板中,右键单击 app 并选择 新建 | 文件夹 | AIDL 文件夹。
-
从此文件夹作为 aidl,创建一个 新建 | 包,并按照如下填写结果对话框:
![应用内购买]()
-
在
sdk\extras\google\play_billing目录中找到并复制IinAppBillingService.aidl文件。 -
将文件粘贴到
com.android.vending.billing包中。 -
在 Java 文件夹中创建一个 新建 | 包,从对话框中选择
...\app\src\main\java。 -
将包命名为
com.你的包名.util并点击 完成。 -
从
play_billing目录中,找到并打开TrivialDrive\src\com\example\android\trivialdrivesample\util文件夹。 -
将九个 Java 文件复制到您刚刚创建的
util包中。
现在,您已经为任何希望包含应用内购买的应用有了工作模板。或者,您可以在已经开发好应用内产品的项目中完成上述步骤。无论哪种方式,您无疑都将利用 IabHelper 类,该类极大地简化了编码,为购买过程的每个步骤提供了监听器。有关 IAB 的文档可以在 developer.android.com/google/play/billing/billing_reference.html 找到。
小贴士
在您开始实现应用内购买之前,您需要为您的应用获取一个 许可证密钥。这可以在您的开发者控制台中的应用详情中找到。
付费应用和在应用内产品只是从应用中赚钱的两种方式,许多人选择另一种,通常是更有利可图的,通过广告来货币化他们的工作。Google AdMob 提供了很大的灵活性以及熟悉的编程接口,正如我们接下来将要看到的。
包含广告
我们可以从广告中赚取很多钱的方法,但 AdMob 提供了其中最简单的一种。该服务不仅允许您选择您希望广告的产品类型,而且还提供了出色的分析工具和无缝的付款到您的 Checkout 账户。
此外,AdView 可以以与我们习惯和熟悉的方法几乎相同的方式进行编程处理,正如我们将在最后的练习中看到的那样,我们将开发一个带有演示横幅 AdMob 广告的 Hello World 应用。
在开始这个练习之前,您需要先在 www.google.com/admob/ 上注册一个 AdMob 账户。
-
打开一个您想要测试广告的项目或开始一个新的 Android 项目。
-
确保您已通过 SDK 管理器安装了 Google 仓库。
-
在
build.gradle文件中,添加以下依赖项:compile 'com.google.android.gms:play-services:7.0.+' -
重新构建项目。
-
在清单中设置这两个权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -
在
application节点内,添加以下meta-data标签:<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> -
将此第二个 Activity 包含到清单中:
<activity android:name="com.google.android.gms.ads.AdActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize" android:theme="@android:style/Theme.Translucent" /> -
将以下字符串添加到
res/values/strings.xml文件中:<string name="ad_id">ca-app-pub-3940256099942544/6300978111</string> -
打开
main_activity.xml布局文件。 -
将此第二个命名空间添加到根布局中:
-
在
TextView下添加这个AdView:<com.google.android.gms.ads.AdView android:id="@+id/ad_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" ads:adSize="BANNER" ads:adUnitId="@string/ad_id"></com.google.android.gms.ads.AdView> -
在
MainActivity的onCreate()方法中插入以下行:AdView adView = (AdView) findViewById(R.id.ad_view); AdRequest adRequest = new AdRequest.Builder() .addTestDevice(AdRequest.DEVICE_ID_EMULATOR) .build(); adView.loadAd(adRequest); -
现在在设备上测试应用。
![包含广告]()
大多数我们在这里做的事情都类似于我们编程其他元素的方式,只有一个或两个例外。使用ACCESS_NETWORK_STATE权限并不是严格必要的;在这里使用它是为了在请求广告之前检查连接。
任何显示广告的活动都需要一个单独的 ID,并在清单中声明。这里提供的 ID 仅用于测试目的,并且禁止使用实时 ID 进行测试。android.gms.ads包中只有六个类,所有这些类的文档都可以在developers.google.com/android/reference/com/google/android/gms/ads/package-summary找到。
AdMob 广告有两种类型,这里我们看到的是横幅广告和插屏广告,或全屏广告。我们在这里只处理了横幅广告,但插屏广告的处理方式非常相似。掌握了如何实现付费应用、应用内购买和 AdMob 的知识,我们现在可以收获我们辛勤工作的回报,并最大限度地发挥我们应用的优势。
摘要
在本章中,我们涵盖了应用开发过程的最后一个方面:打包和部署。我们首先使我们的应用向后兼容,包括我们最初为 Android 5 设计的许多功能;通过这样做,我们能够触及更广泛的受众。然后我们准备并在 Google Play Store 发布我们的应用。一旦发布,我们就看到了推广和货币化 Android 应用是多么容易。
这标志着我们进入 Android 开发世界的旅程结束。我们从安装到发布,并希望覆盖了你计划中的应用所需的大部分组件。如果你是开发新手或 IDE,如 Android Studio,并且已经通读了这本书,那么之前令人生畏的工具集现在将显得熟悉且富有成效的工作场所。
毫无疑问,Android 平台将继续以新的和意想不到的方式繁荣发展。Android 5 是一个完美的入门点;以 Material Design 为核心,拥有最强大的移动 API 集合,对于 Android 开发者来说,事情只会变得更好、更光明。













































































浙公网安备 33010602011771号