精通安卓穿戴应用开发-全-
精通安卓穿戴应用开发(全)
原文:
zh.annas-archive.org/md5/85721a5dc2a64fc4dc1a64685fd22ae5译者:飞龙
前言
本书旨在为在移动、桌面或网络平台上工作的开发者提供指导,帮助他们学习如何为可穿戴设备(也称为 wear apps)构建应用程序。此外,您可能已经在 Google Play 商店上发布了应用程序,并希望为现有的 Android 应用提供 Android Wear 支持。如果这两个陈述中的任何一个是真的,那么这本书就是为您准备的。
本书的主要目标是向您,读者,提供对构建设计良好且稳健的 Android Wear 应用程序所涉及的哲学、思维过程、开发细节和方法论有一个坚实的理解。我们将讨论可穿戴计算范式的优缺点,并在此过程中,希望为构建满足实际和现实世界用例的可穿戴应用提供一个坚实的基础。
我们将探讨从基础到中级再到高级的广泛概念和功能,复杂程度各异。每章附带的代码示例旨在让您通过实际操作了解使用工具、库、SDK 和其他相关技术来构建 Android Wear 应用所需的知识。
随着您阅读本书的章节,您可以期待实现以下目标:
-
理解可穿戴计算技术
-
使用 Android Studio 为构建 Android Wear 应用设置开发环境
-
开始掌握 Android Wear SDK 和 API
-
理解围绕 Android Wear 应用开发的常用 UI 模式和 UX 原则
-
与不同形态的可穿戴设备(圆形、方形)协同工作
-
利用 Android Wear 设备上的传感器
-
开发 Android Wear 示例应用以尝试您所学的概念
-
在 Android 移动(手持)应用和 Android Wear 应用之间进行通信
-
学习如何将 Android Wear 应用发布到 Google Play 商店
本书涵盖内容
第一章, 可穿戴计算简介, 介绍了可穿戴计算的基本知识以及该技术的演变过程。它还包括了关于移动计算、普适计算和云计算的讨论。
第二章, 设置开发环境, 重点介绍让读者熟悉设置开发环境的过程,从 IDE 安装到讨论 Android Wear 开发所需的 SDK 和库。
第三章, 开发 Android Wear 应用, 指导读者逐步学习如何使用 Android Studio 从零开始开发 Android Wear 应用程序,即 Today 应用。
第四章, 开发表盘 UI,通过使用 Android Wear SDK 中可用的 UI 组件扩展 Today 应用,并使用自定义布局构建自定义 UI 组件。
第五章, 同步数据,介绍了需要伴侣手持应用的想法,包括将手持设备与 Android Wear 模拟器配对的步骤,从而扩展了你的可穿戴应用开发环境。Today 应用进一步扩展以演示这些概念。
第六章, 上下文通知,讨论了 Android Wear 中的通知,并通过 On This Day 活动扩展 Today 应用来演示 Android Wear 通知 API。
第七章, 语音交互、传感器和跟踪,讨论了 Wear API 提供的语音功能。我们定义了一个语音操作来启动我们的应用。我们介绍了设备传感器,并讨论了如何使用它们来跟踪数据。
第八章, 创建自定义 UI,涵盖了 Android Wear UI 空间中至关重要的设计原则,并考察了几种常见的 Wear UI 模式。我们还增强了 On This Day 活动,以便以用户友好的格式显示。
第九章, 材料设计,提供了对材料设计的概念理解,并触及了几个特定于可穿戴应用设计和开发的几个关键原则。通过将前几章中的 Todo 应用扩展以包含一个导航抽屉,我们可以在此抽屉中切换待办事项类别、查看项目并执行针对每个类别的特定操作,从而巩固我们的理解。
第十章, 表盘,在本章中介绍了表盘的概念。在简要概述了可用于帮助我们开发表盘的 Android Wear API 之后,我们开发了一个简单的交互式表盘。
第十一章, 高级功能和概念,描述了与使应用始终运行相关的 API 功能和设计考虑因素。我们开发了一个活动来演示 Wear API 提供的始终在线功能。我们还简要讨论了通过蓝牙连接调试可穿戴应用。
第十二章, 将应用发布到 Google Play,讨论了可用于测试 Android Wear 应用的工具以及如何自动化 UI 测试。我们通过逐步说明使应用准备发布的过程来结束本章。
你需要为此书准备的东西
您将需要以下工具集来尝试书中的代码并自己练习应用开发:
-
Android Studio v2 或更高版本
-
JDK v7 或更高版本
-
Git 版本控制
-
一个具有良好硬件配置的开发系统,例如用于开发移动应用的高速 CPU 和足够的 RAM
本书面向的对象
Java 应用程序开发者——无论是 Web、桌面还是移动,都希望接触 Android Wear 平台并掌握开发 Android Wear 应用所需的知识。
约定
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"我们可以通过使用include指令来包含其他上下文。"
代码块设置如下:
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
}
任何命令行输入或输出都应如下书写:
vi run json
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"点击下一步按钮将您带到下一个屏幕。"
注意
警告或重要注意事项以如下框的形式出现。
小贴士
小技巧和技巧如下所示。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发您真正能从中获得最大收益的标题。要发送一般反馈,请简单地发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书籍的标题。如果您在某个主题领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 书籍的骄傲拥有者,我们有一些可以帮助您充分利用购买的东西。
下载示例代码
您可以从www.packtpub.com的账户下载此书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的“支持”标签上。
-
点击“代码下载与勘误”。
-
在搜索框中输入书籍名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买此书的来源。
-
点击“代码下载”。
下载文件后,请确保您使用最新版本的以下软件解压缩或提取文件夹:
-
Windows 版的 WinRAR / 7-Zip
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
本书的相关代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Mastering-Android-Wear-Application-Development。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/MasteringAndroidWearApplicationDevelopment_ColorImages.pdf下载此文件。
勘误
尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
侵权
互联网上对版权材料的侵权是一个持续存在的问题,跨越所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过版权@packtpub.com 与我们联系,并提供涉嫌侵权材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
问答
如果您对本书的任何方面有问题,您可以通过 questions@packtpub.com 与我们联系,我们将尽力解决问题。
第一章. 可穿戴计算简介
| *"了解过去越多,你对未来的准备就越充分。" | ||
|---|---|---|
| --西奥多·罗斯福 |
在本章中,我们将讨论可穿戴计算的发展,并了解它如何与其他计算范例相匹配,例如桌面、移动和普适计算。
进化
可穿戴计算,尽管普遍被认为是最新的技术创新,但在算盘时代就已经存在了,算盘是一种几个世纪前由商人和贸易商使用的计算工具。根据中国文化的历史资料,人们认为在清朝时期,嵌在戒指中的算盘被用作计算器(www.chinaculture.org/classics/2010-04/20/content_383263_4.htm):

一种相对较新的现代可穿戴计算设备是卡西欧 Databank。它是卡西欧在 20 世纪 80 年代初制造的一系列电子手表,集成了计算器、计时器、世界时钟、联系人管理和电视及 VCR 的遥控器等功能。
它非常受欢迎,并被认为那个时代的科技奇迹。与那个时代可用的手动或简单的数字手表相比,它非常方便。它不仅用于查看时间和设置闹钟,还帮助执行一些实用功能,例如快速计算或在飞行中回忆存储的联系人信息:

创新者和发明家总是着迷于将技术与生活方式尽可能紧密地结合在一起。无论是被认为是第一个已知现代可穿戴设备用于控制 iPod 的 Burton Amp 夹克,还是用于宠物主人追踪宠物位置和活动的最新可穿戴设备Whistle,可穿戴设备正变得越来越普遍。
摩尔定律
英特尔联合创始人戈登·摩尔 50 年前预测,集成电路中晶体管数量的增长将大约每两年翻一番。这是计算能力爆炸性增长的基准。随着时间的推移,电子元件的尺寸逐渐减小,设备的处理能力变得更强大。
20 世纪 60 年代和 70 年代用于运行企业和大型企业的主机计算机,占据了客厅的大小。随后,它们缩小到中端服务器和台式计算机。计算机中使用的集成电路芯片和微处理器变得越来越强大,存储设备的尺寸变得很小,存储容量也增加了。
桌面电脑逐渐转变,以笔记本电脑和笔记本电脑的形式变得更加便携。笔记本电脑配备了可充电电池,可以让用户在需要的时候,无论何时何地,都可以使用他们的电脑来满足个人或商业需求。
个人数字助理(PDA)被用作移动计算设备,用于管理联系人和执行一些基本的与业务相关的任务。
然后进入了智能手机时代。当史蒂夫·乔布斯在 2007 年推出 iPhone 时,市场上已经有一些智能手机了。然而,苹果推出 iPhone 以及谷歌随后推出的 Android 平台,引领了智能手机行业的强劲和健康竞争。
我们现在在可穿戴设备趋势中看到的是历史的重演。但这一次,谷歌在 2014 年推出了 Android Wear 平台,而苹果在 2015 年 4 月宣布了其首款可穿戴手表。
三星、LG、Pebble 和 Jawbone 等主要公司都纷纷加入了这一行列,市场上已经出现了各种可穿戴设备。
普适计算
普适计算是一种计算范式,其中人类与计算机的交互可以在任何地方、任何地方以及通过他们周围的任何设备发生。比如说,你正在使用办公室的台式电脑处理一个重要的商业提案,你几乎完成了你的提案文档,但现在是离开办公室去接孩子放学,并带她去游泳练习的时间。你离开了工作,接了孩子,带她去了游泳学校。当她正在做游泳练习时,你继续使用你的智能手机从你离开的地方继续处理商业提案,并在她完成游泳练习之前将文档发送给客户。
当你开车回家时,你收到了客户的回复电子邮件。你驾驶的汽车中集成了像Siri或Alexa这样的应用程序或系统,它会大声朗读你从客户那里收到的电子邮件。当你到家时,你使用你的智能手表回复客户的商业提案,甚至为下一次会议安排了日期和地点。
这个例子可能听起来有些夸张,但这里要强调的重要观点是,不是技术正在接管人类的生活,而是人类在随时随地做他们想做的事情,无缝连接,并使用简单的交互。他们周围的设备会帮助他们完成他们想做的事情,而无需知道或感觉到他们在这样做。这就是普适计算的基础哲学。它只是让你在需要的地方做事情,无需询问或了解是否可以在那里完成。
人类与计算设备的交互可以是普遍的,甚至可能在不意识到它发生的情况下发生。
云计算和无线通信协议以及蓝牙、低功耗蓝牙(BLE)、近场通信(NFC)、射频识别(RFID)和 ZigBee 等技术通过形成所有这些设备相互通信和构建所需上下文的基础设施,使得与设备的这种互动成为可能。
应用开发者、设计师和服务提供商应该设计他们的应用程序和服务,以便用户可以在任何地方、使用周围的任何设备与他们互动。每种设备都有其自身的形态和为特定需求而设计。在构建将提供卓越用户体验的应用程序时,理解用户环境和与设备互动的需求非常重要。例如,由于尺寸和形态因素,在手表应用程序中拥有类似键盘的用户界面组件可能并不实用,而使用可穿戴平台内提供的语音输入功能进行语音输入则可能可行。
移动与可穿戴设备
在过去十年左右的时间里,智能手机已经成为我们日常生活的有机组成部分。它们已经成为我们自身的自然延伸,并使我们随身携带它们,放在口袋、手提包或钱包中,以帮助我们完成日常任务。它们被用来执行从日常到更重要任务的各种任务。过去使用个人电脑或笔记本电脑执行的任务,现在正逐渐通过口袋大小的智能手机或平板电脑来完成。
智能手机之所以达到如此高的采用率和普及率,是因为它们的便携性。与个人笔记本电脑相比,它们更轻便,便于携带,用户几乎可以在任何需要的地方使用它们。
尽管智能手机和平板电脑可以在许多情况下满足移动计算需求,但它们在许多情况下并不方便。当你已经忙于一只手时,另一只手又想使用手机,手机就变得不方便。为了完成诸如查看当前时间或快速查看收到的文本通知等微妙任务,你仍然需要从口袋或钱包中取出手机。可穿戴设备可以帮助我们通过更简单、更快捷的交互更快地完成任务。
可穿戴计算是计算创新中的下一个重大前沿。它拥有各种可能性和优势。尽管智能手机被认为是非常个人化的设备,但它们并不像智能手表或健身活动追踪器这样的可穿戴设备那样亲密。可穿戴设备或携带式设备的优势在于它们始终在身上,并测量诸如心率、步数和体温等重要指标。
它们在医疗保健市场具有巨大的潜力,可以每分钟监测我们的健康状况,并指导用户通过必要的步骤来拥有健康的生活方式。
可穿戴设备也可以用于生物识别身份验证。有一些初创公司,如Nymi(nymi.com/using_the_nymi_band),使用个人心率和脉搏率作为身份验证的识别因素。
我们是否可以停止携带基于 RFID 的进入建筑物的访问卡,并使用可穿戴手表进行身份验证?这甚至可能阻止我们记住各种在线网站的密码;相反,我们可以使用生物识别数据,如心率虹膜识别,为登录这些系统建立身份验证配置文件。
Hello Android Wear
安卓穿戴是谷歌为可穿戴设备(如智能手表)设计的安卓操作系统的移植版本。截至写作时,有超过一打制造商,如 LG、摩托罗拉、华为、华硕、Fossil 和 TAG Heuer,生产安卓穿戴手表:

安卓穿戴平台与其竞争对手苹果的watchOS平台的主要区别在于对设备和屏幕尺寸的支持。
与目前仅提供 42 毫米和 38 毫米矩形屏幕尺寸的苹果手表不同,安卓穿戴提供圆形、方形和矩形屏幕形状,而且除了标准的 42 毫米和 38 毫米尺寸外,还有各种不同的屏幕尺寸。
另一个需要注意的关键点是,安卓穿戴设备可以通过安卓穿戴应用在 Android 和 iOS 平台上进行配对。
在这本书中,我们将涵盖安卓穿戴应用程序开发涉及的主题,并帮助你掌握平台,以便编写丰富强大的安卓穿戴应用程序。
摘要
在本章中,我们讨论了可穿戴计算范式,并将其与移动和桌面计算平台进行了对比。
在下一章中,我们将深入探讨使用 Android Studio IDE 设置安卓穿戴应用程序开发环境的相关主题。所以,系好安全带,准备好享受接下来的乐趣和刺激之旅。
第二章 设置开发环境
| *"给我六个小时砍倒一棵树,我会花前四个小时磨斧头。" | ||
|---|---|---|
| --亚伯拉罕·林肯 |
在本章中,我们将讨论使用 Android Studio 设置开发环境所需的步骤、主题和过程。如果您已经使用 Android Studio 进行过 Android 应用程序开发,那么这里讨论的一些项目可能已经对您熟悉。然而,还有一些 Android Wear 平台特定的项目可能对您感兴趣。
Android Studio
Android Studio 集成开发环境(IDE)基于IntelliJ IDEA平台。如果您使用 IntelliJ IDEA 平台进行过 Java 开发,那么您在使用 Android Studio IDE 时会感到很自在。
Android Studio 平台捆绑了开发 Android 应用程序所需的所有必要工具和库。如果您是第一次在开发系统上设置 Android Studio,请确保在安装前满足所有要求。请参考 Android 开发者网站(developer.android.com/sdk/index.html#Requirements)以检查您选择的操作系统所需的项。
注意,您需要在机器上至少安装 JDK 版本 7,Android Studio 才能正常工作。您可以在终端窗口中输入以下命令来验证您的 JDK 版本:

如果您的系统不符合该要求,请使用您操作系统的特定方法进行升级。
安装
Android Studio 平台包括 Android Studio IDE、SDK 工具、Google API 库以及开发 Android 应用程序所需的系统镜像:

访问developer.android.com/sdk/index.html页面下载适用于您对应操作系统的 Android Studio,并按照安装说明进行操作。
Git 和 GitHub
Git 是一个广泛用于开源项目的分布式版本控制系统。在接下来的过程中,我们将使用 Git 进行示例代码和示例项目的开发。
确保您在终端窗口中输入以下命令,以便在您的系统上安装 Git:

如果您还没有安装,请访问git-scm.com/downloads链接下载适用于您对应操作系统的 Git。
如果您正在使用 Mac OS El Capitan 或 Yosemite,或者使用 Ubuntu、Kubuntu 或 Mint 等 Linux 发行版,那么您可能已经安装了 Git。
GitHub (github.com) 是一个免费且流行的基于 Git 的开源项目托管服务。他们使得检查和贡献开源项目变得比以往任何时候都容易。如果您还没有账户,请注册 GitHub 以获取免费账户。
我们将使用 GitHub 来检查与 Android Wear 相关的各种示例项目,以及为本书开发的应用程序的示例代码。我们不需要成为 Android 应用程序开发的 Git 专家,但我们需要熟悉 Git 命令的基本用法,以便与项目一起工作。
Android Studio 默认集成了 Git 和 GitHub。它有助于从 Google 的 GitHub 存储库导入示例代码,并帮助您通过检查各种应用程序代码示例来学习。
Gradle
Android 应用程序开发使用 Gradle (gradle.org/) 作为构建系统。它用于构建、测试、运行和打包应用程序,以便运行和测试 Android 应用程序。
Gradle 是声明性的,并使用约定优于配置来设置和配置构建设置。它管理所有库依赖项,用于编译和构建代码工件。
幸运的是,Android Studio 抽象了大多数开发中需要的常见 Gradle 任务和操作。然而,在某些情况下,拥有一些额外的 Gradle 知识会非常有帮助。我们现在不会深入探讨 Gradle,我们将在旅途中根据需要讨论它。
Android SDK 包
当您安装 Android Studio 时,它不包括开发所需的所有 Android SDK 包。Android SDK 将工具、平台和其他组件和库分成可以下载的包,您可以使用 Android SDK Manager 按需下载。在我们开始创建应用程序之前,我们需要将一些必需的包添加到 Android SDK 中。
从 Android Studio 启动 SDK Manager,工具 | Android | SDK Manager:

让我们快速浏览一下前面截图中的几个项目。
如您所见,在我的机器上,Android SDK 的位置是/opt/android-sdk。根据您在 Android Studio 安装过程中选择的选项,它可能在您的机器上有所不同。需要注意的是,Android SDK 的安装位置与 Android Studio 的路径(/Applications/Android\ Studio.app/)不同。
这被认为是一种良好的实践,因为 Android SDK 的安装可能不会受到 Android Studio 的新安装或升级(反之亦然)的影响。
在SDK 平台选项卡中,选择一些最近的 Android SDK 版本,例如 Android 版本 6.0、5.1.1 和 5.0.1。
根据您计划在可穿戴应用程序中支持哪些 Android 版本,您可以选择其他较旧的 Android 版本。
在右下角检查 显示包详细信息 选项将显示为给定 Android SDK 版本安装的所有包:

为了安全起见,选择所有包。如你所注意到的,Android Wear ARM 和 Intel 系统镜像已包含在包选择中。
现在,当你点击 SDK 工具 选项卡时,确保以下项目被选中:
-
Android SDK 构建工具
-
Android SDK 工具 24.4.1(最新版本)
-
Android SDK 平台工具
-
Android 支持仓库,版本 25(最新版本)
-
Android 支持库,版本 23.1.1(最新版本)
-
Google Play 服务,版本 29(最新版本)
-
Google 仓库,版本 24(最新版本)
-
Intel X86 模拟器加速器 (HAXM 安装程序),版本 6.0.1(最新版本)
-
Android SDK 文档(可选)
SDK 窗口将看起来如下:

不要在 SDK 更新站点 选项卡中更改任何内容。保持更新站点为默认配置。
点击 确定 按钮。下载和安装所选的所有组件和包将需要一些时间。
Android 虚拟设备
Android 虚拟设备(AVD)将使我们能够使用 Android 模拟器测试代码。它允许我们选择和选择所需的 Android 系统目标版本和形态,以便进行测试。
从 工具 | Android | AVD 管理器 启动 Android 虚拟设备管理器。
从 AVD 管理器 窗口,点击左下角的 创建新虚拟设备 按钮,进入下一屏幕并选择 Wear 类别:

选择 Marshmallow API 级别 23 on x86,并将其他所有设置保留为默认值,如下面的截图所示:

注意
注意,截至写作时,当前最新的 Android 版本是 API 级别 23 的 Marshmallow。在你阅读本章时,它可能不是最新版本。请随意选择当时可用的最新版本。此外,如果你想在早期 Android 版本中进行支持或测试,请在该屏幕上自由操作。
点击 下一步 按钮后,将出现配置窗口:

在成功选择虚拟设备后,你应该会在 Android 虚拟设备 列表中看到它,如下面的截图所示:

注意
虽然在开发期间使用真实的 Android Wear 设备不是必需的,但有时在真实的物理设备上开发可能更方便且更快。但为了这本书的目的,我们将主要介绍使用 Android 模拟器进行开发和测试。
让我们构建一个骨架应用程序
由于我们已经拥有了构建可穿戴应用所需的所有组件和配置,让我们构建一个骨骼应用并测试到目前为止我们所做的工作。
从 Android Studio 的快速开始菜单中,点击导入 Android 代码示例选项:

从可穿戴类别中选择骨骼可穿戴应用:

点击下一步并选择您首选的项目位置。
如您所见,骨骼项目是从 GitHub 上的谷歌样本代码库克隆的:

点击完成按钮将拉取源代码,Android Studio 将编译和构建代码,并使其准备就绪以执行。
以下截图表明 Gradle 构建已成功完成,没有错误。点击以下截图中的绿色播放按钮以运行配置:

当应用开始运行时,Android Studio 将提示我们选择部署目标。我们可以选择之前创建的模拟器并点击确定:

在代码编译并上传到模拟器后,骨骼应用的主活动将如以下所示启动:

点击显示通知标签将显示通知:

点击开始计时器标签将启动计时器并运行 5 秒,点击完成活动将关闭活动并将模拟器带回到主屏幕:

摘要
我们通过介绍安装说明、要求、SDK 工具、包和其他 Android Wear 开发所需的组件,讨论了设置 Android Studio 开发环境的涉及过程。
我们还检查了来自谷歌样本代码库的骨骼可穿戴应用的源代码,并在 Android 设备模拟器上成功运行和测试了它。
在下一章中,我们将从头开始构建一个真实的 Android Wear 应用,包括我们迄今为止创建的所有配置和设置。
第三章:开发 Android Wear 应用程序
| "所有妥协都是基于给予和接受,但在基本原则上不能有任何妥协。对基本原则的任何妥协都是投降。因为那全是给予而没有接受。" | ||
|---|---|---|
| --圣雄甘地 |
在本章中,我们将讨论上一章使用 Android Studio 导入的示例骨架项目所涉及的概念。我们将详细讨论相关的代码,以帮助我们了解 Android Wear 应用程序的基本构建块。
然后,我们将使用 Android Studio 从头开始创建一个新的 Android Wear 应用程序。我们将逐步讲解创建应用程序所需的步骤,并讨论所需的代码更改以及运行应用程序以查看预期结果。
让我们卷起袖子,看看一些代码的实际效果。
骨架应用程序
如果您还记得上一章的内容,我们使用 Android Studio 导入了示例项目来构建一个基本的可穿戴应用程序。如果您想知道所有代码的来源,这里是这个项目的 GitHub 仓库链接,github.com/googlesamples/android-SkeletonWearableApp/.
Android Studio 一直在更新。在撰写本文时,Android Studio 2.0 预览版 7 是当前可用的最新版本。当您阅读这本书时,它可能或可能不是同一版本。
如果由于某种原因,Android Studio 不允许您导入骨架可穿戴应用程序,或者示例代码的仓库不在 Google 的 Samples GitHub 仓库中可用,您可以从我在 GitHub 仓库中为该项目创建的分支中克隆它,github.com/siddii/android-SkeletonWearableApp.
注意
如果您之前进行过任何 Android 应用程序开发,您将能够跟随本章内容。如果没有,这是一个复习一些 Android 应用程序开发基本概念和基础的好时机。
Android 清单文件
每个 Android 应用程序都包含一个名为 AndroidManifest.xml 的 Android 清单文件。它包含 Android 操作系统启动应用程序所需的全部必要信息。清单文件用于声明活动、服务、意图、SDK 版本、功能、权限以及 Android 应用程序的其他特定组件和行为元素。
我们应该密切关注包含在骨架可穿戴应用程序中的 Android 清单文件的第 23 行:
<uses-feature android:name="android.hardware.type.watch" />
这一行基本上向 Android 操作系统解释说它是一个 Android Wear 应用程序。为了证明这一点,让我们尝试从 AndroidManifest.xml 文件中注释掉这一行并启动应用程序。当您使用模拟器启动应用程序时,您应该看到以下错误信息:

Gradle 构建文件
让我们来看看 Gradle 构建文件是如何配置的。根目录中的settings.gradle文件包括此项目的所有模块。在这种情况下,只有一个模块,即Wearable模块文件夹:

你会注意到有两个build.gradle文件。一个位于项目级别,另一个位于Wearable文件夹的模块级别。
项目的build.gradle文件是空的,因为我们没有针对项目构建设置的具体内容,而Wearable模块中的build.gradle文件包含此应用程序的所有构建配置:

build.gradle文件的 20、21 和 22 行指定了此项目的外部构建依赖项。第 20 行包括对Google Play Services的依赖,它是 Android Wear 平台的一个组成部分。Google Play Services 在数据同步和 Android 手机与可穿戴设备之间的通信中被大量使用。我们将在后面的章节中详细讨论它们。
第 21 行包括对v13支持库的依赖。原因是我们在GridExampleActivity类中使用了Fragment。
显然,第 22 行包括对可穿戴支持库的依赖,因为这是一个可穿戴应用程序。
其余的配置对于 Android 应用程序来说相当标准,包括源路径、编译和目标 SDK 版本以及应用程序的版本设置。
应用活动
骨干项目包含两个活动,MainActivity和GridExampleActivity。MainActivity活动包含一个带有Main Activity标题文本和位于ScrollView布局中的三个按钮。
让我们来看看main_activity.xml文件,看看组件布局是如何结构的。使用 Android Studio 开发 Android 应用程序的最大优势之一是其布局编辑器。尽管在运行时可能并不完全相同,但它可以在 UI 组件运行时渲染方面提供一个更直观的概念。
在你打开main_activity.xml文件后,确保你已经在 Android Studio 右侧选择了预览工具窗口:

Android Studio 布局编辑器将显示布局文件更改的实时预览。它还有助于了解组件在不同形态因子上的布局方式。正如我们所知,Android Wear 有方形和圆形的形态因子,因此检查组件在方形和圆形形式上的布局将是一个好主意。
点击布局下拉菜单将显示 SDK 和工具配置中可用的所有形态因子:

如果你选择了Android Wear Round布局,那么你将能够看到main_activity.xml文件在圆形表盘的 Android Wear 手表上的渲染效果。
为了提供最佳的用户体验,你应该确保组件布局合理,并且用户能够无障碍地访问 UI 元素。我们能够做到这一点的唯一方法是,在多种形态和布局设置上测试应用程序:

随着本书的进一步学习,我们将利用一些 Android 支持库的技术,帮助服务方形和圆形的 Android Wear 设备。
到目前为止,你已经对如何使用一些基本的 Android Wear API 对象和组件开发骨架可穿戴应用程序有了基本的了解。花些时间阅读GridExampleActivity和其他项目部分,以了解这些项目组件是如何相互连接的。
如果你对这个项目中的代码不太熟悉,不要害怕。本书的后续章节中,我们将深入探讨 Android Wear 应用程序开发的各个方面。
让我们构建一个 Android Wear 应用程序
到目前为止,我们一直在查看从 Google 的 GitHub 仓库克隆的骨架可穿戴应用程序样本。我们熟悉了骨架应用程序的结构,并对代码和组件的结构有了一些想法。
现在,是我们构建自己的 Android Wear 应用程序的时候了。我们将使用 Android Studio 从头开始创建这个应用程序。
从 Android Studio 欢迎屏幕,点击如下截图所示的创建新 Android Studio 项目选项:

使用适合你系统设置的名称、域名和包名配置项目:

在目标 Android 设备屏幕上,取消选中手机和平板选项,并确保选择Wear选项。Android Studio 将根据你系统上安装的 SDK 和系统镜像自动选择创建应用程序所需的最安全的最低 SDK。
我们将查看 Android Studio 为这个应用程序选择的默认设置:

点击下一步按钮后,选择空白 Wear 活动。默认情况下,Android Studio 将选择始终开启 Wear 活动:

在下一屏幕上,保持活动名称和其他配置不变:

点击完成按钮后,Android Studio 将花费一些时间来编译和构建项目。
完成后,你将看到包含MainActivity和三个布局文件的项目,分别是activity_main.xml、rect_activity_main.xml和round_activity_main.xml。
Android Studio 还会为应用程序创建一个默认的运行配置:

点击 运行 将列出所有 Android 设备模拟器。你可以从我们之前创建的列表中选择一个。
在你选择模拟器之后,设备将启动并且 Android Studio 将将应用部署到模拟器上。在这个阶段请保持耐心,因为设备启动并运行我们刚刚构建的应用可能需要一段时间。尽量不要干扰正在运行的模拟器,因为它可能会影响应用的运行。
小贴士
虽然使用模拟器开发 Android 应用听起来可能很简单,但令人沮丧的部分是模拟器的启动和应用加载时间。如果你有使用物理设备开发的选择,请选择它,而不是浪费大量时间等待和观察。
在本书的某个阶段,我们将使用物理 Android 手机和可穿戴设备进行开发。当我们到达那里时,我们将介绍使用这些设备所需的设置和配置。
下面是成功运行此应用后屏幕的截图:

现在我们来为这个项目添加一些定制功能。更新 MainActivity 文件的 onCreate 方法,如下所示:
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);
Date today = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("EEEE, MMMM d - yyyy");mTextView.setText("Today is " + dateFormat.format(today));
}
});
}
我们在这里做的基本上是为 mTextView 组件设置一个动态文本,内容为今天的日期。
使用以下内容更新 rect_activity_main.xml 文件。我们刚刚更新了布局的背景颜色,并更新了文本视图的颜色和填充选项:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.siddique.androidwear.today.MainActivity"
tools:deviceIds="wear_square"
android:background="@color/orange"
>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_square"
android:textSize="25sp"
android:textAlignment="center"
android:textColor="@color/black"
android:paddingTop="25dp"
/>
</LinearLayout>
当你重新运行应用时,你的输出应该看起来像以下截图:

注意
如果你遇到了前面步骤中的任何问题,不要担心。我们讨论的代码可以在 GitHub 仓库中找到(github.com/siddii/mastering-android-wear/tree/master/Chapter_3)。你可以用它作为参考或与你的项目进行比较。
摘要
我们通过查看骨架可穿戴应用的示例代码,讨论了我们的 Android Wear 项目的各种组件和方面。我们查看了 Android Studio 的布局编辑器,并了解了它是如何用于预览布局文件的实时更新的。
我们使用 Android Studio 从头开始开发了 Today 这个 Android Wear 应用。随着本书的进一步进展,我们将扩展这个应用。
第四章:开发手表 UI
| "要创造非凡的东西,你的心态必须始终如一地专注于细节。" | ||
|---|---|---|
| --乔治·阿玛尼 |
在本章中,我们将扩展在前一章中开始使用的 Android Wear SDK 中可用的 UI 组件的Today应用。我们还将探讨使用自定义布局构建自定义 UI 组件,这些布局将适合手表的尺寸。
我们将随着在本书各章节中的学习,迭代和逐步开发Today应用。当相关时,我们将介绍 Android Wear SDKs 和 API 的各种概念和功能,并利用它们使这个应用尽可能功能丰富。
请注意,当我们完成这一章时,Today应用绝对不会完成。随着我们在后续章节中介绍更多 API 概念,它将得到改进。
注意
本章的代码可在 GitHub 上参考(github.com/siddii/mastering-android-wear/tree/master/Chapter_4)。
为了简洁起见,仅包含所需的代码片段。鼓励读者从 GitHub 下载引用的代码,并在阅读章节时跟随进度。
可穿戴 UI
到现在为止,你可能已经意识到 Android Wear 手表不仅仅比其前辈(如手机和平板)体积更小。它有各种细微差别和特性,使得 Android Wear 手表与其他大屏幕设备不同。
首先,没有键盘输入,至少目前没有。这给为 Android Wear 平台设计应用带来了巨大的挑战。由于缺少键盘(物理或虚拟)数据输入,用户交互性非常有限。
此外,我们也没有其他所有 Android 设备都有的通用返回按钮。在 Android 平台上,返回按钮使得在应用内导航和切换应用变得容易得多。没有它,在应用内和之间导航将会困难得多。Android Wear 手表上的滑动手势就像返回按钮一样使用。
在我们开始编写 Android Wear 应用的 UI 组件和导航代码之前,如果你对 Android Wear 手表平台组件和导航流程不是很熟悉,现在花些时间在物理设备或模拟器上看看它们是如何工作的会是个不错的选择。探索各种内置应用,看看滑动手势和导航是如何工作的。
这里要记住的重要一点是,尽管 Android Wear 设备与手机和平板电脑的工作方式不同,但它运行的是每个设备上都可用的相同 Android 平台(操作系统)。然而,并非所有 UI 组件和小部件都适用于或与 Android Wear 平台相关。它将是一个组件的子集,在某些情况下,它将是手机和平板电脑上可用的简化版本。
Android 清单文件
Today应用目前有两个活动。主活动称为TodayActivity,另一个是DayOfYearActivity,用于显示特定于年份的数据。
注意使用uses-feature标签,这使得它成为一个 Android Wear 手表应用:
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.siddique.androidwear.today">
<uses-feature android:name="android.hardware.type.watch" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<activity
android:name=".TodayActivity"
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=".DayOfYearActivity"
android:label="@string/day_of_year_card_title">
</activity>
</application>
</manifest>
TodayActivity 活动
让我们看看主活动中有哪些内容——TodayActivity。在onCreate方法中,我们将activity_main.xml布局设置为内容视图。并且我们有一个与ListViewAdapter类关联的WearableListView。
注意,TodayActivity活动还实现了WearableListView类的点击监听器,这就是为什么你会看到onClick处理方法紧挨着onCreate方法实现的原因.
到目前为止,onClick监听器方法只处理列表视图中的第一个项目。点击时会启动DayOfYearActivity,并且当传递默认的Intent包时:
public class TodayActivity extends Activity implements WearableListView.ClickListener
{
private static final String *TAG* = TodayActivity.class.getName();
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WearableListView listView = (WearableListView) findViewById(R.id.action_list);
listView.setAdapter(new ListViewAdapter(this));
listView.setClickListener(this);
}
@Override
public void onClick(WearableListView.ViewHolder viewHolder)
{
Log.i(*TAG*, "Clicked list item" + viewHolder.getAdapterPosition());
if (viewHolder.getAdapterPosition() == 0)
{
Intent intent = new Intent(this, DayOfYearActivity.class);
startActivity(intent);
}
}
@Override
public void onTopEmptyRegionClick()
{
.. .
}
private static final class ListViewAdapter extends WearableListView.Adapter
{
private final Context mContext;
private final LayoutInflater mInflater;
private String[] actions = null;
private ListViewAdapter(Context context)
{
mContext = context;
mInflater = LayoutInflater.from(context);
actions = mContext.getResources().getStringArray(R.array.*actions*);
}
@Overridepublic
WearableListView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
return new
WearableListView.ViewHolder(mInflater.inflate(R.layout.*list_item*, null));
}
@Overridepublic
void onBindViewHolder(WearableListView.ViewHolder holder, int position)
{
TextView view = (TextView) holder.itemView.findViewById(R.id.*name*);
view.setText(actions[position]);
holder.itemView.setTag(position);
}
@Overridepublic
int getItemCount()
{
return actions.length;
}
}
}
arrays.xml文件中的操作
列表视图操作的字符串值在arrays.xml文件中声明. 随着我们对这个应用进行改进或添加功能,我们可以向此文件添加更多操作:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="actions">
<item>Day of Year</item>
<item>On this day...</item>
</string-array>
</resources>
主活动布局文件
主活动布局文件activity_main.xml相当简单。它只包含在布局中定义的WearableListView组件。正如我们之前提到的,WearableListView组件是ListView方法的优化版本,适用于小屏幕设备。它处理滚动和过渡所需的所有滚动:
<?xml version="1.0" encoding="utf-8"?><android.support.wearable.view.WearableListView
android:id="@+id/action_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:dividerHeight="0dp"/>
当你使用 Android Wear 模拟器启动应用时,你应该看到带有自定义启动图标的列表中列出了应用,如下面的截图所示。请注意,各种设备分辨率的图标放置在app/src/main/res/mipmap-*文件夹中:

WearableListItemLayout 组件
WearableListView组件用于在应用中显示可用的操作列表,而WearableListItemLayout组件用于对单个列表项中的组件进行样式或布局。
在这个特定的情况下,我们有ImageView和TextView标签. 注意android:src="img/wl_circle"行的使用。它本质上是一个位于res/drawable/wl_circle.xml目录中的可绘制文件。
TextView标签用于显示来自arrays.xml文件的单独操作字符串:
<com.siddique.androidwear.today.WearableListItemLayout
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="80dp">
<ImageView
android:id="@+id/circle"
android:layout_height="25dp"
android:layout_margin="16dp"
android:layout_width="25dp"
android:src="img/wl_circle"/>
<TextView
android:id="@+id/name"
android:gravity="center_vertical|left"
android:layout_width="wrap_content"
android:layout_marginRight="16dp"
android:layout_height="match_parent"
android:fontFamily="sans-serif-condensed-light"
android:lineSpacingExtra="-4sp"
android:textColor="@color/text_color"
android:textSize="16sp"/>
</com.siddique.androidwear.today.WearableListItemLayout>
这里是WearableListItemLayout类的实现。它基本上是LinearLayout类的一个扩展,包含一些针对WearableListView组件的OnCenterProximityListener组件的处理方法。花点时间理解当列表项滚动并带到中心位置时,组件的colors和alpha特性是如何更新的:
package com.siddique.androidwear.today;
import android.content.Context;
import android.graphics.drawable.GradientDrawable;
import android.support.wearable.view.WearableListView;
import android.util.AttributeSet;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
public class WearableListItemLayout extends LinearLayoutimplements WearableListView.OnCenterProximityListener
{
private final float mFadedTextAlpha;private final int mFadedCircleColor;
private final int mChosenCircleColor;
private ImageView mCircle;
private TextView mName;
public WearableListItemLayout(Context context)
{
this(context, null);
}
public WearableListItemLayout(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}
public WearableListItemLayout(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
mFadedTextAlpha = getResources().getInteger(R.integer.action_text_faded_alpha) / 10f;
mFadedCircleColor = getResources().getColor(R.color.wl_gray);
mChosenCircleColor = getResources().getColor(R.color.wl_orange);
}
@Override
protected void onFinishInflate()
{
super.onFinishInflate();
mCircle = (ImageView) findViewById(R.id.circle);
mName = (TextView) findViewById(R.id.name);
}
@Override
public void onCenterPosition(boolean animate)
{
mName.setAlpha(1f);
((GradientDrawable) mCircle.getDrawable()).setColor(mChosenCircleColor);
}
@Override
public void onNonCenterPosition(boolean animate)
{
((GradientDrawable) mCircle.getDrawable()).setColor(mFadedCircleColor);mName.setAlpha(mFadedTextAlpha);
}
}
这里是我们可以看到的操作列表的截图:

DayOfYearActivity 类
DayOfYearActivity活动是一个非常简单的类,它使用 Java 的默认java.util.Calendar实例来计算已经过去了多少天以及到年底还有多少天:
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import java.util.Calendar;
public class DayOfYearActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_day_of_year);
Calendar calendar = Calendar.getInstance();
String dayOfYearDesc = getString(R.string.day_of_year_card_desc,
calendar.get(Calendar.DAY_OF_YEAR),
calendar.getActualMaximum(Calendar.DAY_OF_YEAR) -
calendar.get(Calendar.DAY_OF_YEAR));
TextView desc = (TextView) findViewById(R.id.day_of_year_desc);
desc.setText(dayOfYearDesc);
}
}
activity_day_of_year.xml 文件
关于BoxInsetLayout组件的一个有趣之处在于,它是一个屏幕感知组件,将其子组件框在中心方形中。它是一个安全的组件,试图在方形或圆形屏幕上很好地适应:
<android.support.wearable.view.BoxInsetLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@drawable/sunrise">
<android.support.wearable.view.CardScrollView
android:id="@+id/card_scroll_view"
andoid:layout_height="match_parent"
android:layout_width="match_parent"
app:layout_box="bottom">
<android.support.wearable.view.CardFrame
android:layout_height="wrap_content"
android:layout_width="fill_parent">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="vertical"
android:paddingLeft="5dp">
<TextView
android:fontFamily="sans-serif-light"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:text="@string/day_of_year_card_title"
android:textColor="@color/black"
android:textSize="18sp"/>
<TextView
android:id="@+id/day_of_year_desc"
android:fontFamily="sans-serif-light"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:text="@string/day_of_year_card_desc"
android:textColor="@color/black"
android:textSize="12sp"/>
</LinearLayout>
</android.support.wearable.view.CardFrame>
</android.support.wearable.view.CardScrollView>
</android.support.wearable.view.BoxInsetLayout>
你可以在模拟器中看到以下操作:

注意
当我们构建布局组件时,预览它们在圆形和方形轮廓中看起来如何是个好主意。
看看以下截图,了解年度活动布局如何在圆形屏幕上显示。由于我们使用了BoxInsetLayout布局组件,它在方形和圆形屏幕上渲染得相当不错:

这里是DayOfYearActivity活动的实际操作。你可以向右滑动返回上一个活动,在这个例子中是主活动:

摘要
我们讨论了如何利用针对可穿戴设备的列表视图和布局。我们在Today应用中开发了一系列操作,并实现了列表项的操作。我们创建了一个动作处理器,从主活动启动活动以在BoxInsetLayout布局中显示组件。
这只是一个微小的用例,展示了我们如何利用 Android Wear UI 组件并根据我们的需求进行定制。我们无法讨论在示例应用中使用的所有文件。花些时间研究本章的示例代码。这将帮助你连接点,理解各个部分是如何组合在一起的。
我们现在可以进入 Wear 开发的高级主题。这是回顾 Android 平台上 UI 和布局组件如何一般性协同工作的好时机。
第五章。数据同步
| *"我们需要另一个灵魂来依靠。" | ||
|---|---|---|
| --西尔维亚·普拉斯 |
在前一章中,我们向您介绍了创建独立可穿戴应用的过程。在本章中,我们介绍了伴侣手持应用的概念,以及为什么需要它。然后,我们向您介绍了将手持设备与 Android Wear 模拟器配对所需的步骤,以扩展您的可穿戴应用开发环境。
然后,我们将在前一章中开始的Today应用增强功能,使其能够显示历史上的今天,通过伴侣应用从公共内容页面获取内容。
注意
本章的代码示例可在 GitHub 上找到(github.com/siddii/mastering-android-wear/tree/master/Chapter_5)。请参考实际代码进行操作。
究竟什么是伴侣应用?
可穿戴应用直接在可穿戴设备上运行,这样您就可以在设备本身上访问设备的硬件、活动和服务。由于规模较小以及需要高效管理处理能力和内存,可穿戴设备上可能执行的操作范围受到设计限制。此外,可穿戴设备不支持 Google Play 商店。此外,Android Wear 1.x 版本不允许直接从 Google Play 商店安装应用。
伴侣手持应用解决了这些问题,让我们能够在可穿戴设备上获得丰富的用户体验。需要记住的是,可穿戴应用是打包在伴侣手持应用中的。伴侣应用是发布到 Google Play 商店的内容,如以下图所示:

当用户将伴侣应用下载到手持设备时,其中的可穿戴应用会自动推送到所有已连接的可穿戴设备,如以下图所示:

此外,在手持设备上运行的伴侣应用更适合执行应用执行网络操作、密集计算和其他资源密集型工作时所需的繁重工作。然后,伴侣应用将结果发送到可穿戴设备,从而传达其操作的结果。
在我们能够创建包含伴侣应用模块及其可穿戴应用模块的项目之前,我们需要设置我们的开发环境,以便我们可以在手持设备上与可穿戴设备一起工作。
小贴士
预计 Android Wear 2.0 将改变从 Google Play 商店打包和安装 Wear 应用的方式。Wear 1.x 中 Wear 应用的自动安装将被淘汰。相反,预计 Wear 2.0 应用将拥有完整的网络访问权限,并且它们的安装将完全独立于手持应用。Google 正在转向独立可穿戴应用作为首选的打包方法,但尚不清楚是否需要独立应用(没有自动安装选项)或者它们是否仅仅作为附加功能得到支持。
设置 Android Wear 虚拟设备
这些步骤发布在 Android 开发者网站上(developer.android.com/training/wearables/apps/creating.html),在创建和运行可穿戴应用部分。这里重复并扩展了这些步骤以方便使用。
要设置 Android Wear 虚拟设备,在 Android Studio 中点击工具|Android|AVD 管理器,并执行以下步骤:
-
点击创建虚拟设备...选项。
-
在类别列表中点击Wear:
-
选择Android Wear Square或Android Wear Round。
-
点击下一步按钮。
-
选择一个发布名称(例如,KitKat Wear)。
-
点击下一步。
-
更改虚拟设备的任何偏好设置(可选)。
-
点击完成。
-
-
启动模拟器:
-
选择您刚刚创建的虚拟设备。
-
点击绿色的播放按钮。
-
等待模拟器初始化并显示 Android Wear 主屏幕。
-
-
将 Android 手持设备与可穿戴模拟器配对:
-
在您的手持设备上安装 Google 提供的 Android Wear 应用从 Google Play。
-
通过 USB 将手持设备连接到您的计算机。
-
将 AVD 的通信端口转发到已连接的手持设备(每次设备连接时都必须这样做)。如果在以下命令运行后我们没有看到任何错误,那么一切正常:
![设置 Android Wear 虚拟设备]()
-
在您的手持设备上启动 Android Wear 应用并通过选择连接模拟器连接到模拟器,如图所示:
![设置 Android Wear 虚拟设备]()
以下图像展示了成功的连接:
![设置 Android Wear 虚拟设备]()
-
打开设置菜单并选择尝试手表通知:
![设置 Android Wear 虚拟设备]()
-
-
从列表中选择提醒(按时间)选项:

在可穿戴模拟器上出现以下屏幕:

回顾 Today 应用
现在我们有了在手持设备上与可穿戴设备工作的能力,让我们回顾一下在前一章中开发的Today应用。
那个简化的应用版本无疑帮助我们开始了,但对我们来说这还不够。为了更全面地体验可穿戴设备的功能,我们需要扩展我们的需求。因此,我们决定将本章的其余部分用于显著增强我们的 Today 应用。
我们将在稍后描述新应用的功能,但首先,让我们在 Android Studio 中创建一个新的项目——该项目包括一个可穿戴应用以及一个伴侣应用;并使用本章的示例代码进行设置。
我们认为从头开始可能会有所清新;这就是为什么我们的新应用仍然命名为 Today。请随意称它为任何你喜欢的名字:

请确保选择以下截图所示的应用形式因素——手机和平板,以及 Wear:

通过点击 Empty Activity 在 Mobile 模块中添加一个空活动:

在以下屏幕中给你的活动起一个合适的名字:

目前,请在 Wear 模块中选择 Add No Activity 选项,然后点击 Finish:

Android Studio 创建了以下截图所示的 Wear 和 Mobile 模块:

上述截图显示了我们的项目状态。请注意,Android Studio 为它们创建了两个模块——移动和可穿戴。它还为它们创建了 Gradle 脚本并添加了必要的依赖项。此外,还为这两个模块创建了 run target 配置。
我们将代码(即活动、资源、图标等)从第四章中创建的 Today 项目复制到这个新创建的项目中的可穿戴模块,然后扩展它以满足我们的扩展需求。现在是检查这些需求的好时机。
新 Today 应用的范围
我们都知道日期的重要性。谁不喜欢了解到他们的另一半与一个著名或更糟糕的是臭名昭著的明星共享生日呢?考虑到这一点,让我们让我们的 Today 应用不仅仅显示当前日期。让它从名为 On This Day 的公共内容源中获取内容(en.wikipedia.org/wiki/Special:FeedItem/onthisday/20160615000000/en),它分享一个或多个历史上重要事件/发生的周年纪念日,恰好与今天相符。
这似乎足够深入地咬进了可穿戴 API 栈,这将让我们能够在不过多增加复杂性的情况下研究可穿戴设备和其伴侣应用之间的交互。
在我们深入应用程序代码之前,我们有必要介绍一些概念、工具和 API 对象,这些对于我们的应用程序至关重要。本意是提供足够的信息,让您理解示例代码的核心部分。您随时可以返回并参考本章以及其中引用的文档。
可穿戴数据层 API
Google Play 服务包括一个可穿戴数据层 API (developer.android.com/training/wearables/data-layer/index.html),通过该 API,您的手持设备和可穿戴应用可以相互通信。
我们鼓励您研究位于前述页面的 Android 开发者网站上可穿戴数据层 API 文档,但 API 中的某些关键数据对象值得特别注意。
MessageApi
此接口公开了可穿戴设备和手持设备相互发送消息的方法。发送到连接的网络节点(即配对设备)的消息将排队等待发送。重要的是要记住,由应用程序创建的消息仅对该应用程序是私有的,并且只能由运行在其他节点上的该应用程序访问。
WearableListenerService
此类应由希望在后台运行时接收事件通知的应用程序扩展。事件包括收到消息、数据变化以及节点连接到或断开与 Android Wear 网络(即不断变化的可穿戴设备和它们可以连接到或与之交互的手持设备网络)的连接。
DataListener
虽然WearableListenerService类在后台运行时通知应用程序,但DataListener接口在应用程序在前台运行时通知实现它的应用程序关于数据层事件。
云节点
除了所有用户的连接设备(节点)之外,Google 的服务器在网络中隐式地托管一个云节点。云节点的作用是在直接连接的设备之间同步数据。手持设备上应用程序状态的变化会推送到所有用户的可穿戴设备,反之亦然,如下面的图所示:

GoogleApiClient 类
每次您想要连接到 Google Play 服务库中提供的 Google API 之一时,都需要创建GoogleApiClient类的实例。Google API 客户端为所有 Google Play 服务提供了一个共同的入口点,并管理用户设备与每个 Google 服务之间的网络连接。
我们使用此类让我们的移动设备连接到 Google Play 服务库中的可穿戴 API,以便访问连接的可穿戴设备。
Volley 库
我们将使用 Volley 从维基百科获取 HTML 内容。你可以在开发者的网页上阅读有关这个 HTTP 库的所有信息(developer.android.com/training/volley/index.html)。
JSoup 库
JSoup 库(jsoup.org)将是我们首选的库来解析我们从维基百科拉取的 HTML 内容源。现在,让我们看看代码。
构建脚本
研究移动和伴随应用build.gradle文件中指定的依赖项。注意移动应用的build.gradle文件为Volley和JSoup库添加了额外的依赖项。记住,伴随应用必须承担繁重的工作:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
wearApp project(':wear')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.google.android.support:wearable:2.0.0-alpha1'
// This version should match the same version in wearable app
compile 'com.google.android.gms:play-services-wearable:8.1.0'
// Use volley to make HTTP requests
compile 'com.android.volley:volley:1.0.0'
// Use JSoup for parsing HTML data
compile "org.jsoup:jsoup:1.8.1"
}
伴随应用的 Android 清单文件
请查看伴随应用的基本TodayMobileActivity和HandheldListenerService活动的AndroidManifest.xml文件:
<manifest
package="com.siddique.androidwear.today">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<activity
android:name=".TodayMobileActivity"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden"
android:configChanges="keyboardHidden|orientation|screenSize" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!— Listens for incoming messages from Wearable devices—>
<service android:name=".HandheldListenerService">
<intent-filter>
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data
android:scheme="wear"
android:host="*"
android:pathPrefix="/today" />
</intent-filter>
</service>
</application>
</manifest>
TodayMobileActivity类
TodayMobileActivity类目前是一个便利活动,旨在仅连接到与移动设备配对的任何现有可穿戴设备。我们将运行移动/伴随应用的目标在移动设备上:
public class TodayMobileActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener
{
private GoogleApiClient mGoogleApiClient;
public static final String TAG = TodayMobileActivity.class.getName();
private int CONNECTION_TIME_OUT_MS = 15000;
private TextView devicesConnectedTextView = null;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Log.i(*TAG*, "Creating Google Api Client");
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.*API*)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
devicesConnectedTextView = (TextView) findViewById(R.id.devicesConnected);
}
@Override
protected void onStart()
{
super.onStart();
if (!mGoogleApiClient.isConnected())
{
mGoogleApiClient.connect();
}
}
@Override
public void onConnected(Bundle connectionHint)
{
Log.i(TAG, "Google Api Client Connected");
new Thread(new Runnable()
{
@Override
public void run()
{
mGoogleApiClient.blockingConnect(CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS);
NodeApi.GetConnectedNodesResult result = Wearable.*NodeApi*.getConnectedNodes(mGoogleApiClient).await();
final List<Node> nodes = result.getNodes();
runOnUiThread(new Runnable()
{
public void run()
{
Log.i(*TAG*, "Connected devices = " + nodes.size());
devicesConnectedTextView.setText(String.valueOf(nodes.size()));
}
});
}
}).
start();
}
...
}
一旦我们成功连接到可穿戴设备,我们应该能够看到至少有一个设备已连接的确认,如下面的图所示:

用户可以启动TodayMobileActivity类来查看设备是否已连接。如果显示的“设备连接”值不大于零,则表示移动设备未成功配对,这意味着它未连接到可穿戴设备或模拟器。我们将在未来的章节中扩展此活动。
可穿戴应用的 Android 清单文件
这里是带有三个菜单项活动(如TodayActivity、DayOfYearActivity和OnThisDayActivity活动)的AndroidManifest.xml文件的可穿戴应用:
<manifest package="com.siddique.androidwear.today">
<uses-feature android:name="android.hardware.type.watch" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<!— We need this entry to use Google Play Services —>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<activity
android:name=".TodayActivity"
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=".DayOfYearActivity"
android:label="@string/day_of_year_card_title" />
<activity
android:name=".OnThisDayActivity"
android:label="@string/on_this_day_title" />
</application>
</manifest>
OnThisDayActivity类
OnThisDayActivity类使用GoogleApiClient API 向移动设备(即伴随应用)发送消息,表示它需要从维基百科获取内容。
注意在此活动中定义的onDataChanged处理程序方法。onDataChanged方法是当伴随应用向可穿戴设备发送数据包时被处理的回调监听器:
public class OnThisDayActivity extends Activity implementsDataApi.DataListener, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener
{
private GoogleApiClient mGoogleApiClient;private boolean mResolvingError;
private static final String TAG = OnThisDayActivity.class.getName();
private OnThisDay onThisDay = null;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_on_this_day);
if (onThisDay == null)
{
Toast.makeText(this, "Fetching from Wikipedia...", Toast.LENGTH_LONG).show();
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
else
{
showOnThisDay(onThisDay);
}
}
@Override
protected void onStart()
{
super.onStart();
if (!mResolvingError && onThisDay == null)
{
Log.i(TAG, "Connecting to Google Api Client");
mGoogleApiClient.connect();
}
else
{
showOnThisDay(onThisDay);
}
}
@Override
public void onConnected(Bundle connectionHint)
{
Log.i(TAG, "Connected to Data Api");
Wearable.DataApi.addListener(mGoogleApiClient, this);
*// send a message to the companion app that it needs to fetch data*
sendMessage(Constants.ON_THIS_DAY_REQUEST, "OnThisDay".getBytes());
}
private void sendMessage(final String path, final byte[] data)
{
Log.i(TAG, "Sending message to path " + path);
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).setResultCallback (new ResultCallback<NodeApi.GetConnectedNodesResult>()
{
@Override
public void onResult(NodeApi.GetConnectedNodesResult nodes)
{
for (Node node : nodes.getNodes())
{
Wearable.MessageApi.sendMessage(mGoogleApiClient, node.getId(), path, data);
}
}
});
}
@Override
public void onConnectionSuspended(int i)
{
Log.i(TAG, "Connection Suspended");
}
@Override
protected void onStop()
{
if (null != mGoogleApiClient && mGoogleApiClient.isConnected())
{
Wearable.DataApi.removeListener(mGoogleApiClient, this);
mGoogleApiClient.disconnect();
}
super.onStop();
}
@Override
public void onDataChanged(DataEventBuffer dataEvents)
{
Log.i(TAG, "###### onDataChanged");
for (DataEvent event : dataEvents)
{
if (event.getType() == DataEvent.TYPE_CHANGED)
{
DataItem dataItem = event.getDataItem();
DataMap dataMap = DataMapItem.fromDataItem(dataItem).getDataMap();
String heading = dataMap.get(Constants.ON_THIS_DAY_DATA_ITEM_HEADER);
ArrayList<String> listItems = dataMap.get(Constants.ON_THIS_DAY_DATA_ITEM_CONTENT);
onThisDay = new OnThisDay(heading, listItems);
showOnThisDay(onThisDay);
}
}
}
private void showOnThisDay(OnThisDay onThisDay)
{
TextView heading = (TextView) findViewById(R.id.on_this_day_heading);
heading.setText(Html.fromHtml(onThisDay.getHeadingHtml()));
TextView content = (TextView) findViewById(R.id.on_this_day_content);
content.setText(Html.fromHtml(onThisDay.getListItemsHtml()));
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult)
{
Log.i(TAG, "Connection Failed " + connectionResult);
mResolvingError = true;
}
}
HandheldListenerService类
HandheldListenerService类监听来自可穿戴设备的消息。当接收到消息时,onMessageReceived处理程序会检查该消息是否为内容请求,如果是,则调用辅助程序读取源并相应地解析响应:
public class HandheldListenerService extends WearableListenerService implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener
{
...
@Override
public void onMessageReceived(MessageEvent messageEvent)
{
super.onMessageReceived(messageEvent);
Log.i(TAG, "Message received" + messageEvent);
if (Constants.ON_THIS_DAY_REQUEST.equals(messageEvent.getPath()))
{
//read Today's content from Wikipedia
getOnThisDayContentFromWikipedia();
}
}
private void getOnThisDayContentFromWikipedia()
{
// Instantiate the RequestQueue
RequestQueue queue = Volley.newRequestQueue(this);
String url = "https://en.wikipedia.org/wiki/Special:FeedItem/onthisday/" + DATE_FORMAT.format(new Date()) + "000000/en";
// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,new Response.Listener<String>()
{
@Override
public void onResponse(String response)
{
Log.i(TAG, "Wikipedia response = " + response);
Document doc = Jsoup.parse(response);
Element heading = doc.select("h1").first();
Log.i(TAG, "Heading node = " + heading);if (heading != null)
{
Log.i(TAG, "Wikipedia page heading = " + heading);
PutDataMapRequest dataMapRequest = PutDataMapRequest.create(Constants.ON_THIS_DAY_DATA_ITEM_HEADER);
DataMap dataMap = dataMapRequest.getDataMap();
// We add a timestamp is to make this dataMap 'dirty'. This lets the wearable get updates
dataMap.putLong(Constants.ON_THIS_DAY_TIMESTAMP, new Date().getTime());
dataMap.putString(Constants.ON_THIS_DAY_DATA_ITEM_HEADER, heading.text());
Element listNode = doc.select("ul").first();if (listNode != null)
{
Elements itemNodes = listNode.select("li");int size = itemNodes.size();
ArrayList<String> items = new ArrayList<String>();for (int i = 0; i < size; i++)
{
items.add(itemNodes.get(i).text());
}
dataMap.putStringArrayList(Constants.ON_THIS_DAY_DATA_ITEM_CONTENT, items);
}
Log.i(TAG, "Sending dataMap request ...");
PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi.putDataItem(mGoogleApiClient, dataMapRequest.asPutDataRequest());
PendingResult.setResultCallback(new ResultCallback<DataApi.DataItemResult>()
{
@Override
public void onResult(final DataApi.DataItemResult result)
{
if (result.getStatus().isSuccess())
{
Log.d(TAG, "Data item set: " + result.getDataItem().getUri());
}
}
});
}
}
},
new Response.ErrorListener()
{
@Override
public void onErrorResponse(VolleyError error)
{
Log.e(TAG, "Error reading online content = " + error);
}
});
// Add the request to the RequestQueue.
queue.add(stringRequest);
}
如你所见,这里提供的代码片段是不完整的,仅作为快速参考。我们鼓励你下载并尝试从 GitHub 获取最新的代码。
如果你将应用运行在你的可穿戴设备上,你将看到以下内容:

如您在前面的截图中所见,我们在使用手持设备的伴侣应用请求从维基百科获取“这一天”内容时显示了一个Toast消息:

注意,在我们的activity_on_this_day XML 布局中,我们将TextView布局嵌套在ScrollView布局中,这有效地允许我们滚动查看所有内容项。这引发了对可穿戴应用开发 UX 方面的讨论。我们当然可以利用更好的 UI 组件来完成我们刚才所做的。更多内容将在未来的章节中介绍。
消息未传达到您的穿戴应用?
这可能会让人感到沮丧,这就是我们提到它的原因。如果您看到任何同步问题,即消息未传达到您的穿戴应用,请检查您的 Google Play 服务模块版本是否在您的伴侣应用和可穿戴应用AndroidManifest.xml文件之间匹配。版本不匹配可能导致这种意外的行为,并让您浪费数小时进行无效的调试。考虑以下 Android Studio 窗口的截图:

摘要
我们描述了需要伴侣手持应用的需求,并逐步介绍了创建 Android Wear 虚拟设备和将手持设备与其配对的过程。然后我们创建了一个新的Today应用,该应用通过伴侣应用从公共内容页获取内容,并将结果推送到可穿戴设备。
在下一章中,我们将介绍上下文感知通知和语音交互,这些功能为 Android Wear 提供了丰富的用户体验。
第六章. 上下文通知
| *"生活就是关于时机。" | ||
|---|---|---|
| --卡尔·刘易斯 |
在本章中,我们将讨论 Android Wear 中的通知。在快速比较可穿戴设备和手持设备中的通知之后,我们将继续扩展上一章中的“今日”应用,以演示 Android Wear 通知 API。
注意
本章的代码可在 GitHub 上参考(github.com/siddii/mastering-android-wear/tree/master/Chapter_6)。为了简洁起见,仅包含所需的代码片段。鼓励读者从 GitHub 下载引用的代码,并在阅读章节时跟随。
接收通知
令人惊讶的是,可穿戴设备在向用户发送通知方面自然优于手持设备。使用手持设备时,你会听到蜂鸣声,你需要从钱包、口袋或你在最近一次 eBay 拍卖中抓到的任何选择皮套中取出你的设备。
但是,在智能手表上,事情相当不同。当你听到那个蜂鸣声时,你只需瞥一眼你的手腕。这种便利性是可穿戴设备技术的标志性特征。
而且不仅如此。借助语音交互,用户可以通过发出可识别的语音命令来对通知采取行动。当然,语音交互 API 早于 Wear API,并且在手持设备上已经使用了一段时间。但不可否认的是,它为可穿戴设备带来的巨大价值,与这种易于访问的特性完美契合。我们将在下一章中处理语音交互。
通知模型对可穿戴设备来说如此核心,以至于大多数教程通常将通知作为 Android Wear 的核心用例介绍;其他功能随后介绍。显然,我们在本书中选择了不同的处理方式。现在,我们来到了本书的一半,我们遇到了关于通知的第一次严肃讨论。
我们之所以这样做,是因为通知,无论它们多么重要或核心,仍然是应用程序的功能,因此是应用程序核心功能的次要部分。我们认为,在没有接触到 Android Wear 应用“筋骨”的情况下深入研究通知,是不切实际的。
你已经看到了“今日”应用的骨架,你已经增强了它以与配套手持应用一起工作。这是对 Wear API 的一些良好了解。现在,你准备好在此基础上构建知识。放心,如果你一直跟随着,你会发现通知 API 并不令人畏惧。
谈话到此为止;在我们看到示例应用中的实际操作之前,让我们先介绍 API 中的核心类。
通知 API 的核心类
这里是我们将在应用程序中使用的通知 API 的核心类。
NotificationCompat.Builder
当您在可穿戴设备上处理通知时,您希望得到某种保证,即您的通知在智能手表显著缩小的比例上看起来是可接受的。这就是通知构建器类发挥作用的地方。这个类负责正确显示通知,无论它们出现在手持设备还是可穿戴设备上。
要使用通知构建器,您必须在您的build.gradle文件中添加以下行:
compile "com.android.support:support-v4:20.0.+"
然后,您需要从支持库中导入以下核心类:
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.app.NotificationCompat.WearableExtender;
创建通知变成实例化NotificationCompat.Builder类并发布通知的问题,正如我们将在我们的示例应用程序中看到的那样。
通知中的操作按钮
addAction方法允许您向通知添加一个操作。只需将PendingIntent实例传递给addAction方法。虽然这个操作在手持设备上作为附加到通知的按钮出现,但在可穿戴设备上,当用户将通知向左滑动时,它将显示为一个大的按钮。点击操作将在手持设备上调用相关的意图。
仅适用于可穿戴设备的行为
如果您希望可穿戴设备上的操作与手持设备上的操作不同,请使用NotificationCompat.WearableExtender类上的addAction方法。这样做可以确保可穿戴设备不会显示添加到NotificationCompat.Builder.addAction类的操作。
交付
使用NotificationManagerCompat API 而不是NotificationManager来发送您的通知,如下所示。这确保了与旧平台的兼容性:
// Get an instance of the NotificationManager service
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mContext);
// Issue the notification with notification manager
notificationManager.notify(notificationId, notif);
带有待办事项通知的 Today 应用程序
我们将增强我们的Today应用程序,添加一个允许用户添加待办事项并将它们与特定位置关联的活动,例如家或工作。这些位置提供了上下文,从而驱动通知。实际上,这使得我们的通知具有上下文感知性。
例如,如果系统检测到用户接近家位置,那么与Home类别关联的待办事项将通过通知 API 呈现给用户。
地理围栏
我们将使用Geofencing API 来确定位置,即上下文。本质上,这个 API 允许我们在一个坐标周围绘制一个半径已定的圆圈。实际上,纬度、经度和半径共同定义了一个地理围栏,即感兴趣坐标周围的圆形区域。进入和退出事件表示设备进入或退出地理围栏位置。可选的持续时间属性在设备进入并保持在地理围栏内时延迟触发该时间间隔的事件。
您可以通过访问developer.android.com/training/location/geofencing.html来详细了解Geofencing API。
模拟 GPS
首先要说明的是,在可穿戴设备模拟器上模拟位置/GPS 传感器行为并不容易,我们所有的示例代码都是这样做的。需要配备 GPS 的物理设备才能做到这一点。此外,即使我们能够访问一个完全功能的 GPS,我们也可能会遇到令人沮丧的场景来测试我们的应用程序——考虑需要物理移动到不同的位置以触发 GPS 传感器。
因此,为了演示上下文感知通知,我们需要一个 GPS 模拟服务,它允许我们使用我们的可穿戴设备模拟器以及物理手持设备,并且可以按需模拟不同的位置。
这就是 ByteRev 的 FakeGPS 应用程序发挥作用的地方 (play.google.com/store/apps/details?id=com.lexa.fakegps&hl=en)。
这个免费应用程序让我们能够模拟不同的位置,并有效地提供在物理设备上使用完全功能的 GPS 单元的等效体验。缺点是用户可能需要重新运行应用程序以模拟预期的行为。但这是为了灵活性而可以接受的权衡。
可穿戴和移动应用程序的 build.gradle 文件
移动手持应用程序的 build.gradle 文件应包含以下行以支持位置服务:
compile 'com.google.android.gms:play-services-location:9.0.2'
可穿戴和移动应用程序都应包含以下编译依赖项以支持通知:
compile 'com.android.support:support-v13:23.4.0'

手持应用程序的 Android 替换文件
注意权限授予允许应用程序访问精确位置,即纬度和经度坐标:
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.siddique.androidwear.today">
<uses-sdk android:minSdkVersion = "18" android:targetSdkVersion="22"/>
<uses-permission android:name = "android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"
/>
<activity
android:name=".TodayMobileActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden">
</activity>
<!— Listens for incoming messages from Wearable devices —>
<service android:name=".HandheldListenerService">
<intent-filter>
<action android:name="com.google.android.gms.wearable.DATA_CHANGED"/>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED"/>
data
android:host="*"
android:pathPrefix="/today"
android:scheme="wear" />
</intent-filter>
</service>
<activity
android:name=".TodoMobileActivity"
android:label="@string/title_activity_todo_mobile"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".GeofenceTransitionsIntentService"
android:exported="false">
</service>
</application>
</manifest>
我们添加了一个名为 TodoMobileActivity 的新活动,以便我们添加待办事项。由于我们需要访问 GPS 传感器,ACCESS_FINE_LOCATION 权限对于这个新活动是必要的。
GeofenceTransitionsIntentService 服务将响应位置的变化。
TodoMobileActivity 类
TodoMobileActivity 类是一个简单的活动,向用户展示一个列表视图并允许添加待办事项。每个添加的项目可能与一组已知的地点(家庭或工作)相关联,每个地点都硬编码到一个 GPS 坐标:
public class TodoMobileActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener
{
private ListView mTaskListView;
private ArrayAdapter<String> mAdapter;
public static final String TAG = TodoMobileActivity.class.getName();
private List<Geofence> geofenceList;
private PendingIntent mGeofencePendingIntent; private GoogleApiClient mGoogleApiClient;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_todo_mobile);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mTaskListView = (ListView) findViewById(R.id.list_todo); refreshItems();
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.addTodo);
if (fab != null) {
fab.setOnClickListener(new View.OnClickListener()
{
@Override public void onClick(View view)
{
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final View addTodoItemView = inflater.inflate(R.layout.add_todo_item, null);
final Spinner spinner = (Spinner) addTodoItemView.findViewById(R.id.todoItemType);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(TodoMobileActivity.this, R.array.todoItemTypes, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
AlertDialog dialog = new AlertDialog.Builder(TodoMobileActivity.this)
.setTitle("Add a new todo item") .setView(addTodoItemView) .setPositiveButton("Add", new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
EditText taskEditText = (EditText) addTodoItemView.findViewById(R.id.todoItem);
Log.i(TAG, "Todo Item = " + taskEditText.getText());
Spinner todoItemTypeSpinner = (Spinner) addTodoItemView.findViewById(R.id.todoItemType);
String todoItemType = (String) todoItemTypeSpinner.getSelectedItem();
Log.i(TAG, "Todo Item type = " + todoItemType);
String task = String.valueOf(taskEditText.getText());
Set<String> todoItems = TodoItems.readItems(TodoMobileActivity.this, todoItemType); todoItems.add(task);
TodoItems.saveItems(TodoMobileActivity.this, todoItemType, todoItems);
refreshItems();
}
})
.setNegativeButton("Cancel", null)
.create();
dialog.show();
}
});
}
if(null == mGoogleApiClient)
{
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
Log.i(TAG, "GoogleApiClient created");
}
if(!mGoogleApiClient.isConnected())
{
mGoogleApiClient.connect();
Log.i(TAG, "Connecting to GoogleApiClient..");
}
}
private void createGeofences()
{
Log.i(TAG, "Creating geo fences");
geofenceList = new ArrayList<Geofence>();
geofenceList.add(new SimpleGeofence(
Constants.HOME_GEOFENCE_ID,
Constants.HOME_LATITUDE,
Constants.HOME_LONGITUDE).toGeofence());
geofenceList.add(new SimpleGeofence(
Constants.WORK_GEOFENCE_ID,
Constants.WORK_LATITUDE,
Constants.WORK_LONGITUDE).toGeofence());
}
private void refreshItems()
{
ArrayList<String> taskList = new ArrayList<>();
String[] todoItemTypes =
getResources().getStringArray(R.array.todoItemTypes);
for (String todoItemType : todoItemTypes)
{
Set<String> todoItems = TodoItems.readItems(this,
todoItemType);
for (String todoItem : todoItems)
{
taskList.add(todoItemType + " - " + todoItem);
}
}
if (mAdapter == null) {
mAdapter = new ArrayAdapter<>(this,
R.layout.item_todo,
R.id.task_title,
taskList);
mTaskListView.setAdapter(mAdapter); }
else
{
mAdapter.clear();
mAdapter.addAll(taskList);
mAdapter.notifyDataSetChanged();
}
}
public void deleteTodoItem(View view)
{
View parent = (View) view.getParent();
TextView textView = (TextView) parent.findViewById(R.id.task_title);
String removingItem = (String) textView.getText();
Log.i(TAG, "Removing Item = " + removingItem);
String[] todoItemTypes = getResources().getStringArray(R.array.todoItemTypes);
TodoItems.removeItem(this, todoItemTypes, removingItem);
refreshItems();
}
@Override public void onConnected(@Nullable Bundle bundle)
{
if(mGoogleApiClient != null)
{
mGeofencePendingIntent = getGeofenceTransitionPendingIntent();
createGeofences();
Log.i(TAG, "Adding geofences to API location services");
LocationServices.GeofencingApi.addGeofences(mGoogleApiClient, geofenceList,mGeofencePendingIntent); }
}
private PendingIntent getGeofenceTransitionPendingIntent()
{
Intent intent = new Intent(this, GeofenceTransitionsIntentService.class);
return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
@Override public void onConnectionSuspended(int i)
{
Log.i(TAG, "onConnectionSuspended called");
}
@Override public void onConnectionFailed(@NonNull ConnectionResult connectionResult)
{
Log.i(TAG, "onConnectionFailed called"); }
}
注意我们使用的 SimpleGeofence 类需要三个参数;它内部将半径设置为 50 米。有关更多实现细节,请参阅 GitHub 上的示例代码。
待办事项列表视图
下图展示了待办事项列表视图的外观。用户可以添加待办事项,并删除现有的项目。列表中的每个项目都会显示,包括其位置:

添加待办事项
下图显示了向 Today-Todo 应用程序添加新待办事项的示例输入:

模拟位置
您可能已经注意到我们选择定义了两个位置——家和办公室。现在,因为我们有很高的抱负,我们冒险为最有资格的个人构建这个应用,即美国总统。所以,这就是为什么家庭坐标对应于白宫,工作坐标对应于国会山(好吧,我们知道这是一个糟糕的例子。总统在白宫西翼的椭圆形办公室工作。但是,想象一下,如果“工作”和“家”有相同的坐标,我们的示例代码将有多么无意义?)。包含这些值的Constants文件如下所示:

使用模拟 GPS 应用模拟位置
启动模拟 GPS 应用,并按如下方式搜索位置“白宫”:

在您点击“设置位置”按钮后,模拟 GPS 将开始模拟我们设置的位置。注意前图中纬度和经度。看看它们与我们定义在Constant文件中的Constants.HOME_LATITUDE和Constants.HOME_LONGITUDE有多接近:

地理围栏转换 IntentService 类
您可能还记得我们之前提到的TodoMobileActivity活动,其中GeofenceIntentService类将在位置发生变化时被调用。onHandleIntent方法是我们放置代码以根据用户可能输入的geofence位置通知用户任何待办事项的地方:
public class GeofenceTransitionsIntentService extends IntentService
{
private static final String TAG = GeofenceTransitionsIntentService.class.getName();
public GeofenceTransitionsIntentService()
{
super(GeofenceTransitionsIntentService.class.getSimpleName());
}
@Override
public void onCreate()
{
super.onCreate();
}
/*
*Handles incoming intents.*
* @param intent The Intent sent by Location Services. This Intent is provided to Location
*Services (inside a PendingIntent) when addGeofences() is called.
*/
@Override
protected void onHandleIntent(Intent intent)
{
Log.i(TAG, "Location changed " + intent);
GeofencingEvent geoFenceEvent = GeofencingEvent.fromIntent(intent);
if (geoFenceEvent.hasError())
{
int errorCode = geoFenceEvent.getErrorCode();
Log.e(TAG, "Location Services error: " + errorCode);
}
else
{
int transitionType = geoFenceEvent.getGeofenceTransition();
// Get an instance of the NotificationManager service
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
Log.i(TAG, "Notifying home todo items");
String triggeredGeoFenceId = geoFenceEvent.getTriggeringGeofences().get(0)
.getRequestId();
switch (triggeredGeoFenceId)
{
case Constants.HOME_GEOFENCE_ID:
if (Geofence.GEOFENCE_TRANSITION_ENTER == transitionType)
{
Log.i(TAG, "Notifying home todo items");
notifyTodoItems(notificationManager, "Home", Constants.HOME_TODO_NOTIFICATION_ID, R.drawable.white_house);
}
break;
case Constants.WORK_GEOFENCE_ID:
if (Geofence.GEOFENCE_TRANSITION_ENTER == transitionType)
{
Log.i(TAG, "Notifying work todo items");
notifyTodoItems(notificationManager, "Work", Constants.WORK_TODO_NOTIFICATION_ID, R.drawable.capitol_hill);
}
break;
}
}
}
private void notifyTodoItems(NotificationManagerCompat notificationManager, String todoItemType, int notificationId, int background)
{
Set<String> todoItems = TodoItems.readItems(this, todoItemType);
Intent viewIntent = new Intent(this, TodoMobileActivity.class);
PendingIntent viewPendingIntent = PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_today_notification)
.setLargeIcon(BitmapFactory.decodeResource(
getResources(), background))
.setContentTitle(todoItems.size() + " " + todoItemType + " todo items found!") .setContentText(todoItems.toString() )
.setContentIntent(viewPendingIntent);
// Build the notification and issues it with notification manager.
notificationManager.notify(notificationId, notificationBuilder.build());
}
}
手持应用通知
下图显示了将模拟 GPS 应用设置为Home时的应用程序运行情况。请看手持设备上显示的与家庭位置相关的三个待办事项的通知:

可穿戴应用通知
下图显示了在可穿戴设备上显示的相同通知:

现在,如果我们更改模拟 GPS 应用中的位置为“国会山”并重新启动“今日-待办”应用,正如预期的那样,在可穿戴设备上我们得到了不同的通知,如下面的截图所示:

摘要
在本章中,我们将“今日”应用扩展以包含待办活动。我们使用这个扩展来演示使用“通知”API 实现的环境感知通知。通知在移动设备和可穿戴设备模拟器上显示。我们介绍了地理围栏的概念,并使用“地理围栏”API 以及一个模拟 GPS 应用来模拟我们的位置。
第七章. 语音交互、传感器和跟踪
| "All I have is a voice." | |
|---|---|
| --W. H. Auden |
在本章中,我们介绍了 Wear API 提供的语音功能,并定义了与上一章中的Today应用交互的语音操作。我们还介绍了设备传感器,并讨论了如何使用它们来跟踪数据。
备注
本章的代码可在 GitHub 上参考(github.com/siddii/mastering-android-wear/tree/master/Chapter_7)。为了简洁起见,只包括所需的代码片段。鼓励读者从 GitHub 下载引用的代码,并在阅读本章时跟随。
语音功能
如果你是在 80 年代度过你的青少年时期,那么你关于可穿戴设备语音交互的所有知识可能都来自这个人:

三十年过去了,你现在正迫不及待地想知道 Wear API 是否提供了系统提供的语音操作,让你能够召唤你的汽车。恐怕,目前还没有。完整的系统提供的语音操作列表将在下面的子节中展示。
备注
你可以访问 Android 开发者网站(developer.android.com/training/wearables/apps/voice.html),了解更多关于你的可穿戴应用语音功能的信息。
通过系统提供的语音操作,我们指的是内置在 Wear 平台中的语音操作,即开发者开箱即用的操作。
相比之下,术语应用提供的语音操作指的是特定于你的应用的那些操作。
系统提供的语音操作
系统提供的语音操作必须根据你想要启动的具体活动进行过滤。例如,记下自用笔记。
Wear 平台支持以下语音意图:
-
记笔记
-
打电话叫车/出租车
-
设置闹钟
-
设置计时器
-
开始/停止跑步
-
开始/停止骑行
-
开始计时器
-
开始/停止跑步
-
开始/停止锻炼
-
显示步数
-
显示心率
应用提供的语音操作
根据你的需求,系统提供的语音操作可能不够用。在这种情况下,你可以选择以注册手持设备上的启动器图标相同的方式为你的应用注册一个启动操作。
要使用语音操作启动TodayActivity,请指定一个标签属性,其文本值设置为在Start关键字之后所说的任何内容。在这个示例代码中,我们使用我们的应用名称作为标签属性。存在一个 intent-filter 标签可以识别语音操作Start Today并启动TodayActivity活动:
<activity
android:name=".TodayActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
新功能 - 通过语音命令添加待办事项
让我们准备好编写一些代码。在上一个章节中,我们增强了 Today 应用,使我们能够通过配对的便携式应用添加待办事项。然后,可穿戴设备根据配置的上下文显示通知。
现在,让我们通过添加语音交互来增加一些趣味。我们将使用可穿戴应用通过语音命令来记录待办事项。这将涉及扩展我们实现的有上下文感知通知功能,并能够使用语音输入添加待办事项。此外,我们将提供上下文以及待办事项。
例如,如果我们说“home make dinner”,我们的可穿戴应用将创建一个名为 Make dinner 的待办事项,并将其与 Home 上下文相关联。同样,如果我们说“work set up monthly review meeting”,应用将创建一个名为 Set up monthly review meeting 的待办事项,并将其与 Work 上下文相关联。
在我们逐步查看代码之前,有一些事情需要记住:
-
在撰写本文时,Android Wear 模拟器不支持语音输入。因此,我们选择使用物理可穿戴设备。
-
现在,如果您还记得,我们之前提到过,我们实际上并不需要物理设备来构建 Android Wear 应用。虽然这在很大程度上是正确的,但有些情况下模拟器无法模拟物理设备的行为,例如语音输入、运动感应等。在这些情况下,我们别无选择,只能获取物理设备以获得更完整的 Android Wear 开发体验。此外,如果您对 Android Wear 开发认真负责,您不妨考虑购买物理设备,因为它真的有助于加快您的开发速度。
-
重要的是要注意,尽管语音交互目前在 Android Wear 模拟器中不受支持,但 Google 可能会在未来提高对语音交互的支持。我们将密切关注这一点。
添加待办事项 - 可穿戴应用中的新操作
让我们开始吧。我们将要做的第一件事是将 Add Todo Item 操作添加到我们的 arrays.xml 文件中:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="actions">
<item>Day of Year</item>
<item>On this day...</item>
<item>Add Todo Item</item>
</string-array>
</resources>
这个新配置的操作现在显示在我们的屏幕上,如下所示:

可穿戴应用中的 AddTodoItem 活动
我们为 AddTodoItem 活动的选择连接了处理程序:
@Override
public void onClick(WearableListView.ViewHolder viewHolder)
{
Log.i(TAG, "Clicked list item" + viewHolder.getAdapterPosition());
if (viewHolder.getAdapterPosition() == 0)
{
Intent intent = new Intent(this, DayOfYearActivity.class);
startActivity(intent);
}
else if (viewHolder.getAdapterPosition() == 1)
{
Intent intent = new Intent(this, OnThisDayActivity.class);
startActivity(intent);
}
else if (viewHolder.getAdapterPosition() == 2)
{
displaySpeechRecognizer();
}
//Create an intent that can start the Speech Recognizer activity
private void displaySpeechRecognizer()
{
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
// Start the activity, the intent will be populated with the speech text
startActivityForResult(intent, Constants.SPEECH_REQUEST_CODE);
}
点击 添加待办事项 操作会产生以下效果:

处理语音输入
当语音识别器返回带有语音输入意图时,onActivityResult 方法回调被触发。注意我们如何提取语音文本,然后在语音命令以我们预定义的上下文之一(即 home 或 work)开始时调用 GoogleApiClient API:
// This callback is invoked when the Speech Recognizer returns.
// This is where you process the intent and extract the speech text from the intent.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == Constants.SPEECH_REQUEST_CODE && resultCode == RESULT_OK)
{
List<String> results = data.getStringArrayListExtra( RecognizerIntent.EXTRA_RESULTS);
spokenText = results.get(0);
// Do something with spokenText
Log.i(TAG, "Spoken Text = " + spokenText);
if (spokenText.startsWith("home") || spokenText.startsWith("work"))
{
Log.i(TAG, "Creating Google Api Client");
mGoogleApiClient = new GoogleApiClient.Builder(this)
addApi(Wearable.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
mGoogleApiClient.connect();
}
}
else
{
super.onActivityResult(requestCode, resultCode, data);
}
}
Android Wear 解析语音输入,并将语音文本作为确认显示,如下所示:

一旦GoogleClient连接成功,即onConnected处理程序被触发,我们将在排除上下文(home或work)之后提取todoItem文本,并使用Wearable Data API 将待办事项作为消息发送到手持应用:
@Override
public void onConnected(Bundle bundle)
{
Log.i(TAG, "Connected to Data Api");
if(spokenText != null)
{
if (spokenText.startsWith("home"))
{
String todoItem = spokenText.substring("home".length());
sendMessage(Constants.HOME_TODO_ITEM, todoItem.getBytes());
}
else if(spokenText.startsWith("work"))
{
String todoItem = spokenText.substring("work".length());
sendMessage(Constants.WORK_TODO_ITEM, todoItem.getBytes());
}
}
}
private void sendMessage(final String path, final byte[] data)
{
Log.i(TAG, "Sending message to path " + path);
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).setResultCallback(
new ResultCallback<NodeApi.GetConnectedNodesResult>()
{
@Override
public void onResult(NodeApi.GetConnectedNodesResult nodes)
{
for (Node node : nodes.getNodes())
{
Wearable.MessageApi
sendMessage(mGoogleApiClient, node.getId(), path, data);
spokenText = null;
}
}
});
}
手持应用
在手持应用上,我们实现onMessageReceived处理程序来处理从可穿戴设备接收到的消息。记住,手持应用是我们进行繁重工作的地方。在这种情况下,它是创建待办事项:
@Override
public void onMessageReceived(MessageEvent messageEvent)
{
super.onMessageReceived(messageEvent);
Log.i(TAG, "Message received" + messageEvent);
if(Constants.ON_THIS_DAY_REQUEST.equals(messageEvent.getPath()))
{
//read Today's content from Wikipedia
getOnThisDayContentFromWikipedia();
}
else
{
String todo = new String(messageEvent.getData());
if (Constants.HOME_TODO_ITEM.equals(messageEvent.getPath()))
{
Log.i(TAG, "Adding home todo item '" + todo + "'");
TodoItems.addItem(this, "Home", todo);
}
else if (Constants.WORK_TODO_ITEM.equals(messageEvent.getPath()))
{
Log.i(TAG, "Adding work todo item '" + todo + "'");
TodoItems.addItem(this, "Work", todo); }
}
}
添加的待办事项在手持设备的Today - Todos应用中的待办事项列表中显示,如图所示:

运动传感器
运动传感器使我们能够通过空间监控设备的运动,例如旋转、摆动、摇晃或倾斜。这种运动可能与其直接环境相关,例如在汽车模拟中模仿方向盘时的情况。在这种情况下,我们监控其相对于自身参考系或运行在其上的应用程序的参考系的运动。
然而,这种运动也可能相对于设备周围的环境,即世界。后者的一个例子是从移动车辆内部确定绝对速度。设备可能在车辆内部是静止的,但它以与车辆本身相同的速度相对于地球移动。
安卓平台使我们能够通过一系列传感器监控设备的运动——一些是基于硬件的,例如陀螺仪和加速度计。其他是基于软件的,或者可能是基于硬件的,但依赖于其他硬件传感器。例如,旋转矢量传感器、重力传感器、显著运动传感器、步数计数传感器和步检测传感器。你可以在开发者网站上了解所有这些信息(developer.android.com/guide/topics/sensors/sensors_motion.html)。
在本节中,我们的关注点是简要介绍两个位于所有运动传感核心的硬件传感器:陀螺仪和加速度计。理解这些传感器背后的原理将使我们欣赏到贯穿所有运动传感器行为的物理学,并让我们对如何通过 API 间接使用这些传感器来解决我们的应用程序问题有一个直观的认识。
陀螺仪
陀螺仪在最基本层面上,是一个由轮子或圆盘组成,安装得使其能够绕轴自由旋转,而不会受到包围它的安装方向的任何影响的装置。
以下图像有助于我们更好地可视化其结构:

为了获得直观的理解,我们只需要消化这样一个事实:陀螺仪的特性仅在转子(盘)绕其轴旋转时才会显现。当盘不旋转时,设备不会表现出任何有用的特性。但是当旋转时,该轴的取向不受安装倾斜或旋转的影响。这与角动量守恒定律相符,本质上,这也是陀螺仪用于测量或保持方向的原因。
加速度计
加速度计是一种测量加速度的仪器,通常用于测量汽车、船舶、飞机或宇宙飞船的加速度,或涉及机器、建筑物或其他结构的振动。
加速度计在科学和工业的许多领域都有应用。例如,加速度计用于检测和监控旋转机械的振动。它们也被用于平板电脑和数码相机,以确保图像始终在屏幕上正确显示。
在可穿戴设备领域,加速度传感器测量作用于设备上的加速度,包括重力。一般来说,如果您正在监控设备运动,加速度计通常是一个不错的选择。它几乎在所有基于 Android 的手持设备和平板电脑上都有。它比其他运动传感器消耗的电量要少得多。
新功能 - 跟踪我们的步数
每个人都喜欢步数计数器。我们为什么不为我们的可穿戴设备构建一个呢?这里没有太多可说的,所以让我们直接进入代码。
添加待办事项 - 可穿戴应用中的新操作
我们在这里要做的第一件事是为可穿戴应用添加一个菜单项。让我们称它为“步数计数”。我们对arrays.xml文件的更改如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="actions">
<item>Day of Year</item>
<item>On this day...</item>
<item>Add Todo Item</item>
<item>Step Count</item>
</string-array>
</resources>
现在您应该可以在可穿戴应用中看到这个操作,如图所示:

点击“步数计数”菜单项以启动相应的StepCounterActivity活动。该类的代码如下。注意该活动如何实现SensorEventListener类。我们使用SensorManager类在该活动的onCreate处理程序中连接正确的传感器类型。请注意,由于该活动实现了SensorEventListener类,您期望与之关联的其他处理程序:
public class StepCounterActivity extends Activity implements SensorEventListener
{
private SensorManager mSensorManager;
private Sensor mSensor;
// Steps counted since the last reboot
private int mSteps = 0;
private static final String TAG = StepCounterActivity.class.getName();
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_daily_step_counter);
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER); }
@Override
protected void onResume()
{
super.onResume();
mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_NORMAL);
refreshStepCount();
}
@Override
protected void onPause()
{
super.onPause();
mSensorManager.unregisterListener(this);
}
@Override
public void onSensorChanged(SensorEvent event)
{
Log.i(TAG,"onSensorChanged - " + event.values[0]);
if(event.sensor.getType() == Sensor.TYPE_STEP_COUNTER)
{
Log.i(TAG,"Total step count: " + mSteps);
mSteps = (int) event.values[0];
refreshStepCount();
}
}
private void refreshStepCount()
{
TextView desc = (TextView) findViewById(R.id.daily_step_count_desc);
desc.setText(getString(R.string.daily_step_count_desc, mSteps));
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy)
{
Log.i(TAG,"onAccuracyChanged - " + sensor);
}
}
这就是我们的新活动在可穿戴设备上的外观:

如前述代码所示,我们在这里使用的传感器类型由Sensor类的TYPE_STEP_COUNTER常量表示。这种类型的传感器获取用户自上次可穿戴设备重启以来所走的步数。关于这种传感器类型,需要记住的重要一点是,应用程序需要保持注册状态,因为如果未激活,步数计数器不会跟踪步数。
我们选择这种基本类型的传感器,因为我们关注的重点是使用 API。请随意探索这里的Sensor API 类,以研究您可用的其他传感器。特别是,看看TYPE_STEP_DETECTOR传感器类型。这种传感器每次用户迈步时都会触发一个事件。与追踪一段时间内所迈步数的计步器不同,步数检测器非常适合在迈步的瞬间检测步数。
您也可以思考一下如何实现给定一天的计步器——这是一个留给有兴趣的读者去充分利用我们的Today应用的练习。
摘要
在本章中,我们展示了使用Wear API 创建应用提供的语音动作,以启动我们的Today Todo应用。我们还介绍了运动传感器概念,并检查了让我们可以利用这些传感器的 API 类。然后,我们将这些概念应用于增强我们的示例Today应用,通过一个简单的活动来跟踪用户所迈步数。
第八章:创建自定义 UI
| "我说她没有脸,但这意味着她有一千张脸。" | ||
|---|---|---|
| --C. S. Lewis |
在本章中,我们将介绍对 Android Wear UI 空间至关重要的设计原则,并简要介绍一些常见的 Wear UI 模式。然后我们将扩展OnThisDay活动,以用户友好的格式展示信息流。
注意
本章伴随的代码可在 GitHub 上参考(github.com/siddii/mastering-android-wear/tree/master/Chapter_8)。为了简洁起见,仅包含所需的代码片段。鼓励读者从 GitHub 下载引用的代码,并在阅读本章时跟随。
Android Wear UI 设计
到现在为止,我们应该已经清楚,可穿戴应用并不总是可以遵循其手持设备对应的应用相同的 UI 模式。可穿戴设备具有显著较小的形态因素,与之交互对用户动作施加了更重的限制。因此,Android Wear 用户界面 API 在功能上分为建议函数和需求函数。
建议 函数体现在Context流中——以一种主动和提示的方式呈现的信息流。用户会看到一个信息卡片的垂直列表,可以滚动浏览,直到用户希望与特定的卡片交互。
需求 函数体现在提示卡隐喻中。提示卡可以通过说出“OK Google”来打开,或者,我们也可以通过轻触主屏幕的背景来打开它。每个语音命令激活一种语音意图,而这种意图又可以与多个应用程序相关联。
当用户面对意图时,他们将有选择他们希望激活该意图的应用程序的机会。应用程序可以通过添加/更新流卡片或启动另一个应用程序来响应。
常见 UI 模式
我们将简要介绍在 Android Wear 开发中最常实现的 UI 模式。
卡片
在上下文流中显示的卡片可以是标准通知、单操作卡片,或者是一个可展开的堆叠,将相关的通知分组在一起。在每种情况下,卡片右上角的位置都有一个图标,表示该卡片关联的应用程序。
在某些情况下,单个通知卡不足以满足需求,可能需要更多细节。从右向左滑动可以显示详细卡片,除了主要上下文流卡片之外。在卡片上从左向右滑动会导致它从上下文流中移除。
值得注意的是,在可穿戴设备上取消的通知也会因为配对设备之间共享的同步状态而在配对的手持设备上取消。
卡片可以可选地显示在详细卡片右侧的操作按钮。这些操作可以在可穿戴设备上运行。或者,它们可以委托给伴侣手持设备,或者它们可以导致全屏活动运行。
倒计时和确认
当用户点击显示在详细卡片右侧的操作按钮时,系统可以在操作完成后显示确认动画。
在某些情况下,可能希望给用户一个在操作执行之前中断操作的机会。解决这一问题的方法之一是在操作调用之前显示一个可定制的倒计时动画。
一些操作可能是关键性的,可能希望通过向用户显示确认步骤来突出显示这一点。然后用户必须确认他们执行该操作的意思。
作为开发者,我们应该始终权衡在可能的情况下将任何繁重的工作委托给伴侣手持设备的选项。如果对上下文流中卡片上调用的操作这样做,那么我们可以在用户点击操作按钮并在手持设备上启动相应的应用程序后,在可穿戴设备上显示动画。
还可以选择在卡片上执行操作。这些是在卡片本身上执行的操作。当只有一个可能的操作可以由点击执行时,这些操作是理想的。例如,显示在通知中的地址上的汽车图标只能表示方向,因此是一个很好的候选者,可以作为卡片上的操作。卡片上的操作在其目的上应该是明确的。
另一种选择(即当有多个可能的操作时)是通过详细卡片右侧的操作按钮调用它们。例如,在人的姓名的情况下,卡片上的操作是不明确的,因此单独的操作按钮会更好,例如,用于呼叫、电子邮件、显示详细信息等操作。
卡片堆叠
一些卡片可能相关,将它们分组在一起在堆叠中是有意义的。例如,为了显示新邮件通知,卡片堆叠可以将所有新邮件通知分组在一起。
用户可以点击卡片堆叠使其展开并显示堆叠中每张卡片的上边缘。进一步点击展开的卡片将在上下文流中的垂直列表中将其展开到完全展开状态。
当用户垂直滑动远离卡片堆叠时,堆叠中的所有卡片都会恢复到完全折叠状态,并且单个堆叠再次在上下文中显示。
2D 选择器
2D 选择器是一种灵活的 UI 模式,用于 Android Wear 应用。它允许我们根据需求构建一维卡片列表或二维卡片网格。
此外,可以设置滚动方向为水平或垂直。呈现给用户的数据分布在页面上,然后每一页对应一张卡片。
一种直观的展示方式是将包含(例如)搜索结果的垂直卡片列表显示给用户。垂直列表中的每个卡片都展示少量信息,并且可以通过水平滚动从它获取更多信息,以显示包含剩余信息页面的后续卡片。
通过向相关活动的布局中添加一个 GridViewPager 元素实例来实现 2D 选择器模式。然后,此页面必须为其设置一个 GridPagerAdapter 类型的适配器。
为了使事情更简单,一个扩展 GridPageAdapter 类的抽象类名为 FragmentGridPageAdapter 定义了您的适配器将需要的常见行为,因此您只需扩展 FragmentGridPageAdapter 类来实现您自己的适配器,以提供一组页面来填充 GridViewPager 元素。
在使用 2D 选择器演示时,我们必须确保我们优化它以获得速度。这可以通过保持卡片简单并最小化选择器中的卡片数量来实现。
当用户做出选择时,应该销毁 2D 选择器。用户还可以通过在第一张卡片上向下滑动或在最左侧的卡片上向右滑动来从 2D 选择器中退出。
选择列表
这是一个常见的模式,其中可能的选项以简单的可滚动列表的形式呈现。用户从列表中选择一个项目,从而触发一个动作。
Android Wear UI 库提供了一个针对可穿戴设备优化的列表实现,即 WearableListView 元素。要创建此类列表,您需要将 WearableListView 元素添加到您的活动布局定义中,然后将其适配器设置为您的自定义布局实现的实例。
再次访问 OnThisDay 活动
注意,我们最初在 第五章,同步数据 中实现的 OnThisDayActivity 活动使用了 ScrollView 方法内的 TextView 方法。让我们使用本章中介绍的一些简单 UI 模式来改进它。我们将故意保持此代码简单,以便您有机会掌握我们使用的新的 API 类。我们鼓励您构思用例并尝试更多有趣的模式,如 2D 选择器。
谈话已经足够。现在是时候编写一些代码了。我们现在准备好通过一个包含卡片并可以左右滑动的垂直列表来改进我们的初始 On this day 首页展示。每个卡片都可以通过从左到右的滑动来取消。
OnThisDayActivity 活动
以下 showOnThisDay 方法创建并设置一个 GridViewPager 实例,该实例根据显示正确处理布局:
private void showOnThisDay(OnThisDay onThisDay)
{
final Resources res = getResources();
final GridViewPager pager = (GridViewPager) findViewById(R.id.pager);
pager.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener()
{
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets)
{
// Adjust page margins:
// A little extra horizontal spacing between pages looks a bit
// less crowded on a round display.
final boolean round = insets.isRound();
int rowMargin = res.getDimensionPixelOffset(R.dimen.page_row_margin);
int colMargin = res.getDimensionPixelOffset(round ? R.dimen.page_column_margin_round : R.dimen.page_column_margin);
pager.setPageMargins(rowMargin, colMargin);
// GridViewPager relies on insets to properly handle
// layout for round displays. They must be explicitly
// applied since this listener has taken them over.
pager.onApplyWindowInsets(insets);
return insets;
}
});
pager.setAdapter(new OnThisDayGridPagerAdapter(this, getFragmentManager(), onThisDay));
DotsPageIndicator dotsPageIndicator = (DotsPageIndicator) findViewById(R.id.page_indicator);
DotsPageIndicator.setPager(pager);
}
DotsPageIndicator 是 GridViewPager 类的一个页面指示器,它有助于根据当前行上的可用页面来识别当前页面。点代表页面;当前页面可以通过一个不同颜色和/或大小的点来区分。
活动布局
下面的活动布局显示了之前(在第五章(Synchronizing Data,第 5.5.1 节)中)我们有一个 TextView 方法在 ScrollView 方法内的声明:
<android.support.wearable.view.BoxInsetLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/yellow_orange">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent" >
<android.support.wearable.view.GridViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true" />
<android.support.wearable.view.DotsPageIndicator
android:id="@+id/page_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom">
</android.support.wearable.view.DotsPageIndicator>
</FrameLayout>
</android.support.wearable.view.BoxInsetLayout>
我们定义了一个名为 OnThisDayGridPagerAdapter 的类,它扩展了 FragmentGridPagerAdapter 类。这个类的实例被设置为 GridViewPager 元素的适配器属性。一个名为 Row 的私有内部类被用作 Fragment 对象的便利容器:
public class OnThisDayGridPagerAdapter extends FragmentGridPagerAdapter
{
private final Context mContext;
private OnThisDay onThisDay;
private List<Row> mRows;
private ColorDrawable mDefaultBg;
private ColorDrawable mClearBg;
public OnThisDayGridPagerAdapter(Context ctx, FragmentManager fm, OnThisDay onThisDay)
{
super(fm);
mContext = ctx;
this.onThisDay = onThisDay;
mRows = new ArrayList<OnThisDayGridPagerAdapter.Row>();
ArrayList<String> listItems = onThisDay.getListItems();
for (String listItem: listItems)
{
mRows.add(new Row(cardFragment("On This Day - " + (listItems.indexOf(listItem) + 1), listItem)));
}
}
private Fragment cardFragment(String title, String content)
{
Resources res = mContext.getResources();
CardFragment fragment = CardFragment.create(title, content);
// Add some extra bottom margin to leave room for the page indicator
fragment.setCardMarginBottom( res.getDimensionPixelSize(R.dimen.card_margin_bottom));
return fragment;
}
/** A convenient container for a row of fragments. */
private class Row
{
final List<Fragment> columns = new ArrayList<Fragment>();
public Row(Fragment... fragments)
{
for (Fragment f : fragments)
{
add(f);
}
}
public void add(Fragment f)
{
columns.add(f);
}
Fragment getColumn(int i)
{
return columns.get(i);
}
public int getColumnCount()
{
return columns.size();
}
}
@Override
public Fragment getFragment(int row, int col)
{
Row adapterRow = mRows.get(row);
return adapterRow.getColumn(col);
}
@Override
public Drawable getBackgroundForRow(final int row)
{
return mContext.getResources().getDrawable(R.drawable.page_background);
}
@Override
public int getRowCount()
{
return mRows.size();
}
@Override
public int getColumnCount(int rowNum)
{
return mRows.get(rowNum).getColumnCount();
}
}
我们运行应用程序并选择 On this day… 活动功能,如下面的截图所示:

现在从源结果中获取的每个项目都显示为一个可滚动的垂直列表中的卡片,如下面的截图所示:

您可以通过垂直滚动查看第二个示例:

摘要
在本章中,我们介绍了 Android Wear 设计原则,并调查了大多数可穿戴应用程序实现的常见 UI 模式。然后我们通过编写一些代码来使用 API,增强了来自第五章(Synchronizing Data,第 5.5.1 节)的 Today 应用程序的 On this day... 活动功能,使用了一个显示卡片列表并允许用户与之交互的 GridViewPager 组件。
第九章. 材料设计
"这个世界只是我们想象力的画布。" - 亨利·大卫·梭罗
在本章中,我们提供了对材料设计的概念理解,并简要介绍了适用于可穿戴应用设计和开发的几个关键原则。通过将前几章的Todo应用扩展,我们巩固了对材料设计的理解,以包含一个导航抽屉,使我们能够在Todo类别之间切换,查看项目并执行每个类别的特定操作。
注意
本章的代码可在 GitHub 上参考(github.com/siddii/mastering-android-wear/tree/master/Chapter_9)。为了简洁起见,仅包含所需的代码片段。鼓励读者从 GitHub 下载引用的代码,并在阅读章节时跟随。
接近材料设计
您理解材料设计的首要资源是material.google.com,这是概述材料设计原则和信条的活在线文档。它真的应该被任何热衷于材料设计的认真设计师或开发者收藏。
虽然我们鼓励您阅读谷歌的文档,但我们认为如果我们提出一种思考材料设计的方法,那将不会是多余的。我们的目标是向您,一个对材料设计哲学感兴趣的人,提供一个直观和象征性的范式理解。我们希望这个简短的介绍为您准备好一种心态,这将加快您通过材料设计在线文档的旅程,并激发一种创造力,这将使您能够将想象力投射到您可穿戴应用的实体设计理念上。
本节仅针对那些可能对这一概念较新,可能需要一本希望足够充分的入门指南,以便点燃火花并让我们重新思考的人。
与世界互动
我们可以讨论材料设计的正式定义,也许甚至更多。但这不会是我们的时间的好用途。谷歌的文档在这方面做得很好,还有更多。与其陷入对材料设计是什么的表述,不如退一步,了解其背后的动机是什么。
在我们的目的上,让我们考虑这样一个场景:您在一家咖啡馆,坐在桌旁。看看下面图中的空桌面:

您的桌面有很大的潜力成为您的工作空间。让我们称这种潜力为可用性。这个术语通常意味着一个对象或环境动作的可能性。例如,看一辆汽车的方向盘,自然会想到应该旋转它以使其运作,而不是拉或推。
让我们花一点时间研究我们的工作空间:
-
它是平的
-
它有清晰的边界;在这种情况下,定义我们桌子形状的圆形的连续边缘
-
在其初始状态下,它不持有任何物体
让我们停下来,与我们的桌子稍微互动一下。下面是:

现在桌子不再空了。让我们继续我们的观察:
-
放在桌子上的物体会留在桌子上;它们不会自己滑落,也不会飘走(感谢重力)。
-
物体可以在桌子上移动,但受限于其边界。
-
只有我们关心的物体在桌子上。我们不关心的物体,我们倾向于把它们从桌子上拿下来。你看到我的记事本了吗?还没有,因为我还没有足够关心它。
让我们改变一下:

好吧,现在看起来开始有点熟悉了。这里有一些更多的观察:
-
如果需要方便地访问,物体可以共享空间。
-
当一个物体吸引我的注意力时,它就在上面。
-
在任何给定的时间,桌子上每个物体都占据一个特定的(我该如何表达)高度,这取决于它是否在所有其他物体之上,在底部(即直接在桌子上),还是位于其他物体之间。
-
随着桌子上物体的数量增加,可用性可以增加。可用性是人在机交互(HCI)中用来描述用户在交互过程中可以采取的可能行动的术语。
我们可以继续下去,对关于我们的表面(桌子)和上面的物体可能做到和做不到的事情做出各种智能的观察。然而,重要的是,所有这些观察对我们成功使用桌子以及堆放在上面的物体堆,无论是为了完成工作还是为了玩耍,都是完全不必要的。
原因可能更适合于关于人机交互(HCI)或更广泛地说,感知/认知/环境心理学的讨论。我们只需要理解和欣赏的是,我们对如何使用表面及其交互的直观理解,可能是用户界面高效和有效设计的钥匙。
一种视觉语言
我们需要问的问题是,所有这些如何帮助我们设计更好的用户界面?
谷歌的设计师们将我们对现实世界中与表面交互的知识和经验提炼成一套原则和信条,统称为材料设计。这些原则和信条在material.google.com上列出,这应该是我们所有关于材料设计的首要参考资料。
小贴士
我们所说的“材料”是什么意思?
值得明确的是,当谷歌文档使用“材料”一词时,它本质上是指您视觉设计中任何图形对象。这些可能包括导航对象、操作栏、对话框等。您可以与之交互的每个材料对象都有尺寸(高度和宽度),它具有标准厚度,并且位于一个假想表面上,在特定高度(沿z轴)的三维空间中。
为了理解什么是材料设计,我们需要采取必要的材料设计思维方式。当然,我们与真实世界的互动方式并不总是直接转化为我们与设备的互动方式。但是,在存在重叠的地方,我们必须利用用户的直观能力来提高系统的可用性。在我们与真实世界的互动相当有限的情况下,软件通过扩展可能性领域来丰富用户体验,使其超越物理世界,同时始终尊重和调动用户直觉。
当您阅读material.google.com上的教条时,您将遇到以下内容的详细讨论,仅举几例:
-
受纸张和墨水启发的材料理念
-
关键光和周围光作为高度(阴影大小和锐度)的视觉线索
-
运动尊重用户作为主要运动发起者的角色
-
每个材料对象都有三个维度(x、y和z坐标)并且始终占据一个固定的 z 轴位置
-
尽可能无缝地向用户展示对象
-
物理规则得到尊重
-
当规则被违反时,它们是有意为之的,例如,为了吸引用户的注意
-
对象可以如何以及应该如何被操作,以及它们不应该如何操作
-
材料运动和转换
-
核心图标和字体设计
-
导航组件和模式
当我们与可穿戴设备应用一起工作时,由于形式因素显著降低,因此我们的可用性挑战增加,此时采用材料设计哲学在我们的设计和开发实践中变得更加重要。建立在广泛认可隐喻的基础上是确保我们可穿戴应用可用性和持久性的关键。
现在,让我们编写一些代码。
待办事项菜单
让我们通过一个强大的设计隐喻——导航抽屉——来增强“今日待办”应用。
我们需要做的第一件事是在我们的arrays.xml文件中添加一个Todos操作,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="actions">
<item>Day of Year</item>
<item>On this day...</item>
<item>Todos</item>
<item>Step Count</item>
</string-array>
</resources>
这就是它在列表菜单中的显示方式。请点击“待办事项”菜单项,我们将在以下部分与之交互:

接下来,我们将使用 Android Wear API 中的 WearableNavigationDrawer 组件实现我们的 Todo 应用程序的菜单。该菜单将允许我们选择与待办事项类型(例如,家庭、工作等)对应的不同的视图(标签),并在选择抽屉标签时列出该类型的待办事项。
关于导航抽屉
导航抽屉是从屏幕顶部边缘向下滑动的纸上的材料对象。导航抽屉非常适合具有多个视图的应用程序。分页点通过左右滑动在视图之间引导用户。
导航抽屉提供了一种功能,即当用户滚动到视图顶部时,每个视图的内容变得可见。如果空闲,抽屉将保持打开五秒钟,之后将其隐藏。
补充导航抽屉的是可从屏幕底部边缘向上滑动的材料纸上的操作抽屉对象。向上滑动会显示包含额外可执行内容的操作抽屉。
TodosActivity 类
实现导航抽屉涉及使用 WearableDrawerLayout 类创建抽屉布局,并向其中添加一个包含屏幕主要内容的视图。这个主要视图包含包含抽屉内容的子视图。TodosActivity 类将控制导航抽屉并初始化抽屉布局:
public class TodosActivity extends WearableActivity implements WearableActionDrawer.OnMenuItemClickListener
{
private static final String TAG = TodosActivity.class.getName();
private WearableDrawerLayout mWearableDrawerLayout;
private WearableNavigationDrawer mWearableNavigationDrawer;
private WearableActionDrawer mWearableActionDrawer;
private List<TodoItemType> todoItemTypes = Arrays.asList(TodoItemType.HOME, TodoItemType.WORK);
private TodoItemType mSelectedTodoItemType;
private TodoItemTypeFragment mTodoItemTypeFragment;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate()");
setContentView(R.layout.activity_todo_main);
setAmbientEnabled();
//defaulted to Home todo item type
mSelectedTodoItemType = TodoItemType.HOME;
// Initialize content
mTodoItemTypeFragment = new TodoItemTypeFragment();
Bundle args = new Bundle();
args.putString(TodoItemTypeFragment.ARG_TODO_TYPE, mSelectedTodoItemType.toString());
mTodoItemTypeFragment.setArguments(args);
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction().replace(R.id.content_frame, mTodoItemTypeFragment).commit();
// Main Wearable Drawer Layout that wraps all content
mWearableDrawerLayout = (WearableDrawerLayout) findViewById(R.id.drawer_layout);
//Top Navigation Drawer
mWearableNavigationDrawer = (WearableNavigationDrawer) findViewById(R.id.top_navigation_drawer);
Log.i(TAG, "mWearableNavigationDrawer = " + mWearableNavigationDrawer);
mWearableNavigationDrawer.setAdapter(new NavigationAdapter(this));
// Peeks Navigation drawer on the top.
mWearableDrawerLayout.peekDrawer(Gravity.TOP);
// Bottom Action Drawer
mWearableActionDrawer = (WearableActionDrawer) findViewById(R.id.bottom_action_drawer);
mWearableActionDrawer.setOnMenuItemClickListener(this);
// Peeks action drawer on the bottom.
mWearableDrawerLayout.peekDrawer(Gravity.BOTTOM);
}
}
TodoItemTypeFragment 类
TodoItemTypeFragment 类是 TodosActivity 活动的内部类,包含每种类型待办事项的内容。为了简化,我们展示了以下代码中突出显示的一些静态内容。有关在可穿戴设备和手持设备之间同步数据的更多信息,请参阅第五章,同步数据:
public static class TodoItemTypeFragment extends Fragment
{
public static final String ARG_TODO_TYPE = "todo_type";
TextView titleView = null;
TextView descView = null;
public TodoItemTypeFragment()
{
// Empty constructor required for fragment subclasses
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout.fragment_todo_item, container, false);
titleView = (TextView) rootView.findViewById(R.id.todo_card_title);
descView = (TextView) rootView.findViewById(R.id.todo_card_desc);
String todoType = getArguments().getString(ARG_TODO_TYPE);
TodoItemType todoItemType = TodoItemType.valueOf(todoType);
updateFragment(todoItemType);
return rootView;
}
public void updateFragment(TodoItemType todoItemType)
{
titleView.setText(todoItemType.getTypeValue() + " Todos");
//The following line is hardcoded on purpose for simplicity
descView.setText("List description");
}
}
这就是待办事项卡片的外观。请注意,Home 待办事项项被默认选中,如前述代码示例中所述:

NavigationAdapter 类
导航适配器控制导航状态中显示的内容。我们实现 WearableNavigationDrawerAdapter 类来填充导航抽屉的内容:
private final class NavigationAdapter extends WearableNavigationDrawer.WearableNavigationDrawerAdapter
{
private final Context mContext;
public NavigationAdapter(Context context)
{
mContext = context;
}
@Override
public int getCount()
{
return todoItemTypes.size();
}
@Override
public void onItemSelected(int position)
{
Log.d(TAG, "WearableNavigationDrawerAdapter.onItemSelected(): " + position);
mSelectedTodoItemType = todoItemTypes.get(position);
String selectedTodoImage = mSelectedTodoItemType.getBackgroundImage();
int drawableId = getResources().getIdentifier(selectedTodoImage, "drawable", getPackageName());
mTodoItemTypeFragment.updateFragment(mSelectedTodoItemType);
}
@Override
public String getItemText(int pos)
{
return todoItemTypes.get(pos).getTypeValue();
}
@Override
public Drawable getItemDrawable(int position)
{
mSelectedTodoItemType = todoItemTypes.get(position);
String navigationIcon = mSelectedTodoItemType.getBackgroundImage()
int drawableNavigationIconId = getResources().getIdentifier(navigationIcon, "drawable", getPackageName())
return mContext.getDrawable(drawableNavigationIconId);
}
}
导航项
当在 首页 Todos 屏幕上(如果你还记得,Home 是默认类型),从顶部向下滑动。正如预期的那样,Home 待办事项类型被预先选中:

WearableDrawerLayout 类
activity_todo_main.xml 文件包含定义根抽屉布局的代码,该布局包含顶部导航抽屉和底部操作抽屉。请注意高亮的菜单布局:
<android.support.wearable.view.drawer.WearableDrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
tools:context=".TodosActivity"
tools:deviceIds="wear">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/content_frame"/>
<android.support.wearable.view.drawer.WearableNavigationDrawer
android:id="@+id/top_navigation_drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_grey" />
<android.support.wearable.view.drawer.WearableActionDrawer
android:id="@+id/bottom_action_drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:action_menu="@menu/action_todo_drawer_menu"
android:background="@color/dark_grey"/>
</android.support.wearable.view.drawer.WearableDrawerLayout>
菜单项
activity_todo_drawer_menu.xml 文件包含各个抽屉的定义:
<menu >
<item android:id="@+id/menu_add_todo"
android:icon="@drawable/ic_add_to_list"
android:/>
<item android:id="@+id/menu_update_todo"
android:icon="@drawable/ic_todo_list"
android: />
<item android:id="@+id/menu_clear_todos"
android:icon="@drawable/ic_clear_list"
android: />
</menu>
当在 Home Todos 索引卡片(在之前的图像中显示)上时,从底部向上滑动将显示操作抽屉:

菜单监听器
点击单个菜单项,我们只显示提示信息。正如我们之前所说的,我们希望代码简洁且易于阅读。基于我们之前覆盖的章节,我们应该对如何执行这些单个菜单动作的数据同步有所了解。我们使用了onMenuItemClick类来执行菜单监听活动,如下所示:
@Override
public boolean onMenuItemClick(MenuItem menuItem)
{
Log.d(TAG, "onMenuItemClick(): " + menuItem);
final int itemId = menuItem.getItemId();
String toastMessage = "";
switch (itemId)
{
case R.id.menu_add_todo:
toastMessage = "Adding " + mSelectedTodoItemType.getTypeValue() + " Todo";
break;
case R.id.menu_update_todo:
toastMessage = "Updating " + mSelectedTodoItemType.getTypeValue() + " Todo";
break;
case R.id.menu_clear_todos:
toastMessage = "Clearing " + mSelectedTodoItemType.getTypeValue() + " Todos";
break;
}
mWearableDrawerLayout.closeDrawer(mWearableActionDrawer);
if (toastMessage.length() > 0)
{
Toast toast = Toast.makeText(getApplicationContext(), toastMessage, Toast.LENGTH_SHORT);
toast.show();
return true;
}
else
{
return false;
}
}
点击添加待办选项执行以下操作:

切换待办类型
现在如果我们从屏幕顶部边缘向下拉抽屉并向左滑动,我们将切换到不同的待办项,如下面的图像所示,这实际上显示了一个新的导航项:

如果我们将抽屉拉回顶部,它将导航项设置为当前选择。这是通过TodosActivity活动实现的WearableActionDrawer.OnMenuItemClickListener类的onItemSelected方法来实现的:
@Override
public void onItemSelected(int position)
{
Log.d(TAG, "WearableNavigationDrawerAdapter.onItemSelected(): " + position);
mSelectedTodoItemType = todoItemTypes.get(position);
String selectedTodoImage = mSelectedTodoItemType.getBackgroundImage();
int drawableId = getResources().getIdentifier(selectedTodoImage, "drawable", getPackageName());
mTodoItemTypeFragment.updateFragment(mSelectedTodoItemType);
}
下面是我们看到的内容:

从底部向上拉,我们看到菜单项如添加待办、更新待办列表和清除列表等。当我们点击清除列表菜单项时,我们看到以下内容:

摘要
在本章中,我们获得了对什么是真正材料设计的直观理解,并探讨了与 Android Wear 设计和开发相关的几个关键原则。我们在Todo应用中实现了导航抽屉,增加了切换待办类型和查看针对每种类型执行特定动作的待办项的功能。
第十章. 手表表盘
"如果你花太多时间去思考一件事情,你永远也完成不了。" - 李小龙*
我们将从这个章节开始,介绍手表表盘的概念,并概述可用于帮助我们开发它们的 Android Wear API。然后,我们将开发一个简单的交互式手表表盘,除了显示时间外,还会通过触摸动作显示年内已过去的天数和剩余的天数。
注意
本章附带代码可在 GitHub 上参考(github.com/siddii/mastering-android-wear/tree/master/Chapter_10)。
为了简洁起见,代码片段仅按需包含。鼓励读者从 GitHub 下载引用的代码,并在阅读章节时跟随。
显示时间
当我们提到可穿戴设备时,我们主要是指智能手表,一个不能显示时间的手表实际上并不算手表。这就像拥有一艘配备最先进 GPS 技术的豪华游艇,但难以浮在水面上。为了打一个更贴近生活的比喻,考虑一下一部难以作为电话使用的智能手机。(等等!这实际上发生过!)
什么是手表表盘?
“手表表盘”这个术语用来指代可穿戴设备上当前时间的数字显示,以便用户可以一眼看出时间,这与佩戴手表时的情况非常相似。
然而,与传统手表不同,我们的可穿戴设备拥有众多额外的功能,这些功能仅受其自身内部内存及其与一个或多个配对的便携式伴侣设备的通信限制。因此,在可穿戴设备上显示时间,涉及到比为可能具有特定功能列表(如时间、日期、双时和闹钟模式等)的数字手表编写的软件更为复杂的软件。
不言而喻,手表表盘本身就是可更换的组件。你可以随心所欲地更换你喜欢的表盘。拥有各种风格和形状的手表表盘,一些提供相关上下文数据,可以通过 Android Wear 伴侣应用获取。用户只需在可穿戴设备或伴侣应用中选择一个可用的表盘,可穿戴设备就会显示表盘的预览,并允许用户进行配置。如果你找不到你喜欢的,你可以继续编写你自己的。因此,有了这一章。当我们进入下一节构建一个表盘时,我们的手表表盘实现之旅将更加真实。让我们首先谈谈手表表盘设计包含的内容,以及哪些 Wear API 类在手表表盘的开发中发挥作用。
设计考虑因素
虽然 Android Wear 可以通过提供各种功能,如吸引人的颜色、动态背景、动画和数据集成等,在您的设计工作中提供巨大的帮助,但您的设计中还有一些非 API 方面需要考虑。以下是一些在 Android 开发社区中得到广泛认可的要点:
-
考虑您想向用户展示的内容以及这些内容如何适应表盘的上下文。信息过多可能会分散注意力。
-
您的表盘应在方形和圆形设备上可靠运行。
-
为环境模式提供合适的实现。当设备空闲时,用户会感谢您没有耗尽他们的可穿戴设备的电池寿命。
-
UI 指示器(如通知卡片)仍然应该显示,而不会使读取时间变得不可能。
-
通过智能查询和显示通过伴侣手持设备可用的上下文相关信息来丰富您的表盘。记住,伴侣应用承担了所有繁重的工作,因此您的可穿戴应用(在这种情况下,您的表盘)应将任何计算密集型工作或第三方数据查找(如天气信息)委托给伴侣应用。
允许用户配置表盘。
我们强烈建议您阅读 Android 开发者网站上关于“Android Wear 表盘”的部分,它将作为设计指南帮助您,链接为 developer.android.com/design/wear/watchfaces.html。
实现考虑
考虑背景图像。在交互模式下,背景图像可能与环境模式中使用的不同。此外,如果设备的分辨率低于图像,背景图像应缩小(作为一次性操作)。
获取上下文相关数据的应用程序代码应仅在需要时运行,并将结果存储以供重用时重绘表盘使用。
在环境模式下更新表盘应尽可能简单,通过使用有限的颜色、固定的黑色背景以及仅绘制轮廓来最小化工作并节省电池寿命。
表盘服务
表盘是通过服务实现的,并打包在可穿戴应用中。您已经知道,可穿戴应用反过来又打包在手持应用中。当用户安装包含一个或多个表盘的手持应用时,这些表盘在可穿戴设备上的表盘选择器中变为可选。它们也将在手持设备上的 Android Wear 伴侣应用中可用。当选择其中一个表盘(无论是在手持设备上还是在可穿戴设备的选择器上)时,表盘将在可穿戴设备上显示,这会根据表盘的生命周期调用所需的服务回调方法。
要创建手表表盘实现,我们扩展了 Wearable 支持库中提供的类(android.support.wearable.watchface 包)。当手表表盘变为活跃状态时,系统会在各种事件发生时调用其服务类中的方法,例如时间变化、切换到环境模式以及通知警报。相应的处理程序实现随后通过绘制手表表盘来响应,使用更新后的时间或通知数据或事件可能消耗的其他数据。可能需要在手表表盘服务中实现的关键方法包括以下内容:
-
onCreate方法 -
onPropertiesChanged方法 -
onTimeTick方法 -
onAmbientModeChanged方法 -
onDraw方法 -
onVisibilityChanged方法
查看 Wearable 支持库developer.android.com/reference/android/support/wearable/watchface/package-summary.html,以详细了解可用的手表表服务类对象模型。
一旦实现,手表表盘服务必须在可穿戴应用的清单文件(AndroidManifest.xml)中注册。这样,当用户安装应用时,系统就可以在 Android Wear 伴侣应用和可穿戴设备上的手表表盘选择器中提供手表表盘。
交互式手表表盘
手表表盘支持有限的用户交互。只要不与另一个也监听该手势的 UI 元素冲突,手表表盘上特定位置的单一轻点手势就会被接受。在我们下一节的示例代码中,我们支持一个轻点手势,该手势显示当前年份已过去的天数以及剩余的天数。
处理轻点事件涉及实现 WatchFaceService.Engine 类所有扩展中都可用的 setWatchFaceStyle 方法。应用通知系统手表表盘接收到了轻点事件,如下面的代码片段所示:
setWatchFaceStyle(new WatchFaceStyle.Builder(mService)
.setAcceptsTapEvents(true)
// other style customizations
.build());
性能考虑
在手表表盘的上下文中,节省电量非常关键,因为手表表盘始终处于活跃状态。以下是 Wear 开发社区针对手表表盘开发提出的几项最佳实践:
-
确保手表表盘仅在活跃时执行操作。使用
WatchFaceService.Engine类的onVisibilityChanged和isVisible方法来确定这一点。 -
避免使用
WearableListenerService元素来监听事件,因为它无论手表表盘是否活跃都会被调用。相反,使用与DataApi.addListener元素注册的监听器。 -
关注我们可穿戴应用的实际功耗。Android Wear 伴侣应用让我们可以看到可穿戴设备上不同进程消耗了多少电量。
-
在使用动画时,请注意降低帧率。每秒 30 帧足以提供平滑的动画体验。我们应该尽可能少地使用动画,并且当我们使用它们时,我们应该利用每个机会让 CPU 在动画运行之间休眠。每个空闲周期都有助于更大程度地节省电池寿命。
-
保持位图小。在合理的地方,将多个位图合并成一个。减少我们绘制的图形资源数量有助于节省电力。
-
仅使用
Engine.onDraw方法执行绘图操作。将任何加载资源、调整图像大小或执行绘图外计算的工作移出onDraw方法。考虑将这些代码放在onCreate方法中。
让我们构建一个表盘
是时候看到之前介绍的概念的实际应用了。我们将构建一个简单的表盘,使用相当标准的时、分、秒显示来显示时间。轻触表盘会显示当前年份已过去的天数。第二次轻触会显示当前年份剩余的天数。
在接下来的小节中,我们将定义一个 WatchFaceService 类,它扩展了 API CanvasWatchFaceService 类,并重写了与我们的示例应用程序相关的相关事件处理程序。
Android 的清单文件
我们首先声明 TodayWatchFaceService 服务和 WatchFaceConfigActivity 活动,这有助于为表盘选择背景颜色:
<!— Required to act as a custom watch face. —>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<service
android:name=".TodayWatchFaceService"
android:label="@string/digital_name"
android:permission="android.permission.BIND_WALLPAPER" >
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_digital" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_digital_circular" />
<meta-data
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
android:value="com.siddique.androidwear.today.CONFIG_DIGITAL"/>
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
android:value="com.siddique.androidwear.today.CONFIG_DIGITAL"/>
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
<activity
android:name=".WatchFaceConfigActivity"
android:label="@string/digital_config_name" >
<intent-filter>
<action android:name="com.siddique.androidwear.today.CONFIG_DIGITAL" />
<category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
TodayWatchFace 服务
在我们仔细查看 TodayWatchFaceService 类的实现之前,让我们运行我们的示例代码,看看它的行为如何,这样我们可以从用户的角度观察应用程序。
注意,长按设备屏幕会导致已安装的表盘显示:

注意,在自定义表盘的标签 TodayWatchFace 下面出现了一个 齿轮 图标,因为我们为 WatchFace 元素定义了一个配置活动。让我们选择我们的自定义表盘。以下是它的渲染效果。默认情况下,它显示星期和完整的日期,秒 分针闪烁:

在表盘上轻触一次会显示年份中的某一天,如下面的图片所示:

在表盘上第二次轻触会显示当年剩余的天数。

再次轻触会返回默认显示。
TodayWatchFaceService 类
TodayWatchFaceService 类执行所有工作,包括设置布局、读取配置值以及为每秒的每个滴答绘制 UI。在本章中讨论超过 700 行代码是不现实的。因此,我们将查看从这个类定义中提取的重要片段:
public class TodayWatchFaceService extends CanvasWatchFaceService {
@Override
public Engine onCreateEngine()
{
return new Engine();
}
private class Engine extends CanvasWatchFaceService.Engine implements DataApi.DataListener, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener
{
...
}
}
提示
总是如此,本章以及所有其他章节的示例源代码,都可以在每章开头提供的 GitHub 链接中找到。GitHub 上托管的是我们深入了解该服务如何工作的主要参考。
onTimeTick 方法
每次时间滴答时都会调用此方法。我们使 UI 无效(请参阅对 invalidate() 方法的调用),以强制调用 onDraw 方法。实际上,我们在正常模式下每 500 毫秒重新渲染一次 UI,在环境或静音模式下每分钟重新渲染一次:
@Override
public void onTimeTick()
{
super.onTimeTick();
if (Log.isLoggable(TAG, Log.DEBUG))
{
Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
}
invalidate();
}
绘制手表表盘
onDraw() 方法用所有必要的信息绘制手表表盘。遵循代码中的注释,以便完全理解以下代码片段:
@Override
public void onDraw(Canvas canvas, Rect bounds)
{
long now = System.currentTimeMillis();
mCalendar.setTimeInMillis(now);
mDate.setTime(now);
boolean is24Hour = DateFormat.is24HourFormat(TodayWatchFaceService.this);
// Show colons for the first half of each second so the colons blink on when the time
// updates.
mShouldDrawColons = (System.currentTimeMillis() % 1000) < 500;
// Draw the background.
canvas.drawRect(0, 0, bounds.width(), bounds.height(), mBackgroundPaint);
// Draw the hours.
float x = mXOffset;
String hourString;
if (is24Hour)
{
hourString = formatTwoDigitNumber(mCalendar.get(Calendar.HOUR_OF_DAY)); }
else
{
int hour = mCalendar.get(Calendar.HOUR);
if (hour == 0)
{
hour = 12;
}
hourString = String.valueOf(hour);
}
canvas.drawText(hourString, x, mYOffset, mHourPaint);
x += mHourPaint.measureText(hourString);
// In ambient and mute modes, always draw the first colon. Otherwise, draw the
// first colon for the first half of each second.
if (isInAmbientMode() || mMute || mShouldDrawColons)
{
canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
}
x += mColonWidth;
// Draw the minutes.
String minuteString = formatTwoDigitNumber(mCalendar.get(Calendar.MINUTE));
canvas.drawText(minuteString, x, mYOffset, mMinutePaint);
x += mMinutePaint.measureText(minuteString);
// In unmuted interactive mode, draw a second blinking colon followed by the seconds.
// Otherwise, if we're in 12-hour mode, draw AM/PM
if (!isInAmbientMode() && !mMute)
{
if (mShouldDrawColons)
{
canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
}
x += mColonWidth;
canvas.drawText(formatTwoDigitNumber(mCalendar.get(Calendar.SECOND)), x, mYOffset, mSecondPaint);
}
else if (!is24Hour)
{
x += mColonWidth;
canvas.drawText(getAmPmString( mCalendar.get(Calendar.AM_PM)), x, mYOffset, mAmPmPaint);
}
// Only render the day of week and date if there is no peek card, so they do not bleed
// into each other in ambient mode.
if (getPeekCardPosition().isEmpty())
{
if (tapCount == 0)
{
// Day of week
canvas.drawText(mDayOfWeekFormat.format(mDate), mXOffset, mYOffset + mLineHeight, mDatePaint);
canvas.drawText(mDateFormat.format(mDate), mXOffset, mYOffset + mLineHeight * 2, mDatePaint);
}
else if (tapCount == 1)
{
// Day of Year
canvas.drawText("Day of year", mXOffset, mYOffset + mLineHeight, mDatePaint);
canvas.drawText(Integer.toString(TodayUtil.getDayOfYear()), mXOffset, mYOffset + mLineHeight * 2, mDatePaint);
}
else if (tapCount == 2)
{
// Days left in Year
canvas.drawText("Days left in year", mXOffset, mYOffset + mLineHeight, mDatePaint);
canvas.drawText(Integer.toString(TodayUtil.getDaysLeftInYear()), mXOffset, mYOffset + mLineHeight * 2, mDatePaint);
}
}
}
环境模式
与交互模式相比,环境模式是节能模式。根据手表及其配置,例如点击表盘的操作,将手表表盘渲染为环境模式:

由于我们的应用程序非常简单,这可能一眼看不出来,但如果我们将前面的截图与交互模式下手表表盘的截图进行比较,我们会看到秒针没有显示,冒号符号也没有闪烁。
这是当手表表盘从交互模式切换到环境模式时被调用的监听器:
@Override
public void onAmbientModeChanged(boolean inAmbientMode)
{
super.onAmbientModeChanged(inAmbientMode);
if (Log.isLoggable(TAG, Log.DEBUG))
{
Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
}
adjustPaintColorToCurrentMode(mBackgroundPaint, mInteractiveBackgroundColor,WatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND);
adjustPaintColorToCurrentMode(mHourPaint, mInteractiveHourDigitsColor,WatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS);
adjustPaintColorToCurrentMode(mMinutePaint, mInteractiveMinuteDigitsColor, WatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS);
// Actually, the seconds are not rendered in the ambient mode, so we could pass just any
// value as ambientColor here.
adjustPaintColorToCurrentMode(mSecondPaint, mInteractiveSecondDigitsColor, WatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS);
if (mLowBitAmbient)
{
boolean antiAlias = !inAmbientMode;
mDatePaint.setAntiAlias(antiAlias);
mHourPaint.setAntiAlias(antiAlias);
mMinutePaint.setAntiAlias(antiAlias);
mSecondPaint.setAntiAlias(antiAlias);
mAmPmPaint.setAntiAlias(antiAlias);
mColonPaint.setAntiAlias(antiAlias);
}
invalidate();
// Whether the timer should be running depends on whether we're in ambient mode (as well
// as whether we're visible), so we may need to start or stop the timer.
updateTimer();
}
定制手表表盘
我们不想将这个例子做得太复杂,因此出于简单起见,我们决定提供一个可配置的手表表盘背景色。点击手表表盘设置中的齿轮图标,我们可以选择背景色,如图所示:

WatchFaceConfigActivity 类
WatchFaceConfigActivity 类渲染了一个简单的颜色选择器,以确定背景色:
public class WatchFaceConfigActivity extends Activity implements WearableListView.ClickListener, WearableListView.OnScrollListener
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_digital_config);
mHeader = (TextView) findViewById(R.id.header);
WearableListView listView = (WearableListView) findViewById(R.id.color_picker);
BoxInsetLayout content = (BoxInsetLayout) findViewById(R.id.content);
}
}
这是我们选择海军蓝背景色时的手表表盘截图:

我们只是刚刚触及了手表表盘设计和开发的表面,但希望这里的基本处理已经让我们尝到了这项工作的味道,并激发了我们的兴趣。对于给定的一天,我们可以更加富有创意地展示相关的上下文信息,例如待办事项的数量、与天气相关的信息等等。正如所有知识获取一样,我们看到当我们完成时,我们实际上才刚刚开始。
摘要
在本章中,我们介绍了手表表盘的概念,并探讨了它们的设计、实现和性能考虑。然后我们调查了 WatchFaceService.Engine 类,在实现一个简单的交互式手表表盘并看到这些概念和 API 类的实际应用之前。
第十一章. 高级特性和概念
"当人做梦时,他就是天才。" —— 黑泽明
在本章中,我们介绍了与使应用始终运行相关的设计关注点和 API 功能。我们开发了一个活动来演示始终在线的能力。然后我们简要介绍了通过蓝牙连接调试可穿戴应用,并以 Android Wear 2.0 的预览结束。
注意
本章的代码可在 GitHub 上参考(github.com/siddii/mastering-android-wear/tree/master/Chapter_11)。请注意,为了简洁起见,代码片段仅按需包含。鼓励读者从 GitHub 下载引用的代码,并在阅读本章时跟随进度。
保持手表运行
你可能还记得我们在上一章中关于表盘的讨论,其中表盘最初以交互模式运行。当屏幕超时后,表盘将继续运行,因为设备进入了省电的环境模式。
虽然这个功能,即表盘的始终在线能力,是表盘固有的——我们不希望在我们需要看时间时,我们的手表偷懒——但这并不一定适用于所有可穿戴应用。例如,如果我们有一个todo应用或Step counter应用处于活动状态,那么屏幕超时并使表盘失效只是时间问题。如果我们想回到我们的应用,我们就必须与我们的可穿戴设备交互,将其从环境模式中唤醒,并显示我们最后使用的活动或应用。我们可以想象出这种情况可能会成为用户挫败感的来源。
幸运的是,如果我们的设备正在运行 Android 版本 5.1 或更高版本,我们可以利用 Android Wear API 在执行我们的可穿戴应用期间节省电量。这些设备允许应用在保持前台的同时仍然节省电量。应用可以被编码来控制即使在继续履行其主要目的的同时,在环境模式中显示的内容。这样的应用实际上就是始终在线的。
制作始终在线的应用
当我们想要为我们的可穿戴应用启用环境模式时,以下是一些我们需要做和/或需要注意的事情:
-
我们的 SDK 应该更新到包括 Android 5.1 或更高版本的平台,因为这个版本为活动提供了环境模式支持。有关更多信息,请参阅第二章中的Android SDK 包部分,在 Android Studio 上设置开发环境。
-
我们必须将我们的清单
targetSdkVersion设置为 API 级别 22 或更高(即版本 5.1)。 -
我们可以选择通过指定
minSdkVersion属性为设备运行 Android 5.1 之前版本提供向后兼容性。这样做的话,支持环境模式的活动将自动回退到主屏幕并退出活动。 -
我们的活动应该扩展
WearableActivityAPI 类,以便继承启用环境模式所需的所有方法。 -
我们应该在活动的
onCreate()监听器中调用setAmbientEnabled()方法。 -
我们应该清楚地理解交互式和环境模式之间的转换以及在这些转换期间调用的相关监听器,如图中本节末尾所示。
-
我们应该特别注意在环境模式下更新活动 UI,使用基本布局和最小化的颜色调色板,以最大限度地节约电力。
-
我们应该尝试使用一致的布局来更新活动 UI,以便用户在交互式和环境模式之间的转换看起来尽可能无缝。
-
在环境模式下,我们应该小心不要过于频繁地更新屏幕。记住,环境模式的全局目标就是节省电力。如果更新活动 UI 的频率超过 10 秒,可能会成为电力消耗的来源,并且对启用环境模式整体上起到反作用。如果由于应用性质(如地图或健身)需要更频繁地更新,可以考虑使用 API 的
AlarmManager类(developer.android.com/reference/android/app/AlarmManager.html)。
注意
值得重复的是,运行 Android 5.1(API 级别 22)之前版本的设备可能无法访问新 API 的始终开启功能,但只要我们在清单中指定minSdkVersion属性为 20 或更高,它们仍然可以无错误地运行这些应用。
考虑以下图表,展示了屏幕 UI 活动更新的情况:

始终运行计步器
现在,让我们通过将我们的计步器从第七章,语音交互、传感器和追踪,增强为始终开启,来实际演示之前章节中介绍的所有内容。让我们直接进入正题。
Android 清单文件
首先要做的是更新AndroidManifest.xml文件,并将StepCounterActivity类的launchMode设置为singleInstance。这是在环境模式下每分钟更新屏幕多次所必需的。如果不这样做,AlarmManager类会在每次闹钟触发时启动一个意图来打开一个新的活动,而不是重用同一个(已经激活的)活动。以下是文件中的片段:
<activity
android:name=".StepCounterActivity"
android:label="@string/daily_step_count_title"
android:launchMode="singleInstance"
/>
一旦我们启动计步器,我们就会看到一个多彩的背景图像和一个显示自设备重启以来所走的步数的显示屏,如下面的截图所示:

StepCounterActivity 类
此活动为我们计步器做了大部分工作。我们通过调用setAmbientEnabled()方法将onCreate()方法修改为将其设置为 true。我们还定义了一个辅助方法refreshDisplayAndSetNextUpdate(),我们从onCreate()监听器以及onEnterAmbient()和onUpdateAmbient()监听器中调用它。对isAmbient()方法的调用确定我们是否使用环境间隔值或活动间隔值。此外,在环境模式下,我们移除背景,使像素变黑,并用白色前景绘制数据。最大化使用黑色并最小化使用白色直接有助于节省电池电量。
以下代码列表展示了我们的计步器StepCounterActivity类:
public class StepCounterActivity extends WearableActivity implements SensorEventListener
{
private SensorManager mSensorManager;
private Sensor mSensor;
// Steps counted since the last reboot
private int mSteps = 0;
private static final String TAG = StepCounterActivity.class.getName();
private BoxInsetLayout stepCounterLayout;
private CardFrame cardFrame;
private TextView title, desc;
private AlarmManager mAmbientStateAlarmManager;
private PendingIntent mAmbientStatePendingIntent;
/**
* This custom handler is used for updates in "Active" mode. We use a separate static class to
* help us avoid memory leaks.
*/
private final Handler mActiveModeUpdateHandler = new UpdateHandler(this);
/**
* Custom 'what' for Message sent to Handler.
*/
private static final int MSG_UPDATE_SCREEN = 0;
/**
* Milliseconds between updates based on state.
*/
private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
private static final long AMBIENT_INTERVAL_MS = TimeUnit.SECONDS.toMillis(20);
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_daily_step_counter);
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
setAmbientEnabled();
mAmbientStateAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent ambientStateIntent = new Intent(getApplicationContext(), DailyTotalActivity.class);
mAmbientStatePendingIntent = PendingIntent.getActivity(
getApplicationContext(),
0 /* requestCode */,
ambientStateIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
stepCounterLayout = (BoxInsetLayout) findViewById(R.id.step_counter_layout);
cardFrame = (CardFrame) findViewById(R.id.step_counter_card_frame);
title = (TextView) findViewById(R.id.daily_step_count_title);
desc = (TextView) findViewById(R.id.daily_step_count_desc);
refreshDisplayAndSetNextUpdate();
}
/**
* Loads data/updates screen (via method), but most importantly, sets up the next refresh
* (active mode = Handler and ambient mode = Alarm).
*/
private void refreshDisplayAndSetNextUpdate()
{
Log.i(TAG, "Refresh display and set next update ");
refreshStepCount();
long timeMs = System.currentTimeMillis();
if (isAmbient())
{
/** Calculate next trigger time (based on state). */
long delayMs = AMBIENT_INTERVAL_MS - (timeMs % AMBIENT_INTERVAL_MS);
long triggerTimeMs = timeMs + delayMs;
/**
* Note: Make sure you have set activity launchMode to singleInstance in the manifest.
* Otherwise, it is easy for the AlarmManager launch intent to open a new activity
* every time the Alarm is triggered rather than reusing this Activity
*/
mAmbientStateAlarmManager.setExact(
AlarmManager.RTC_WAKEUP,
triggerTimeMs,
mAmbientStatePendingIntent);
}
else
{
/** Calculate next trigger time (based on state). */
long delayMs = ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);
mActiveModeUpdateHandler.removeMessages(MSG_UPDATE_SCREEN);
mActiveModeUpdateHandler.sendEmptyMessageDelayed (MSG_UPDATE_SCREEN, delayMs);
}
}
/**
* Prepares UI for Ambient view.
*/
@Override
public void onEnterAmbient(Bundle ambientDetails)
{
Log.d(TAG, "onEnterAmbient()");
super.onEnterAmbient(ambientDetails);
/** Clears Handler queue (only needed for updates in active mode). */
mActiveModeUpdateHandler.removeMessages(MSG_UPDATE_SCREEN);
/**
* Following best practices outlined in WatchFaces API (keeping most pixels black,
* avoiding large blocks of white pixels, using only black and white,
* and disabling anti-aliasing, etc.)
*/
stepCounterLayout.setBackgroundColor(Color.BLACK);
cardFrame.setBackgroundColor(Color.BLACK);
desc.setTextColor(Color.WHITE);
desc.getPaint().setAntiAlias(false);
title.setTextColor(Color.WHITE);
title.getPaint().setAntiAlias(false);
refreshDisplayAndSetNextUpdate();
}
@Override
public void onUpdateAmbient()
{
Log.d(TAG, "onUpdateAmbient()");
super.onUpdateAmbient();
refreshDisplayAndSetNextUpdate();
}
/**
* Prepares UI for Active view (non-Ambient).
*/
@Override
public void onExitAmbient()
{
Log.d(TAG, "onExitAmbient()");
super.onExitAmbient();
/** Clears out Alarms since they are only used in ambient mode. */
mAmbientStateAlarmManager.cancel(mAmbientStatePendingIntent);
stepCounterLayout.setBackgroundResource(R.drawable.jogging);
cardFrame.setBackgroundColor(Color.WHITE);
desc.setTextColor(Color.BLACK);
desc.getPaint().setAntiAlias(true);
title.setTextColor(Color.BLACK);
title.getPaint().setAntiAlias(true);
refreshDisplayAndSetNextUpdate();
}
}
由于前面的更改,计步器现在在环境模式下的显示如下:

调试可穿戴应用
开发者可以使用的一个重要且极其有用的工具是设置我们运行在可穿戴设备上的可穿戴应用的调试。我们有从我们的开发机器运行调试命令的能力,以排除我们的可穿戴应用的故障,并将任何调试输出发送到手持设备,然后手持设备必须连接到开发机器。为了完成此操作,需要进行一些设置。设备之间的一般连接性如下所示:

这里最大的好处是我们不需要从开发机器运行两个独立的 USB 连接——一个连接到手持设备,另一个连接到可穿戴设备。相反,我们可以通过蓝牙连接直接从开发机器部署和调试代码到手持设备。这在开发过程中需要重复进行故障排除时特别有帮助。没有这个功能,我们就必须忍受过多电缆的混乱,而我们知道我们可以没有这些。
设备设置
调试必须在伴侣手持设备和可穿戴设备上设置,尽管设置方式略有不同。
手持应用上的 USB 调试
按照以下步骤在手持应用上执行调试:
-
在手持设备上启动设置屏幕,并确保在开发者选项中已开启USB 调试。
-
定位并启动开发者选项。或者,你可能需要点击关于手机菜单,向下滚动到构建号并点击它七次以激活开发者选项菜单项。一旦可用,点击一次。
-
如以下截图所示,选择启用USB 调试:

可穿戴应用上的蓝牙调试
按照此处提到的步骤,使用蓝牙在可穿戴应用上进行调试:
-
通过点击主屏幕两次来启动Wear菜单。
-
启动设置。
-
定位并启动开发者选项。或者,你可能需要点击关于手机,向下滚动到构建号并点击它七次以激活开发者选项菜单项。一旦可用,点击一次。
-
选择启用通过蓝牙进行调试选项。
手持设备应用上的会话设置
执行以下步骤,在手持设备应用中设置会话:
-
在手持设备上启动 Android Wear 伴侣应用。
-
从右上角的菜单中选择设置:
![手持设备应用上的会话设置]()
-
选择启用通过蓝牙进行调试选项。注意,在手持设备上该选项下会显示以下消息:
Host: disconnected
Target: connected

这是因为我们尚未将你的手持设备连接到你的开发机器。让我们接下来做这件事。
现在,让我们使用 USB 线将手持设备连接到我们的开发机器,并在adb命令提示符下输入以下内容。我们使用一个任意的端口4444,我们可以使用任何可用的端口:
adb forward tcp:4444 localabstract:/adb-hub
adb connect localhost:4444
在手持设备的伴侣应用中,你现在应该在通过蓝牙进行调试选项下看到以下内容:
Host: connected
Target: connected

因此,我们已经完成了为我们的可穿戴设备设置调试会话。在成功连接后,我们在可穿戴设备上看到如下通知:

现在,让我们通过执行一些调试命令来测试一下。
注意,如果我们想在adb命令提示符下执行adb devices命令,我们应该看到我们的可穿戴设备显示为localhost:4444。现在我们可以使用以下格式执行adb命令来调试我们的应用程序:
adb -s localhost:4444 <command>
例如,考虑以下命令:
adb -s localhost:4444 shell
在可穿戴设备中的开发者选项中,我们可以看到ADB 调试和通过蓝牙调试选项已启用:

现在我们已经在开发机器、手持设备和可穿戴设备之间建立了成功的连接,我们将能够直接从 Android Studio 通过蓝牙连接部署和调试我们的代码。手表将显示为部署目标,如下截图所示:

前进之路 - 回顾 Android Wear 2.0
当我们开始编写这本书时,Android Wear 2.0 已经超越了构思阶段,进入了设计阶段。Android Wear 预览 API 仍在开发中,预计将在我们完成第一稿的一半时发布。尽管 2.0 API 仍在完善中,开发工作正在进行,但感兴趣的开发商可以尝试作为 Android Wear 2.0 开发者预览版 API 的一部分。
在本节中,我们将突出一些被整合到 2.0 API 中的关键新特性,同时关注本书前几章中我们所看到的内容。
表盘复杂功能
在我们讨论表盘时,我们提出了交互式表盘的概念,我们通过点击手势提供了与表盘的有限用户交互。Android Wear 2.0 将这种额外的显示复杂性正式化为复杂功能的概念。复杂功能本质上是指显示时间之外的数据的任何功能,即小时和分钟。2.0 版本提供了一个表盘复杂功能 API,允许表盘显示额外信息,而无需底层管道获取数据。相反,数据的提供——无论是电池电量指示器还是天气信息——通过复杂功能 API 外部化到一个复杂功能数据提供者,然后该提供者控制数据在表盘上的显示方式。从这种复杂功能数据提供者消耗数据的表盘仍然负责绘制复杂功能。
导航和操作抽屉
Android Wear 2.0 API 从上到下都基于材料设计,我们在核心组件和内置小部件中看到了其设计原则的实现。
我们在 第九章 中介绍了导航和操作抽屉,在讨论材料设计的过程中。Android Wear 2.0 进一步巩固了这些小部件与材料设计概念的协调一致。
有额外的抽屉预览支持,以便用户在滚动时可以访问这些抽屉。此外,预览视图和导航抽屉关闭操作已通过添加在 WearableActionDrawer API 的预览视图中显示第一个操作的能力而自动化。这些抽屉小部件在新 2.0 API 中也是可扩展的,支持创建自定义抽屉。
扩展和消息样式通知
Android Wear 2.0 对通知及其视觉交互进行了重大改进。用户可以通过所谓的扩展通知获得改进的体验。当我们为通知指定额外的内容页面和操作时,它们会在扩展通知中向用户开放。每个扩展通知都遵循材料设计原则。用户可以通过简单地点击通知来查看扩展通知。然而,通知必须由配对的伴侣手持设备上的应用程序生成,并且不应为其设置Notification.contentIntent类。
2.0 版本还提供了一个Notification.MessagingStyle类,它使用包含在MessagingStyle通知中的聊天消息。结果是扩展通知中增强了类似应用程序的体验。
输入法框架
Android 的输入法框架(IMF)允许用户使用系统的默认输入法或第三方输入法输入文本。输入可以通过点击单个键或通过手势输入来完成。Android Wear 2.0 将这些相同的功能扩展到了可穿戴设备上。用户将能够从已安装的输入法列表中选择多个输入法,并设置其中一个为默认输入法。
远程输入和智能回复
Wear 2.0 允许用户通过远程输入 API 从一系列输入选项中进行选择。这些包括语音输入、表情符号、智能回复、开发者提供的预设回复列表以及默认输入法。
此外,开发者可以为他们的通知启用一个智能回复功能,让用户获得快速可靠的回复聊天消息的方式。与上下文相关的选项可以出现在扩展通知以及远程输入中。
手腕手势
想象一下,只需轻轻一挥手腕就能与我们的可穿戴设备交互。这正是 Wear 2.0 API 的设计师在提供两个手腕手势(外翻手腕和内翻手腕手势)供应用程序使用时所考虑的。这种典型用例可能是在一只手必须进行交互的情况下滚动通知列表或新闻文章,例如当另一只手拿着一杯大咖啡时。
在 2.0 版本中,可以通过前往设置 | 手势 | 手腕手势来启用/禁用手腕手势。
桥接模式
默认情况下,通知是从伴侣手持设备上的应用程序共享到可穿戴设备的(也称为桥接)。如果还有独立的应用程序发出相同的通知,那么这可能会引起烦恼,因为相同的通知会从独立的应用程序以及伴侣手持设备上出现,这是由于桥接的原因。
为了改善这个问题,Android Wear 2.0 预览版包含了一个名为桥接模式的功能。此模式允许独立应用通过其清单来开启或关闭从伴侣手持应用桥接通知的功能。此外,API 允许通过声明取消 ID 来在不同设备间同步通知取消。
独立的可穿戴设备
这是不可避免的。伴侣手持设备在使应用智能使用资源方面是一个有价值的设计隐喻。但随着将计算能力和内存集中到小型设备中的持续进步,减少对伴侣手持设备的依赖,甚至完全淘汰它们的可能性正在变得越来越现实。
独立设备将使可穿戴应用能够在没有伴侣应用的情况下独立工作。而不是像目前那样将 Android Wear 应用嵌入到相应的伴侣应用中,使用多 APK 交付方法将允许开发者独立于相应的伴侣应用发布 Android Wear 应用。
注意
APK 是用于安装到 Android 操作系统的 Android 文件格式。我们将在下一章中详细讨论这个问题。现在,只需理解 Google Play 提供了多 APK 支持,这使我们能够为我们的应用发布不同的 APK,每个 APK 针对不同的设备配置。因此,每个 APK 都是应用的独立版本,尽管它们在 Google Play 上可能共享相同的应用列表和包名。每个 APK 也使用相同的发布密钥进行签名。
消除对伴侣应用的依赖,反过来,也消除了对可穿戴数据层 API 的需求。Android Wear 应用将能够直接进行网络请求。此外,直接访问网络资源为 Wear 应用提供了新的认证方式。以下是一些方式:
-
使用标准 Google 键盘进行直接文本输入
-
使用
android.accounts.AccountManagerAPI 类同步和存储账户数据
摘要
我们在本章的开头讨论了当我们的可穿戴设备进入环境模式时如何保持我们的应用运行。然后,我们使用来自第七章的Today应用增强了我们的步数计数器活动,使其始终开启,从而接近 Android Wear API 中允许我们为应用启用环境模式的那些部分。然后,我们在展示 Android Wear 2.0 的预览版之前,简要地提到了在蓝牙上调试可穿戴应用。
第十二章。将应用发布到 Google Play
"我一直相信,天空是极限的开始。" - MC Hammer
测试是应用通过 Google Play 商店分发的重要前提。在本章中,我们将概述测试我们的 Android Wear 应用的重要性以及可用的工具,以及如何自动化 UI 测试。我们将以逐步指导结束本章,说明如何使应用准备好发布。
测试
任何程序员学习到测试代码与编码本身一样重要的艰难而有价值的教训都不会花费太多时间。忽视这个教训,一个有价值的 QA 团队肯定会让你屈服。测试本身就是一个值得大量关注的主题。有无数的资源,包括书籍,会向你推销各种各样的测试方法和哲学。如果你是测试的新手,测试驱动开发(TDD)值得研究。
然而,所有这些都超出了本书的范围。在本章中,我们更关注的是针对 Wear 开发提供的 Android 平台测试工具,以及可用的测试 API。让我们在接下来的部分中更详细地看看这些。
测试的需求
测试代码的最有说服力的理由是在应用开发生命周期中尽早捕捉回归。随着每次代码更改,都有可能影响系统其他部分的工作方式,通常是负面的。但是,通过为每个独立的、尽可能小的代码单元精心设计的(可重复的)测试,我们有一种确保它继续按预期工作的手段。这些单元测试是关键验证点,通过它们的失败,会引发代码不稳定性的红旗。
由于每一块代码都需要单独测试,因此通常有必要模拟与待测试代码单元外部的作用力。与单元测试一起使用的模拟框架使这变得容易;例如,模拟被测试单元调用的外部服务。
单元测试类型
根据代码单元是否独立于 Android 平台运行,有两种测试类型,本地测试和仪器化测试:
-
本地测试:这些测试是在本地 Java 虚拟机(JVM)上运行的单元测试。任何作为本地测试运行的代码都将无需依赖 Android 系统,或者至少能够通过模拟框架模拟这种依赖。
- 关于单元测试的逐步指导的在线文档可以在
developer.android.com/training/testing/unit-testing/index.html找到。
- 关于单元测试的逐步指导的在线文档可以在
-
仪器化测试:相比之下,这些测试在 Android 设备或模拟器上运行,是运行具有过于复杂或涉及 Android 依赖关系的单元测试的推荐方法。这些测试提供了对仪器化信息的直接访问,例如通过
android.content.Context类访问关于应用程序环境的全局信息。- 关于构建仪器化测试的逐步说明,请参阅
developer.android.com/training/testing/unit-testing/instrumented-unit-tests.html上的文档。
- 关于构建仪器化测试的逐步说明,请参阅
一个会立即引起你注意的区别是,在你的 Android Studio 项目中,本地单元测试的源文件存储在module-name/src/test/java文件夹中,而仪器化单元测试的源文件存储在module-name/src/androidTest/java文件夹中。
自动化用户界面测试
对于特定于 Wear 的应用,有一些开发方面需要非常仔细地测试,单元测试在这些情况下可能不足以满足需求。复杂的 UI 交互就是一个例子。理想情况下,人类测试员能够捕捉到许多这些问题,但很快就会证明这在时间和成本上效率低下,更不用说容易受到人为错误和疏忽的影响。
通过编写我们的 UI 测试来模拟人类交互,我们可以节省时间并提高我们对测试质量的信心。自动化的 UI 测试是用与我们的仪器化单元测试相同的指定 Android 测试文件夹编写的,即module-name/src/androidTest/java文件夹。
在此文件夹中实现的代码是由 Android 插件为 Gradle 构建的,并在应用打算运行的同一设备上执行。这使得我们可以使用 UI 测试框架来模拟目标应用上的用户交互。此外,自动化的 UI 测试可以跨越单个应用或多个应用。
单应用测试,使用如Espresso这样的 UI 测试框架,允许我们以编程方式模拟用户交互,例如在特定活动上输入特定的输入。它们还允许我们通过测试正确的 UI 输出是否在用户交互后呈现来锻炼用户交互对应用各种活动的影响。
多应用测试(也称为跨应用功能测试),使用如 UI Automator 这样的 UI 测试框架,使我们能够验证应用之间的交互。例如,如果我们想使我们的测试启动(比如说)计算器应用并执行一个计算,这个计算反过来将被用来驱动我们应用中的一个字段输入,UI Automator使这成为可能。
测试 API
Android 测试基于JUnit。我们编写我们的单元或集成测试类作为 JUnit 4 类。
JUnit
JUnit 是单元测试框架的 xUnit 架构的一个实例。它为我们提供了在单元测试中执行常见设置、拆解和断言操作的方法。一个测试类可以包含一个或多个方法。常见的 JUnit 注解可以用来标记执行设置工作(@Before 类)或拆解工作(@After 类)的方法。@Test 注解标记测试方法。
在 JUnit 测试类内部,我们可以使用 AndroidJUnitRunner 测试运行器类来调用 Espresso 或 UI Automator API 以实现我们的用户交互和应用程序间模拟。
AndroidJUnitRunner 类
AndroidJUnitRunner 类是一个测试运行器,它允许我们在 Android 设备上运行 JUnit 测试类。测试运行器将我们的测试包和我们的应用到设备上加载,然后运行我们的测试并报告结果。除了 JUnit 支持,AndroidJUnitRunner 类还包括以下功能:
-
访问仪器信息:
InstrumentationRegistry类提供了对仪器对象、目标应用的Context对象和测试应用的Context对象的便捷访问。当我们的测试使用 UI Automator 框架时,这些数据变得特别有用。 -
测试过滤:除了 JUnit 4 支持的标准注解外,还有一些特定于 Android 的注解也可用。
@RequiresDevice注解指定测试应在物理设备上运行(而不是在模拟器上)。@SdkSuppress注解阻止测试在低于指定级别的 Android API 级别上运行;例如,@SDKSupress(minSdkVersion=18)注解将抑制所有低于 18 的 API 级别的测试。 -
测试分片:
AndroidJUnitRunner类提供了将测试套件拆分为多个分片的支持,从而允许通过任何给定的分片(通过索引号识别)对测试进行分组。
Espresso
Espresso 是一个针对测试应用内部用户流程的测试框架。它提供了一套 API,使我们能够创建使用正在测试的应用的实现细节的测试。功能包括视图和适配器匹配、动作 API 和 UI 线程同步,这些内容将在以下各节中简要介绍。
视图和适配器匹配
Expresso.onView() 方法让我们能够访问目标应用中的特定 UI 组件。该方法在视图层次结构中搜索匹配项,并返回一个满足指定标准(作为方法传递的匹配器参数的一部分提供)的 View 引用。考虑以下示例:
onView(withId(R.id.my_button));
返回的引用可以用来执行用户操作或对其执行断言。
当 View 匹配让您能够恢复 View 引用时,Adapter 匹配在目标 View 位于继承自 AdapterView 类的布局内部时非常有用。在这种情况下,当前视图层次结构中可能只加载布局的子集视图。可以使用 Espresso.onData() 方法来访问目标视图元素。
动作 API
使用 android.support.test.espresso.action.ViewActions API,我们可以执行用户操作,如点击、滑动、按钮按下、文本输入和超链接。
UI Automator
Google 的 UI Automator 提供了一套 API,它使 UI 测试能够与用户应用和系统应用交互。UI Automator API 允许我们以编程方式在测试设备上打开 设置 菜单或应用启动器。如果测试代码不依赖于目标应用的实现细节,那么 UI Automator 框架可以是一个编写自动化测试的好选择。
此框架包括以下组件:
-
UI Automator 查看器用于检查设备前台可见的 UI 组件的布局层次结构和视图属性。此工具位于
<android-sdk>/tools目录中。 -
android.support.test.uiautomator.UiDeviceAPI 用于检索目标应用运行设备的状态信息并执行操作。UiDevice类支持更改设备旋转、按下后退、主页或菜单按钮以及截取当前视图的屏幕截图等操作。以下代码片段演示了如何轻松使用
UiDevice类来模拟对主按钮的短按:mDevice = UiDevice.getInstance(getInstrumentation()); mDevice.pressHome(); -
支持跨应用 UI 测试的 UI Automator API。这些 API 允许我们捕获和操作多个应用中的 UI 组件。
Monkey 和 monkeyrunner
Monkey 是一个命令行工具,它向设备发送伪随机的手势、按键和触摸流。它通过 Android 调试桥(ADB)工具运行,主要用于对您的应用进行压力测试。
Monkeyrunner 是一个用于测试程序员的 API 和执行环境,这些程序是用 Python 编写的。它包括连接到设备、安装和卸载包、截图等功能。Monkeyrunner 命令行工具可用于运行使用 monkeyrunner API 的程序。
要深入了解这些主题,以及如何衡量 UI 性能和自动化 UI 性能测试,我们应该查看开发者网站上关于这些主题的在线文档(developer.android.com/training/testing/start/index.html)作为我们的主要参考。
人的触摸
无论我们的自动化测试策略多么稳固,我们直到使用它之前都没有真正测试过我们的应用。这就是为什么在开发周期中留出时间来测试我们应用的各种功能和用户交互至关重要。每个 UI 实现代码路径都必须进行测试。在确认我们的屏幕在方形和圆形手表表面上都能良好渲染时,视觉验证是无可替代的。
我们也可能获得有价值的见解,帮助我们改进用户交互的实现方式。我们应该利用我们在前几章中介绍的材料设计概念,并尽可能多地利用它们。
应用分发
在上一节中,我们详细介绍了如何测试我们的应用。测试是分发的先决条件,了解在质量方面区分 Wear 应用的不同之处对我们大有裨益。查看在线文档中的文章developer.android.com/distribute/essentials/quality/wear.html,这可以作为这方面的提醒。
一旦我们实现了精心设计的应用并尽可能多地进行了测试,我们就可以开始准备将其分发给潜在用户。本节的重点是检查如何通过 Google Play 准备和分发我们的可穿戴应用给用户。
打包
在我们使用 Android Studio 构建发布 APK 的过程中,我们发现生成了两个不同的 APK,一个用于移动,一个用于可穿戴。
在 Android Studio 中打包可穿戴应用涉及以下步骤:
-
将可穿戴应用模块的清单文件中的所有权限复制到手持应用模块的清单文件中。
-
确保可穿戴和手持应用模块具有相同的包名和版本号。
-
在手持应用的
build.gradle文件中指定一个 Gradle 依赖项到可穿戴应用模块。 -
导航到构建 | 生成签名 APK...。
这些步骤在以下屏幕截图中进行了说明:

选择一个模块来生成移动或可穿戴的 APK,如图所示:

通过创建一个新的或选择你已有的一个来指定你的发布密钥库:

在这里,我们创建一个新的密钥库路径,并使用它对我们的应用进行签名:

指定 APK 文件的目标文件夹,然后点击完成,如图所示:

我们现在应该找到我们在指定文件夹中可用的两个 APK 文件:

发布和同意
一旦我们构建了 APK 文件,通过 Wear app 质量测试并确定其已准备好发布,我们将其上传到开发者控制台。这是我们设置分发选项并更新 Wear app 商店列表的截图。详细的发布清单可在在线文档中找到(developer.android.com/distribute/tools/launch-checklist.html),建议在发布前阅读。
一旦我们的 app 准备就绪,我们可以在开发者控制台的定价和分发部分选择 Android Wear。自愿参与意味着我们的 app 符合 Wear app 质量标准,并且是我们希望通过 Google Play 使我们的 app 对 Android Wear 用户更具可发现性的确认。以下图表展示了该过程:

一旦我们自愿参与,我们就可以像往常一样发布我们的 app,此时 Google Play 会根据Wear App 质量标准对我们的 app 进行审查。一旦结果可用,我们会收到通知。如果发现 app 符合所有 Wear App 质量标准,Google Play 将采取措施使其对 Android Wear 用户更具可发现性。
然而,如果发现应用存在不足,则会向我们的开发者账户地址发送电子邮件通知,其中突出显示需要我们注意的区域。一旦我们解决了这些问题,我们就可以将新版本的 app 上传到开发者控制台,以启动另一轮的自愿参与和审查。
Google Play 开发者控制台中Android Wear部分的定价和分发页面持有我们 app 在任何给定时间的审查和批准状态。
如下截图所示,我们点击添加新应用按钮来上传我们的 app:

在上传 APK 之前,我们指定一个默认语言和标题:

选择发布类型,即生产、Beta或Alpha 测试,然后点击相应的上传按钮:

我们随后选择我们的穿戴(或移动)APK 文件并尝试上传,如下截图所示:

在此阶段,我们会提示填写必要的 app 元数据以进行发布:

我们可以点击右上角的为什么我无法发布?链接以显示任何缺失的项目。以下是需要发布 app 的所有项目的示例。一旦添加,app 应准备好发布:

摘要
在本章中,我们介绍了 Android 测试,并区分了本地单元测试和仪器化测试。然后,在我们简要了解如何通过 UI 测试实现自动化之前,我们总结了在 Android Studio 中测试我们的可穿戴应用可用的工具。最后,我们讨论了在准备通过 Google Play 分发我们的应用时必须跨越的阶段。







浙公网安备 33010602011771号