安卓穿戴项目-全-

安卓穿戴项目(全)

原文:zh.annas-archive.org/md5/C54AD11200D923EEEED7A76F711AA69C

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Android Wear 2.0 是针对可穿戴智能设备的一个强大平台。从 Wear 发布之日起,Wear 2.0 使可穿戴设备市场的新设备激活率接近 72%。谷歌正在与多个标志性品牌合作,为智能手表带来最佳用户体验。市场上新设备的穿戴硬件不断改进,展示了穿戴设备所具有的潜力。谷歌通过材料设计、独立应用程序、表盘创新等,引入了一种全新的体验可穿戴技术的方式。

穿戴平台越来越受欢迎,Android 开发者可以提高他们为穿戴设备编程的能力,从而获益。

这本书帮助创建五个穿戴设备应用程序,并配有详尽的解释。我们从探索穿戴设备特定的用户界面组件开始,制作一个可穿戴的笔记应用程序,并构建一个可以在地图上保存快速便签的穿戴地图应用程序。我们还将构建一个带有配套移动应用程序的完整聊天应用程序。我们还将开发一个健康和健身应用程序,用于监测脉搏速率,提醒喝水等,并编写一个数字手表。我们通过探索 Wear 2.0 平台的能力来完成本书。

在构建出色的穿戴应用程序中享受乐趣。

本书涵盖的内容

第一章,让你准备起飞 - 设置你的开发环境,教你编写第一个穿戴应用程序,探索特定于穿戴应用程序的基本 UI 组件,并讨论 Android Wear 设计原则。

第二章,让我们帮助你捕捉思维 - WearRecyclerView 及更多,涵盖了WearableRecyclerViewWearableRecyclerView适配器,以及SharedPreferencesBoxInsetLayout和动画DelayedConfirmation

第三章,让我们帮助你捕捉思维 - 保存数据和定制 UI,探讨了 Realm 数据库的集成、自定义字体、UI 更新以及项目的最终确定。

第四章,测量你的健康 - 传感器,展示了传感器的准确性、电池消耗、Wear 2.0 休眠模式、材料设计等。

第五章,测量你的健康 - 同步收集的传感器数据,专注于同步收集的传感器数据,从穿戴设备收集传感器数据,处理接收到的数据以计算卡路里和距离,从移动应用程序向穿戴应用程序发送数据,Realm 数据库集成,WearableRecyclerViewCardView

第六章,无处不在的方法 - WearMap 和 GoogleAPIclient,解释了开发者 API 控制台;地图 API 密钥;以及 SHA1 指纹,SQlite 集成,Google Maps,Google API 客户端和 Geocoder。

第七章,无处不在的方法 - UI 控件及更多,探讨了理解 UI 控件、标记控件、地图缩放控件、Wear 中的 StreetView 以及最佳实践。

第八章,智能聊天方式 - 消息传递 API 及更多,讨论了为移动应用程序配置 Firebase,创建用户界面,理解消息传递 API,使用 Google API 客户端以及构建 Wear 模块。

第九章,智能聊天方式 - 通知及更多,涵盖了 Firebase 功能、通知、材质设计 Wear 应用程序 Wear 2.0 输入法框架等。

第十章,仅为你的时间 - WatchFace 和服务,概述了CanvasWatchFaceService和注册一个手表表盘,CanvasWatchFaceService.Engine和回调,手表表盘元素及其初始化编写手表表盘,以及处理手势和点击事件。

第十一章,关于 Wear 2.0 的更多信息,探讨了独立应用程序,曲线布局和更多 UI 组件的 Complications API,不同的导航和动作,手腕手势,输入法框架,以及将 Wear 应用程序分发到 Play 商店。

阅读本书所需的条件

要能够跟随本书,你需要一台安装了最新版 Android Studio 的计算机。你需要互联网来设置 Wear 开发所需的所有 SDK。如果你有一个 Wear 设备来测试应用程序,那将很好;否则,Android Wear 模拟器将完成这项工作。

本书的目标读者

本书面向已经对编程和 Android 应用开发有深入了解的 Android 开发者。本书帮助读者从中级开发者进阶为专家级 Android 开发者,通过增加 Wear 开发技能来丰富他们的知识。

约定

在本书中,你将发现多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。

界面中特定的命令或工具将如下标识:

选择“保存”按钮。

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理程序将如下显示:"我们可以通过使用include指令包含其他上下文。"

代码块按照以下方式设置:

compile 'com.google.android.support:wearable:2.0.0' compile 'com.google.android.gms:play-services-wearable:10.0.1' provided 'com.google.android.wearable:wearable:2.0.0'

当我们希望您注意代码块中的特定部分时,相关的行或项目会以粗体显示:

<?xml version="1.0" encoding="utf-8"?> <android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"  xmlns:app="http://schemas.android.com/apk/res-auto"  xmlns:tools="http://schemas.android.com/tools"  android:id="@+id/container"  android:layout_width="match_parent"  android:layout_height="match_parent"  tools:context="com.ashok.packt.wear_note_1.MainActivity"  tools:deviceIds="wear"> </android.support.wearable.view.BoxInsetLayout>

任何命令行输入或输出都如下所示:

adb connect 192.168.1.100

新术语和重要词汇会以粗体显示。您在屏幕上看到的词,例如菜单或对话框中的,会像这样出现在文本中:"让默认选择的模板为始终开启的穿戴应用代码存根 Always On Wear Activity。"

警告或重要提示会像这样出现。

提示和技巧会像这样出现。

读者反馈

我们一直欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者的反馈对我们很重要,因为它能帮助我们开发出您真正能从中获益的图书。要给我们发送一般性反馈,只需发送电子邮件至feedback@packtpub.com,并在邮件的主题中提及书籍的标题。如果您在某个主题上有专业知识,并且有兴趣撰写或参与书籍编写,请查看我们的作者指南:www.packtpub.com/authors

客户支持

既然您已经拥有了 Packt 的一本书,我们有很多方法可以帮助您充分利用您的购买。

下载示例代码

您可以从您的账户www.packtpub.com下载本书的示例代码文件。如果您在其他地方购买了这本书,可以访问www.packtpub.com/support注册,我们会直接将文件通过电子邮件发送给您。您可以按照以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标悬停在顶部的“支持”标签上。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍的名称。

  5. 选择您要下载代码文件的书。

  6. 从下拉菜单中选择您购买本书的地方。

  7. 点击“代码下载”。

下载文件后,请确保您使用最新版本的以下软件解压或提取文件夹:

  • 对于 Windows 用户,可以使用 WinRAR / 7-Zip。

  • 对于 Mac 用户,可以使用 Zipeg / iZip / UnRarX。

  • 对于 Linux 用户,可以使用 7-Zip / PeaZip。

本书的代码包也托管在 GitHub 上:github.com/PacktPublishing/Android-Wear-Projects。我们还有其他丰富的书籍和视频代码包,可以在github.com/PacktPublishing/查看。请查看!

勘误

尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果您在我们的书中发现了一个错误——可能是文本或代码中的错误——如果您能报告给我们,我们将不胜感激。这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书后续版本。如果您发现任何勘误信息,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误信息被验证,您的提交将被接受,勘误信息将被上传到我们的网站或添加到该标题勘误部分下现有的勘误列表中。要查看之前提交的勘误信息,请前往www.packtpub.com/books/content/support,并在搜索字段中输入书名。所需信息将在勘误部分下显示。

盗版

互联网上版权材料的盗版问题在所有媒体中持续存在。在 Packt,我们非常重视保护我们的版权和许可。如果您在任何形式的互联网上发现我们作品非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。请通过 copyright@packtpub.com 联系我们,并提供疑似盗版材料的链接。我们感谢您帮助保护我们的作者和我们提供有价值内容的能力。

问题

如果您对本书的任何方面有问题,可以通过 questions@packtpub.com 联系我们,我们将尽力解决问题。

第一章:让你准备好起飞 - 设置你的开发环境

穿戴实用性工具的文化一直是现代文明的一部分,它帮助我们执行某些动作。对人类来说,手表已经成为检查时间和日期的增强工具。佩戴手表可以让你只需一瞥就能检查时间。技术将这种戴表体验带到了下一个层次。第一个现代可穿戴手表是计算器和手表的组合,于 1970 年推出。几十年来,微处理器和无线的进步导致了“无处不在的计算”这一概念的引入。在此期间,大多数领先的电子行业初创公司开始工作在自己的想法上,这使得可穿戴设备变得非常流行。

技术巨头公司,如谷歌、苹果、三星和索尼,都加入了可穿戴设备时代的行列。它们推出了自己的竞争性可穿戴产品,在可穿戴设备市场上取得了极大的成功。更有趣的是,谷歌的 Android Wear 非常强大,遵循与安卓智能手机开发相同的实践,并且与苹果手表操作系统和三星的 Tizen 操作系统开发者社区相比,拥有非常好的开发者社区。

谷歌在 2014 年 3 月宣布了 Android Wear。自那时起,作为智能手表和可穿戴软件平台的 Android Wear 一直在发展。谷歌在设计和用户体验方面的持续进步导致了新一代 Android Wear 操作系统的诞生,它能够以前所未有的方式处理生物识别传感器,并在平台上拥有更多功能;谷歌称之为 Android Wear 2.0。

Android Wear 2.0 在应用开发方面将带来很多令人兴奋的竞争性功能。Android Wear 2.0 允许开发者构建和雕琢自己针对 Android Wear 的特定想法;无需将手表和移动应用配对。谷歌称之为独立应用。Android Wear 2.0 引入了一种在 Android 手表内输入的新方式:一个新的应用程序编程接口,称为 Complications,它允许表盘显示来自生物识别和其他传感器的重要信息。Android Wear 2.0 新的通知更新支持将帮助用户和开发者以更全面的方式呈现通知。

在本章中,我们将探讨以下内容:

  • 安卓穿戴设计原则

  • 探索特定于穿戴应用的基本 UI 组件

  • 为穿戴应用开发设置开发环境

  • 创建你的第一个安卓穿戴应用

安卓穿戴设计原则

设计穿戴应用与设计移动或平板应用不同。穿戴操作系统非常轻量级,并且有特定的任务要通过与穿戴者分享正确信息来完成。

通用穿戴原则是及时、一目了然、易于点击、节省时间。

及时

在正确的时间提供正确的信息。

一目了然

保持穿戴应用用户界面的清洁和整洁。

易于点击

用户将点击的动作应该具有适当的间距和图片大小。

节省时间

创建能够快速完成任务的最佳应用流程。

对于任何穿戴应用,我们需要适当的构建块来控制应用程序的业务逻辑和其他架构实现。以下是开发穿戴应用的情况,有助于我们更好地雕刻穿戴应用:

  • 定义布局

  • 创建列表

  • 显示确认信息

  • 穿戴设备导航与操作

  • 多功能按钮

定义布局

穿戴应用可以使用我们在手持 Android 设备编程中使用的相同布局,但需要针对穿戴应用的具体限制。在穿戴应用中,我们不应执行类似于手持 Android 设备的繁重处理操作,并期待良好的用户体验。

针对圆形屏幕设计的应用在方形穿戴设备上可能不会看起来很好。为了解决这个问题,Android Wear 支持库提供了以下两个解决方案:

  • BoxInsetLayout

  • 曲线布局

我们可以提供不同的资源,让 Android 在运行时检测 Android Wear 的形状。

创建列表

列表允许用户从一组条目中选择一个条目。在旧的 Wear 1.x API 中,WearableListView帮助程序员构建列表和自定义列表。Wearable UI 库现在有支持curvedLayoutWearableRecyclerView,在穿戴设备上拥有最佳的实现体验。

我们可以添加手势和其他出色的功能:

探索穿戴设备的 UI 组件

在这一小节中,让我们探索常用的穿戴特定 UI 组件。在穿戴应用编程中,我们可以使用在移动应用编程中使用的所有组件,但在使用之前需要仔细考虑如何在穿戴设备中容纳这些组件的视觉外观。

WatchViewStubWatchViewStub帮助针对不同形态的穿戴设备渲染视图。如果你的应用被安装在圆形手表设备上,WatchViewStub将加载针对圆形手表的特定布局配置。如果是方形,它将加载方形布局配置:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.WatchViewStub 

    android:id="@+id/watch_view_stub"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
 app:rectLayout="@layout/rect_activity_main"
    app:roundLayout="@layout/round_activity_main"
    tools:context="com.ashokslsk.wearapp.MainActivity"
    tools:deviceIds="wear"></android.support.wearable.view.WatchViewStub>

WearableRecyclerViewWearableRecyclerView是特定于穿戴设备的recyclerview实现。它为穿戴设备视口中的数据集提供了灵活的视图。我们将在接下来的章节中详细探索WearableRecyclerView

 <android.support.wearable.view.WearableRecyclerView
   android:id="@+id/recycler_launcher_view"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:scrollbars="vertical" /> 

注意:WearableListView已弃用;Android 社区建议使用WearableRecyclerView

CircledImageVIew:一个由圆形环绕的Imageview。对于展示圆形形态穿戴设备中的图片来说,这是一个非常方便的组件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.CircledImageView

    android:id="@+id/circledimageview"
    app:circle_color="#2878ff"
    app:circle_radius="50dp"
    app:circle_radius_pressed="50dp"
    app:circle_border_width="5dip"
    app:circle_border_color="#26ce61"
    android:layout_marginTop="15dp"
    android:src="img/skholinguaicon"
    android:layout_gravity="center_horizontal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

BoxInsetLayout:这个布局直接扩展到Framelayout,并且能够识别 Wearable 设备的形状因素。形状感知的FrameLayout可以将子元素框定在屏幕中心的正方形内:

<android.support.wearable.view.BoxInsetLayout    

android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.ranjan.androidwearuicomponents.BoxInsetLayoutDemo">

<TextView
    android:text="@string/hello_world"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_box="all" />

</android.support.wearable.view.BoxInsetLayout>

在 Wear 2.0 发布之后,为了沉浸式活动体验,一些组件被弃用,谷歌严格禁止使用它们;我们仍然可以使用 Android 编程中我们了解的所有组件。

显示确认操作

与手持 Android 设备中的确认操作相比,在 Wear 应用程序中,确认操作应该占据整个屏幕或比手持设备显示的对话框更多。这确保用户可以一眼看到这些确认操作。Wearable UI 库帮助在 Android Wear 中显示确认计时器和动画计时器。

DelayedConfirmationView(延迟确认视图)

DelayedConfirmationView是一个基于计时器的自动确认视图:

图片********图片

<android.support.wearable.view.DelayedConfirmationView
    android:id="@+id/delayed_confirm"
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:src="img/cancel_circle"
    app:circle_border_color="@color/lightblue"
    app:circle_border_width="4dp"
    app:circle_radius="16dp">
</android.support.wearable.view.DelayedConfirmationView>

Wear 导航与操作

在 Android Wear 的新版本中,Material design库增加了以下两个交互式抽屉:

  • 导航抽屉

  • 操作抽屉

导航抽屉

允许用户在应用程序中的视图之间切换。开发者可以通过将setShouldOnlyOpenWhenAtTop()方法设置为 false,允许抽屉在滚动父内容内的任何位置打开:

<android.support.wearable.view.drawer.WearableNavigationDrawer
    android:id="@+id/top_drawer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_red_light"
    app:navigation_style="single_page"/>

操作抽屉

操作抽屉为您的应用程序提供了简单且常用的操作。默认情况下,操作抽屉出现在屏幕底部,并为用户提供特定操作:

<android.support.wearable.view.drawer.WearableActionDrawer
    android:id="@+id/bottom_drawer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_dark"
    app:show_overflow_in_peek="true"/>

多功能按钮

除了电源按钮外,Android Wear 还支持设备上的多功能按钮。Wearable 支持库提供了由制造商包含的多功能按钮的 API:

@Override
// Activity
public boolean onKeyDown(int keyCode, KeyEvent event){
  if (event.getRepeatCount() == 0) {
    if (keyCode == KeyEvent.KEYCODE_STEM_1) {
      // Do stuff
      return true;
    } else if (keyCode == KeyEvent.KEYCODE_STEM_2) {
      // Do stuff
      return true;
    } else if (keyCode == KeyEvent.KEYCODE_STEM_3) {
      // Do stuff
      return true;
    }
  }
  return super.onKeyDown(keyCode, event);
}

对于有关 Wear 设备编程的设计指南的任何疑问,请访问developer.android.com/training/wearables/ui/index.html

为 Wear 开发设置开发环境

在本节中,我们将为 Wear 应用程序开发设置一个开发环境。

先决条件

  1. 您喜欢的操作系统(Windows、macOS 或 Linux)

  2. 确定您的操作系统上是否安装了最新版本的 JRE

  3. 安装最新版本的 JDK 或 Open JDK

  4. 安装最新版本的 Android Studio(在撰写本书时,最新版本为 2.2.3,任何更新的版本应该都可以)

安装 Android Studio

访问developer.android.com/studio/index.html下载最新版本的 Android Studio。谷歌强烈建议为所有 Android 应用程序开发使用 Android Studio,因为 Android Studio 与 Gradle 紧密集成并提供了有用的 Android API:

图片

安装完 Android Studio 之后,现在需要在 SDK 管理器的 SDK Platforms 标签中下载必要的 SDK。安装一个完整的 Android 版本;在本书的范围内,我们将安装 Android 7.1.1 API 级别 25:

成功安装了 Nougat 7.1.1 API 级别 25 的 SDK 后,在SDK Tools标签下,确保你已经安装了以下组件,如下面的截图所示:

  • Android 支持库

  • Google Play 服务

  • Google 仓库

  • Android 支持仓库

谷歌会定期更新 IDE 和 SDK 工具,请保持你的开发环境是最新的。

注意:如果你打算让你的应用程序在中国可用,那么你必须使用 Google Play 服务客户端库的特殊发布版本 7.8.87 来处理手机和手表之间的通信:developer.android.com/training/wearables/apps/creating-app-china.html

访问以下链接查看 SDK 工具的更新发行说明:developer.android.com/studio/releases/sdk-tools.html.

强烈建议从稳定版本通道更新你的 IDE。Android Studio 的更新在四个不同的通道上可用:

  • 金丝雀版本

  • 开发版本

  • 测试版本

  • 稳定版本

金丝雀版本(Canary channel):Android Studio 的工程团队持续工作以改进 Android Studio。在这个版本通道中,每周都会发布一次更新,其中将包含新的功能更改和改进;你可以在发行说明中查看这些更改。但此通道的更新不推荐用于应用程序的生产环境。

开发版本(Dev Channel):在这个通道上,发布版本会在 Android Studio 团队完成一轮完整的内部测试后进行。

测试版本(Beta channel):在这个通道上,更新完全基于稳定的金丝雀版本。在将这些版本发布到稳定通道之前,谷歌会在测试版本通道中发布它们以获取开发者的反馈。

稳定版本(Stable Channel):是 Android Studio 的官方稳定版本,你可以在谷歌的官方页面developer.android.com/studio.下载。

默认情况下,Android Studio 会从稳定版本通道接收更新。

创建你的第一个 Android Wear 应用程序

在这一节中,让我们了解创建你的第一个 Wear 项目所需的基本步骤。

在你继续创建应用程序之前,请确保你已经安装了一个带有 Wear 系统映像的完整 Android 版本,并且你有最新版本的 Android Studio。

下面的图片是 Android Studio 的初始界面。在这个窗口中,可以导入旧的 ADT Android 项目,配置 Android SDK,以及更新 Android Studio。

Android Studio 欢迎窗口,带有开始操作的基本控制:

创建你的第一个穿戴项目

在 Android Studio 窗口中点击“开始新的 Android Studio 项目”选项。你将会看到一个包含项目详细信息的窗口。

下面的截图显示了允许用户配置他们的项目详细信息(如项目名称、包名称以及项目是否需要本地 C++ 支持)的窗口:

你可以按自己的意愿为项目命名。选择项目名称和项目在本地系统的位置后,你可以在窗口中按下“下一步”按钮,这将打开另一个包含一些配置查询的窗口,如下截图所示:

在这个窗口中,如果你取消勾选“手机和平板电脑”选项,可以选择编写一个独立的穿戴应用程序。这样,你将只看到穿戴应用程序模板:

现在,Android Studio 模板仅以下列选项提示 Android Wear 活动模板:

  • 添加无活动

  • 始终开启的穿戴活动

  • 空白穿戴活动

  • 显示通知

  • Google 地图穿戴活动

  • 表盘界面

活动模板选择器可以帮助你访问默认的基础模板代码,这些代码已经模板化,可以直接在项目中使用:

要创建第一个项目,我们将选择“空白穿戴活动”,并在窗口中点击“下一步”按钮。Android Studio 将提示另一个窗口以创建活动和布局文件的名称。在这个模板中,Android 可穿戴设备的两种形状因素(大部分是圆形和方形)已经预填充了基础代码存根:

当你的项目准备好创建时,点击“完成”按钮。点击完成后,Android Studio 将会花一些时间为我们创建项目。

做得好!你现在已经为 Android Wear 独立应用程序创建了一个工作基础模板代码,无需手机伴侣应用程序。成功创建后,你会看到以下文件和代码默认添加到你的项目中:

如果你的 SDK 没有更新到 API 级别 25,你可能会在 Android Studio 项目创建提示中看到带有 Android Wear 支持库 1.x 的穿戴选项;你可以在 Wear 模块的 Gradle 文件中用以下依赖关系进行更新:

compile 'com.google.android.support:wearable:2.0.0'

创建穿戴模拟器

创建穿戴模拟器的过程与创建手机模拟器非常相似。

在 AVD 管理器中,点击“创建虚拟设备...”按钮:

根据你的应用程序需求,选择所需的模拟器形状因素。现在,让我们创建一个 Android Wear 方形模拟器:

选择适合你的穿戴的正确模拟器后,你将得到另一个提示,选择穿戴操作系统。让我们选择如下的 API 级别 25 Nougat 模拟器:

最后一个提示会根据您的需求询问模拟器名称和其他方向配置:

做得好!现在,我们已经成功为项目创建了一个方形表盘的模拟器。让我们在模拟器中运行我们创建的项目:

谷歌建议在真实硬件设备上开发 Wear 应用以获得最佳用户体验。然而,在模拟器上工作可以创建不同的屏幕表盘尺寸,以检查应用程序的渲染效果。

使用实际的 Wear 设备工作

  1. 在 Wear 设备上打开设置菜单

  2. 前往关于设备

  3. 点击构建号七次以启用开发者模式

  4. 现在,在手表上启用 ADB 调试

您现在可以通过 USB 电缆直接将 Wear 设备连接到您的机器。通过以下设置,您可以通过 Wi-Fi 和蓝牙调试应用程序。

通过 Wi-Fi 调试

确保您的手表已启用开发者选项。只有当 Wear 设备和机器连接到同一网络时,才能通过 Wi-Fi 进行调试。

  • 在 Wear 设备开发者选项中,点击通过 Wi-Fi 调试

  • 手表将显示其 IP 地址(例如,192.168.1.100)。记下这个信息;下一步我们需要用到。

  • 将调试器连接到设备

  • 使用以下命令,我们可以将实际设备连接到 ADB 调试器:

adb connect 192.168.1.100

启用蓝牙调试

我们需要确保在开发者选项中启用了调试,如下所示:

adb forward tcp:4444 localabstract:/adb-hub
adb connect 127.0.0.1:4444

在您的 Android Wear 中,当它询问时允许 ADB 调试。

既然我们已经有了开发环境的工作设置,让我们了解基本的 Android Wear 特定 UI 组件。

摘要

在本章中,我们了解了 Wear 应用程序开发的初步设置。我们了解了需要下载的必要组件,设置 Wear 模拟器,将 Wear 模拟器连接到 ADB 桥,通过 Wi-Fi 进行调试以及特定于 Wear 开发的用户界面组件。在下一章中,我们将探讨如何构建一个记事本应用程序,该程序可以保存用户输入的数据。

第二章:让我们帮助捕捉你的想法 —— WearRecyclerView 及更多功能

戴上能帮助我们执行某些动作的实用工具,这种文化一直是现代文明的一部分。对人类来说,腕表已经成为检查时间和日期的增强工具。佩戴手表可以让你只需一瞥就能知道时间。科技将这种戴表体验带到了下一个层次。第一款现代可穿戴手表是计算器和手表的组合,于 1970 年推出。几十年来,微处理器和无线的进步导致了“无处不在的计算”这一概念的出现。在这段时间里,大多数领先的电子行业初创公司开始着手他们的想法,这使得可穿戴设备变得非常流行。

科技巨头公司,如谷歌、苹果、三星和索尼,都加入了可穿戴设备时代的行列。他们推出了自己的竞争性可穿戴产品,这些产品在可穿戴设备市场上取得了极大的成功。更有趣的是,谷歌的 Android Wear 功能强大,遵循与 Android 智能手机开发相同的实践,并且与 Apple Watch OS 和三星的 Tizen OS 开发社区相比,拥有非常好的开发者社区。

谷歌在 2014 年 3 月宣布了 Android Wear。从那时起,作为智能手表和可穿戴软件平台的 Android Wear 一直在发展。谷歌在设计和用户体验方面的持续进步导致了新一代 Android Wear 操作系统的诞生,该系统具有前所未有的处理生物识别传感器的能力,平台功能更多;谷歌称之为 Android Wear 2.0。

Android Wear 2.0 在应用开发方面将引起极大的兴奋,具有显著竞争力的特性待开发。Android Wear 2.0 允许开发者构建和雕琢他针对 Android Wear 的特定想法;无需将手表与移动应用配对。谷歌称之为独立应用程序。Android Wear 2.0 引入了一种在 Android 表中输入的新方式:一个新的应用程序编程接口,称为 Complications,它允许表盘显示来自生物识别和其他传感器的重要信息。Android Wear 2.0 的新更新通知支持将帮助用户和开发者以更全面的方式呈现通知。

在本章中,我们将探讨以下内容:

  • Android Wear 设计原则

  • 探索特定于可穿戴应用的基本 UI 组件

  • 为可穿戴应用开发设置开发环境

  • 创建你的第一个 Android Wear 应用程序

Android Wear 设计原则

设计可穿戴应用与设计移动或平板应用不同。Wear 操作系统非常轻量级,并且有特定的任务要完成,即与佩戴者分享正确信息。

通用可穿戴原则是:及时性、一瞥即得、易于轻触、节省时间。

及时性

在正确的时间提供正确的信息。

一瞥即得

保持穿戴应用用户界面干净、不杂乱。

易于点击

用户将点击的动作应该具有适当的间距和图片大小。

节省时间

创建能够快速完成任务的最佳应用流程。

对于任何穿戴应用,我们需要合适的构建块来控制应用程序的业务逻辑和其他架构实现。以下是开发穿戴应用的场景,帮助我们更好地雕刻穿戴应用:

  • 定义布局

  • 创建列表

  • 显示确认信息

  • 穿戴导航和动作

  • 多功能按钮

定义布局

穿戴应用可以使用我们在手持式 Android 设备编程中使用的相同布局,但需要针对穿戴应用的具体限制。我们不应该在穿戴应用中进行类似于手持式 Android 设备的繁重处理操作,并期待良好的用户体验。

为圆形屏幕设计的应用在方形穿戴设备上看起来不会很好。为了解决这个问题,Android Wear 支持库提供了以下两个解决方案:

  • BoxInsetLayout

  • Curved Layout(曲线布局)

我们可以提供不同的资源,允许 Android 在运行时检测 Android Wear 的形状。

创建列表

列表允许用户从一组项目中选择一个项目。在旧版的 Wear 1.x API 中,WearableListView帮助程序员构建列表和自定义列表。现在,穿戴 UI 库有了支持curvedLayoutWearableRecyclerView,并在穿戴设备上拥有最佳的实现体验。

我们可以添加手势和其他出色的功能:

探索穿戴设备的 UI 组件

在这一小节中,让我们探索常用的穿戴特定 UI 组件。在穿戴应用编程中,我们可以使用我们在移动应用编程中使用的所有组件,但在使用之前,我们需要仔细考虑如何在穿戴设备中容纳组件的视觉外观。

WatchViewStubWatchViewStub有助于为不同表盘形状的穿戴设备渲染视图。如果您的应用安装在圆形手表设备上,WatchViewStub将加载针对圆形手表的特定布局配置。如果是方形,它将加载方形布局配置:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.WatchViewStub 

    android:id="@+id/watch_view_stub"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
 app:rectLayout="@layout/rect_activity_main"
    app:roundLayout="@layout/round_activity_main"
    tools:context="com.ashokslsk.wearapp.MainActivity"
    tools:deviceIds="wear"></android.support.wearable.view.WatchViewStub>

WearableRecyclerViewWearableRecyclerView是专为穿戴设备实现的recyclerview。它为穿戴设备视口中的数据集提供了灵活的视图。我们将在接下来的章节中详细探讨WearableRecyclerView

 <android.support.wearable.view.WearableRecyclerView
   android:id="@+id/recycler_launcher_view"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:scrollbars="vertical" /> 

注意:WearableListView已弃用;Android 社区建议使用WearableRecyclerView

CircledImageVIew:一个由圆形环绕的Imageview。这是在圆形表盘穿戴设备上展示图片的非常实用的组件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.CircledImageView

    android:id="@+id/circledimageview"
    app:circle_color="#2878ff"
    app:circle_radius="50dp"
    app:circle_radius_pressed="50dp"
    app:circle_border_width="5dip"
    app:circle_border_color="#26ce61"
    android:layout_marginTop="15dp"
    android:src="img/skholinguaicon"
    android:layout_gravity="center_horizontal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

BoxInsetLayout:此布局直接扩展到Framelayout,并且能够识别 Wearable 设备的形状因素。形状感知的FrameLayout可以将子元素框定在屏幕中心正方形内:

<android.support.wearable.view.BoxInsetLayout    

android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.ranjan.androidwearuicomponents.BoxInsetLayoutDemo">

<TextView
    android:text="@string/hello_world"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_box="all" />

</android.support.wearable.view.BoxInsetLayout>

自从 Wear 2.0 版本发布后,为了提供沉浸式活动体验,一些组件被弃用,谷歌严格禁止使用它们;我们仍然可以在 Android 编程中使用我们所熟知的所有组件。

显示确认信息

与手持 Android 设备中的确认操作相比,在 Wear 应用程序中,确认操作应该占据整个屏幕或比手持设备显示的对话框更多。这确保用户可以一眼看到这些确认信息。Wearable UI 库帮助在 Android Wear 中显示确认计时器和动画计时器。

DelayedConfirmationView

DelayedConfirmationView是基于计时器的自动确认视图:

图片********图片

<android.support.wearable.view.DelayedConfirmationView
    android:id="@+id/delayed_confirm"
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:src="img/cancel_circle"
    app:circle_border_color="@color/lightblue"
    app:circle_border_width="4dp"
    app:circle_radius="16dp">
</android.support.wearable.view.DelayedConfirmationView>

Wear 导航和动作

在 Android Wear 的新版本中,Material design库新增了以下两种交互式抽屉:

  • 导航抽屉

  • 动作抽屉

导航抽屉

允许用户在应用程序中的视图之间切换。开发者可以通过将setShouldOnlyOpenWhenAtTop()方法设置为 false,允许抽屉在滚动父内容内的任何位置打开。

<android.support.wearable.view.drawer.WearableNavigationDrawer
    android:id="@+id/top_drawer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_red_light"
    app:navigation_style="single_page"/>

动作抽屉

动作抽屉提供了访问应用程序中简单且常见的操作。默认情况下,动作抽屉出现在屏幕底部,并为用户提供特定操作:

<android.support.wearable.view.drawer.WearableActionDrawer
    android:id="@+id/bottom_drawer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_dark"
    app:show_overflow_in_peek="true"/>

多功能按钮

除了电源按钮外,Android Wear 还支持设备上的另一个按钮,即多功能按钮。Wearable 支持库提供了确定制造商包含的多功能按钮的 API:

@Override
// Activity
public boolean onKeyDown(int keyCode, KeyEvent event){
  if (event.getRepeatCount() == 0) {
    if (keyCode == KeyEvent.KEYCODE_STEM_1) {
      // Do stuff
      return true;
    } else if (keyCode == KeyEvent.KEYCODE_STEM_2) {
      // Do stuff
      return true;
    } else if (keyCode == KeyEvent.KEYCODE_STEM_3) {
      // Do stuff
      return true;
    }
  }
  return super.onKeyDown(keyCode, event);
}

如果您对 Wear 设备编程的设计指南有任何疑问,请访问developer.android.com/training/wearables/ui/index.html

设置 Wear 开发的开发环境

在本节中,我们将为 Wear 应用程序开发设置一个开发环境。

先决条件

  1. 您喜欢的操作系统(Windows、macOS 或 Linux)

  2. 确定您的操作系统上是否安装了最新版本的 JRE

  3. 安装最新版本的 JDK 或 Open JDK

  4. 安装最新版本的 Android Studio(在撰写本书时,最新版本是 2.2.3,任何更新的版本应该都可以)

安装 Android Studio

访问developer.android.com/studio/index.html下载最新版本的 Android Studio。谷歌强烈建议在所有 Android 应用程序开发中使用 Android Studio,因为 Android Studio 与 Gradle 紧密集成并提供了有用的 Android API:

图片

安装完 Android Studio 后,现在是在 SDK 管理器的 SDK Platforms 标签下下载必要的 SDK 的时候了。安装一个完整的 Android 版本;在本书的范围内,我们将安装 Android 7.1.1 API 级别 25:

在成功安装了 Nougat 7.1.1 API 级别 25 的 SDK 后,在SDK 工具标签下,确保你已经安装了以下组件,如下面的截图所示:

  • Android 支持库

  • Google Play 服务

  • Google 仓库

  • Android 支持仓库

谷歌频繁发布 IDE 和 SDK 工具的更新;请保持你的开发环境是最新的。

注意:如果你打算让你的应用程序在中国可用,那么你必须使用 Google Play 服务客户端库的特殊发布版本 7.8.87 来处理手机和手表之间的通信:developer.android.com/training/wearables/apps/creating-app-china.html

访问以下链接查看 SDK 工具的更新发布说明:developer.android.com/studio/releases/sdk-tools.html.

强烈建议从稳定渠道更新你的 IDE。Android Studio 的更新在四个不同的渠道上可用:

  • 金丝雀渠道

  • 开发渠道

  • 测试版渠道

  • 稳定渠道

金丝雀渠道:Android Studio 工程团队不断努力使 Android Studio 变得更好。在这个渠道上,每周将有一个更新发布,并将包括新的功能变更和改进;你可以在发布说明中查看这些更改。但此渠道的更新不建议用于应用程序生产。

开发渠道:在这个渠道上,发布会在 Android Studio 团队完成一轮完整的内部测试后进行。

测试版渠道:在这个渠道上,更新完全基于稳定的金丝雀构建。在将这些构建发布到稳定渠道之前,谷歌会在测试版渠道中发布它们以获取开发者的反馈。

稳定渠道:是 Android Studio 的官方稳定版本,可在谷歌官方页面 developer.android.com/studio. 下载。

默认情况下,Android Studio 从稳定渠道接收更新。

创建你的第一个 Android Wear 应用程序

在本节中,让我们了解创建你的第一个 Wear 项目的必要步骤。

在你继续创建你的应用程序之前,请确保你已经安装了一个带有 Wear 系统镜像的完整版 Android,并且你有最新版本的 Android Studio。

下面的图片是 Android Studio 的初始界面。在这个窗口中,你可以导入传统的 ADT Android 项目,配置 Android SDK,以及更新 Android Studio。

Android Studio 欢迎窗口,带有开始使用的基本控制:

图片

创建你的第一个 Wear 项目

在 Android Studio 窗口中点击“开始新的 Android Studio 项目”选项。您将看到一个带有项目详情的窗口。

下面的截图显示了允许用户配置项目详情的窗口,如项目名称、包名称以及项目是否需要本地 C++支持:

图片

您可以按自己的意愿为项目命名。选择项目名称和项目在本地系统的位置后,您可以点击窗口中的“下一步”按钮,这将打开另一个带有一些配置查询的窗口,如下截图所示:

图片

在此窗口中,如果您取消勾选“手机和平板电脑”选项,可以选择编写独立的 Wear 应用程序。这样,您将只看到 Wear 应用程序模板:

图片

现在,Android Studio 模板仅提示以下选项集的 Android Wear 活动模板:

  • 添加无活动

  • 始终开启的 Wear 活动

  • 空白 Wear 活动

  • 显示通知

  • Google Maps Wear 活动

  • 表盘界面

活动模板选择器可以帮助您访问默认的模板化样板代码,这些代码可以直接在项目中使用:

图片

要创建第一个项目,我们将选择“空白 Wear 活动”,并在窗口中点击“下一步”按钮。Android Studio 将提示另一个窗口以创建活动和布局文件的名称。在此模板中,Android 可穿戴设备的两种形式因素(主要是圆形和方形)已经预填充了样板代码存根:

图片

当您的项目准备好创建时,点击“完成”按钮。点击完成后,Android Studio 将花费一些时间为我们创建项目。

做得好!您现在已经为 Android Wear 独立应用程序创建了工作的样板代码,而没有手机伴侣应用程序。成功创建后,您将看到以下文件和代码默认添加到您的项目中:

图片

如果您的 SDK 没有更新到 API 级别 25,那么在 Android Studio 项目创建提示中,您可能会看到带有 Android Wear 支持库 1.x 的 Wear 选项;你可以在 Wear 模块的 Gradle 文件中用以下依赖关系进行更新:

compile 'com.google.android.support:wearable:2.0.0'

创建 Wear 模拟器

创建 Wear 模拟器的过程与创建手机模拟器非常相似。

在 AVD 管理器中,点击“创建虚拟设备...”按钮:

图片

根据您的应用程序需求选择所需的形式因素模拟器。现在,让我们创建一个 Android Wear 方形模拟器:

图片

在为您的 Wear 选择合适的模拟器之后,您将看到一个提示,选择 Wear 操作系统。如下截图所示,我们选择 API 级别 25 的 Nougat 模拟器:

图片

最后一个提示会根据你的需要询问模拟器名称和其他方向配置:

做得好!现在,我们已经成功为项目创建了一个方形尺寸的模拟器。让我们在模拟器中运行我们创建的项目:

谷歌建议在真实硬件设备上开发 Wear 应用以获得最佳用户体验。然而,在模拟器上工作可以创建不同的屏幕尺寸,以检查应用程序的渲染情况。

使用实际的 Wear 设备工作

  1. 在 Wear 设备上打开设置菜单

  2. 前往关于设备

  3. 点击构建号七次以启用开发者模式

  4. 现在在手表上启用 ADB 调试

现在你可以用 USB 线将 Wear 设备直接连接到你的机器上。通过以下设置,你可以在 Wi-Fi 和蓝牙上进行应用调试。

通过 Wi-Fi 进行调试

确保你的手表已启用开发者选项。只有在 Wear 设备和机器连接到同一网络时,才能通过 Wi-Fi 进行调试。

  • 在 Wear 设备开发者选项中,点击“通过 Wi-Fi 调试”

  • 手表将显示其 IP 地址(例如,192.168.1.100)。保留参考,下一步我们需要这个。

  • 将调试器连接到设备

  • 使用以下命令,我们可以将实际设备连接到 ADB 调试器:

adb connect 192.168.1.100

启用蓝牙调试

我们需要确保在开发者选项中启用了调试,如下所示:

adb forward tcp:4444 localabstract:/adb-hub
adb connect 127.0.0.1:4444

在你的 Android Wear 上,当它提示时允许 ADB 调试。

现在我们已经设置了开发环境,让我们了解基本的 Android Wear 特定 UI 组件。

概述

在本章中,我们了解了 Wear 应用开发的初步设置。我们了解了需要下载的必要组件,设置 Wear 模拟器,将 Wear 模拟器连接到 ADB 桥,通过 Wi-Fi 进行调试,以及特定于 Wear 开发的界面组件。在下一章中,我们将探讨如何构建一个笔记应用,该应用可以保存用户输入的数据。

第三章:让我们帮助捕捉你的想法 - 保存数据和定制 UI

从零开始构建记事本应用程序是一次很好的学习经历。在本章中,我们将了解如何按照 Wear 设计标准升级同一代码以实现新的用户界面。Wear-note 1 应用程序将通过以下更新迁移到 Wear-note 2

  • 集成 Realm 数据库

  • UI 更新

  • 集成自定义字体和资源

  • 完成项目收尾工作

为了在 Android Studio 中进一步协助开源 wear-note 1 项目,编译项目并逐屏检查项目。你会发现该应用程序的主要功能是在 Wear 设备上保存便签。在此应用程序中,我们有白色的背景和黑色的字体颜色。SharedPreferences 帮助应用程序持久化数据。

进一步回顾,我们知道如何使用 WearableRecyclerViewDelayedConfirmationViewBoxInsetLayout 在 Wear 设备上获得最佳的应用体验。

接下来,让我们按照之前提到的更改来完成项目。我们将这个应用程序称为 wear-note-2。在你的 res 目录下的 values 目录中,打开 string.xml 文件,将应用程序名称更改为 Wear-note-2,如下所示:

<string name="app_name">Wear-note-2</string>

Wear-note-2

让我们开始在这个子模块中用 RealmDB 替换数据库并开发功能。

RealmDB 在现代 Android 编程世界中引起了轰动;它是内置在 Android SDK 中的 SQLite 数据库的一个简单替代品。Realm 并没有使用 SQLite 作为其核心引擎;它有自己的 C++ 核心,专为移动设备而设计。Realm 使用 C++ 核心以通用、基于表的形式保存数据。Realm 处理一系列复杂的查询,并允许从多种语言以及许多临时查询访问数据。

Realm 的优势

  • Realm 的速度是 SQLite 的 10 倍

  • 易于使用

  • 跨平台

  • 内存高效

  • 文档齐全且社区支持优秀

进一步探索 Realm,我们会发现 Realm 做了许多优化,比如整数打包和将常见字符串转换为枚举,这比 SQLite + ORM 数据库解决方案的性能更好。传统的 SQLite + ORM 抽象是泄漏的,ORM 仅将对象及其方法转换为 SQL 语句,导致性能不佳。另一方面,Realm 是一个面向对象的数据库,这意味着你的数据以对象形式保存,这在你的数据库中得到了体现。Realm 使用 B+ 树在内存中映射整个数据,每当查询数据时,Realm 计算偏移量,从内存映射区域读取,并返回原始值。通过这种方式,Realm 避免了零拷贝(传统的从数据库读取数据的方式会导致不必要的复制(原始数据 > 反序列化表示 > 语言级对象))。

当开发者想要实现延迟加载时,Realm 是一个完美的选择;因为属性以列的形式表示而不是行,它可以按需延迟加载属性,并且由于列结构,读取速度更快,而插入速度较慢。但对于移动应用程序的上下文来说,这是一个很好的权衡。

Realm 使用 多版本并发控制MVCC)模型,这意味着可以同时进行多个读事务,并且在提交写事务的同时也可以进行读取。

欲了解更多信息,请访问realm.io/news/jp-simard-realm-core-database-engine/.

Realm 的缺点

在选择 Realm 之前,应该考虑其一些瓶颈。不过,对于高扩展性的 Android 应用程序,这些瓶颈是可以接受的:

  • 我们无法将 Realmdb 导入到其他应用程序中

  • 我们不能跨线程访问对象

  • 不支持 ID 的自动递增

  • 迁移 Realmdb 是一项痛苦的工作

  • 缺乏复合主键

  • 仍在积极开发中

Wear-note-1 项目中,将 string.xml 中的名称更改为 Wear-note-2 后,我们需要在 gradle-project 模块中添加适当的 Realm 依赖。在撰写本书时,最新的 Realm 版本是 3.0.0,因此我们将详细讨论依赖项和代码。

重新构建代码和依赖关系

Realm 现在有了一个新机制。它不仅仅是一个 Gradle 依赖项,还是一个 Gradle 插件。我们将学习如何在项目中添加和使用 Realm。Gradle 插件依赖项应该添加到项目范围,即项目级别的 Gradle 依赖项。添加以下类路径,并通过互联网使其与项目同步:

classpath "io.realm:realm-gradle-plugin:3.0.0"

将此依赖项添加到如下截图所示位置的 Gradle 项目级别文件中。在依赖项部分添加 classpath

添加类路径依赖后,完整的 Gradle 文件将如下所示:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.3'

        //Realm plugin
        classpath "io.realm:realm-gradle-plugin:3.0.0"
        // NOTE: Do not place your application dependencies here; they 
        belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

接下来,我们将把 Realm 插件应用到项目中,以使用如以下截图所示的所有 Realm 功能:

apply plugin: 'realm-android'

Realm 社区已经开源了一些修改过的 Gradle 文件样本:

[`github.com/realm/realm-java/blob/master/examples/build.gradle

https://github.com/realm/realm-java/blob/master/examples/introExample/build.gradle`](https://github.com/realm/realm-java/blob/master/examples/build.gradle)

现在,我们已经准备好将 SharedPreferences 替换为 Realm。让我们开始吧。

打开 Android Studio,在 model 包中,我们定义了 Note.java POJO 类,其中包含原始数据字符串 notes 和字符串 ID。这些变量有它们的 getter 和 setter。额外的更改如下:

将 POJO 模型扩展到 RealmObject 并创建一个空构造函数:

public class Note extends RealmObject {

    private String notes = "";
    private String id = "";

   //Empty constructor
    public Note() {

    }

    public Note(String id, String notes) {
        this.id = id;
        this.notes = notes;
    }

    public String getNotes() {
        return notes;
    }

    public void setNotes(String notes) {
        this.notes = notes;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

}

MainActivity 中,我们将全局实例化 Realm 类,并在 onCreate 方法中如下初始化:

//MainActivity scope
//Realm Upgrade
private Realm realm;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ....
    //Realm init
    Realm.init(this);
    realm = Realm.getDefaultInstance();
}

updateAdapter()方法中,我们将必须添加从 Realm 读取的查询,如下所示:

 RealmResults<Note> results = realm.where(Note.class).findAll();

完整的方法将如下所示:

private void updateAdapter() {
    RealmResults<Note> results = realm.where(Note.class).findAll();
    myDataSet.clear();
    myDataSet.addAll(SharedPreferencesUtils.getAllNotes(this));
    mAdapter.setListNote(myDataSet);
    mAdapter.notifyDataSetChanged();
}

Realm 数据库提供了许多查询,这些查询为存储的数据建立了一对一和一对多的关系,对于这个项目,前面的查询完成了所需的所有神奇工作。

createNote()方法中,我们将更改代码以使用 Realm 而不是SharedPreference,如下所示:

private Note createNote(String id, String note) {
    if (id == null) {
        id = String.valueOf(System.currentTimeMillis());
        realm.beginTransaction();
        Note notes = realm.createObject(Note.class);
        notes.setId(id);
        notes.setNotes(note);
        realm.commitTransaction();
    }
    return new Note(id, note);
}

为了删除记录,让我们创建一个新方法,并将其命名为deleteRecord()。在这个方法中,我们将传递笔记的 ID,并从 Realm 中删除该笔记:

public void deleteRecord(String id){
    RealmResults<Note> results = realm.where(Note.class).equalTo("id", 
    id).findAll();

    realm.beginTransaction();

    results.deleteAllFromRealm();

    realm.commitTransaction();
}

现在,让我们在updateData()方法中调用删除记录的方法,如下所示:

private void updateData(Note note, int action) {
    if (action == Constants.ACTION_ADD) {
        ConfirmationUtils.showMessage(getString(R.string.note_saved), 
        this);
    } else if (action == Constants.ACTION_DELETE) {
        deleteRecord(note.getId());
        ConfirmationUtils.showMessage(getString(R.string.note_removed), 
        this);
    }
    updateAdapter();
}

完整的MainActivity代码如下所示:

public class MainActivity extends WearableActivity implements RecyclerViewAdapter.ItemSelectedListener {

    private static final String TAG = "MainActivity";
    private static final int REQUEST_CODE = 1001;
    private RecyclerViewAdapter mAdapter;
    private List<Note> myDataSet = new ArrayList<>();

    //Realm Upgrade
    private Realm realm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        configLayoutComponents();

        Realm.init(this);
        realm = Realm.getDefaultInstance();

    }

    private void configLayoutComponents() {
        WearableRecyclerView recyclerView = (WearableRecyclerView) 
        findViewById(R.id.wearable_recycler_view);
        recyclerView.setHasFixedSize(true);
        LinearLayoutManager mLayoutManager = new 
        LinearLayoutManager(this);
        recyclerView.setLayoutManager(mLayoutManager);

        mAdapter = new RecyclerViewAdapter();
        mAdapter.setListNote(myDataSet);
        mAdapter.setListener(this);
        recyclerView.setAdapter(mAdapter);

        EditText editText = (EditText) findViewById(R.id.edit_text);

        editText.setOnEditorActionListener(new 
        TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int 
            action, KeyEvent keyEvent) {
                if (action == EditorInfo.IME_ACTION_SEND) {
                    String text = textView.getText().toString();
                    if (!TextUtils.isEmpty(text)) {
                        Note note = createNote(null, text);
                        SharedPreferencesUtils.saveNote(note, 
                        textView.getContext());
                        updateData(note, Constants.ACTION_ADD);
                        textView.setText("");
                        return true;
                    }
                }
                return false;
            }
        });
    }

    private void updateAdapter() {
        RealmResults<Note> results = realm.where(Note.class).findAll();
        myDataSet.clear();
        myDataSet.addAll(results);
        mAdapter.setListNote(myDataSet);
        mAdapter.notifyDataSetChanged();
    }

    @Override
    protected void onResume() {
        super.onResume();
        updateAdapter();
    }

    @Override
    public void onItemSelected(int position) {
        Intent intent = new Intent(getApplicationContext(), 
        DeleteActivity.class);
        intent.putExtra(Constants.ITEM_POSITION, position);
        startActivityForResult(intent, REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, 
    Intent data) {
        if (data != null && requestCode == REQUEST_CODE && resultCode 
        == RESULT_OK) {
            if (data.hasExtra(Constants.ITEM_POSITION)) {
                int position = 
                data.getIntExtra(Constants.ITEM_POSITION, -1);
                if (position > -1) {
                    Note note = myDataSet.get(position);
                    updateData(note, Constants.ACTION_DELETE);
                }
            }
        }
    }

    private void prepareUpdate(String id, String title, int action) {
        if (!(TextUtils.isEmpty(id) && TextUtils.isEmpty(title))) {
            Note note = createNote(id, title);
            updateData(note, action);
        }
    }

    private void updateData(Note note, int action) {
        if (action == Constants.ACTION_ADD) {
            ConfirmationUtils.showMessage
            (getString(R.string.note_saved), this);

        } else if (action == Constants.ACTION_DELETE) {
            deleteRecord(note.getId());
            ConfirmationUtils.showMessage(getString
            (R.string.note_removed), this);
        }
        updateAdapter();
    }

    /**
     * Notifica a Data Layer API que os dados foram modificados.
     */

    private Note createNote(String id, String note) {
        if (id == null) {
            id = String.valueOf(System.currentTimeMillis());
            realm.beginTransaction();
            Note notes = realm.createObject(Note.class);
            notes.setId(id);
            notes.setNotes(note);
            realm.commitTransaction();
        }
        return new Note(id, note);
    }

    public void deleteRecord(String id){
        RealmResults<Note> results = realm.where(Note.class)
        .equalTo("id", id).findAll();

        realm.beginTransaction();

        results.deleteAllFromRealm();

        realm.commitTransaction();
    }

    @Override
    protected void onDestroy() {
        realm.close();
        super.onDestroy();
    }
}

现在,从功能上讲,我们已经将 Realmdb 集成到了 Wear-note-2 应用中。让我们编译一下看看结果。

Wear 记事本应用的主屏幕如下所示:

Wear 操作系统处理的 IMF 屏幕,用于获取用户的输入:

通过 Wear 支持库中的默认confirmationActivity实现的‘保存动画’:

保存了笔记的主屏幕:

现在,我们已经将Sharedpreference替换成了我们这个时代 Android 的最佳数据库。

在 Wear 用户界面上的工作

在 Wear-note 应用中,我们使用白色背景和黑色文字,并使用默认的 Roboto 字体。在准备一个好的 Wear 应用设计时,谷歌建议使用深色来提高电池效率。典型材料设计中使用的浅色方案在 Wear 设备上并不节能。浅色在 OLED 显示屏上耗能更低。

浅色需要以更亮的强度点亮像素。白色需要将像素中的 RGB 二极管点亮到 100%;应用中白色和浅色越多,应用的电池效率就越低。

浅色在暗光下或夜间使用 Wear 设备时会产生干扰。这种情况在使用深色时可能不会发生。与浅色不同,深色在激活时使屏幕亮度降低,从而在 OLED 显示屏上节省电池。

让我们开始着手 Wear-note-2 用户界面工作

让我们更改应用主题以适应标准设计指南。

activity_main.xml文件中,我们将编辑父容器的背景,即BoxInsetLayout的背景android:background="#01579B"改为钴蓝色。

values目录下添加一个新的color.xml文件,并加入以下代码:

//Add this color value in the color.xml in res directory 
<color name="cobalt_blue">#01579B</color>

添加颜色后,我们可以在生产应用中使用该颜色,如下所示:

<android.support.wearable.view.BoxInsetLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent" 
    android:layout_height="match_parent"
    android:id="@+id/container"
    tools:context="com.ashok.packt.wear_note_1.activity.MainActivity"
    tools:deviceIds="wear"
    android:background="@color/cobalt_blue"
    app:layout_box="all"
    android:padding="5dp">

进一步,让我们更改EditText提示文字的颜色,如下所示:

<EditText
    android:id="@+id/edit_text"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:layout_gravity="center"
    android:gravity="center"
    android:hint="@string/add_a_note"
    android:imeOptions="actionSend"
    android:inputType="textCapSentences|textAutoCorrect"
 android:textColor="@color/white"
    android:textColorHint="@color/white"
    android:layout_alignParentTop="true"
    android:layout_alignParentStart="true" />

each_item.xml布局中,按照以下方式修改 XML 代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:layout_gravity="center"
    android:clickable="true"
    android:background="?android:attr/selectableItemBackground"
    android:orientation="vertical">

    <TextView
        android:id="@+id/note"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_gravity="center"
        android:textColor="@color/white"
        tools:text="note"/>

</LinearLayout>

现在,在activity_delete.xml布局容器的相同位置进行更改,改变其背景颜色和TextView的颜色。使用xmlns属性可以更改DelayedConfirmationView的颜色,如下代码所示:

<android.support.wearable.view.DelayedConfirmationView
    android:id="@+id/delayed_confirmation"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="img/ic_delete"
    app:circle_border_color="@color/white"
    app:circle_color="@color/white"
    app:circle_border_width="8dp"
    app:circle_radius="30dp"/>

开发人员无需担心更改DelayedConfirmationView中的动画颜色,Wear 2.0 会自动适应DelayedConfirmationView的主题,并改变主色方案,以创建统一的体验。

所有这些更改将如下反映在应用程序中:

IMF 屏幕用于从用户获取输入:

使用 Realm 保存数据后:

从数据库中删除便签:

更好的字体,更好的阅读体验

在数字设计的世界中,让你的应用程序视觉效果对用户的眼睛友好是很重要的。来自谷歌收藏的 Lora 字体拥有平衡的现代衬线,其根源在于书法。这是一种中等对比度的文本字体,非常适合正文字体。设置为 Lora 的段落将因其刷过的曲线与有力的衬线形成对比而令人难忘。Lora 的整体排版风格完美传达了现代故事或艺术论文的情绪。从技术上讲,Lora 针对屏幕显示进行了优化。

要将自定义字体添加到 Android 项目中,我们需要在根目录中创建assets文件夹。查看以下截图:

我们可以直接将.ttf文件添加到资产目录中,或者我们可以创建另一个目录和字体。你可以通过这个 URL 下载 Lora 字体:fonts.google.com/download?family=Lora

将字体文件添加到资产文件夹后,我们需要创建自定义的TextviewEditText

utils包中,让我们创建一个名为LoraWearTextViewLoraWearEditTextView的类,如下所示:

现在,将LoraWearTextView扩展到TextView类,LoraWearEditTextView扩展到EditText类。在两个类中实现构造方法。创建一个名为init()的新方法。在init方法内部,实例化Typeface类,并使用createFromAsset方法,我们可以设置我们的自定义字体:

public void init() {
    Typeface tf = Typeface.createFromAsset(getContext().getAssets(), 
    "fonts/Lora.ttf");
    setTypeface(tf ,1);

}

前面的init方法在这两个类中是相同的。在两个新自定义类的所有不同参数化构造函数中调用init方法。

完整的类如下所示:LoraWearTextView.java

public class LoraWearTextView extends TextView {
    public LoraWearTextView(Context context) {
        super(context);
        init();
    }

    public LoraWearTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LoraWearTextView(Context context, AttributeSet attrs, int 
    defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public LoraWearTextView(Context context, AttributeSet attrs, int 
    defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    public void init() {
        Typeface tf = Typeface.createFromAsset(getContext()
        .getAssets(), "fonts/Lora.ttf");
        setTypeface(tf ,1);

    }
}

完整的类如下所示:LoraWearEditTextView.java

public class LoraWearEditTextView extends EditText {

public LoraWearEditTextView(Context context) {
 super(context);
 init();
}

public LoraWearEditTextView(Context context, AttributeSet attrs) {
 super(context, attrs);
 init();
}

public LoraWearEditTextView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 init();
}

public LoraWearEditTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
 super(context, attrs, defStyleAttr, defStyleRes);
 init();
}

public void init() {
 Typeface tf = Typeface.createFromAsset(getContext().getAssets(), 
 "fonts/Lora.ttf");
 setTypeface(tf ,1);
}

在我们的 UI 布局中添加这两个类之后,我们可以替换实际的textviewedittext

<com.ashok.packt.wear_note_1.utils.LoraWearEditTextView
    android:id="@+id/edit_text"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:layout_gravity="center"
    android:gravity="center"
    android:hint="@string/add_a_note"
    android:imeOptions="actionSend"
    android:inputType="textCapSentences|textAutoCorrect"
    android:textColor="@color/white"
    android:textColorHint="@color/white"
    android:layout_alignParentTop="true"
    android:layout_alignParentStart="true" />

<com.ashok.packt.wear_note_1.utils.LoraWearTextView
    android:id="@+id/note"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:layout_gravity="center"
    android:textColor="@color/white"
    tools:text="note"/>

替换textviewedittext之后,编译程序,让我们看看结果:

通过confirmationViewActivity删除便签动画:

在本章中,我们探讨了如何集成 Realm 流行数据库,理解了可穿戴设备的设计理念,并创建了自定义视图组件以获得更好的应用程序用户体验。

编写自定义布局以获得更好的用户体验

Android 提供了很好的方法来定制我们用作容器的布局。我们可以有复合视图并自定义布局以实现我们自己的目的。可穿戴设备使用与手持 Android 设备相同的布局技术,但需要针对特定的约束和配置进行设计。将手持 Android 设备组件的功能移植到可穿戴应用设计并不是一个好主意。要设计一个优秀的可穿戴应用,请遵循 Google 的设计指南,访问developer.android.com/design/wear/index.html。让我们创建自定义视图并在可穿戴笔记应用程序中使用它们。

让我们按照加载项目的方式来为我们的布局实现动画。我们将有一个简单的滑动动画,我们将用我们编写的自定义布局来实现这一点。

让我们创建一个名为AnimatedLinearLayout的类文件,并按照以下方式扩展它到LinearLayout

复合视图是将两个或多个视图捆绑为一个组件,例如,可勾选的相对布局。当用户点击布局时,它会像复选框一样突出显示布局。

public class AnimatedLinearLayout extends LinearLayout {

...

}

现在,我们需要从View获取Animation类。除了Animation类,还要为currentChild视图声明View实例。由于我们正在编写布局,它可以保存子层次结构,因此我们需要一个视图实例作为引用:

Animation animation;
View currentChild;

当我们扩展LinearLayout类时,我们会得到一些构造函数回调,我们必须实现这些回调:

public AnimatedLinearLayout(Context context) {
    super(context);
}

public AnimatedLinearLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
    super.onWindowFocusChanged(hasWindowFocus);
    ...
}

onWindowFocusChanged()方法中,我们可以编写自定义动画的逻辑。在这里,本书介绍了SlideDownSlideDownMoreRotateClockWiseRotateAntiClockWiseZoomInAndRotateClockWise。现在,为了实现这一点,我们需要检查视图是否已膨胀并且有窗口显示,以及布局有多少个childviews

if (hasWindowFocus) {
    for (int index = 0; index < getChildCount(); index++) {
        View child = getChildAt(index);
        currentChild=child;
}

检查子元素是否是Viewgroup的实例;如果视图是完全以其他隔离方式开发的,那么这个自定义布局将无法帮助该视图进行动画处理。使用子视图的标签属性,我们可以为动画分配一个字符串关联,如下所示:

if(!(child instanceof ViewGroup)) {
   switch (child.getTag().toString()) {
   case SLIDE_DOWN:
     // write logic to slide down animation

对于向下滑动动画,使用我们全局创建的动画实例,我们必须设置插值器并通过子视图的高度和数值 2 来减速插值。查看以下代码:

case SLIDE_DOWN:
    animation = new TranslateAnimation(0, 0, -
    ((child.getMeasuredHeight()/2) * (index + 1)), 0);
    animation.setInterpolator(new DecelerateInterpolator());
    animation.setFillAfter(true);
    animation.setDuration(1000);
    child.post(new AnimationRunnable(animation,child));
    //child.startAnimation(animation);
    break; 

同样,我们将完成其他情况。让我们开始完成onWindowFocusChanged()方法:

@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
    super.onWindowFocusChanged(hasWindowFocus);
    final String SLIDE_DOWN = "SlideDown";
    final String SLIDE_DOWN_MORE = "SlideDownMore";
    final String ROTATE_CLOCKWISE = "RotateClockWise";
    final String ROTATE_ANTI_CLOCKWISE = "RotateAntiClockWise";
    final String ZOOMIN_AND_ROTATE_CLOCKWISE = 
    "ZoomInAndRotateClockWise";
    if (hasWindowFocus) {
        for (int index = 0; index < getChildCount(); index++) {
            View child = getChildAt(index);
            currentChild=child;
            if(!(child instanceof ViewGroup)) {
                switch (child.getTag().toString()) {
                    case SLIDE_DOWN:
                        animation = new TranslateAnimation(0, 0, -
                        ((child.getMeasuredHeight()/2) * 
                        (index + 1)), 0);
                        animation.setInterpolator(new 
                        DecelerateInterpolator());
                        animation.setFillAfter(true);
                        animation.setDuration(1000);
                        child.post(new 
                        AnimationRunnable(animation,child));
                        //child.startAnimation(animation);
                        break;
                    case SLIDE_DOWN_MORE:
                        animation = new TranslateAnimation(0, 0, -
                        (child.getMeasuredHeight() * (index + 25)), 0);
                        animation.setInterpolator(new 
                        DecelerateInterpolator());
                        animation.setFillAfter(true);
                        animation.setDuration(1000);
                        child.post(new 
                        AnimationRunnable(animation,child));
                        //child.startAnimation(animation);
                        break;
                    case ROTATE_CLOCKWISE:
                        animation = new RotateAnimation(0, 360,            
                        child.getMeasuredWidth() / 2, 
                        child.getMeasuredHeight() / 2);
                        animation.setInterpolator(new 
                        DecelerateInterpolator());
                        animation.setFillAfter(true);
                        animation.setDuration(1000);
                        child.post(new 
                        AnimationRunnable(animation,child));
                        //child.startAnimation(animation);
                        break;
                    case ROTATE_ANTI_CLOCKWISE:
                        animation = new RotateAnimation(0, -360,                 
                        child.getMeasuredWidth() / 2, 
                        child.getMeasuredHeight() / 2);
                        animation.setInterpolator(new 
                        DecelerateInterpolator());
                        animation.setFillAfter(true);
                        animation.setDuration(1000);
                        child.post(new 
                        AnimationRunnable(animation,child));
                        //child.startAnimation(animation);
                        break;
                    case ZOOMIN_AND_ROTATE_CLOCKWISE:
                        AnimationSet animationSet = new 
                        AnimationSet(true);
                        animationSet.setInterpolator(new 
                        DecelerateInterpolator());
                        animation = new ScaleAnimation(0, 1, 0, 1, 
                        child.getMeasuredWidth() / 2, 
                        child.getMeasuredHeight() / 2);
                        animation.setStartOffset(0);
                        animation.setFillAfter(true);
                        animation.setDuration(1000);
                        animationSet.addAnimation(animation);
                        animation = new RotateAnimation(0, 360, 
                        child.getMeasuredWidth() / 2, 
                        child.getMeasuredHeight() / 2);
                        animation.setStartOffset(0);
                        animation.setFillAfter(true);
                        animation.setDuration(1000);
                        animationSet.addAnimation(animation);
                        child.post(new 
                        AnimationSetRunnable(animationSet,child));
                        //child.startAnimation(animationSet);
                        break;
                }
            }
        }
    }
}

现在,我们需要创建一个AnimationRunnable类,它实现了Runnable接口以启动动画:

private class AnimationRunnable implements Runnable{
    private Animation animation;
    private View child;
    AnimationRunnable(Animation animation, View child) {
        this.animation=animation;
        this.child=child;
    }

    @Override
    public void run() {
        child.startAnimation(animation);
    }
}

我们将实现另一个AnimationSetRunnable类到可运行接口以设置动画:

private class AnimationSetRunnable implements Runnable{
    private AnimationSet animation;
    private View child;
    AnimationSetRunnable(AnimationSet animation, View child) {
        this.animation=animation;
        this.child=child;
    }

    @Override
    public void run() {
        child.startAnimation(animation);
    }
}

现在,我们已经完成了自己的自定义布局,该布局具有几种动画方法,可以应用于布局中的所有视图子项。这个自定义布局的完整类代码如下所示:

public class AnimatedLinearLayout extends LinearLayout {
    Animation animation;
    View currentChild;

    public AnimatedLinearLayout(Context context) {
        super(context);
    }

    public AnimatedLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        final String SLIDE_DOWN = "SlideDown";
        final String SLIDE_DOWN_MORE = "SlideDownMore";
        final String ROTATE_CLOCKWISE = "RotateClockWise";
        final String ROTATE_ANTI_CLOCKWISE = "RotateAntiClockWise";
        final String ZOOMIN_AND_ROTATE_CLOCKWISE = 
        "ZoomInAndRotateClockWise";
        if (hasWindowFocus) {
            for (int index = 0; index < getChildCount(); index++) {
                View child = getChildAt(index);
                currentChild=child;
                if(!(child instanceof ViewGroup)) {
                    switch (child.getTag().toString()) {
                        case SLIDE_DOWN:
                            animation = new TranslateAnimation(0, 0, -
                            ((child.getMeasuredHeight()/2) * 
                             (index + 1)), 0);
                            animation.setInterpolator(new 
                            DecelerateInterpolator());
                            animation.setFillAfter(true);
                            animation.setDuration(1000);
                            child.post(new 
                            AnimationRunnable(animation,child));
                            //child.startAnimation(animation);
                            break;
                        case SLIDE_DOWN_MORE:
                            animation = new TranslateAnimation(0, 0, -
                            (child.getMeasuredHeight() * 
                            (index + 25)), 0);
                            animation.setInterpolator(new 
                            DecelerateInterpolator());
                            animation.setFillAfter(true);
                            animation.setDuration(1000);
                            child.post(new 
                            AnimationRunnable(animation,child));
                            //child.startAnimation(animation);
                            break;
                        case ROTATE_CLOCKWISE:
                            animation = new RotateAnimation(0, 360, 
                            child.getMeasuredWidth() / 2, 
                            child.getMeasuredHeight() / 2);
                            animation.setInterpolator(new 
                            DecelerateInterpolator());
                            animation.setFillAfter(true);
                            animation.setDuration(1000);
                            child.post(new 
                            AnimationRunnable(animation,child));
                            //child.startAnimation(animation);
                            break;
                        case ROTATE_ANTI_CLOCKWISE:
                            animation = new RotateAnimation(0, -360, 
                            child.getMeasuredWidth() / 2, 
                            child.getMeasuredHeight() / 2);
                            animation.setInterpolator(new 
                            DecelerateInterpolator());
                            animation.setFillAfter(true);
                            animation.setDuration(1000);
                            child.post(new 
                            AnimationRunnable(animation,child));
                            //child.startAnimation(animation);
                            break;
                        case ZOOMIN_AND_ROTATE_CLOCKWISE:
                            AnimationSet animationSet = new 
                            AnimationSet(true);
                            animationSet.setInterpolator(new 
                            DecelerateInterpolator());
                            animation = new ScaleAnimation(0, 1, 0, 1, 
                            child.getMeasuredWidth() / 2, 
                            child.getMeasuredHeight() / 2);
                            animation.setStartOffset(0);
                            animation.setFillAfter(true);
                            animation.setDuration(1000);
                            animationSet.addAnimation(animation);
                            animation = new RotateAnimation(0, 360, 
                            child.getMeasuredWidth() / 2, 
                            child.getMeasuredHeight() / 2);
                            animation.setStartOffset(0);
                            animation.setFillAfter(true);
                            animation.setDuration(1000);
                            animationSet.addAnimation(animation);
                            child.post(new 
                            AnimationSetRunnable(animationSet,child));
                            //child.startAnimation(animationSet);
                            break;
                    }
                }
            }
        }
    }

    private class AnimationRunnable implements Runnable{
        private Animation animation;
        private View child;
        AnimationRunnable(Animation animation, View child) {
            this.animation=animation;
            this.child=child;
        }

        @Override
        public void run() {
            child.startAnimation(animation);
        }
    }
    private class AnimationSetRunnable implements Runnable{
        private AnimationSet animation;
        private View child;
        AnimationSetRunnable(AnimationSet animation, View child) {
            this.animation=animation;
            this.child=child;
        }

        @Override
        public void run() {
            child.startAnimation(animation);
        }
    }
}

现在,在完全编写完这个类之后,是时候在我们的项目中使用它了。我们可以将这个类名作为一个 XML 标签,并在recyclerview的行项布局each_item.xml中使用它:

<com.ashok.packt.wear_note_1.utils.AnimatedLinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical">

</com.ashok.packt.wear_note_1.utils.AnimatedLinearLayout>

使用新的AnimatedLinearLayout替换布局代码。我们需要在childviews中传递标签以实现动画效果。以下代码详细解释了这一点:

<?xml version="1.0" encoding="utf-8"?>
<com.ashok.packt.wear_note_1.utils.AnimatedLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:background="?android:attr/selectableItemBackground"
    android:clickable="true"
    android:gravity="center"
    android:orientation="vertical">

    <com.ashok.packt.wear_note_1.utils.AnimatedLinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:orientation="horizontal">

        <com.ashok.packt.wear_note_1.utils.LoraWearTextView
            android:id="@+id/note"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center"
 android:tag="ZoomInAndRotateClockWise"            android:textColor="@color/white"
            tools:text="note" />
    </com.ashok.packt.wear_note_1.utils.AnimatedLinearLayout>

</com.ashok.packt.wear_note_1.utils.AnimatedLinearLayout>

这种布局将通过小型动画绘制所有视图并显示列表项。

ZoomInAndRotateClockWise动画,尝试将字符串更改为Custom类中给出的完全相同的字符串:

概述

在本章中,我们了解了暗色主题在穿戴设备上的重要性。我们使用字体资源更改了自定义的TextView。我们已经了解了如何编写自定义布局并在其中定义了一些动画。在后续的项目中,我们将进一步探索 Wear 2.0 独家引入的设计指南和组件,如Wearable action drawerwearable navigation drawer

第四章:测量你的健康 - 传感器

我们生活在科技领域!这绝对不是这里的重点。我们也生活在复杂生活方式的领域,这些生活方式正在将每个人的健康推向某种疾病。我们的存在让我们回到海洋的根源;我们都知道,我们是从水中进化而来的生物。如果我们追溯,我们会清楚地了解我们身体组成 60%是水,其余的是肌肉水分。当我们谈论照顾我们的健康时,我们会忽略简单的事情,比如喝足够的水。适量、定期饮水将确保新陈代谢良好和健康、功能正常的器官。然后,新千年的技术进步表达了如何利用技术来做正确的事情。

Android Wear 集成了众多传感器,可用于帮助 Android Wear 用户测量他们的心率、步数等。说到这里,编写一个应用程序如何?该程序可以每 30 分钟提醒我们喝水,测量我们的心率和步数,并提供一些健康小贴士。

本章将帮助你了解如何列出 Wear 设备中所有可用的传感器,并使用它们来测量步数、心率等。

在本章中,我们将探讨以下内容:

  • 列出 Wear 中的可用传感器

  • 传感器的准确性和电池消耗

  • Wear 2.0 省电模式

  • 编写具有初始逻辑的应用程序

  • 开始为 Wear 设计材料设计

  • 为应用程序创建用户界面

概念化应用程序

我们将要构建的 Wear 应用程序将有三个主要界面,用于启动饮水提醒、心率分析和步数计数器。

以下是启动提醒服务的饮水提醒屏幕。使用导航抽屉中的导航功能,我们可以切换到其他屏幕:

列出 Wear 中的可用传感器

了解您正在工作的 Wear 设备中可用的传感器列表实际上是有好处的,可以确保您不会得到错误的结果和不必要的等待。一些 Android Wear 设备没有心率传感器。在这种情况下,编写针对心率传感器的应用程序是没有意义的。以下是我们获取可用传感器列表的方法:

public class SensorActivity extends Activity implements SensorEventListener {
  private SensorManager mSensorManager;
  private Sensor mLight;

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

    mSensorManager = (SensorManager)  
    getSystemService(Context.SENSOR_SERVICE);
    mLight = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);

    mSensorManager = (SensorManager)     
    getSystemService(Context.SENSOR_SERVICE); 
    final List<Sensor> deviceSensors = 
    mSensorManager.getSensorList(Sensor.TYPE_ALL); 

    for(Sensor type : deviceSensors){      
        Log.e("sensors",type.getStringType()); 
    }
}

  @Override
  public final void onAccuracyChanged(Sensor sensor, int accuracy){
    // Do something here if sensor accuracy changes.
  }

  @Override
  public final void onSensorChanged(SensorEvent event) {

  }

  @Override
  protected void onResume() {
    super.onResume();

  }

  @Override
  protected void onPause() {
    super.onPause();
    mSensorManager.unregisterListener(this);
  }
}

我们将在控制台中看到 Wear 设备中所有可用的传感器,如下所示:

E/sensors: android.sensor.accelerometer
E/sensors: android.sensor.magnetic_field
E/sensors: android.sensor.orientation
E/sensors: android.sensor.temperature
E/sensors: android.sensor.proximity
E/sensors: android.sensor.light
E/sensors: android.sensor.pressure
E/sensors: android.sensor.relative_humidity
E/sensors: android.sensor.geomagnetic_rotation_vector

传感器的准确性

所有集成在可穿戴设备中的传感器都展现出尽可能高的精确度和准确度。在为健康领域编写应用程序时,考虑传感器的准确性非常重要。由于 Android Wear 根据 Google IO 2017 的数据已经生产了近 40 个及以上的不同品牌,所以可穿戴设备中使用的传感器也取决于制造商。如果我们从零开始编写应用程序,却不知道可穿戴设备能提供什么功能,我们肯定会遇到很多挑战。大多数可穿戴设备都配备了步数检测器和步数计数传感器。或许,我们不必担心编写一个能预测设备三维动作并据此预测步数的加速度计程序,这种做法通常不准确。线性加速度预测的步数大多数时候会导致错误的结果。为了在某些使用场景中节省电池并做出有意义的决策,传感器发挥着至关重要的作用。运动传感器通过在手腕未被查看时开启环境模式来帮助节省可穿戴设备的电池。光线传感器允许设备根据外部光线影响增加或减少屏幕亮度。心率传感器在可穿戴设备中很常见。本质上,心率传感器是一种光学传感器。它不会像心电图(EKG)那样准确,但当你休息时使用它们,这些传感器的准确度会接近。使用相同的传感器,可以预测睡眠监测和活动跟踪,比如卡路里消耗。这些传感器正在变得更好,未来将极为准确,但为了硬件质量,可穿戴设备中配备的硬件问题将通过软件修复和软件级别的控制来解决。

常见的可穿戴集成传感器如下:

  • 加速度计

  • 磁力计

  • 环境传感器

  • GPS

  • 心率传感器

  • 氧饱和度传感器

未来,可穿戴设备将配备更多更精确的传感器,并通过这些传感器瞄准健康领域。

电池消耗

电池至关重要。我们编写的程序应当旨在节省电池。Wear 1.x 的设计指南和标准没有采用材质设计,这对电池消耗造成了巨大影响。在 Wear 2.0 引入材质设计后,深色主题能节省更多电池变得显而易见。Android Wear 团队一直在努力改善电池续航。谷歌推出了一系列最佳实践来提升 Wear 设备的电池寿命。Wear 设备中的显示屏幕消耗大量电池。不同类型的显示屏在开启模式、常显模式和交互模式下消耗的电量各不相同。交互模式消耗的电量最大,因此我们需要关注何时编写交互模式。当然,对于大多数用例来说,电池消耗是必要的。当我们深入查看某个特定用例时,会发现有许多地方可以避免 Wear 电池消耗最大电量。我们应该始终在生产应用中仔细检查,确保释放所有会消耗电池的传感器和其他硬件。Android 提供了WAKE_LOCK机制,使开发者能够识别哪个应用正在使用哪些硬件,并在应用进入后台时帮助释放它们。编写服务对于运行长时间进程是必要的,但如果我们利用后台服务来获取硬件,它们将始终消耗电池,在这种情况下编写终止服务非常重要。

Android Studio 提供了一个名为 Battery Historian developer.android.com/studio/profile/battery-historian.html的开源工具,用于分析应用的电池统计信息。Battery Historian 将电池数据转换为 HTML 可视化表示,可以在浏览器中查看。在尝试节省电池之前,我们需要知道哪个进程从电池中消耗了多少电流。之后,使用 Battery Historian 的数据,我们将确定哪个进程消耗了更多数据,并能够进行电池优化工作。

休眠模式

对于开发者来说,电池优化始终是一项挑战。谷歌启动了许多内部项目,例如 Project Volta,它坚持每秒 60 帧的动画速率。制造商开始流行发布配备最高电池容量的手机和平板电脑。这里的挑战在于引入了太多的新硬件组件,设备的使用时间仅延长了几个小时。谷歌没有简单地增大电池容量,而是从 API 级别 23 开始优化电池消耗过程,以适应应用适应休眠模式后的额外几小时使用时间。

当设备长时间未使用或设备长时间处于空闲状态时,设备将进入休眠模式。当系统尝试访问网络和 CPU 密集型服务时,休眠模式会阻止这种情况发生。休眠模式在周期时间后自动退出,访问网络,并同步所有任务和闹钟。以下限制适用于应用在休眠模式时:

  • 网络操作被终止

  • 系统忽略唤醒锁

  • 标准的闹钟管理器需要等待退出休眠模式

  • 要在休眠模式下触发闹钟,应使用 setAndAllowWhileIdle()

  • 设备将不会执行 Wi-Fi 扫描

  • 同步适配器受到限制

  • 任务调度器受到限制

根据应用提供的能力,调整休眠模式可能会对它们产生不同的影响。几乎所有的应用在休眠模式下都能正常工作。在某些用例中,我们必须修改网络请求任务、闹钟和同步,并且应用应该在退出休眠模式时有效地管理所有活动。考虑到在 Android 5.1(API 级别 22)或更低版本中,当系统处于休眠模式时,闹钟不会触发,休眠模式可能会影响 AlarmManager 提醒的活动和定时器管理。为了帮助安排闹钟,Android 6.0(API 级别 23)引入了两个新的 AlarmManager 方法:setAndAllowWhileIdle()setExactAndAllowWhileIdle()。使用这些方法,我们可以在休眠模式下触发闹钟。

创建一个项目

现在,让我们启动 Android Studio 并构建一个帮助我们完成所有这些操作的 Wear 应用。

  1. 创建一个名为 Upbeat 的 Android 项目。由于这是一个与健康管理相关的应用,这个名字非常合适:

图片

  1. 在“目标 Android 设备”屏幕中选择 Phone 和 Wear 模块:

图片

  1. 现在,为手机模板选择 Empty Activity,为 Wear 选择 Always On Wear Activity。在项目成功创建后,进入 Wear 模块并添加配色方案文件,开始按照材料设计标准推动开发。

  2. res/values 目录中创建一个 colors.xml 文件,并添加以下颜色代码:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#607d8b</color>
    <color name="colorPrimaryDark">#34515e</color>
    <color name="colorAccent">#FFF</color>
</resources>

为了使项目代码的可读性更好,让我们创建名为 fragmentsservicesutils 的包。在这三个包中,我们将编写所有的工作代码。

  1. 在这个项目中,我们将使用 WearableDrawerLayoutWearableNavigationDrawer 在片段之间导航。让我们在 wear 模块中设置 MainActivity 以使用 WearableNavigationDrawer。在 activity_main.xml 文件中,我们需要将根元素更改为 WearableDrawerLayout,如下所示:
<android.support.wearable.view.drawer.WearableDrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context="com.packt.upbeat.MainActivity"
    tools:deviceIds="wear">

    .
    .
    .

</android.support.wearable.view.drawer.WearableDrawerLayout>

  1. 在支持库中的 nestedScrollview 内,我们将添加 framelayout 作为要附加和分离的片段的容器。之后,我们可以附加 WearableNavigationDrawer 元素的另一个子元素。如果我们希望 Wear 应用拥有一个动作菜单,我们确实需要,那么我们应该添加另一个名为 WearableActionDrawer 的元素。在 WearableDrawerLayout 范围内添加以下代码:
<android.support.wearable.view.drawer.WearableDrawerLayout
 ...

<android.support.v4.widget.NestedScrollView
 android:id="@+id/content"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:fillViewport="true">

 <FrameLayout
 android:id="@+id/content_frame"
 android:layout_width="match_parent"
 android:layout_height="match_parent" />

</android.support.v4.widget.NestedScrollView>

<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/colorPrimaryDark" />

<android.support.wearable.view.drawer.WearableActionDrawer
 android:id="@+id/bottom_action_drawer"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:background="@color/colorPrimaryDark"
 app:action_menu="@menu/drawer_menu" />

</android.support.wearable.view.drawer.WearableDrawerLayout>

添加一个抽屉菜单

我们将立即添加 drawer_menu 菜单 XML 资源。所有视觉元素都已为 MainActivity 做好准备。让我们在 Java 代码方面进行操作,为选定的导航抽屉项实现动态滑动和片段切换。在开始 MainActivity 之前,我们需要创建一个带有构造函数的抽屉项的 POJO 类:

public class DrawerItem {
    private String name;
    private String navigationIcon;

    public DrawerItem(String name, String navigationIcon) {
        this.name = name;
        this.navigationIcon = navigationIcon;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNavigationIcon() {
        return navigationIcon;
    }

    public void setNavigationIcon(String navigationIcon) {
        this.navigationIcon = navigationIcon;
    }
}

这些获取器和设置器将帮助设置抽屉图标和抽屉标题,正如我们之前在笔记应用中讨论 POJO 一样。

MainActivity.java 中,让我们实现 WearableActionDrawer.OnMenuItemClickListener 并重写 onMenuItemClick,如下所示:

public class MainActivity extends WearableActivity  implements
        WearableActionDrawer.OnMenuItemClickListener{

....

@Override
public boolean onMenuItemClick(MenuItem menuItem) {
    Log.d(TAG, "onMenuItemClick(): " + menuItem);
    final int itemId = menuItem.getItemId();
  }
}

让我们在 MainActivity 的范围内初始化所有 WearableDrawerLayoutWearableNavigationDrawerWearableActionDrawer 的实例,并进行必要的设置:

private static final String TAG = "MainActivity";
private WearableDrawerLayout mWearableDrawerLayout;
private WearableNavigationDrawer mWearableNavigationDrawer;
private WearableActionDrawer mWearableActionDrawer;
private ArrayList<DrawerItem> drawer_itemArrayList;
private int mSelectedScreen;

onCreate 方法中,让我们通过 findViewById() 方法映射所有视觉组件。之后,我们将使用一个名为 ViewTreeObserver 的新类来进行快速预览和隐藏:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    setAmbientEnabled();

   //Initialise the arraylist
    drawer_itemArrayList = initializeScreenSystem();
    mSelectedScreen = 0;

    // Initialize here for content to first screen.
    ...

    // Main Wearable Drawer Layout that holds all the content
    mWearableDrawerLayout = (WearableDrawerLayout) 
    findViewById(R.id.drawer_layout);

    // Top Navigation Drawer
    mWearableNavigationDrawer =
            (WearableNavigationDrawer) 
    findViewById(R.id.top_navigation_drawer);

    mWearableNavigationDrawer.setAdapter(new NavigationAdapter(this));

    // Bottom Action Drawer
    mWearableActionDrawer =
            (WearableActionDrawer) 
    findViewById(R.id.bottom_action_drawer);

    mWearableActionDrawer.setOnMenuItemClickListener(this);

    // Temporarily peeks the navigation and action drawers to ensure 
    the user is aware of them.
    ViewTreeObserver observer = 
    mWearableDrawerLayout.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new 
    ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            mWearableDrawerLayout.getViewTreeObserver()
            .removeOnGlobalLayoutListener(this);
            mWearableDrawerLayout.peekDrawer(Gravity.TOP);
            mWearableDrawerLayout.peekDrawer(Gravity.BOTTOM);
        }
    });
}

我们在 setAmbientEnabled() 方法后看到了数组初始化代码。我们需要为它编写关联的方法。在方法内部,初始化 DrawerItem 的列表,遍历这些项,并获取标题和图标,如下所示:

private ArrayList<DrawerItem> initializeScreenSystem() {
    ArrayList<DrawerItem> screens = new ArrayList<DrawerItem>();
    String[] FragmentArrayNames = 
    getResources().getStringArray(R.array.screens);

    for (int i = 0; i < FragmentArrayNames.length; i++) {
        String planet = FragmentArrayNames[i];
        int FragmentResourceId =
                getResources().getIdentifier(planet, "array", 
                getPackageName());
        String[] fragmentInformation = 
        getResources().getStringArray(FragmentResourceId);

        screens.add(new DrawerItem(
                fragmentInformation[0],   // Name
                fragmentInformation[1]));
    }

    return screens;
}

FragmentArrayNames 实例中,我们需要找到一个字符串数组,这是需要创建的。在 res/values 文件夹中,创建 arrays.xml 文件以及以下一系列数组:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string-array name="screens">
        <item>water</item>
        <item>heart</item>
        <item>step</item>
    </string-array>

    <string-array name="water">
       // drawer item title  
        <item>Drink water</item>
      // drawer item icon
        <item>water_bottle_flat</item>
    </string-array>

    <string-array name="heart">
        <item>Heart Beat</item>
        <item>ic_heart_icon</item>
    </string-array>

    <string-array name="step">
        <item>Step Counter</item>
        <item>ic_step_icon</item>
    </string-array>

</resources>

为了理解,我们以 screens 字符串数组中的水项为例。我们为水片段屏幕的项添加标题和确切的图标名称,其他数组项也同理。

对于 WearableActionDrawer 项,我们需要在继续编写 Java 逻辑之前配置动作。我们需要将 menu XML 文件添加到 res/menu 目录中。让我们将文件命名为 drawer_menu.xml

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/menu_about"
          android:icon="@drawable/ic_info"
          android:title="About"/>
    <item android:id="@+id/menu_helathtips"
          android:icon="@drawable/ic_info"
          android:title="Health tips" />
    <item android:id="@+id/menu_calarie"
          android:icon="@drawable/ic_info"
          android:title="Calories chart" />
</menu>

我们必须重写 onMenuItemclicklistener 并在用户点击任何菜单项时触发一个动作:

@Override
public boolean onMenuItemClick(MenuItem menuItem) {
    Log.d(TAG, "onMenuItemClick(): " + menuItem);

    final int itemId = menuItem.getItemId();

    String toastMessage = "";

    switch (itemId) {
        case R.id.menu_about:
            toastMessage = 
            drawer_itemArrayList.get(mSelectedScreen).getName();
            break;
        case R.id.menu_helathtips:
            toastMessage = 
            drawer_itemArrayList.get(mSelectedScreen).getName();
            break;
        case R.id.menu_volume:
            toastMessage = 
            drawer_itemArrayList.get(mSelectedScreen).getName();
            break;
    }

 mWearableDrawerLayout.closeDrawer(mWearableActionDrawer);

    if (toastMessage.length() > 0) {
        Toast toast = Toast.makeText(
                getApplicationContext(),
                toastMessage,
                Toast.LENGTH_SHORT);
        toast.show();
        return true;
    } else {
        return false;
    }
}

actionDrawer 从 Wear 设备底部弹出时,我们将关闭导航 drawerlayout 以便活动抽屉可以明显地被感知。当用户点击项时,我们将显示我们之前在数组 XML 中创建的片段屏幕名称。

现在来处理 MainActivity 的最后一步。让我们编写 NavigationDrawer 适配器,以便在切换时附加框架。

创建一个导航抽屉适配器

创建一个扩展自 WearableNavigationDrawerAdapter 的类并重写以下方法:

private final class NavigationAdapter
        extends WearableNavigationDrawer
        .WearableNavigationDrawerAdapter {

@Override
public String getItemText(int i) {
    return null;
}

@Override
public Drawable getItemDrawable(int i) {
    return null;
}

@Override
public void onItemSelected(int i) {

}

@Override
public int getCount() {
    return 0;
}

}

之后,创建一个以上下文为参数的构造函数,并在getCount方法中返回drawer_item ArrayList及其大小。

onItemSelected方法中,根据位置参数,我们可以切换碎片:

@Override
public void onItemSelected(int position) {
    Log.d(TAG, "WearableNavigationDrawerAdapter.onItemSelected(): " + 
    position);
    mSelectedScreen = position;

    if(position==0) {
        final DrinkWaterFragment drinkWaterFragment = new 
        DrinkWaterFragment();
        getFragmentManager()
                .beginTransaction()
                .replace(R.id.content_frame, drinkWaterFragment)
                .commit();
    }

}

无论如何,我们必须编写这些碎片。稍等一会儿。在getItemText中,我们可以从draweritem ArrayList恢复名称,如下所示:

@Override
public String getItemText(int pos) {
    return drawer_itemArrayList.get(pos).getName();
}

为了获取可绘制的图标并设置图标,我们将使用以下自定义方法:

@Override
public Drawable getItemDrawable(int pos) {
    String navigationIcon =   
    drawer_itemArrayList.get(pos).getNavigationIcon();

    int drawableNavigationIconId =
            getResources().getIdentifier(navigationIcon, "drawable", 
            getPackageName());

    return mContext.getDrawable(drawableNavigationIconId);
}

是的,我们已经完成了MainActivity的代码。让我们把所有方法放在一起,看看完整的MainActivity代码:

public class MainActivity extends WearableActivity  implements
        WearableActionDrawer.OnMenuItemClickListener{

    private static final String TAG = "MainActivity";
    private WearableDrawerLayout mWearableDrawerLayout;
    private WearableNavigationDrawer mWearableNavigationDrawer;
    private WearableActionDrawer mWearableActionDrawer;
    private ArrayList<DrawerItem> drawer_itemArrayList;
    private int mSelectedScreen;

    private DrinkWaterFragment mDrinkFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setAmbientEnabled();
        drawer_itemArrayList = initializeScreenSystem();
        mSelectedScreen = 0;

        // Initialize content to first screen.
        mDrinkFragment = new DrinkWaterFragment();
        FragmentManager fragmentManager = getFragmentManager();
        fragmentManager.beginTransaction().replace(R.id.content_frame, 
        mDrinkFragment).commit();

        // Main Wearable Drawer Layout that holds all the content
        mWearableDrawerLayout = (WearableDrawerLayout) 
        findViewById(R.id.drawer_layout);

        // Top Navigation Drawer
        mWearableNavigationDrawer =
                (WearableNavigationDrawer) 
        findViewById(R.id.top_navigation_drawer);

        mWearableNavigationDrawer.setAdapter(new 
        NavigationAdapter(this));

        // Bottom Action Drawer
        mWearableActionDrawer =
                (WearableActionDrawer) 
                findViewById(R.id.bottom_action_drawer);

        mWearableActionDrawer.setOnMenuItemClickListener(this);

        // Temporarily peeks the navigation and action drawers to 
        ensure the user is aware of them.
        ViewTreeObserver observer = 
        mWearableDrawerLayout.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new 
        ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mWearableDrawerLayout.getViewTreeObserver()
                .removeOnGlobalLayoutListener(this);
                mWearableDrawerLayout.peekDrawer(Gravity.TOP);
                mWearableDrawerLayout.peekDrawer(Gravity.BOTTOM);
            }
        });
    }

    private ArrayList<DrawerItem> initializeScreenSystem() {
        ArrayList<DrawerItem> screens = new ArrayList<DrawerItem>();
        String[] FragmentArrayNames = 
        getResources().getStringArray(R.array.screens);

        for (int i = 0; i < FragmentArrayNames.length; i++) {
            String planet = FragmentArrayNames[i];
            int FragmentResourceId =
                    getResources().getIdentifier(planet, "array", 
                    getPackageName());
            String[] fragmentInformation = 
            getResources().getStringArray(FragmentResourceId);

            screens.add(new DrawerItem(
                    fragmentInformation[0],   // Name
                    fragmentInformation[1]));
        }

        return screens;
    }

    @Override
    public boolean onMenuItemClick(MenuItem menuItem) {
        Log.d(TAG, "onMenuItemClick(): " + menuItem);

        final int itemId = menuItem.getItemId();

        String toastMessage = "";

        switch (itemId) {
            case R.id.menu_about:
                toastMessage = 
                drawer_itemArrayList.get(mSelectedScreen).getName();
                break;
            case R.id.menu_helathtips:
                toastMessage = 
                drawer_itemArrayList.get(mSelectedScreen).getName();
                break;
            case R.id.menu_volume:
                toastMessage = 
                drawer_itemArrayList.get(mSelectedScreen).getName();
                break;
        }

        mWearableDrawerLayout.closeDrawer(mWearableActionDrawer);

        if (toastMessage.length() > 0) {
            Toast toast = Toast.makeText(
                    getApplicationContext(),
                    toastMessage,
                    Toast.LENGTH_SHORT);
            toast.show();
            return true;
        } else {
            return false;
        }
    }

    private final class NavigationAdapter
            extends 
    WearableNavigationDrawer.WearableNavigationDrawerAdapter {

        private final Context mContext;

        public NavigationAdapter(Context context) {
            mContext = context;
        }

        @Override
        public int getCount() {
            return drawer_itemArrayList.size();
        }

        @Override
        public void onItemSelected(int position) {
            Log.d(TAG,              
            "WearableNavigationDrawerAdapter.onItemSelected():"
            + position);
            mSelectedScreen = position;

            if(position==0) {
                final DrinkWaterFragment drinkWaterFragment = new 
                DrinkWaterFragment();
                getFragmentManager()
                        .beginTransaction()
                        .replace(R.id.content_frame, 
                        drinkWaterFragment)
                        .commit();

            }
            }

        }

        @Override
        public String getItemText(int pos) {
            return drawer_itemArrayList.get(pos).getName();
        }

        @Override
        public Drawable getItemDrawable(int pos) {
            String navigationIcon = 
            drawer_itemArrayList.get(pos).getNavigationIcon();

            int drawableNavigationIconId =
                    getResources().getIdentifier(navigationIcon, 
                    "drawable", getPackageName());

            return mContext.getDrawable(drawableNavigationIconId);
        }
    }
        @Override
    public void onEnterAmbient(Bundle ambientDetails) {
        super.onEnterAmbient(ambientDetails);
    }

    @Override
    public void onUpdateAmbient() {
        super.onUpdateAmbient();
    }

    @Override
    public void onExitAmbient() {
        super.onExitAmbient();
    }
}

创建碎片(Fragments)

我们需要创建一个名为DrinkWaterFragment的碎片。在这个碎片中,我们将处理启动和结束WaterDrink提醒。

res/layout目录中创建一个新的布局文件,创建drink_water_fragment.xml文件,并在boxinsetlayout内添加两个按钮,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.packt.upbeat.fragments.HeartRateFragment"
    tools:deviceIds="wear">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="horizontal">

        <android.support.v7.widget.AppCompatButton
            android:id="@+id/start"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_margin="10dp"
            android:layout_weight="1"
            android:background="@drawable/button_background"
            android:elevation="5dp"
            android:gravity="center"
            android:text="Start"
            android:textAllCaps="true"
            android:textColor="@color/white"
            android:textStyle="bold" />

        <android.support.v7.widget.AppCompatButton
            android:id="@+id/stop"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_margin="10dp"
            android:layout_weight="1"
            android:background="@drawable/button_background"
            android:elevation="5dp"
            android:gravity="center"
            android:text="Stop"
            android:textAllCaps="true"
            android:textColor="@color/white"
            android:textStyle="bold" />

    </LinearLayout>
</android.support.wearable.view.BoxInsetLayout>

你也可以使用正常的Button类,但在这个项目中,我们将使用AppCompatButton来实现材料设计图示,如高度和其他功能。我们必须自定义按钮背景选择器。在drawable目录中创建一个文件,并将其命名为button_background.xml。添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/colorPrimaryDark" 
    android:state_pressed="true"/>
    <item android:drawable="@color/grey" android:state_focused="true"/>
    <item android:drawable="@color/colorPrimary"/>
</selector>

DrinkWaterFragment内部,实例化按钮,并为开始和停止服务的按钮附加监听器,如下所示:

public class DrinkWaterFragment extends Fragment {

    private AppCompatButton mStart;
    private AppCompatButton mStop;

    public DrinkWaterFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup 
    container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View rootView = inflater.inflate(R.layout.drink_water_fragment, 
        container, false);

        mStart = (AppCompatButton) rootView.findViewById(R.id.start);
        mStop = (AppCompatButton) rootView.findViewById(R.id.stop);
        mStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) { 

}
});

mStop.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

    }

});

     return rootView;
  }
}

现在是时候在服务包内编写一个BroadcastReceiver类,并在其中配置通知。让我们将这个类命名为WaterReminderReceiver并继承自BroadcastReceiver。重写onReceive方法,每当AlarmManager触发这个接收器时,它将接收到数据,我们会看到一个通知:

public class WaterReminderReceiver extends BroadcastReceiver {
    public static final String CONTENT_KEY = "contentText";

    @Override
    public void onReceive(Context context, Intent intent) {
        Intent intent2 = new Intent(context, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = 
        PendingIntent.getActivity(context, 0, intent2,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri = 
        RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);

        NotificationCompat.Builder notificationBuilder = 
        (NotificationCompat.Builder) new 
        NotificationCompat.Builder(context)
                .setAutoCancel(true)   
                //Automatically delete the notification
                .setSmallIcon(R.drawable.water_bottle_flat) 
                //Notification icon
                .setContentIntent(pendingIntent)
                .setContentTitle("Time to hydrate")
                .setContentText("Drink a glass of water now")
                .setCategory(Notification.CATEGORY_REMINDER)
                .setPriority(Notification.PRIORITY_HIGH)
                .setSound(defaultSoundUri);

        NotificationManagerCompat notificationManager = 
        NotificationManagerCompat.from(context);
        notificationManager.notify(0, notificationBuilder.build());

        Toast.makeText(context, "Repeating Alarm Received", 
        Toast.LENGTH_SHORT).show();
    }
}

在应用的清单文件中注册这个接收器,放在 application 标签的作用域内:

<receiver android:name=".services.WaterReminderReceiver" android:process=":remote" />

现在,在DrinkWaterFragment中,在开始按钮的onClickListener中启动AlarmManager服务,并注册广播接收器:

mStart.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(getActivity(), 
        WaterReminderReceiver.class);
        PendingIntent sender = 
        PendingIntent.getBroadcast(getActivity(),
                0, intent, 0);

        // We want the alarm to go off 5 seconds from now.
        long firstTime = SystemClock.elapsedRealtime();
        firstTime += 5 * 1000;

        // Schedule the alarm!
        AlarmManager am = (AlarmManager) 
        getActivity().getSystemService(ALARM_SERVICE);
        am.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                firstTime, 5 * 1000, sender);

        //DOZE MODE SUPPORT       am.setAndAllowWhileIdle(AlarmManager
        .ELAPSED_REALTIME_WAKEUP, firstTime, sender);

        // Tell the user about what we did.
        if (mToast != null) {
            mToast.cancel();
        }
        mToast = Toast.makeText(getActivity(), 
        "Subscribed to water alarm",
                Toast.LENGTH_LONG);
        mToast.show();
    }
});

现在,为了调试目的,我们将设置 15 秒的延迟,尽管你可以根据你的需要和应用场景改变延迟时间。我们还确保当 Wear 设备进入休眠模式时,饮水提醒仍然可以启动支持休眠模式的方法来启动闹钟管理器。

当我们点击停止按钮以停止饮水提醒服务时,onClickListener将停止在后台运行的闹钟管理器,并为取消饮水提醒显示一个快速吐司提示:

  mStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Create the same intent, and thus a matching 
                IntentSender, for the one that was scheduled.
                Intent intent = new Intent(getActivity(), 
                WaterReminderReceiver.class);
                PendingIntent sender = 
                PendingIntent.getBroadcast(getActivity(),
                        0, intent, 0);

                // And cancel the alarm.
                AlarmManager am = (AlarmManager) 
                getActivity().getSystemService(ALARM_SERVICE);
                am.cancel(sender);

                // Tell the user about what we did.
                if (mToast != null) {
                    mToast.cancel();
                }
                mToast = Toast.makeText(getActivity(), "Unsubscribed 
                from water reminder",
                        Toast.LENGTH_LONG);
                mToast.show();
            }
        });

完整的碎片类代码如下:

public class DrinkWaterFragment extends Fragment {

    private AppCompatButton mStart;
    private AppCompatButton mStop;
    private Toast mToast;

    public DrinkWaterFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup 
    container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View rootView = inflater.inflate(R.layout.drink_water_fragment, 
        container, false);

        mStart = (AppCompatButton) rootView.findViewById(R.id.start);
        mStop = (AppCompatButton) rootView.findViewById(R.id.stop);

        mStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getActivity(), 
                WaterReminderReceiver.class);
                PendingIntent sender = 
                PendingIntent.getBroadcast(getActivity(),
                        0, intent, 0);

                // We want the alarm to go off 5 seconds from now.
                long firstTime = SystemClock.elapsedRealtime();
                firstTime += 5 * 1000;

                // Schedule the alarm!
                AlarmManager am = (AlarmManager) 
                getActivity().getSystemService(ALARM_SERVICE);
                am.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                        firstTime, 5 * 1000, sender);
                //DOZE MODE SUPPORT
                am.setAndAllowWhileIdle
                (AlarmManager.ELAPSED_REALTIME_WAKEUP, 
                firstTime, sender);

                // Tell the user about what we did.
                if (mToast != null) {
                    mToast.cancel();
                }
                mToast = Toast.makeText(getActivity(), "Subscribed to 
                water alarm",
                        Toast.LENGTH_LONG);
                mToast.show();
            }
        });

        mStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Create the same intent, 
                and thus a matching IntentSender, for
                // the one that was scheduled.
                Intent intent = new Intent(getActivity(), 
                WaterReminderReceiver.class);
                PendingIntent sender = 
                PendingIntent.getBroadcast(getActivity(),
                        0, intent, 0);

                // And cancel the alarm.
                AlarmManager am = (AlarmManager) 
                getActivity().getSystemService(ALARM_SERVICE);
                am.cancel(sender);

                // Tell the user about what we did.
                if (mToast != null) {
                    mToast.cancel();
                }
                mToast = Toast.makeText(getActivity(), "Unsubscribed 
                from water reminder",
                        Toast.LENGTH_LONG);
                mToast.show();
            }
        });

        return rootView;
    }
}

这个碎片附加在MainActivityNavigationDrawerAdapter上。确保你通过在onCreate方法中附加碎片,将此碎片作为默认碎片附加到MainActivity

@Override
protected void onCreate(Bundle savedInstanceState) {

...
// Initialize content to first screen.
mDrinkFragment = new DrinkWaterFragment();
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction().replace(R.id.content_frame, mDrinkFragment).commit();

}

我们已经成功为应用完成了饮水提醒功能。现在,让我们通过光学心率传感器来构建心率检测功能。

在 fragments 包内创建一个片段,并命名为HeartRateFragment.java。在res/layout目录中重构,创建 XML,或者创建一个新的布局文件,并命名为heart_rate_fragment.xml,并添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.packt.upbeat.fragments.HeartRateFragment"
    tools:deviceIds="wear">

    <LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:orientation="horizontal">

        <ImageView
        android:layout_weight="1"
        android:src="img/ic_heart_icon"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

        <TextView
            android:id="@+id/heart_rate"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:gravity="left|center"
            android:hint="Reading"
            android:textColor="@color/colorAccent"
            android:textColorHint="#eaeaea"
            android:textSize="20sp"
            android:textStyle="bold" />

    </LinearLayout>
</android.support.wearable.view.BoxInsetLayout>

与前面代码中显示的静态ImageView相比,如何添加一个由传感器返回的心率动态心跳?我们将基于开源项目github.com/scottyab/HeartBeatView创建一个自定义缩放动画。在 utils 包内创建一个类,命名为HeartBeatView.java,它继承自AppCompatImageView。在开始处理CustomView之前,我们必须设置其 styleables,如下所示,这有助于管理我们传递的自定义值。在res/values中创建一个文件,命名为heartbeat_attrs.xmlStyleable为自定义视图定义属性和属性;例如,当自定义视图需要一个缩放因子时,我们可以按以下代码示例进行定义:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="HeartBeatView">
        <attr name="scaleFactor" format="float" />
        <attr name="duration" format="integer" />
    </declare-styleable>

</resources>

通过继承AppCompatImageView类来创建HeartBeatView类。我们需要创建属于AppCompatImageView类的构造函数,如下所示:

public class HeartBeatView extends AppCompatImageView{

    public HeartBeatView(Context context) {
        super(context);
    }

    public HeartBeatView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public HeartBeatView(Context context, AttributeSet attrs, int 
    defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

让我们在全局范围内为类添加以下实例,以配置缩放因子和动画时长:

private static final float DEFAULT_SCALE_FACTOR = 0.2f;
private static final int DEFAULT_DURATION = 50;
private Drawable heartDrawable;

private boolean heartBeating = false;

float scaleFactor = DEFAULT_SCALE_FACTOR;
float reductionScaleFactor = -scaleFactor;
int duration = DEFAULT_DURATION;

我们需要一个心形的矢量图形,并且需要在drawable文件夹中创建它。创建一个文件,命名为heart_red_24dp.xml,并添加以下代码:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0">
    <path
        android:fillColor="#FFFF0000"
        android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 
        2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81
        14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86
        -8.55,11.54L12,21.35z"/>
</vector>

为了初始化矢量图形,我们将使用drawable实例并访问矢量图形:

private void init() {
    //make this not mandatory
    heartDrawable = ContextCompat.getDrawable(getContext(), 
    R.drawable.ic_heart_red_24dp);
    setImageDrawable(heartDrawable);

}

res/values目录中创建的styleable,我们可以填充缩放因子和动画时长。通过TypedArray类的实例,我们可以获取以下属性:

(Context context, AttributeSet attrs) {
    TypedArray a = context.getTheme().obtainStyledAttributes(
            attrs,
            R.styleable.HeartBeatView,
            0, 0
    );
    try {
        scaleFactor = a.getFloat(R.styleable.HeartBeatView_scaleFactor, 
        DEFAULT_SCALE_FACTOR);
        reductionScaleFactor = -scaleFactor;
        duration = a.getInteger(R.styleable.HeartBeatView_duration, 
        DEFAULT_DURATION);

    } finally {
        a.recycle();
    }

}

我们将需要三个方法,即toggle()start()stop()来初始化动画:

/**
 * toggles current heat beat state
 */
public void toggle() {
    if (heartBeating) {
        stop();
    } else {
        start();
    }
}

/**
 * Starts the heat beat/pump animation
 */
public void start() {
    heartBeating = true;
    animate().scaleXBy(scaleFactor).scaleYBy(scaleFactor)
    .setDuration(duration).setListener(scaleUpListener);
}

/**
 * Stops the heat beat/pump animation
 */
public void stop() {
    heartBeating = false;
    clearAnimation();
}

现在,为了根据 BPM 设置时长,我们将简单地使用Math.round操作将 bpm 分配给时长,使其成为一个 3 位小数:

public void setDurationBasedOnBPM(int bpm) {
    if (bpm > 0) {
        duration = Math.round((milliInMinute / bpm) / 3f);
    }
}

为了检查heartBeat动画是否已开始以及动画的持续时间,我们必须编写以下两个方法:

public boolean isHeartBeating() {
    return heartBeating;
}

public int getDuration() {
    return duration;
}

使用这个指针,我们将分配时长和缩放因子:

public void setDuration(int duration) {
    this.duration = duration;
}

public float getScaleFactor() {
    return scaleFactor;
}

public void setScaleFactor(float scaleFactor) {
    this.scaleFactor = scaleFactor;
    reductionScaleFactor = -scaleFactor;
}

最后,我们将为ScaleUpAnimationScaleDownAnimation编写两个动画监听器方法。我们将编写一个Animator.AnimatorListener类型的方法,并在ScaleUpListener中增加缩放,在ScaleDownListener中减少缩放,如下所示:

//Scale up animation 
private final Animator.AnimatorListener scaleUpListener = new Animator.AnimatorListener() {

    @Override
    public void onAnimationStart(Animator animation) {
    }

    @Override
    public void onAnimationRepeat(Animator animation) {

    }

    @Override
    public void onAnimationEnd(Animator animation) {
        //we ignore heartBeating as we want to ensure the heart is 
        reduced back to original size
        animate().scaleXBy(reductionScaleFactor)
        .scaleYBy(reductionScaleFactor).setDuration(duration)
        .setListener(scaleDownListener);
    }

    @Override
    public void onAnimationCancel(Animator animation) {

    }
};

//Scale down animation 
private final Animator.AnimatorListener scaleDownListener = new Animator.AnimatorListener() {

    @Override
    public void onAnimationStart(Animator animation) {
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        if (heartBeating) {
            //duration twice as long for the upscale
            animate().scaleXBy(scaleFactor).scaleYBy(scaleFactor)
            .setDuration(duration * 2).setListener(scaleUpListener);
        }
    }

    @Override
    public void onAnimationCancel(Animator animation) {
    }
};

完成的自定义视图类如下所示:

public class HeartBeatView extends AppCompatImageView {

    private static final String TAG = "HeartBeatView";

    private static final float DEFAULT_SCALE_FACTOR = 0.2f;
    private static final int DEFAULT_DURATION = 50;
    private Drawable heartDrawable;

    private boolean heartBeating = false;

    float scaleFactor = DEFAULT_SCALE_FACTOR;
    float reductionScaleFactor = -scaleFactor;
    int duration = DEFAULT_DURATION;

    public HeartBeatView(Context context) {
        super(context);
        init();
    }

    public HeartBeatView(Context context, AttributeSet attrs) {
        super(context, attrs);
        populateFromAttributes(context, attrs);
        init();
    }

    public HeartBeatView(Context context, AttributeSet attrs, int 
    defStyleAttr) {
        super(context, attrs, defStyleAttr);
        populateFromAttributes(context, attrs);
        init();
    }

    private void init() {
        //make this not mandatory
        heartDrawable = ContextCompat.getDrawable(getContext(), 
        R.drawable.ic_heart_red_24dp);
        setImageDrawable(heartDrawable);

    }

    private void populateFromAttributes(Context context, AttributeSet 
    attrs) {
        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.HeartBeatView,
                0, 0
        );
        try {
            scaleFactor = 
            a.getFloat(R.styleable.HeartBeatView_scaleFactor, 
            DEFAULT_SCALE_FACTOR);
            reductionScaleFactor = -scaleFactor;
            duration = a.getInteger(R.styleable.HeartBeatView_duration, 
            DEFAULT_DURATION);

        } finally {
            a.recycle();
        }

    }

    /**
     * toggles current heat beat state
     */
    public void toggle() {
        if (heartBeating) {
            stop();
        } else {
            start();
        }
    }

    /**
     * Starts the heat beat/pump animation
     */
    public void start() {
        heartBeating = true;
        animate().scaleXBy(scaleFactor).scaleYBy(scaleFactor)
        .setDuration(duration).setListener(scaleUpListener);
    }

    /**
     * Stops the heat beat/pump animation
     */
    public void stop() {
        heartBeating = false;
        clearAnimation();
    }

    /**
     * is the heart currently beating
     *
     * @return
     */
    public boolean isHeartBeating() {
        return heartBeating;
    }

    public int getDuration() {
        return duration;
    }

    private static final int milliInMinute = 60000;

    /**
     * set the duration of the beat based on the beats per minute
     *
     * @param bpm (positive int above 0)
     */
    public void setDurationBasedOnBPM(int bpm) {
        if (bpm > 0) {
            duration = Math.round((milliInMinute / bpm) / 3f);
        }
    }

    public void setDuration(int duration) {
        this.duration = duration;
    }

    public float getScaleFactor() {
        return scaleFactor;
    }

    public void setScaleFactor(float scaleFactor) {
        this.scaleFactor = scaleFactor;
        reductionScaleFactor = -scaleFactor;
    }

    private final Animator.AnimatorListener scaleUpListener = new 
    Animator.AnimatorListener() {

        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            //we ignore heartBeating as we want to ensure the heart is 
            reduced back to original size
            animate().scaleXBy(reductionScaleFactor)
            .scaleYBy(reductionScaleFactor).setDuration(duration)
            .setListener(scaleDownListener);
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }
    };

    private final Animator.AnimatorListener scaleDownListener = new 
    Animator.AnimatorListener() {

        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            if (heartBeating) {
                //duration twice as long for the upscale
                animate().scaleXBy(scaleFactor).scaleYBy(scaleFactor)
                .setDuration(duration * 2)
                .setListener(scaleUpListener);
            }
        }

        @Override
        public void onAnimationCancel(Animator animation) {
        }
    };

}

heart_rate_fragment.xml中,用新创建的自定义视图替换imageview代码。给它一个唯一的 ID:

<com.packt.upbeat.utils.HeartBeatView
    android:id="@+id/heartbeat"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="25dp"
    android:layout_weight="1" />

创建HeartRateFragment类,扩展自Fragment类,并实现SensorEventListenerSensorEventListener将监控所有传感器更新并返回更改后的结果:

public class HeartRateFragment extends Fragment implements SensorEventListener
{
      public HeartRateFragment() {
    // Required empty public constructor
     }

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View rootView = inflater.inflate(R.layout.heart_rate_fragment, 
    container, false);

return rootView;

  }
}

在程序的全球范围内,实例化以下必要的组件:

private BoxInsetLayout mContainerView;
private TextView mTextView;
private HeartBeatView heartbeat;
private Sensor mHeartRateSensor;
private SensorManager mSensorManager;
private Integer currentValue = 0;
private static final String TAG = "HeartRateFragment";
private static final int SENSOR_PERMISSION_CODE = 123;

onCreateView方法中,映射 XML 文件中添加的所有视觉组件。在onCreateView中初始化传感器是很好的做法,这样可以减少 NPE 的数量:

heartbeat = (HeartBeatView)rootView.findViewById(R.id.heartbeat);

mContainerView = (BoxInsetLayout)rootView.findViewById(R.id.container);
mTextView = (TextView)rootView.findViewById(R.id.heart_rate);
mSensorManager = ((SensorManager)getActivity().getSystemService(SENSOR_SERVICE));
mHeartRateSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_HEART_RATE);

访问HeartRate传感器需要在清单中设置权限。在 Wear 2.0 中,我们还需要设置运行时权限。因此,在清单文件中注册身体传感器权限:

<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BODY_SENSORS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

对于运行时权限,我们需要请求BODY_SENSORS的访问权限,然后重写onRequestPermissionResult()方法。以下代码说明了运行时权限模型:

  //Requesting permission
    private void requestSensorPermission() {
        if (ContextCompat.checkSelfPermission(getActivity(),  
        Manifest.permission.BODY_SENSORS) == 
        PackageManager.PERMISSION_GRANTED)
            return;

        if (ActivityCompat.shouldShowRequestPermissionRationale
        (getActivity(), Manifest.permission.BODY_SENSORS)) {
            //If the user has denied the permission previously your 
            code will come to this block
            //Here you can explain why you need this permission
            //Explain here why you need this permission
        }
        //And finally ask for the permission
        ActivityCompat.requestPermissions(getActivity(), new String[]
        {Manifest.permission.BODY_SENSORS}, SENSOR_PERMISSION_CODE);
    }

    //This method will be called when the user will tap 
    on allow or deny
    @Override
    public void onRequestPermissionsResult(int requestCode, 
    @NonNull String[] permissions, @NonNull int[] grantResults) {

        //Checking the request code of our request
        if (requestCode == SENSOR_PERMISSION_CODE) {

            //If permission is granted
            if (grantResults.length > 0 && grantResults[0] == 
            PackageManager.PERMISSION_GRANTED) {
                //Displaying a toast
                Toast.makeText(getActivity(), "Permission granted now 
                you can read the storage", Toast.LENGTH_LONG).show();
            } else {
                //Displaying another toast if permission is not granted
                Toast.makeText(getActivity(), "Oops you just denied the 
                permission", Toast.LENGTH_LONG).show();
            }
        }
    }
}

现在,在onCreateView()方法中调用requestSensorPermission()

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View rootView = inflater.inflate(R.layout.heart_rate_fragment, 
    container, false);

  //other components.

    requestSensorPermission();

    return rootView;
}

onSensorChanged()方法中,使用参数中的sensorevent对象,我们现在将获取HeartRate。以下代码获取传感器的类型及其返回值。稍后,在 for 循环内部,我们可以设置HeartBeat动画的持续时间及其切换方法以启动动画:

@Override
public void onSensorChanged(SensorEvent sensorEvent) {

    if(sensorEvent.sensor.getType() == Sensor.TYPE_HEART_RATE && 
    sensorEvent.values.length > 0) {

        for(Float value : sensorEvent.values) {

            int newValue = Math.round(value);

            if(currentValue != newValue) {
                currentValue = newValue;

                mTextView.setText(currentValue.toString());
                heartbeat.setDurationBasedOnBPM(currentValue);
                heartbeat.toggle();
            }

        }

    }
}

onStartonDestroy方法中,注册HeartRate传感器和解注册传感器:

@Override
public void onStart() {
    super.onStart();
    if (mHeartRateSensor != null) {
        Log.d(TAG, "HEART RATE SENSOR NAME: " +  
        mHeartRateSensor.getName() + " TYPE: " + 
        mHeartRateSensor.getType());
        mSensorManager.unregisterListener(this, this.mHeartRateSensor);
        boolean isRegistered = mSensorManager.registerListener(this, 
        this.mHeartRateSensor, SensorManager.SENSOR_DELAY_FASTEST);
        Log.d(TAG, "HEART RATE LISTENER REGISTERED: " + isRegistered);
    } else {
        Log.d(TAG, "HEART RATE SENSOR NOT READY");
    }
}

@Override
public void onDestroy() {
    super.onDestroy();
    mSensorManager.unregisterListener(this);
    Log.d(TAG, "SENSOR UNREGISTERED");
}

完整的HeartRateFragment类代码如下:

public class HeartRateFragment extends Fragment implements SensorEventListener {

    private BoxInsetLayout mContainerView;
    private TextView mTextView;
    private HeartBeatView heartbeat;
    private Sensor mHeartRateSensor;
    private SensorManager mSensorManager;
    private Integer currentValue = 0;
    private static final String TAG = "HeartRateFragment";
    private static final int SENSOR_PERMISSION_CODE = 123;

    private GoogleApiClient mGoogleApiClient;

    public HeartRateFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup 
    container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View rootView = inflater.inflate(R.layout.heart_rate_fragment, 
        container, false);

        heartbeat = (HeartBeatView)rootView.findViewById
        (R.id.heartbeat);

        mContainerView = (BoxInsetLayout)rootView.findViewById
        (R.id.container);
        mTextView = (TextView)rootView.findViewById(R.id.heart_rate);
        mSensorManager = ((SensorManager)getActivity()
        .getSystemService(SENSOR_SERVICE));
        mHeartRateSensor = mSensorManager.getDefaultSensor
        (Sensor.TYPE_HEART_RATE);

        mGoogleApiClient = new GoogleApiClient.Builder(getActivity())
        .addApi(Wearable.API).build();
        mGoogleApiClient.connect();

        requestSensorPermission();

        return rootView;
    }

    @Override
    public void onStart() {
        super.onStart();
        if (mHeartRateSensor != null) {
            Log.d(TAG, "HEART RATE SENSOR NAME: " +  
            mHeartRateSensor.getName() + " TYPE: " + 
            mHeartRateSensor.getType());
            mSensorManager.unregisterListener(this, 
            this.mHeartRateSensor);
            boolean isRegistered = mSensorManager.registerListener
            (this, this.mHeartRateSensor, 
            SensorManager.SENSOR_DELAY_FASTEST);
            Log.d(TAG, "HEART RATE LISTENER REGISTERED: " + 
            isRegistered);
        } else {
            Log.d(TAG, "HEART RATE SENSOR NOT READY");
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mSensorManager.unregisterListener(this);
        Log.d(TAG, "SENSOR UNREGISTERED");
    }

    @Override
    public void onSensorChanged(SensorEvent sensorEvent) {

        if(sensorEvent.sensor.getType() == Sensor.TYPE_HEART_RATE && 
        sensorEvent.values.length > 0) {

            for(Float value : sensorEvent.values) {

                int newValue = Math.round(value);

                if(currentValue != newValue) {
                    currentValue = newValue;

                    mTextView.setText(currentValue.toString());
                    heartbeat.setDurationBasedOnBPM(currentValue);
                    heartbeat.toggle();
                }

            }

        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {
        Log.d(TAG, "ACCURACY CHANGED: " + i);
    }

    //Requesting permission
    private void requestSensorPermission() {
        if (ContextCompat.checkSelfPermission(getActivity(),  
        Manifest.permission.BODY_SENSORS) == 
        PackageManager.PERMISSION_GRANTED)
            return;

        if (ActivityCompat.shouldShowRequestPermissionRationale
        (getActivity(), Manifest.permission.BODY_SENSORS)) {
            //If the user has denied the permission previously your 
            code will come to this block
            //Here you can explain why you need this permission
            //Explain here why you need this permission
        }
        //And finally ask for the permission
        ActivityCompat.requestPermissions(getActivity(), new String[]
        {Manifest.permission.BODY_SENSORS}, SENSOR_PERMISSION_CODE);
    }

    //This method will be called when the user will tap 
    on allow or deny
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull 
    String[] permissions, @NonNull int[] grantResults) {

        //Checking the request code of our request
        if (requestCode == SENSOR_PERMISSION_CODE) {

            //If permission is granted
            if (grantResults.length > 0 && grantResults[0] == 
            PackageManager.PERMISSION_GRANTED) {
                //Displaying a toast
                Toast.makeText(getActivity(), "Permission granted now 
                 you can read the storage", Toast.LENGTH_LONG).show();
            } else {
                //Displaying another toast if permission is not granted
                Toast.makeText(getActivity(), "Oops you just denied the 
                permission", Toast.LENGTH_LONG).show();
            }
        }
    }
}

最后,在MainActivity导航适配器中,我们可以为第二个索引值附加HeartRateFragment。在onItemSelected方法中,添加以下代码更改:

if(position==0) {
    final DrinkWaterFragment drinkWaterFragment = new 
    DrinkWaterFragment();
    getFragmentManager()
            .beginTransaction()
            .replace(R.id.content_frame, drinkWaterFragment)
            .commit();

}else if(position == 1){
    final HeartRateFragment sectionFragment = new HeartRateFragment();
    getFragmentManager()
            .beginTransaction()
            .replace(R.id.content_frame, sectionFragment)
            .commit();
}

我们还需要为步数计数器构建一个屏幕。在 fragments 包中创建一个新的片段。创建另一个布局 xml 文件,并将其命名为step_counter_fragment.xml。本章范围的用户界面仅包含两个文本字段。

如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.packt.upbeat.fragments.HeartRateFragment"
    tools:deviceIds="wear">

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:text="@string/steps"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal|bottom"
            android:textSize="24sp"
            android:layout_weight="1"/>

        <TextView
            android:id="@+id/steps"
            android:layout_gravity="center_horizontal|top"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="24sp"
            android:layout_weight="1"/>

    </LinearLayout>
</android.support.wearable.view.BoxInsetLayout>

在创建的片段类中,我们有一个简单的文本视图来显示StepCounter数据。为了使步数计数器传感器在后台运行,我们需要创建一个服务类并将传感器数据附加到服务类。在构建片段之前,让我们先处理步数计数器将需要的服务。

我们将编写一个观察器来接收特定的数据类型,在 UI 中,我们将使用 Handler 线程来接收数据。让我们创建一个名为EventReceiver的类,它监听传感器的变化。我们将使用 Java 的BlockingQueue类,该类等待队列清空。借助ThreadPoolExecutor,我们可以使用一个单独的线程检测事件。以下是完整的类:

public class EventReceiver {
    private static ConcurrentMap<Class<?>, ConcurrentMap<Reporter, 
    String>> events
            = new ConcurrentHashMap<Class<?>, ConcurrentMap<Reporter, 
    String>>();

    private static BlockingQueue<Runnable> queue = new 
    LinkedBlockingQueue<Runnable>();
    private static ExecutorService executorService = new 
    ThreadPoolExecutor(1, 10, 30, TimeUnit.SECONDS, queue);

    public static void register(Class<?> event, Reporter reporter) {
        if (null == event || null == reporter)
            return;

        events.putIfAbsent(event, new ConcurrentHashMap<Reporter, 
        String>());
        events.get(event).putIfAbsent(reporter, "");
    }

    public static void remove(Class<?> event, Reporter reporter) {
        if (null == event || null == reporter)
            return;

        if (!events.containsKey(event))
            return;

        events.get(event).remove(reporter);
    }

    public static void notify(final Object event) {
        if (null == event)
            return;

        if (!events.containsKey(event.getClass()))
            return;

        for (final Reporter m : events.get(event.getClass()).keySet()) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    m.notifyEvent(event);
                }
            });
        }
    }

}

我们将注册来自Reporter接口的通知,并在不需要时移除它。报告接口如下所示:

public interface Reporter {
    public void notifyEvent(Object o);
}

我们需要一个BroadcastReceiver类来接收来自服务的通知:

public class AlarmNotification extends BroadcastReceiver {

    private static final String TAG = "AlarmNotification";

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG, "alarm fired");
        context.startService(new Intent(context, 
        WearStepService.class));
    }
}

让我们开始编写服务类。创建一个扩展自 Service 类的类,并实现它以SensorEventListener以下实例:

public class WearStepService extends Service implements SensorEventListener {

    public static final String TAG = "WearStepService";
    private static final long THREE_MINUTES = 3 * 60 * 1000;
    private static final String STEP_COUNT_PATH = "/step-count";
    private static final String STEP_COUNT_KEY = "step-count";
    private SensorManager sensorManager;
    private Sensor countSensor;

}

创建获取传感器管理器的方法:

private void getSensorManager() {
    if (null != sensorManager)
        return;

    Log.d(TAG, "getSensorManager");
    sensorManager = (SensorManager) 
    getSystemService(Context.SENSOR_SERVICE);
    registerCountSensor();
}

以下方法从 Wear 设备获取步数计数器传感器:

private void getCountSensor() {
    if (null != countSensor)
        return;

    Log.d(TAG, "getCountSensor");
    countSensor = 
    sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
    registerCountSensor();
}

要注册步数计数传感器,我们将使用传感器管理器类,并使用registerListener方法注册传感器:

private void registerCountSensor() {
    if (countSensor == null)
        return;

    Log.d(TAG, "sensorManager.registerListener");
    sensorManager.registerListener(this, countSensor, 
    SensorManager.SENSOR_DELAY_UI);
}

要设置BroadcastReceiver闹钟,我们之前创建了一个AlarmNotification BroadcastReceiver。使用以下方法,我们可以注册该类:

private void setAlarm() {
    Log.d(TAG, "setAlarm");

    Intent intent = new Intent(this, AlarmNotification.class);
    PendingIntent pendingIntent =  
    PendingIntent.getBroadcast(this.getApplicationContext(), 
    234324243, intent, 0);
    AlarmManager alarmManager = (AlarmManager) 
    getSystemService(ALARM_SERVICE);
    long firstRun = System.currentTimeMillis() + THREE_MINUTES;
    alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, firstRun, 
    THREE_MINUTES, pendingIntent);
}

当传感器在传感器事件数据中给出变化时,我们可以使用onSensorChanged方法在后台触发通知:

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_STEP_COUNTER)
            StepsTaken.updateSteps(event.values.length);
        Log.d(TAG, "onSensorChanged: steps count is" + 
        event.values.length);
        updateNotification();
    }

对于UpdateNotification方法,我们将使用NotificationCompat.Builder类来构建通知:

private void updateNotification() {
    // Create a notification builder that's compatible with platforms 
    >= version 4
    NotificationCompat.Builder builder =
            new NotificationCompat.Builder(getApplicationContext());

    // Set the title, text, and icon
    builder.setContentTitle(getString(R.string.app_name))
            .setSmallIcon(R.drawable.ic_step_icon);

    builder.setContentText("steps: " + StepsTaken.getSteps());

    // Get an instance of the Notification Manager
    NotificationManager notifyManager = (NotificationManager)
            getSystemService(Context.NOTIFICATION_SERVICE);

    // Build the notification and post it
    notifyManager.notify(0, builder.build());
}

重写onStartCommand方法,并初始化传感器管理器和步数计数传感器,如下所示:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Log.d(TAG, "onStartCommand");

    getSensorManager();
    getCountSensor();

    return super.onStartCommand(intent, flags, startId);
}

当传感器读数的准确性发生变化时,我们可以按如下方式触发通知:

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
    // drop these messages
    updateNotification();

}

我们需要在应用程序标签中的清单中注册服务和广播接收器:

<service android:name=".services.WearStepService" />

<receiver android:name=".services.AlarmNotification">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>

我们需要为服务创建一个简单的StepsTaken逻辑,以便每天重新开始。我们将创建一个可序列化的类,并使用日历实例,如果一天结束,我们将从零开始初始化步数:

public class StepsTaken implements Serializable {

    private static int steps = 0;
    private static long lastUpdateTime = 0L;
    private static final String TAG = "StepsTaken";

    public static void updateSteps(int stepsTaken) {
        steps += stepsTaken;

        // today
        Calendar tomorrow = new GregorianCalendar();
        tomorrow.setTimeInMillis(lastUpdateTime);
        // reset hour, minutes, seconds and millis
        tomorrow.set(Calendar.HOUR_OF_DAY, 0);
        tomorrow.set(Calendar.MINUTE, 0);
        tomorrow.set(Calendar.SECOND, 0);
        tomorrow.set(Calendar.MILLISECOND, 0);

        // next day
        tomorrow.add(Calendar.DAY_OF_MONTH, 1);

        Calendar now = Calendar.getInstance();

        if (now.after(tomorrow)) {
            Log.d(TAG, "I think it's tomorrow, resetting");
            steps = stepsTaken;
        }

        lastUpdateTime = System.currentTimeMillis();
    }

    public static int getSteps() {
        return steps;
    }
}

下面是WearStepService类的完整代码。

public class WearStepService extends Service implements SensorEventListener {

    public static final String TAG = "WearStepService";
    private static final long THREE_MINUTES = 3 * 60 * 1000;
    private SensorManager sensorManager;
    private Sensor countSensor;

    GoogleApiClient mGoogleApiClient;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
        setAlarm();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand");

        getSensorManager();
        getCountSensor();
        getGoogleClient();

        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    private void getGoogleClient() {
        if (null != mGoogleApiClient)
            return;

        Log.d(TAG, "getGoogleClient");
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addApi(Wearable.API)
                .build();
        mGoogleApiClient.connect();
    }

    /**
     * if the countSensor is null, try initializing it, and try 
     registering it with sensorManager
     */
    private void getCountSensor() {
        if (null != countSensor)
            return;

        Log.d(TAG, "getCountSensor");
        countSensor = sensorManager
        .getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
        registerCountSensor();
    }

    /**
     * if the countSensor exists, then try registering
     */
    private void registerCountSensor() {
        if (countSensor == null)
            return;

        Log.d(TAG, "sensorManager.registerListener");
        sensorManager.registerListener(this, countSensor, 
        SensorManager.SENSOR_DELAY_UI);
    }

    /**
     * if the sensorManager is null, initialize it, and try registering 
     the countSensor
     */
    private void getSensorManager() {
        if (null != sensorManager)
            return;

        Log.d(TAG, "getSensorManager");
        sensorManager = (SensorManager) 
        getSystemService(Context.SENSOR_SERVICE);
        registerCountSensor();
    }

    private void setAlarm() {
        Log.d(TAG, "setAlarm");

        Intent intent = new Intent(this, AlarmNotification.class);
        PendingIntent pendingIntent = PendingIntent.getBroadcast
        (this.getApplicationContext(), 234324243, intent, 0);
        AlarmManager alarmManager = (AlarmManager) 
        getSystemService(ALARM_SERVICE);
        long firstRun = System.currentTimeMillis() + THREE_MINUTES;
        alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, 
        firstRun, THREE_MINUTES, pendingIntent);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_STEP_COUNTER)
            StepsTaken.updateSteps(event.values.length);
        Log.d(TAG, "onSensorChanged: steps count is" + 
        event.values.length);
//        sendToPhone();
        sendData();
        updateNotification();
    }

    private void sendData(){

        if (mGoogleApiClient == null)
            return;

        // use the api client to send the heartbeat value to our 
        handheld
        final PendingResult<NodeApi.GetConnectedNodesResult> nodes = 
        Wearable.NodeApi.getConnectedNodes(mGoogleApiClient);
        nodes.setResultCallback(new 
        ResultCallback<NodeApi.GetConnectedNodesResult>() {
            @Override
            public void onResult(NodeApi.GetConnectedNodesResult 
            result) {
                final List<Node> nodes = result.getNodes();
                final String path = "/stepcount";
                String Message = StepsTaken.getSteps()+"";

                for (Node node : nodes) {
                    Log.d(TAG, "SEND MESSAGE TO HANDHELD: " + Message);
                    node.getDisplayName();
                    byte[] data = 
                    Message.getBytes(StandardCharsets.UTF_8);
                    Wearable.MessageApi.sendMessage(mGoogleApiClient, 
                    node.getId(), path, data);
                }
            }
        });
    }

    private void updateNotification() {
        // Create a notification builder that's compatible with 
        platforms >= version 4
        NotificationCompat.Builder builder =
                new NotificationCompat.Builder
                (getApplicationContext());

        // Set the title, text, and icon
        builder.setContentTitle(getString(R.string.app_name))
                .setSmallIcon(R.drawable.ic_step_icon);

        builder.setContentText("steps: " + StepsTaken.getSteps());

        // Get an instance of the Notification Manager
        NotificationManager notifyManager = (NotificationManager)
                getSystemService(Context.NOTIFICATION_SERVICE);

        // Build the notification and post it
        notifyManager.notify(0, builder.build());
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // drop these messages
        updateNotification();

    }
}

现在,最后,我们将在片段中更新步数计数器代码。在StepCounterFragment中,我们需要实现 Reporter 接口并创建处理程序实例和textview实例,如下所示:

public class StepCounterFragment extends Fragment implements Reporter {

    private TextView tv;
    private Handler handler = new Handler();

    public StepCounterFragment() {
        // Required empty public constructor
    }

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View rootView = inflater.inflate(R.layout.step_counter_fragment, 
    container, false); return rootView;
 }
}

现在,在oncreateView方法中,注册WearStepService并将步数计数器textview连接到我们创建的xml标签,如下所示:

getActivity().startService(new Intent(getActivity(), WearStepService.class));

tv = (TextView)rootView.findViewById(R.id.steps);
tv.setText(String.valueOf(StepsTaken.getSteps()));

当我们实现 Reporter 接口时,需要重写NotifyEvent方法,如下所示:

@Override
public void notifyEvent(final Object o) {
    handler.post(new Runnable() {
        @Override
        public void run() {
            if (o instanceof StepsTaken)
                tv.setText(String.valueOf(StepsTaken.getSteps()));
        }
    });

}

在片段生命周期的onResumeonPause方法中,注册和移除我们之前编写的StepsTaken类的事件接收观察者:

@Override
public void onResume() {
    EventReceiver.register(StepsTaken.class, this);
    super.onResume();
}

@Override
public void onPause() {
    EventReceiver.remove(StepsTaken.class, this);
    super.onPause();
}

完整的片段类如下所示:

public class StepCounterFragment extends Fragment implements Reporter {

    private TextView tv;
    private Handler handler = new Handler();

    public StepCounterFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup 
    container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View rootView =    
        inflater.inflate(R.layout.step_counter_fragment, 
        container, false);

        getActivity().startService(new Intent(getActivity(), 
        WearStepService.class));

        tv = (TextView)rootView.findViewById(R.id.steps);
        tv.setText(String.valueOf(StepsTaken.getSteps()));

        return rootView;
    }

    @Override
    public void notifyEvent(final Object o) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (o instanceof StepsTaken)
                    tv.setText(String.valueOf(StepsTaken.getSteps()));
            }
        });

    }

    @Override
    public void onResume() {
        EventReceiver.register(StepsTaken.class, this);
        super.onResume();
    }

    @Override
    public void onPause() {
        EventReceiver.remove(StepsTaken.class, this);
        super.onPause();
    }

}

我们已经成功完成了Stepcounter功能。不要忘记在MainActivity导航适配器中用下一个索引值附加此片段:

if(position==0) {
    final DrinkWaterFragment drinkWaterFragment = new DrinkWaterFragment();
    getFragmentManager()
            .beginTransaction()
            .replace(R.id.content_frame, drinkWaterFragment)
            .commit();

}else if(position == 1){
    final HeartRateFragment sectionFragment = new HeartRateFragment();
    getFragmentManager()
            .beginTransaction()
            .replace(R.id.content_frame, sectionFragment)
            .commit();
}else if(position == 2){
    final StepCounterFragment stepCounterFragment = new 
    StepCounterFragment();
    getFragmentManager()
            .beginTransaction()
            .replace(R.id.content_frame, stepCounterFragment)
            .commit();
}

完成所有片段后,应用将如下所示。

下面的屏幕截图显示了帮助用户启动提醒服务的饮水片段:

下面的屏幕截图显示了如何通过这些按钮启动和停止饮水提醒:

此图像展示了如何读取HeartRate传感器以及我们创建的HeartBeat动画:

此图像展示了显示步数的步数计数器屏幕:

总结

在本章中,我们学习了传感器、电池使用以及最佳实践。此外,我们还了解了休眠模式如何帮助节省电池和 CPU 周期。你已经学会了如何使用诸如为 Wear 设备制作片段、使用WearableNavigationLayout、使用WearableActionDrawer、使用服务和BroadcastReceivers以及使用传感器(例如光学HeartRate传感器和步数计数器)等想法来构建一个材料设计风格的 Wear 应用程序,还有步数计数器和饮水服务的通知,以及身体传感器的运行时权限。

在下一章中,我们预计将通过使用更多元素和功能,使这个应用程序更加稳定。

第五章:测量您的健康状态并同步收集的传感器数据

在上一章中,我们构建了一个提醒我们喝水的 Wear 应用,并通过内置的 Wear 传感器检查步数和心率。Wear 和移动应用的理念是提高浏览的便捷性,不错过任何重要信息,而 Upbeat 项目看似功能不多,但在用户的腕上和口袋中占有一席之地。目前 upbeat Wear 应用的功能仅限于显示从传感器接收到的数据。在本章中,我们将通过 Wear 和移动应用的互操作性来增强这个应用。我们将通过RealmDB持久化所有传输的数据。我们将从移动设备向 Wear 发送通知,启动应用以检查心率。我们将在 Wear 应用中拥有健康提示和食物卡路里卡片列表。

在本章中,我们将探讨以下内容:

  • 收集 Wear 传感器的数据

  • 处理接收到的数据以查找卡路里和距离

  • WearableListenerService和消息传递 API

  • 从移动应用向 Wear 应用发送数据

  • RealmDB集成

  • 使用CardViewWearableRecyclerview

收集 Wear 传感器的数据

从 Wear 设备收集传感器数据需要一个通信机制,而 Wearable DataLayer API 作为 Google Play 服务的一部分,在通信过程中扮演着重要角色。我们将在后续课程中深入探讨通信过程,但本章我们需要在移动应用中接收传感器数据。我们已经创建了一个项目,其中已经包含移动模块和简单的“Hello World”样板代码。当我们从 Wear 应用设置数据发送机制后,我们将处理移动模块。让我们从 Wear 模块的服务包中的步数传感器开始,进入WearStepService类。我们已经构建了这个服务,用于发送通知并监听步数计数器数据。现在,在GoogleApiClient和 Wear 消息传递 API 的帮助下,我们需要将数据发送到移动应用。

WearStepService类中,在类的全局范围内实例化GoogleApiClient

GoogleApiClient mGoogleApiClient;

onStartCommand中,调用初始化mGoogleApiClient的方法:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Log.d(TAG, "onStartCommand");

    getSensorManager();
    getCountSensor();
 getGoogleClient();

    return super.onStartCommand(intent, flags, startId);
}

为了初始化GoogleClient,我们将使用GoogleClient的构建器模式,并需要添加Wearable.API。然后,我们可以使用connect()方法后跟构建器的build()方法连接GoogleClient

private void getGoogleClient() {
    if (null != mGoogleApiClient)
        return;

    Log.d(TAG, "getGoogleClient");
    mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addApi(Wearable.API)
            .build();
    mGoogleApiClient.connect();
}

WearStepService类中,我们将重写属于IBinder接口的onBind方法。我们可以通过以下方式使用它进行远程服务的客户端交互:

@Override
public IBinder onBind(Intent intent) {
    return null;
}

我们返回 null,因为发送数据后我们不希望有任何返回。如果我们希望返回某些信息,那么我们可以按以下方式返回IBinder实例:

private final IBinder mBinder = new LocalBinder();

@Override
public IBinder onBind(Intent intent) {
 return mBinder;
}

为了能够向移动设备发送数据,我们需要两个 DataLayer API 机制,即可穿戴节点和消息 API。我们将使用 Node API 获取连接的节点。使用消息 API,我们将数据发送到特定路径,而在接收端,我们应该监听该路径以获取数据。

在 Node API 中,我们将有Resultcallback类,它返回ConnectedNodes的列表,我们必须实现具有返回连接节点列表能力的onResult方法。我们可以向所有连接的节点发送消息,或者只向已连接的节点发送。我们可以使用节点类的getDisplayname获取连接节点的名称,如下所示:

node.getDisplayName();

目前,我们将使用 Node 和 Message API 并将数据发送到连接的节点:

private void sendData(){

    if (mGoogleApiClient == null)
        return;

    // use the api client to send the heartbeat value to our handheld
    final PendingResult<NodeApi.GetConnectedNodesResult> nodes = 
    Wearable.NodeApi.getConnectedNodes(mGoogleApiClient);
    nodes.setResultCallback(new 
    ResultCallback<NodeApi.GetConnectedNodesResult>() {
        @Override
        public void onResult(NodeApi.GetConnectedNodesResult result) {
            final List<Node> nodes = result.getNodes();
            final String path = "/stepcount";
            String Message = StepsTaken.getSteps()+"";

            for (Node node : nodes) {
                Log.d(TAG, "SEND MESSAGE TO HANDHELD: " + Message);
                node.getDisplayName();
                byte[] data = Message.getBytes(StandardCharsets.UTF_8);
                Wearable.MessageApi.sendMessage(mGoogleApiClient, 
                node.getId(), path, data);
            }
        }
    });
}

在上一个方法中,我们将使用PendingResults类来获取连接节点的结果。在我们收到连接节点的列表后,我们可以使用wearableMessageApi类发送消息。别忘了发送和接收数据到相同的路径。

完成的 WearStepService 类

完整的WearStepService类代码如下:

public class WearStepService extends Service implements SensorEventListener {

    public static final String TAG = "WearStepService";
    private static final long THREE_MINUTES = 3 * 60 * 1000;
    private static final String STEP_COUNT_PATH = "/step-count";
    private static final String STEP_COUNT_KEY = "step-count";
    private SensorManager sensorManager;
    private Sensor countSensor;

    GoogleApiClient mGoogleApiClient;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
        setAlarm();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand");

        getSensorManager();
        getCountSensor();
        getGoogleClient();

        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    private void getGoogleClient() {
        if (null != mGoogleApiClient)
            return;

        Log.d(TAG, "getGoogleClient");
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addApi(Wearable.API)
                .build();
        mGoogleApiClient.connect();
    }

    /**
     * if the countSensor is null, try initializing it, and try               
     registering it with sensorManager
     */
    private void getCountSensor() {
        if (null != countSensor)
            return;

        Log.d(TAG, "getCountSensor");
        countSensor = 
        sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
        registerCountSensor();
    }

    /**
     * if the countSensor exists, then try registering
     */
    private void registerCountSensor() {
        if (countSensor == null)
            return;

        Log.d(TAG, "sensorManager.registerListener");
        sensorManager.registerListener(this, countSensor, 
        SensorManager.SENSOR_DELAY_UI);
    }

    /**
     * if the sensorManager is null, initialize it, and try registering 
     the countSensor
     */
    private void getSensorManager() {
        if (null != sensorManager)
            return;

        Log.d(TAG, "getSensorManager");
        sensorManager = (SensorManager) 
        getSystemService(Context.SENSOR_SERVICE);
        registerCountSensor();
    }

    private void setAlarm() {
        Log.d(TAG, "setAlarm");

        Intent intent = new Intent(this, AlarmNotification.class);
        PendingIntent pendingIntent = 
        PendingIntent.getBroadcast(this.getApplicationContext(), 
        234324243, intent, 0);
        AlarmManager alarmManager = (AlarmManager) 
        getSystemService(ALARM_SERVICE);
        long firstRun = System.currentTimeMillis() + THREE_MINUTES;
        alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, 
        firstRun, THREE_MINUTES, pendingIntent);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_STEP_COUNTER)
            StepsTaken.updateSteps(event.values.length);
        Log.d(TAG, "onSensorChanged: steps count is" + 
        event.values.length);
//        sendToPhone();
        sendData();
        updateNotification();
    }

    private void sendData(){

        if (mGoogleApiClient == null)
            return;

        // use the api client to send the heartbeat value to our 
        handheld
        final PendingResult<NodeApi.GetConnectedNodesResult> nodes = 
        Wearable.NodeApi.getConnectedNodes(mGoogleApiClient);
        nodes.setResultCallback(new 
        ResultCallback<NodeApi.GetConnectedNodesResult>() {
            @Override
            public void onResult(NodeApi.GetConnectedNodesResult 
            result) {
                final List<Node> nodes = result.getNodes();
                final String path = "/stepcount";
                String Message = StepsTaken.getSteps()+"";

                for (Node node : nodes) {
                    Log.d(TAG, "SEND MESSAGE TO HANDHELD: " + Message);
                    node.getDisplayName();
                    byte[] data = 
                    Message.getBytes(StandardCharsets.UTF_8);
                    Wearable.MessageApi.sendMessage(mGoogleApiClient, 
                    node.getId(), path, data);
                }
            }
        });
    }

    private void updateNotification() {
        // Create a notification builder that's compatible with 
        platforms >= version 4
        NotificationCompat.Builder builder =
                new NotificationCompat.Builder
                (getApplicationContext());

        // Set the title, text, and icon
        builder.setContentTitle(getString(R.string.app_name))
                .setSmallIcon(R.drawable.ic_step_icon);

        builder.setContentText("steps: " + StepsTaken.getSteps());

        // Get an instance of the Notification Manager
        NotificationManager notifyManager = (NotificationManager)
                getSystemService(Context.NOTIFICATION_SERVICE);

        // Build the notification and post it
        notifyManager.notify(0, builder.build());
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // drop these messages
        updateNotification();

    }
}

我们成功地向提到的路径发送了消息。现在,让我们看看如何从 Wear 设备接收消息。在移动模块内部,为代码可读性创建额外的包。我们将把包命名为 models、services 和 utils。

是时候创建一个扩展了WearableListenerService并覆盖了onMessageReceived方法的类了。创建一个名为StepListener的类,并让它扩展WearableListenerService;代码如下:

public class StepListner extends WearableListenerService {

    private static final String TAG = "StepListner";

    @Override
    public void onMessageReceived(MessageEvent messageEvent) {
        if (messageEvent.getPath().equals("/stepcount")) {
            final String message = new String(messageEvent.getData());
            Log.v(TAG, "Message path received from wear is: " + 
            messageEvent.getPath());
            Log.v(TAG, "Message received on watch is: " + message);

            // Broadcast message to wearable activity for display
            Intent messageIntent = new Intent();
            messageIntent.setAction(Intent.ACTION_SEND);
            messageIntent.putExtra("message", message);
            LocalBroadcastManager.getInstance(this)
            .sendBroadcast(messageIntent);
        }
        else {
            super.onMessageReceived(messageEvent);
        }
    }
}

在清单文件中使用与发送数据相同的路径注册之前的服务类,以下代码说明了可穿戴设备的DATA_CHANGEDMESSAGE_RECEIVED动作以及数据路径:

<service android:name=".services.StepListner">
    <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="/stepcount"
            android:scheme="wear" />
    </intent-filter>
</service>

Steplistner类已完成;我们可以使用这个类进一步处理数据。在steplistener类中,我们注册了localbroadcast接收器类,以在广播接收器的作用域内发送接收到的数据。在我们构建用户界面之前,我们将在移动应用程序的MainActivity中接收所有数据。编写一个内部类以读取收到的步骤:

public class StepReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String message = intent.getStringExtra("message");
            Log.v("steps", "Main activity received message: " + 
            message);

        // Shows the step counts received by the wearlistener //service        
            mSteps.setText("Steps:"+ message);
            int value = Integer.valueOf(message);
      }
    }

使用以下代码在oncreate方法中注册该类:

// Register the local broadcast receiver
IntentFilter StepFilter = new IntentFilter(Intent.ACTION_SEND);
StepReceiver StepReceiver = new StepReceiver();
LocalBroadcastManager.getInstance(this).registerReceiver(StepReceiver, StepFilter);

我们成功收集了步数计数器数据。让我们用同样的过程来收集脉搏速率。我们将持久化步数计数,稍后,我们将实时传输连接节点的心跳脉搏速率的实时流。

切换回 Wear 模块

将项目范围切换到 Wear 模块,并选择HeartRateFragment以实例化GoogleClient对象:

private GoogleApiClient mGoogleApiClient;

oncreate方法中初始化GoogleClient实例,如下所示:

mGoogleApiClient = new GoogleApiClient.Builder(getActivity()).
addApi(Wearable.API).
build();

mGoogleApiClient.connect();

编写一个将脉搏速率计数发送到连接节点的方法,就像我们之前为步数计数器所做的那样:

private void sendMessageToHandheld(final String message) {

    if (mGoogleApiClient == null)
        return;

    // use the api client to send the heartbeat value to our handheld
    final PendingResult<NodeApi.GetConnectedNodesResult> nodes = 
    Wearable.NodeApi.getConnectedNodes(mGoogleApiClient);
    nodes.setResultCallback(new 
    ResultCallback<NodeApi.GetConnectedNodesResult>() {
        @Override
        public void onResult(NodeApi.GetConnectedNodesResult result) {
            final List<Node> nodes = result.getNodes();
            final String path = "/heartRate";

            for (Node node : nodes) {
                Log.d(TAG, "SEND MESSAGE TO HANDHELD: " + message);

                byte[] data = message.getBytes(StandardCharsets.UTF_8);
                Wearable.MessageApi.sendMessage(mGoogleApiClient, 
                node.getId(), path, data);
            }
        }
    });
}

onSensorchanged回调内部调用方法,并从传感器事件触发中接收 BPM 计数:

sendMessageToHandheld(currentValue.toString());

切换到移动项目范围。我们需要一个WearableListenerService类来与心率数据进行通信:

public class HeartListener extends WearableListenerService {

    @Override
    public void onMessageReceived(MessageEvent messageEvent) {

    }

}

onMessageReceived回调中注册一个localbroadcast事件,以在活动中接收数据。完整的监听器类代码如下:

public class HeartListener extends WearableListenerService {

    @Override
    public void onMessageReceived(MessageEvent messageEvent) {
        if (messageEvent.getPath().equals("/heartRate")) {
            final String message = new String(messageEvent.getData());
            Log.v("pactchat", "Message path received on watch is: " + 
            messageEvent.getPath());
            Log.v("packtchat", "Message received on watch is: " + 
            message);

            // Broadcast message to wearable activity for display
            Intent messageIntent = new Intent();
            messageIntent.setAction(Intent.ACTION_SEND);
            messageIntent.putExtra("heart", message);
            LocalBroadcastManager.getInstance(this)
            .sendBroadcast(messageIntent);
        }
        else {
            super.onMessageReceived(messageEvent);
        }
    }
}

在 Manifest 中注册service类如下:

<service android:name=".services.HeartListener">
    <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="/heartRate"
            android:scheme="wear" />
    </intent-filter>
</service>

MainActivity中,我们将编写另一个广播接收器。我们称之为HeartRateReceiver

 public class HeartRateReciver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String data = intent.getStringExtra("heart");
            Log.v("heart", "Main activity received message: " + 
            message);

            mHeart.setText(message);

        }
    }

oncreate方法中如下注册BroadcastReceiver

// Register the local broadcast receiver
IntentFilter DataFilter = new IntentFilter(Intent.ACTION_SEND);
HeartRateReciver DataReceiver = new HeartRateReciver();
LocalBroadcastManager.getInstance(this).registerReceiver(DataReceiver, DataFilter);

我们已经成功从HeartListener直接接收心率数据到broadcastreceiver。现在,让我们处理移动项目的用户界面。我们需要保持 UI 简单而强大;以下设计涉及到与 Wear 应用的互操作性以及距离和卡路里消耗预测。

概念化应用程序

Upbeat 移动应用程序应显示步数和心率。Upbeat 需要向 Wear 应用发送心率请求。历史记录显示从数据库中消耗的距离和卡路里。重置将清除数据库。

着陆页:当用户打开应用时,他将看到类似于以下设计的内容:

图片

在开始设计之前,我们需要确定一些事情,比如颜色、背景等。在res/values目录中,打开colors.xml文件,并添加以下颜色值:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#607d8b</color>
    <color name="colorPrimaryDark">#34515e</color>
    <color name="colorAccent">#FFF</color>
    <color name="grey">#afaeae</color>
    <color name="white">#fff</color>
</resources>

创建一个名为button_bg.xmldrawable资源文件,并添加以下选择器代码以选择背景:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/colorPrimaryDark" 
    android:state_pressed="true"/>
    <item android:drawable="@color/grey" android:state_focused="true"/>
    <item android:drawable="@color/colorPrimary"/>
</selector>

activity_main.xml中,根据设计和计划的功能,我们需要三个按钮和三个文本视图。我们将使用相对布局作为根容器,以下代码解释了如何操作:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.packt.upbeat.MainActivity">

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginBottom="102dp"
        android:layout_above="@+id/calory"
        android:layout_centerHorizontal="true">

        <TextView
            android:id="@+id/steps"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@+id/linearLayout"
            android:layout_toStartOf="@+id/calory"
            android:layout_weight="1"
            android:text="Steps!"
            android:textColor="@color/colorPrimaryDark"
            android:textSize="30sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <LinearLayout
            android:layout_weight="1"
            android:id="@+id/linearLayout"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center">

            <ImageView
                android:id="@+id/heartbeat"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignStart="@+id/heart"
                android:layout_below="@+id/heart"/>

            <TextView
                android:id="@+id/heart"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/steps"
                android:layout_alignBottom="@+id/steps"
                android:layout_alignParentEnd="true"
                android:layout_marginEnd="25dp"
                android:text="Heart!"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="30sp"
                android:textStyle="bold" />
        </LinearLayout>

    </LinearLayout>

    <TextView
        android:id="@+id/calory"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Calories!"
        android:textColor="@color/colorPrimary"
        android:textSize="20sp"
        android:textStyle="bold"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentStart="true"
        android:orientation="horizontal">

        <android.support.v7.widget.AppCompatButton
            android:id="@+id/reset"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:background="@drawable/button_background"
            android:elevation="5dp"
            android:gravity="center"
            android:text="Reset"
            android:textAllCaps="true"
            android:textColor="@color/white"
            android:textStyle="bold" />

        <android.support.v7.widget.AppCompatButton
            android:id="@+id/history"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:background="@drawable/button_background"
            android:elevation="5dp"
            android:gravity="center"
            android:text="History"
            android:textAllCaps="true"
            android:textColor="@color/white"
            android:textStyle="bold" />

        <android.support.v7.widget.AppCompatButton
            android:id="@+id/pulseRequest"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:background="@drawable/button_background"
            android:elevation="5dp"
            android:gravity="center"
            android:text="Request pulse"
            android:textAllCaps="true"
            android:textColor="@color/white"
            android:textStyle="bold" />
    </LinearLayout>
</RelativeLayout>

为了显示心率,我们有一个带有ImageviewTextviewLinearLayour,其中imageview将保持静态。相反,用我们在 Wear 模块中创建的HeartBeatView替换imageview,以实现心形自定义动画。让我们最后一次创建它。

res/values文件夹内,添加heartbeatview_attrs.xml文件,并添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="HeartBeatView">
        <attr name="scaleFactor" format="float" />
        <attr name="duration" format="integer" />
    </declare-styleable>

</resources>

在 drawables 中,创建一个矢量图形 XML 文件,并在其中添加以下代码以实现心形:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0">
    <path
        android:fillColor="#FFFF0000"
        android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 
        2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 
        14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 
       -8.55,11.54L12,21.35z"/>
</vector>

我们可以在 utils 包内创建一个名为HeartBearView的类,并添加以下代码以处理所有动画和自定义视图逻辑。关于实现的更多细节,可以参考前一章中的 Wear 模块HeartBeatView类:

public class HeartBeatView extends AppCompatImageView {

    private static final String TAG = "HeartBeatView";

    private static final float DEFAULT_SCALE_FACTOR = 0.2f;
    private static final int DEFAULT_DURATION = 50;
    private Drawable heartDrawable;

    private boolean heartBeating = false;

    float scaleFactor = DEFAULT_SCALE_FACTOR;
    float reductionScaleFactor = -scaleFactor;
    int duration = DEFAULT_DURATION;

    public HeartBeatView(Context context) {
        super(context);
        init();
    }

    public HeartBeatView(Context context, AttributeSet attrs) {
        super(context, attrs);
        populateFromAttributes(context, attrs);
        init();
    }

    public HeartBeatView(Context context, AttributeSet attrs, int 
    defStyleAttr) {
        super(context, attrs, defStyleAttr);
        populateFromAttributes(context, attrs);
        init();
    }

    private void init() {
        //make this not mandatory
        heartDrawable = ContextCompat.getDrawable(getContext(), 
        R.drawable.ic_heart_red_24dp);
        setImageDrawable(heartDrawable);

    }

    private void populateFromAttributes(Context context, AttributeSet 
    attrs) {
        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.HeartBeatView,
                0, 0
        );
        try {
            scaleFactor = a.getFloat(R.styleable
            .HeartBeatView_scaleFactor, DEFAULT_SCALE_FACTOR);
            reductionScaleFactor = -scaleFactor;
            duration = a.getInteger(R.styleable.HeartBeatView_duration, 
            DEFAULT_DURATION);

        } finally {
            a.recycle();
        }

    }

    /**
     * toggles current heat beat state
     */
    public void toggle() {
        if (heartBeating) {
            stop();
        } else {
            start();
        }
    }

    /**
     * Starts the heat beat/pump animation
     */
    public void start() {
        heartBeating = true;
        animate().scaleXBy(scaleFactor)
        .scaleYBy(scaleFactor).setDuration(duration)
        .setListener(scaleUpListener);
    }

    /**
     * Stops the heat beat/pump animation
     */
    public void stop() {
        heartBeating = false;
        clearAnimation();
    }

    /**
     * is the heart currently beating
     *
     * @return
     */
    public boolean isHeartBeating() {
        return heartBeating;
    }

    public int getDuration() {
        return duration;
    }

    private static final int milliInMinute = 60000;

    /**
     * set the duration of the beat based on the beats per minute
     *
     * @param bpm (positive int above 0)
     */
    public void setDurationBasedOnBPM(int bpm) {
        if (bpm > 0) {
            duration = Math.round((milliInMinute / bpm) / 3f);
        }
    }

    public void setDuration(int duration) {
        this.duration = duration;
    }

    public float getScaleFactor() {
        return scaleFactor;
    }

    public void setScaleFactor(float scaleFactor) {
        this.scaleFactor = scaleFactor;
        reductionScaleFactor = -scaleFactor;
    }

    private final Animator.AnimatorListener scaleUpListener = new 
    Animator.AnimatorListener() {

        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            //we ignore heartBeating as we want to ensure the heart is 
            reduced back to original size
            animate().scaleXBy(reductionScaleFactor)
            .scaleYBy(reductionScaleFactor).setDuration(duration)
            .setListener(scaleDownListener);
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }
    };

    private final Animator.AnimatorListener scaleDownListener = new 
    Animator.AnimatorListener() {

        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            if (heartBeating) {
                //duration twice as long for the upscale
                animate().scaleXBy(scaleFactor).scaleYBy(scaleFactor)
                .setDuration
                (duration * 2).setListener(scaleUpListener);
            }
        }

        @Override
        public void onAnimationCancel(Animator animation) {
        }
    };
}

activity_main.xml文件中,用项目中创建的自定义视图替换ImageView代码:

<com.packt.upbeat.utils.HeartBeatView
    android:id="@+id/heartbeat"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignStart="@+id/heart"
    android:layout_below="@+id/heart"/>

既然我们的着陆页用户界面已经准备好了,我们可以开始处理MainActivity

MainActivity中,让我们实例化我们在布局中使用的所有 UI 组件:

private AppCompatButton mReset, mHistory, mHeartPulse;
private TextView mSteps, mHeart, mCalory;
private HeartBeatView heartbeat;

oncreate方法中使用findviewbyid方法将组件与其 ID 进行映射:

heartbeat = (HeartBeatView)findViewById(R.id.heartbeat);
mSteps = (TextView) findViewById(R.id.steps);
mHeart = (TextView) findViewById(R.id.heart);
mCalory = (TextView) findViewById(R.id.calory);
mReset = (AppCompatButton) findViewById(R.id.reset);
mHistory = (AppCompatButton) findViewById(R.id.history);
mHeartPulse = (AppCompatButton) findViewById(R.id.pulseRequest);

HeartRateReceiver类中,获取数据,将数据转换为整数,并在 UI 中显示。以下代码说明如何使用从 Wear 应用接收的数据激活HeartBeatAnimation

 @Override
        public void onReceive(Context context, Intent intent) {
            String data = intent.getStringExtra("heart");
            Log.v("heart", "Main activity received message: " + data);

            mHeart.setText(data);
            heartbeat.setDurationBasedOnBPM(Integer.valueOf(data));
            heartbeat.toggle();
        }

StepReceiver中,我们将数据设置为标记为mSteps的步数textview

mSteps.setText("Steps:"+ message);

我们已经完成了接收脉搏数和步数并在手机的 UI 中显示。现在,我们需要显示这些步骤消耗的卡路里。

根据您的身体质量指数等,可以通过多种不同方法从步数计算消耗的卡路里。关于计步器步数到卡路里的研究引入了一个转换因子,如下所示:

转换因子 = 每英里 99.75 卡路里 / 每英里 2,200 步 = 每步 0.045 卡路里

因此,使用这个值,我们可以通过简单地将这个值与步数相乘来确定卡路里。

消耗的卡路里 = 7,000 步 x 每步 0.045 卡路里 = 318 卡路里

StepReceiver类中,在onReceive方法内,添加以下代码:

int value = Integer.valueOf(message);
mCalory.setText(String.valueOf((int)(value * 0.045)) + "/ncalories" + "/nburnt");

通过手机完成的卡路里消耗和脉搏率检查。在MainActivity中我们还有更多工作要做。我们需要将步数计数器的数据持久化,以显示卡路里和距离的历史记录。让我们使用第一章尝试过的RealmDB

将以下 classpath 添加到项目级别的 gradle 文件中:

classpath "io.realm:realm-gradle-plugin:2.2.1"

在 gradle 移动模块中应用上一个插件:

apply plugin: 'realm-android'

项目中的 Realm 已准备就绪。现在,我们需要为步数数据设置 setters 和 getters。将以下类添加到 models 包中:

public class StepCounts extends RealmObject {

    private String ReceivedDateTime;
    private String Data;

    public String getReceivedDateTime() {
        return ReceivedDateTime;
    }

    public void setReceivedDateTime(String receivedDateTime) {
        ReceivedDateTime = receivedDateTime;
    }

    public String getData() {
        return Data;
    }

    public void setData(String data) {
        Data = data;
    }
}

MainActivity中,实例化 Realm 并在onCreate方法中初始化,如下所示:

private Realm realm;

@Override
protected void onCreate(Bundle savedInstanceState) {
...
Realm.init(this);
realm = Realm.getDefaultInstance();

}

当接收到步数时,将数据添加到RealmDB中。在StepReceiver内部类的onReceive方法内添加以下代码:

realm.beginTransaction();
StepCounts Steps = realm.createObject(StepCounts.class);
Steps.setData(message);
String TimeStamp =  DateFormat.getDateTimeInstance().format(System.currentTimeMillis());
Steps.setReceivedDateTime(TimeStamp);
realm.commitTransaction();

为了在 UI 中显示最后一个值,在onCreate方法中添加以下代码:

RealmResults<StepCounts> results = realm.where(StepCounts.class).findAll();

if(results.size() == 0){
    mSteps.setText("Steps: ");
}else{
    mSteps.setText("Steps: "+results.get(results.size()-1).getData());
    int value = Integer.valueOf(results
    .get(results.size()-1).getData());
    mCalory.setText(String.valueOf((int)(value * 0.045)) 
    + "/ncalories" + "/nburnt");
}

对于按钮,现在将点击监听器附加到onCreate方法中:

mHistory.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

    }
});

mHeartPulse.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

    }
});

mReset.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

    }
});

让我们创建另一个 Activity,并将其命名为HistoryActivity,它将显示接收到的数据列表。在activity_history.xml文件中,添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.packt.upbeat.HistoryActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="5dp" />

</LinearLayout>

现在,我们需要为recyclerview中的每个项目创建row_layout,布局如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="0dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:layout_marginTop="9dp"
        card_view:cardCornerRadius="3dp"
        card_view:cardElevation="0.01dp">

        <LinearLayout
            android:layout_margin="10dp"
            android:orientation="vertical"
            android:id="@+id/top_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:layout_margin="10dp"
                android:id="@+id/steps"
                android:text="Steps"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
            <TextView
                android:layout_margin="10dp"
                android:id="@+id/calories"
                android:text="calory"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
            <TextView
                android:layout_margin="10dp"
                android:id="@+id/distance"
                android:text="distance"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <TextView
                android:layout_margin="10dp"
                android:id="@+id/date"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:layout_gravity="bottom"
                android:background="#ff444444"
                android:gravity="center"
                android:text="Timestamp"
                android:textColor="#fff"
                android:textSize="20dp" />

        </LinearLayout>
    </android.support.v7.widget.CardView>
</LinearLayout>

请记住,在使用cardviewrecyclerview之前,我们需要将依赖项添加到我们的 gradle 模块中:

compile 'com.android.support:cardview-v7:25.1.1'

compile 'com.android.support:recyclerview-v7:25.1.1'

Recyclerview 适配器

我们将不得不创建一个adapter类,它从 Realm 获取数据并适配到创建的row_layout

public class HistoryAdapter extends RecyclerView.Adapter<HistoryAdapter.ViewHolder> {

    public List<StepCounts> steps;
    public Context mContext;

    public HistoryAdapter(List<StepCounts> steps, Context mContext) {
        this.steps = steps;
        this.mContext = mContext;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.row_item, viewGroup, false);
        ViewHolder viewHolder = new ViewHolder(v);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int i) {
        viewHolder.steps.setText(steps.get(i).getData()+" Steps");
        viewHolder.date.setText(steps.get(i).getReceivedDateTime());

        int value = Integer.valueOf(steps.get(i).getData());
        DecimalFormat df = new DecimalFormat("#.00") ;
        String kms = String.valueOf(df.format(value * 0.000762)) + " 
        kms" ;
        viewHolder.calory.setText(String.valueOf((int)(value * 0.045)) 
        + " calories " + "burnt");
        viewHolder.distance.setText("Distance: "+kms);

    }

    @Override
    public int getItemCount() {
        return steps.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder  {

        public TextView steps,calory,distance,date;

        public ViewHolder(View itemView) {
            super(itemView);
            steps = (TextView) itemView.findViewById(R.id.steps);
            calory = (TextView) itemView.findViewById(R.id.calories);
            distance = (TextView) itemView.findViewById(R.id.distance);
            date = (TextView) itemView.findViewById(R.id.date);
        }
    }

}

在适配器中,我们使用转换因子值显示消耗的卡路里。为了找到通用距离,我们还有另一个值,需要将步数乘以它,如适配器所示。

HistoryActivity中,在类全局范围内,声明以下实例:

Realm realm;
RecyclerView mRecyclerView;
RecyclerView.LayoutManager mLayoutManager;
RecyclerView.Adapter mAdapter;

现在,在HistoryActivity类的onCreate方法中,添加以下代码:

mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);

Realm.init(this);
realm = Realm.getDefaultInstance();
RealmResults<StepCounts> results = realm.where(StepCounts.class).findAll();
// The number of Columns
mLayoutManager = new GridLayoutManager(this, 1);
mRecyclerView.setLayoutManager(mLayoutManager);

mAdapter = new HistoryAdapter(results,HistoryActivity.this);
mRecyclerView.setAdapter(mAdapter);

完成的HistoryActivity

完整的类代码如下所示:

public class HistoryActivity extends AppCompatActivity {

    Realm realm;
    RecyclerView mRecyclerView;
    RecyclerView.LayoutManager mLayoutManager;
    RecyclerView.Adapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_history);
        // Calling the RecyclerView
        mRecyclerView = (RecyclerView) 
        findViewById(R.id.recycler_view);
        mRecyclerView.setHasFixedSize(true);

        Realm.init(this);
        realm = Realm.getDefaultInstance();
        RealmResults<StepCounts> results = 
        realm.where(StepCounts.class).findAll();
        // The number of Columns
        mLayoutManager = new GridLayoutManager(this, 1);
        mRecyclerView.setLayoutManager(mLayoutManager);

        mAdapter = new HistoryAdapter(results,HistoryActivity.this);
        mRecyclerView.setAdapter(mAdapter);
    }
}

MainActivity中,当点击mHistory按钮时启动historyActivity

mHistory.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(new Intent(MainActivity.this, 
        HistoryActivity.class));
    }
});

现在,是时候使用我们在 Wear 中使用的方法,将数据从移动设备发送到 Wear 设备了。

我们将创建一个扩展了Thread的类,并使用 Node 和 Message API,以下方式发送数据:

class SendToDataLayerThread extends Thread {
    String path;
    String message;

    // Constructor to send a message to the data layer
    SendToDataLayerThread(String p, String msg) {
        path = p;
        message = msg;
    }

    public void run() {
        NodeApi.GetConnectedNodesResult nodes = 
        Wearable.NodeApi.getConnectedNodes(googleClient).await();
        for (Node node : nodes.getNodes()) {
            MessageApi.SendMessageResult result =  
            Wearable.MessageApi.sendMessage(googleClient, 
            node.getId(), path, message.getBytes()).await();
            if (result.getStatus().isSuccess()) {
                Log.v("myTag", "Message: {" + message + "} sent to: " + 
                node.getDisplayName());
            } else {
                // Log an error
                Log.v("myTag", "ERROR: failed to send Message");
            }
        }
    }
}

mHeartPulse按钮点击监听器内部,按如下方式启动SendToDataLayerThread类:

mHeartPulse.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new SendToDataLayerThread("/heart", "Start upbeat for heart 
        rate").start();
    }
});

现在,切换回 Wear 项目范围,并添加一个扩展了WearableListenerService的新类。当它从移动应用接收到消息时,触发一个通知来启动应用程序。完整的类代码如下:

public class MobileListener extends WearableListenerService {

    @Override
    public void onMessageReceived(MessageEvent messageEvent) {

        if (messageEvent.getPath().equals("/heart")) {
            final String message = new String(messageEvent.getData());
            Log.v("myTag", "Message path received on watch is: " + 
            messageEvent.getPath());
            Log.v("myTag", "Message received on watch is: " + message);

            // Broadcast message to wearable activity for display
            Intent messageIntent = new Intent();
            messageIntent.setAction(Intent.ACTION_SEND);
            messageIntent.putExtra("message", message);
            LocalBroadcastManager.getInstance(this)
            .sendBroadcast(messageIntent);

            Intent intent2 = new Intent
            (getApplicationContext(), MainActivity.class);

            PendingIntent pendingIntent = PendingIntent.getActivity
            (getApplicationContext(), 0, intent2,
                    PendingIntent.FLAG_ONE_SHOT);

            Uri defaultSoundUri = RingtoneManager.getDefaultUri
            (RingtoneManager.TYPE_ALARM);

            NotificationCompat.Builder notificationBuilder = 
            (NotificationCompat.Builder) new 
            NotificationCompat.Builder(getApplicationContext())
                    .setAutoCancel(true)   //Automatically delete the 
                    notification
                    .setSmallIcon(R.drawable.ic_heart_icon) 
                    //Notification icon
                    .setContentIntent(pendingIntent)
                    .setContentTitle("Open upbeat")
                    .setContentText("UpBeat to check the pulse")
                    .setCategory(Notification.CATEGORY_REMINDER)
                    .setPriority(Notification.PRIORITY_HIGH)
                    .setSound(defaultSoundUri);

            NotificationManagerCompat notificationManager =    
            NotificationManagerCompat.from
            (getApplicationContext());
            notificationManager.notify(0, notificationBuilder.build());

        }
        else {
            super.onMessageReceived(messageEvent);
        }
    }
}

现在,在清单文件中使用正确的路径注册之前提到的服务,使用以下代码为移动应用注册:

<service android:name=".services.MobileListener">
    <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="/heart"
            android:scheme="wear" />
    </intent-filter>
</service>

让我们切换回移动项目范围,完成重置按钮点击事件。我们将编写一个方法,该方法刷新RealmDB数据并重新创建活动:

public void Reset(){
    RealmResults<StepCounts> results = 
    realm.where(StepCounts.class).findAll();

    realm.beginTransaction();

    results.deleteAllFromRealm();

    realm.commitTransaction();
}

在点击监听器内部,以下方式添加以下方法:

mReset.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Reset();
        recreate();
    }
});

切换到 Wear 项目范围,并为健康建议创建一个新的 Activity,我们将这个活动称为HealthTipsActivity。在这个屏幕上,我们将列出一些好的健康建议和提示。

activity_health_tips.xml中,添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="5dp"
    app:layout_box="all"
    tools:deviceIds="wear">

    <android.support.wearable.view.WearableRecyclerView
        android:id="@+id/wearable_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</android.support.wearable.view.BoxInsetLayout>

我们需要为建议活动添加一个更多布局的行项目。我们将这个布局称为health_tips_row.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:tag="cards main container">

    <android.support.v7.widget.CardView 
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        card_view:cardBackgroundColor="@color/colorPrimary"
        card_view:cardCornerRadius="10dp"
        card_view:cardElevation="5dp"
        card_view:cardUseCompatPadding="true">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="12dp"
                android:layout_weight="2"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/health_tip"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_marginTop="10dp"
                    android:text="HealthTip"
                    android:textColor="@color/white"
                    android:textAppearance="?
                    android:attr/textAppearanceLarge" />

                <TextView
                    android:id="@+id/tip_details"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_marginTop="10dp"
                    android:text="Details"
                    android:textColor="@color/white"
                    android:textAppearance="?
                    android:attr/textAppearanceMedium" />

        </LinearLayout>

    </android.support.v7.widget.CardView>

</LinearLayout>

创建一个包含所需字段的模型。我们将为所有字段创建带有完整参数化构造函数的设置器和获取器:

public class HealthTipsItem {

    public String Title;
    public String MoreInfo;

    public HealthTipsItem(String title, String moreInfo) {
        Title = title;
        MoreInfo = moreInfo;
    }

    public String getTitle() {
        return Title;
    }

    public void setTitle(String title) {
        Title = title;
    }

    public String getMoreInfo() {
        return MoreInfo;
    }

    public void setMoreInfo(String moreInfo) {
        MoreInfo = moreInfo;
    }
}

我们将有一个保存所有健康建议的另一个数据类:

public class HealthTips {

   public static String[] nameArray =
           {"Food style",
                   "Food style",
                   "Food style",
                   "Drinking water",
                   "Unhealthy drinks",
                   "Alcohol and drugs",
                   "Body Mass index",
                   "Physical excercise",
                   "Physical activities",
                   "Meditation",
                   "Healthy signs"};

    public static String[] versionArray = {
            "Along with fresh vegetables and fruits, eat lean meats (if 
            you're not vegetarian), nuts, and seeds.",
            "Opt for seasonal and local products instead of those 
            exotic imported foodstuff",
            "Make sure you get a proper balanced diet, as often as 
            possible",
            "Drink water - you need to stay hydrated. It is great for 
            your internal organs, and it also keeps your skin healthy 
            and diminishes acne",
            "Stop drinking too much caffeine and caffeinated 
            beverages",
            "Limit alcohol intake. Tobacco and drugs should be a firm 
            No",
            "Maintain a healthy weight.",
            "Exercise at least four days a week for 20 to 30 minutes 
            each day. Another option is to break your workouts into 
            several sessions",
            "Try to have as much physical activity as you can. Take the 
             stairs instead of elevator; walk to the market instead of 
             taking your car etc",
            "Practice simple meditation. It balances your body, mind, 
            and soul",
            "When speaking about health tips, skin, teeth, hair, and 
            nails are all health signs. Loss of hair or fragile nails 
            might mean poor nutrition"};

}

现在,我们将创建一个适配器来处理健康建议列表。以下代码获取数据并在wearablerecyclerview中加载:

public class RecyclerViewAdapter
        extends WearableRecyclerView.Adapter
        <RecyclerViewAdapter.ViewHolder> {

    private List<HealthTipsItem> mListTips = new ArrayList<>();
    private Context mContext;

    public RecyclerViewAdapter(List<HealthTipsItem> mListTips, Context 
    mContext) {
        this.mListTips = mListTips;
        this.mContext = mContext;
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        private TextView Title, info;

        ViewHolder(View view) {
            super(view);
            Title = (TextView) view.findViewById(R.id.health_tip);
            info = (TextView) view.findViewById(R.id.tip_details);
        }
    }

    @Override
    public RecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup 
    parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.health_tips_row, parent, false);

        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {

        holder.Title.setText(mListTips.get(position).getTitle());
        holder.info.setText(mListTips.get(position).getMoreInfo());
    }

    @Override
    public int getItemCount() {
        return mListTips.size();
    }
}

在活动的全局范围内,声明以下实例:

private RecyclerViewAdapter mAdapter;
private List<HealthTipsItem> myDataSet = new ArrayList<>();

oncreate方法内部,我们可以通过添加以下代码来完成应用:

WearableRecyclerView recyclerView = (WearableRecyclerView) findViewById(R.id.wearable_recycler_view);
recyclerView.setHasFixedSize(true);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(mLayoutManager);

myDataSet = new ArrayList<HealthTipsItem>();
for (int i = 0; i < HealthTips.nameArray.length; i++) {
    myDataSet.add(new HealthTipsItem(
            HealthTips.nameArray[i],
            HealthTips.versionArray[i]
    ));
}

mAdapter = new RecyclerViewAdapter(myDataSet,HealthTipsActivity.this);
recyclerView.setAdapter(mAdapter);

让我们创建另一个 Activity,用于通用卡路里图表,从国际食物列表中调用活动CalorychartActivity

CaloryChartActivity布局文件中,我们将添加WearableRecyclerView组件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="5dp"
    app:layout_box="all"
    tools:deviceIds="wear">

    <android.support.wearable.view.WearableRecyclerView
        android:id="@+id/wearable_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</android.support.wearable.view.BoxInsetLayout>

为每个卡路里图表项目创建另一个布局,并添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:tag="cards main container">

    <android.support.v7.widget.CardView 
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        card_view:cardBackgroundColor="@color/colorPrimary"
        card_view:cardCornerRadius="10dp"
        card_view:cardElevation="5dp"
        card_view:cardUseCompatPadding="true">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="12dp"
                android:layout_weight="2"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/health_tip"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_marginTop="10dp"
                    android:text="calory"
                    android:textColor="@color/white"
                    android:textAppearance="?
                    android:attr/textAppearanceLarge" />

        </LinearLayout>

    </android.support.v7.widget.CardView>

</LinearLayout>

我们将以下面的方式为卡路里创建model类:

public class CaloryItem {

    public String Calories;

    public CaloryItem(String calories) {
        Calories = calories;
    }

    public String getCalories() {
        return Calories;
    }

    public void setCalories(String calories) {
        Calories = calories;
    }
}

我们将创建另一个卡路里图表的适配器。该适配器与HealthTips适配器类似。创建一个文件RecyclerViewCaloryAdapter并将以下代码添加到其中:

public class RecyclerViewCaloryAdapter
        extends WearableRecyclerView.Adapter<RecyclerViewCaloryAdapter.ViewHolder> {

    private List<CaloryItem> mCalory = new ArrayList<>();
    private Context mContext;

    public RecyclerViewCaloryAdapter(List<CaloryItem> mCalory, Context 
    mContext) {
        this.mCalory = mCalory;
        this.mContext = mContext;
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        private TextView Title;

        ViewHolder(View view) {
            super(view);
            Title = (TextView) view.findViewById(R.id.health_tip);
        }
    }

    @Override
    public RecyclerViewCaloryAdapter.ViewHolder 
    onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.calory_row, parent, false);

        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.Title.setText(mCalory.get(position).getCalories());
    }

    @Override
    public int getItemCount() {
        return mCalory.size();
    }
}

CaloryChartActivity项目的全局范围内,添加以下实例:

private RecyclerViewCaloryAdapter mAdapter;
private List<CaloryItem> myDataSet = new ArrayList<>();

oncreate方法内部添加以下代码:

WearableRecyclerView recyclerView = (WearableRecyclerView) findViewById(R.id.wearable_recycler_view);
recyclerView.setHasFixedSize(true);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(mLayoutManager);

myDataSet = new ArrayList<CaloryItem>();
for (int i = 0; i < Calory.nameArray.length; i++) {
    myDataSet.add(new CaloryItem(
            Calory.nameArray[i]
    ));
}

mAdapter = new RecyclerViewCaloryAdapter(myDataSet,CaloryChartActivity.this);
recyclerView.setAdapter(mAdapter);

以下屏幕显示了完整的移动和 Wear 应用。

下图展示了监听步骤和脉搏的活动屏幕:

下图展示了 Wear 应用中健康食物建议屏幕。它使用WearableRecyclerView进行设计:

以下图像展示了用于学习不同食品项中可提供热量的热量图表:

总结

在本章中,我们了解了与穿戴设备和移动应用程序协作的基础知识。我们已经探索了从穿戴设备发送和接收数据到移动设备以及反向支持的 API。现在,为任何穿戴项目集成RealmDB将会更加容易。

在下一章中,我们将为穿戴设备构建一个谷歌地图应用程序,并且我们将持久化位置数据,了解穿戴设备的不同地图类型和控制方法。

第六章:任意地点的出行方式 - WearMap 和 Google API 客户端

地图是区域或区域部分的视觉表示。

我们人类会去不同的城市旅行,这些城市可能是国内的也可能是国际的。那么,追踪你所访问的地方怎么样?我们出于不同的原因使用地图,但在大多数情况下,我们使用地图来规划特定的活动,比如户外游览、骑自行车和其他类似活动。地图帮助人类智能找到从起点到目的地的最快路线。在这个项目中,我们将构建一个与 Google Maps 服务配合工作的 Wear 应用程序。

记录一下,Google 地图最初是在 2004 年 10 月作为一个 C++ 桌面程序开始的。Google 地图在 2005 年 2 月正式发布。Google 地图提供了一个 API,允许将地图嵌入第三方应用程序中;Google 地图提供了许多地方的空中和卫星视图。与其他地图服务相比,Google 地图是最佳的,地图进行了优化且其准确率非常高。

在这个项目中,让我们构建一个独立的 Wear 地图应用程序。当用户点击地图时,我们将允许用户写下关于该地点的故事并将其保存到 SQLite 数据库中作为一个标记。当用户点击标记时,我们应该向用户展示已保存的内容。在本章中,我们将了解以下重要概念:

  • 在开发者 API 控制台中创建项目

  • 使用 SHA1 指纹获取 Maps API 密钥

  • SQLite 集成

  • Google 地图

  • Google API 客户端及更多功能

  • 地理编码器

让我们开始创建 WearMap

现在我们知道如何创建一个独立的应用程序。如果你是直接跟随这个项目,而没有参考第 第二章,让我们帮助你捕捉心中所想 - WearRecyclerView 和更多 和第 第三章,让我们帮助你捕捉心中所想 - 保存数据和定制 UI 中介绍的 Wear-note 应用程序,请务必跟进 Wear-note 应用程序以了解更多关于独立应用程序的信息。

我们将这个项目称为 WearMapDiary,因为我们存储的是地点及其详细信息。项目的包地址由开发者决定;在这个项目中,包地址是 com.packt.wearmapdiary,API 级别为 25 Nougat。在活动模板中,选择 Google Maps Wear 活动,如下所示:

从活动选择器中选择 Google Maps Wear 活动模板

创建项目后,我们将看到项目的必要配置,其中包括已经添加的地图片段;它将设置 DismissOverlays 组件:

将为与 Wear 地图活动一起工作而配置的示例代码生成。

我们需要在 res/values 目录下的 google_maps_api.xml 文件中为项目添加 Maps API 密钥:

Google API 控制台

Google API 控制台是一个网络门户,允许开发者为他们的项目开发管理 Google 服务,可以通过console.developers.google.com访问。

  1. 使用你的 Google 账户访问开发者控制台。创建一个项目 packt-wear 或对开发者来说方便的其他名称:

  1. 成功创建项目后,前往 API 管理器 | 库部分,并启用 Google Maps Android API:

  1. 点击“启用”按钮,为 Android 启用地图:

  1. 在控制台启用 API 后,我们需要使用开发机器的 SHA1 指纹和项目的包地址创建 API 密钥,如下所示:

  1. 要获取你的设备的 SHA1 指纹,请打开 Android Studio。在 Android Studio 的右侧,你会看到 Gradle 项目菜单。然后,按照以下步骤操作:

    1. 点击“Gradle”(在右侧面板上,你会看到 Gradle 栏)

    2. 点击“刷新”(在 Gradle 栏中点击“刷新”;你将看到项目的 Gradle 脚本列表)

    3. 点击你的项目(从列表(根)中的项目名称)

    4. 点击“任务”

    5. 点击“Android”

    1. 双击 signingReport(你将在运行栏中获得 SHA1 和 MD5):

    1. 复制你的 SHA1 指纹,粘贴到 Google API 控制台,并保存:

    1. 现在,从控制台复制 API 密钥,并将其粘贴到项目的 google_maps_api.xml 文件中,如下所示:

    1. 现在,将你的 Gradle 范围切换到应用,并在 Wear 模拟器或你的 Wear 设备中编译项目:

如果你的模拟器中没有更新 Google Play 服务,Wear 会显示一个错误屏幕以更新 Play 服务:

如果你有一个实际的 Wear 设备,当最新的 Google Play 服务更新可用时,Wear 操作系统将负责下载更新。对于模拟器,我们需要将其连接到实际设备以添加账户。首先,通过 adb 连接 Android 手机并启动 Wear 模拟器。

从 Play 商店安装 Android Wear 伴侣应用play.google.com/store/apps/details?id=com.google.android.wearable.app&hl=en

在 Android Wear 应用程序中,选择模拟器,然后在 Android Studio 终端中输入以下命令:

adb -d forward tcp:5601 tcp:5601

当模拟器连接到你的真实手机后,你可以添加已经同步到手机的账户,或者添加一个新的账户。

下图展示了 Wear 账户的同步屏幕:

  • 成功添加账户后,开始更新你的 Google Play 服务:

  • 现在,完成所有这些配置后,在 Android Studio 中编译程序,并在 Wear 设备上查看地图:

Google API 客户端

GoogleApiClient扩展了Object类。Google API 客户端为所有 Google Play 服务提供了一个共同的入口点,并在用户设备与每个 Google 服务之间管理网络连接。Google 建议使用GoogleApiClient以编程方式获取用户的位置。

在每个线程上创建GoogleApiClientGoogleApiClient服务连接在内部被缓存。GoogleApiClient实例不是线程安全的,因此创建多个实例很快。GoogleApiClient与各种静态方法一起使用。其中一些方法要求GoogleApiClient已连接;有些会在GoogleApiClient连接之前排队调用。

下面是一个与 Google LocationServices 连接创建GoogleApiClient实例的代码示例:

GoogleApiClient mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();

配置项目以实现功能

我们知道为了更好的代码管理和将来维护代码,创建包的重要性。让我们为项目创建一个包含四个不同名称的包,分别是 adapter、model、util 和 view。

我们在 model 包内编写我们的普通旧 Java 对象。我们将在 util 包中配置所有与数据库相关的类,以及在 view 包中配置自定义视图,如对话框片段、TextView等。对于自定义infoWindow,我们必须在adapter包内创建一个infoWindowAdapter

使用GoogleApiClient获取位置信息非常重要。现在我们已经配置了 Wear 地图活动,并使用我们添加的 API 密钥绘制了地图,是时候利用GoogleApiClient获取位置详情了。

利用GoogleApiClient获取用户的位置信息

现在,在MapActivity类中,我们需要实现以下接口:

  • GoogleApiClient.ConnectionCallback

  • GoogleApiClient.OnConnectionFailedListener

然后,我们需要从这两个接口重写三个方法,它们分别是:

  • public void onConnected(..){}

  • public void onConnectionSuspended(..){}

  • public void onConnectionFailed(..){}

onConnected方法中,我们可以使用GoogleApiClient实例实例化位置服务。首先,让我们将GoogleApiClient添加到项目中。在MapActivity的全局范围内创建一个GoogleApiClient实例:

private GoogleApiClient mGoogleApiClient;

添加一个名为addGoogleAPIClient(){ }的 void 方法,用于获取位置服务 API:

private void addGoogleAPIClient(){
    mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addConnectionCallbacks(this)
            .addOnConnectionFailedListener(this)
            .addApi(LocationServices.API)
            .build();
}

为了让 Google Play 服务处理与位置相关的任务,请在 gradle wear 模块中添加以下依赖项:

compile 'com.google.android.gms:play-services-location:11.0.2'

现在,在onConnected方法中,附加mGoogleApiClient

@Override
public void onConnected(@Nullable Bundle bundle) {

    Location location =      LocationServices.FusedLocationApi
    .getLastLocation(mGoogleApiClient);
    double latitude = location.getLatitude();
    double longitude = location.getLongitude();

}

Locationservice在请求位置之前需要权限检查。让我们在 manifest 和 Activity 中添加权限。

在 Manifest 中添加以下权限:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- The following two permissions are not required to use
     Google Maps Android API v2, but are recommended. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

MapActivity.java类中编写一个检查权限的方法,如下所示:

private boolean checkPermission(){
    int result = ContextCompat.checkSelfPermission(MapsActivity.this, 
    Manifest.permission.ACCESS_FINE_LOCATION);
    if (result == PackageManager.PERMISSION_GRANTED){

        return true;

    } else {

        return false;

    }
}

按如下方式重写onRequestPermissionsResult(..){}方法:

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case PERMISSION_REQUEST_CODE:
            if (grantResults.length > 0 && grantResults[0] == 
            PackageManager.PERMISSION_GRANTED) {

            } else {

            }
            break;
    }
}

现在,我们有了权限检查方法;在onConnected方法中处理它:

@Override
public void onConnected(@Nullable Bundle bundle) {
    if (checkPermission()) {
        Location location = LocationServices.FusedLocationApi
        .getLastLocation(mGoogleApiClient);
        double latitude = location.getLatitude();
        double longitude = location.getLongitude();
    }else{

    }

}

让我们编写一个方法来检查 Wear 设备上是否内置了 GPS。通过使用packagemanager类,我们可以检索 Wear 设备上可用的硬件。让我们写一个名为hasGps()的方法:

private boolean hasGps() {
    return getPackageManager().hasSystemFeature(
      PackageManager.FEATURE_LOCATION_GPS);
}

如果你想要用户知道他们的设备是否有 GPS 设备,或者在开发过程中只是想要记录下来,你可以在onCreate()方法中使用这个方法:

if (!hasGps()) {
    Log.d(TAG, "This hardware doesn't have GPS.");
    // Fall back to functionality that does not use location or
    // warn the user that location function is not available.
}

如果你的可穿戴应用使用内置 GPS 记录数据,你可能想要通过实现onLocationChanged()方法,使用LocationListner接口将位置数据与手持设备同步。

要使你的应用能够感知位置,请使用GoogleAPIclient

想要了解更多关于权限的信息,请点击这个链接:developer.android.com/training/articles/wear-permissions.html

现在,让我们处理onMapclick方法,以处理在地图上添加标记的过程。为此,在你的活动中实现GoogleMap.OnMapClickListener并实现其回调方法,这将为你提供带有经纬度的onmapclick。将点击上下文添加到你的onMapReady回调中,如下所示:

mMap.setOnMapClickListener(this);

onMapClick方法中,我们可以使用latLng添加以下标记:

@Override
public void onMapClick(LatLng latLng) {
    Log.d(TAG, "Latlng is "+latLng);
}

onMapclick方法中添加标记使用MarkerOptions()。对于谷歌设计的高级标记,我们将使用地图的addmarker方法,并添加带有位置、标题和摘要(标题下方的简短描述)的新MarkerOptions

@Override
public void onMapClick(LatLng latLng) {
    Log.d(TAG, "Latlng is "+latLng);
    mMap.addMarker(new MarkerOptions()
            .position(latLng)
            .title("Packt wear 2.0")
            .snippet("Map is cool in wear device"));
}

添加带有infowindow的标记后:

现在,我们已经有了地图,并且正在向地图添加标记,但我们需要处理地理编码以获取坐标的地址名称。

使用 GeoCoder 的地理空间数据

使用GeoCoder类通过坐标获取地址。地理编码通常是将街道地址或位置的其它描述转换为(纬度,经度)坐标的过程。逆地理编码是将(纬度,经度)坐标转换为(部分)地址的过程。

OnMapClick方法中,进行以下更改:

@Override
public void onMapClick(LatLng latLng) {
    Log.d(TAG, "Latlng is "+latLng);

   //Fetching the best address match
    Geocoder geocoder = new Geocoder(this);
 List<Address> matches = null;
 try {
 matches = geocoder.getFromLocation(latLng.latitude, 
        latLng.longitude, 1);
 } catch (IOException e) {
 e.printStackTrace();
 }
 Address bestAddress = (matches.isEmpty()) ? null : matches.get(0);
 int maxLine = bestAddress.getMaxAddressLineIndex(); 
    mMap.addMarker(new MarkerOptions()
            .position(latLng)
            .title(bestAddress.getAddressLine(maxLine - 1))
            .snippet(bestAddress.getAddressLine(maxLine)));
}

上述代码片段将标记添加到地图上,并在信息窗口中显示位置名称:

在点击地图时弹出的视图在 Android 中称为infowindow。它类似于网页开发中的 ToolTip 组件。在这个项目中,我们需要在用户点击地图的任何地方保存数据;我们需要借助infowindow显示自定义地图标记。我们需要编写一个适配器,实现GoogleMap.InfoWindowAdapter与自定义布局,如下所示:

信息窗口适配器

以下实现解释了如何为地图标记编写我们自定义的infowindow适配器:

//XML latout for customising  infowindow 
<LinearLayout 
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/snippet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

windowadapter类实现了GoogleMap.InfoWindowAdapter,包含两个回调方法getInfoWindow(..){}getInfoContents(..){}。我们可以通过getInfoContent方法来填充自定义布局:

public class WearInfoWindowAdapter implements GoogleMap.InfoWindowAdapter {
    private LayoutInflater mLayoutInflater;
    private View mView;
    MarkerAdapter(LayoutInflater layoutInflater){
        mLayoutInflater = layoutInflater;
    }
    @Override
    public View getInfoWindow(Marker marker) {
        return null;
    }
    @Override
    public View getInfoContents(Marker marker) {
        if (mView == null){
            mView = mLayoutInflater.inflate(R.layout.marker, null);
        }
        TextView titleView = (TextView)mView.findViewById(R.id.title);
        titleView.setText(marker.getTitle());
        TextView snippetView = 
        (TextView)mView.findViewById(R.id.snippet);
        snippetView.setText(marker.getSnippet());
        return mView;
    }
}

为了更好的代码访问和维护,将前面的适配器类添加到 adapter 包中。InfoWindowAdapter没有使用任何数据来填充视图;我们使用与标记关联的任何数据来填充视图。如果我们想在标题和摘要之外添加任何内容,适配器本身无法做到这一点。我们需要创建一个机制以编程方式实现这一点。

在 model 包中创建Memory类。Memory类是用户选择添加标记的地方:

public class Memory {
    double latitude;
    double longitude;
    String city; // City name
    String country; // Country name
    String notes; // saving notes on the location 
}

现在,我们已经有了 memory,自定义infowindow适配器已准备好与onMapclick实现一起工作。对于每个标记,我们将添加一个 memory 类关联。为了临时保存所有 memory,让我们使用HashMap

private HashMap<String, Memory> mMemories = new HashMap<>();

让我们将标记添加到HashMap中,以便访问Marker属性,例如Marker ID 等。适配器的完整代码如下:

public class WearInfoWindowAdapter implements GoogleMap.InfoWindowAdapter {

    public LayoutInflater mLayoutInflater;
    public View mView;
    public HashMap<String, Memory> mMemories;

    WearInfoWindowAdapter(LayoutInflater layoutInflater, 
    HashMap<String,Memory> memories){
        mLayoutInflater = layoutInflater;
        mMemories = memories;
    }

    @Override
    public View getInfoWindow(Marker marker) {
        return null;
    }

    @Override
    public View getInfoContents(Marker marker) {
        if (mView == null) {
            mView = mLayoutInflater.inflate(R.layout.marker, null);
        }
        Memory memory = mMemories.get(marker.getId());

        TextView titleView = (TextView)mView.findViewById(R.id.title);
        titleView.setText(memory.city);
        TextView snippetView = 
        (TextView)mView.findViewById(R.id.snippet);
        snippetView.setText(memory.country);
        TextView notesView = (TextView)mView.findViewById(R.id.notes);
        notesView.setText(memory.notes);

        return mView;
    }
}

OnMapClick方法中,添加以下更改:

@Override
public void onMapClick(LatLng latLng) {
    Log.d(TAG, "Latlng is "+latLng);

    Geocoder geocoder = new Geocoder(this);
    List<Address> matches = null;
    try {
        matches = geocoder.getFromLocation(latLng.latitude, 
        latLng.longitude, 1);
    } catch (IOException e) {
        e.printStackTrace();
    }

    Address bestAddress = (matches.isEmpty()) ? null : matches.get(0);
    int maxLine = bestAddress.getMaxAddressLineIndex();

    Memory memory = new Memory();
    memory.city = bestAddress.getAddressLine(maxLine - 1);
    memory.country = bestAddress.getAddressLine(maxLine);
    memory.latitude = latLng.latitude;
    memory.longitude = latLng.longitude;
    memory.notes = "Packt and wear 2.0 notes...";

    Marker marker = mMap.addMarker(new MarkerOptions()
            .position(latLng));

    mMemories.put(marker.getId(), memory);
}

使用以下代码在onMapready方法中将新的Marker附加到地图上:

mMap.setInfoWindowAdapter(new WearInfoWindowAdapter(getLayoutInflater(), mMemories));

现在,编译程序。你应该能够看到如下更新的infoWindow

用于记录位置信息的自定义DialogFragment

DialogFragment是一个在活动中浮动的对话框窗口。在 Wear 设备上它不会浮动,但它提供了 Wear 优化的设计。查看以下实现代码。

在继续之前,将 Memory 类实现为可序列化接口:

public class Memory implements Serializable {
    public double latitude;
    public double longitude;
    public String city;
    public String country;
    public String notes;
}

在 layout 目录中添加以下布局文件,并将布局文件命名为memory_dialog_fragment.xml。创建文件后,在布局文件内添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MapsActivity"
    tools:deviceIds="wear">

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="5dp"
        app:layout_box="all"
        android:layout_gravity="center"
        android:gravity="center">

        <TextView
            android:id="@+id/city"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/country"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <EditText
            android:id="@+id/notes"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </LinearLayout>
</android.support.wearable.view.BoxInsetLayout>

创建布局文件后,让我们处理创建自定义对话框的 Java 代码。创建一个名为MemoryDialogFragment的类,并继承自DialogFragment

创建一个接口来处理DialogFragmentSaveClickedcancelClicked按钮:

public interface Listener{
    public void OnSaveClicked(Memory memory);
    public void OnCancelClicked(Memory memory);
}

现在,将以下实例添加到MemoryDialogFragment的全局范围内。:

private static final String TAG = "MemoryDialogFragment";
private static final String MEMORY_KEY = "MEMORY";
private Memory mMemory;
private Listener mListener;
private View mView;

现在,让我们处理正确地在正确字段中填充布局的数据:

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {

    mView = getActivity().getLayoutInflater()
    .inflate(R.layout.memory_dialog_fragment, null);
    TextView cityView = (TextView) mView.findViewById(R.id.city);
    cityView.setText(mMemory.city);
    TextView countryView = (TextView) mView.findViewById(R.id.country);
    countryView.setText(mMemory.country);

    AlertDialog.Builder builder = new 
    AlertDialog.Builder(getActivity());
    builder.setView(mView)
            .setTitle(getString(R.string.dialog_title))
            .setPositiveButton(getString(R.string.DialogSaveButton), 
            new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) 
                {
                    EditText notesView = (EditText) 
                    mView.findViewById(R.id.notes);
                    mMemory.notes = notesView.getText().toString();
                    mListener.OnSaveClicked(mMemory);
                }
            })
            .setNegativeButton(getString(R.string.DialogCancelButton), 
            new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) 
                {
                    mListener.OnCancelClicked(mMemory);
                }
            });

    return builder.create();
}

我们将在oncreate方法中从Memory获取序列化的数据:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Bundle args = getArguments();
    if (args != null){
        mMemory = (Memory)args.getSerializable(MEMORY_KEY);
    }
}

MemoryDialogFragment的完整代码如下:

public class MemoryDialogFragment extends DialogFragment  {

    private static final String TAG = "MemoryDialogFragment";
    private static final String MEMORY_KEY = "MEMORY";

    private Memory mMemory;
    private Listener mListener;
    private View mView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle args = getArguments();
        if (args != null){
            mMemory = (Memory)args.getSerializable(MEMORY_KEY);
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        mView = getActivity().getLayoutInflater()
        .inflate(R.layout.memory_dialog_fragment, null);
        TextView cityView = (TextView) mView.findViewById(R.id.city);
        cityView.setText(mMemory.city);
        TextView countryView = (TextView) 
        mView.findViewById(R.id.country);
        countryView.setText(mMemory.country);

        AlertDialog.Builder builder = new 
        AlertDialog.Builder(getActivity());
        builder.setView(mView)
                .setTitle(getString(R.string.dialog_title))
                .setPositiveButton(getString
                (R.string.DialogSaveButton), 
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick
                    (DialogInterface dialog, int which) {
                        EditText notesView = (EditText) 
                        mView.findViewById(R.id.notes);
                        mMemory.notes = notesView.getText().toString();
                        mListener.OnSaveClicked(mMemory);
                    }
                })
                .setNegativeButton(getString
                (R.string.DialogCancelButton), 
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick
                    (DialogInterface dialog, int which) {
                        mListener.OnCancelClicked(mMemory);
                    }
                });

        return builder.create();
    }

    public static MemoryDialogFragment newInstance(Memory memory){
        MemoryDialogFragment fragment = new MemoryDialogFragment();
        Bundle args = new Bundle();
        args.putSerializable(MEMORY_KEY, memory);
        fragment.setArguments(args);

        return fragment;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try{
            mListener = (Listener)getActivity();
        }catch (ClassCastException e){
            throw new IllegalStateException("Activity does not 
            implement contract");
        }

    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }

    public interface Listener{
        public void OnSaveClicked(Memory memory);
        public void OnCancelClicked(Memory memory);
    }
}

OnMapClick方法中,进行以下更改:

@Override
public void onMapClick(LatLng latLng) {
    Log.d(TAG, "Latlng is "+latLng);

    Memory memory = new Memory();
 updateMemoryPosition(memory, latLng);
    MemoryDialogFragment.newInstance(memory)
    .show(getFragmentManager(),MEMORY_DIALOG_TAG);
}

现在,编译程序。在mapclick时,你将看到以下屏幕。用户可以在 edittext 字段中输入关于地图位置的自己的想法:

现在我们已经添加了输入对话框,让我们来处理将数据保存到 SQLite 的操作。

配置 SQLite 并保存标记

对于任何优秀的软件来说,持久化所有必要的数据都是基本用例。Android SDK 内置了 SQLite 存储解决方案。它占用的空间非常小,速度也非常快。如果程序员熟悉 SQL 查询和操作,那么使用 SQLite 将会轻松愉快。

模式和合约

本质上,对于数据库,我们需要创建一个数据模式,这是对数据库组织方式的正式声明。该模式反映在 SQLite 查询语句中。合约类是一个包含常量的容器,这些常量定义了 URI、表和列的名称。合约类允许在同一个包中的所有其他类中使用相同的常量。

对于WearMapDiary的范围,我们将在DBHelper类中创建所有实例。现在,让我们创建DBhelper类,它打开并连接应用程序到 SQLite,并处理查询:

public class DbHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "traveltracker.db";
    private static final int DATABASE_VERSION = 3;
    public static final String MEMORIES_TABLE = "memories";
    public static final String COLUMN_LATITUDE = "latitude";
    public static final String COLUMN_LONGITUDE = "longitude";
    public static final String COLUMN_CITY = "city";
    public static final String COLUMN_COUNTRY = "country";
    public static final String COLUMN_NOTES = "notes";
    public static final String COLUMN_ID = "_id";

    private static DbHelper singleton = null;

    public static DbHelper getInstance(Context context){
        if (singleton == null){
            singleton = new DbHelper(context.getApplicationContext());
        }
        return singleton;
    }

    private DbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE "+MEMORIES_TABLE+" ("
                +COLUMN_ID+" INTEGER PRIMARY KEY AUTOINCREMENT, "
                +COLUMN_LATITUDE +" DOUBLE, "
                +COLUMN_LONGITUDE +" DOUBLE, "
                +COLUMN_CITY +" TEXT, "
                +COLUMN_COUNTRY +" TEXT, "
                +COLUMN_NOTES +" TEXT"
                +")");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int 
    newVersion) {
        db.execSQL("DROP TABLE IF EXISTS "+MEMORIES_TABLE);
        onCreate(db);
    }
}

我们需要创建一个Datasource来管理所有查询,以及读写 SQLite 中的数据。在这里,这个类中,我们将创建多个方法来创建数据、读取数据、更新数据和删除数据:

public class MemoriesDataSource {
    private DbHelper mDbHelper;
    private String[] allColumns = {
            DbHelper.COLUMN_ID, DbHelper.COLUMN_CITY,
            DbHelper.COLUMN_COUNTRY, DbHelper.COLUMN_LATITUDE,
            DbHelper.COLUMN_LONGITUDE, DbHelper.COLUMN_NOTES
    };

    public MemoriesDataSource(Context context){
        mDbHelper = DbHelper.getInstance(context);
    }

    public void createMemory(Memory memory){
        ContentValues values = new ContentValues();
        values.put(DbHelper.COLUMN_NOTES, memory.notes);
        values.put(DbHelper.COLUMN_CITY, memory.city);
        values.put(DbHelper.COLUMN_COUNTRY, memory.country);
        values.put(DbHelper.COLUMN_LATITUDE, memory.latitude);
        values.put(DbHelper.COLUMN_LONGITUDE, memory.longitude);
        memory.id = mDbHelper.getWritableDatabase()
        .insert(DbHelper.MEMORIES_TABLE, null, values);
    }

    public List<Memory> getAllMemories(){

        Cursor cursor = allMemoriesCursor();
        return cursorToMemories(cursor);
    }

    public Cursor allMemoriesCursor(){
        return mDbHelper.getReadableDatabase()
        .query(DbHelper.MEMORIES_TABLE, 
        allColumns,null, null, null, null, null);
    }

    public List<Memory> cursorToMemories(Cursor cursor){
        List<Memory> memories =  new ArrayList<>();
        cursor.moveToFirst();
        while (!cursor.isAfterLast()){
            Memory memory = cursorToMemory(cursor);
            memories.add(memory);
            cursor.moveToNext();
        }
        return memories;
    }

    public void updateMemory(Memory memory){
        ContentValues values = new ContentValues();
        values.put(DbHelper.COLUMN_NOTES, memory.notes);
        values.put(DbHelper.COLUMN_CITY, memory.city);
        values.put(DbHelper.COLUMN_COUNTRY, memory.country);
        values.put(DbHelper.COLUMN_LATITUDE, memory.latitude);
        values.put(DbHelper.COLUMN_LONGITUDE, memory.longitude);

        String [] whereArgs = {String.valueOf(memory.id)};

        mDbHelper.getWritableDatabase().update(
                mDbHelper.MEMORIES_TABLE,
                values,
                mDbHelper.COLUMN_ID+"=?",
                whereArgs
        );
    }

    public void deleteMemory(Memory memory){
        String [] whereArgs = {String.valueOf(memory.id)};

        mDbHelper.getWritableDatabase().delete(
                mDbHelper.MEMORIES_TABLE,
                mDbHelper.COLUMN_ID+"=?",
                whereArgs
        );
    }

    private Memory cursorToMemory(Cursor cursor){
        Memory memory = new Memory();
        memory.id = cursor.getLong(0);
        memory.city = cursor.getString(1);
        memory.country = cursor.getString(2);
        memory.latitude = cursor.getDouble(3);
        memory.longitude = cursor.getDouble(4);
        memory.notes = cursor.getString(5);
        return memory;
    }
}

为了在后台使用cursorLoader执行所有这些查询,我们将编写另一个类,我们将这个类称为DBCurserLoader

public abstract class DbCursorLoader extends AsyncTaskLoader<Cursor> {

    private Cursor mCursor;

    public DbCursorLoader(Context context){
        super(context);
    }

    protected abstract Cursor loadCursor();

    @Override
    public Cursor loadInBackground() {
        Cursor cursor = loadCursor();
        if (cursor != null){
            cursor.getCount();
        }

        return cursor;
    }

    @Override
    public void deliverResult(Cursor data) {
        Cursor oldCursor = mCursor;
        mCursor = data;

        if (isStarted()){
            super.deliverResult(data);
        }

        if (oldCursor != null && oldCursor != data){
            onReleaseResources(oldCursor);
        }
    }

    @Override
    protected void onStartLoading() {
        if (mCursor != null){
            deliverResult(mCursor);
        }
        if (takeContentChanged() || mCursor == null){
            forceLoad();
        }
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }

    @Override
    public void onCanceled(Cursor data) {
        super.onCanceled(data);

        if (data != null) {
            onReleaseResources(data);
        }
    }

    @Override
    protected void onReset() {
        super.onReset();

        onStopLoading();

        if (mCursor != null){
            onReleaseResources(mCursor);
        }
        mCursor = null;
    }

    private void onReleaseResources(Cursor cursor){
        if (!cursor.isClosed()){
            cursor.close();
        }
    }
}

创建另一个类,用于从memoryDatasource加载所有记忆,并扩展到DBCursorLoader

public class MemoriesLoader extends DbCursorLoader {

    private MemoriesDataSource mDataSource;

    public MemoriesLoader(Context context, MemoriesDataSource 
    memoriesDataSource){
        super(context);
        mDataSource = memoriesDataSource;
    }

    @Override
    protected Cursor loadCursor() {
        return mDataSource.allMemoriesCursor();
    }
}

现在,我们的 SQLite 配置工作正常。让我们在MapActivity中处理保存数据到 SQLite 的onMapclick

在 SQLite 中保存数据

要将 SQLite 连接到活动,并在 SQLite 中保存数据,请实现活动LoaderManager.LoaderCallbacks<Cursor>并在onCreate方法中实例化数据源:

mDataSource = new MemoriesDataSource(this);
getLoaderManager().initLoader(0,null,this);

实现LoaderManager.LoaderCallbacks<Cursor>接口的回调方法:

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    return null;
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {

}

@Override
public void onLoaderReset(Loader<Cursor> loader) {

}

现在,将addingMarker代码重构为一个方法,如下所示:

private void addMarker(Memory memory) {
    Marker marker = mMap.addMarker(new MarkerOptions()
            .draggable(true)
            .position(new LatLng(memory.latitude, memory.longitude)));

    mMemories.put(marker.getId(), memory);
}

我们仍然需要处理拖动标记以用于将来的实现。让我们将可拖动属性设置为 true。现在,在OnMapClick方法中,调用以下代码:

@Override
public void onMapClick(LatLng latLng) {
    Log.d(TAG, "Latlng is "+latLng);

    Memory memory = new Memory();
    updateMemoryPosition(memory, latLng);
    MemoryDialogFragment.newInstance(memory)
    .show(getFragmentManager(),MEMORY_DIALOG_TAG);
}

让我们重构UpdateMemoryPosition方法,它从latlng获取地址并将其添加到Memory

private void updateMemoryPosition(Memory memory, LatLng latLng) {
    Geocoder geocoder = new Geocoder(this);
    List<Address> matches = null;
    try {
        matches = geocoder.getFromLocation(latLng.latitude, 
        latLng.longitude, 1);
    } catch (IOException e) {
        e.printStackTrace();
    }

    Address bestMatch = (matches.isEmpty()) ? null : matches.get(0);
    int maxLine = bestMatch.getMaxAddressLineIndex();
    memory.city = bestMatch.getAddressLine(maxLine - 1);
    memory.country = bestMatch.getAddressLine(maxLine);
    memory.latitude = latLng.latitude;
    memory.longitude = latLng.longitude;
}

现在,我们正在 SQLite 中保存数据。当我们关闭并重新打开地图时,我们没有读取并将标记数据添加到地图中:

现在,让我们读取 SQLite 数据并将其添加到地图中。

LoaderManager类的onCreateLoader回调方法通过Datasource实例将数据添加到MemoryLoader,如下所示:

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    Log.d(TAG,"onCreateLoader");
    return new MemoriesLoader(this, mDataSource);
}

onLoadFinished方法中,从游标中获取数据并将其添加到地图中:

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    Log.d(TAG,"onLoadFinished");
    onFetchedMemories(mDataSource.cursorToMemories(cursor));
}

从获取的数据中向地图添加标记:

private void onFetchedMemories(List<Memory> memories) {
    for(Memory memory: memories){
        addMarker(memory);
    }
}

现在,我们有一个名为WearMapDiary的功能性 Wear 应用,它查找地址并在地图上关于位置保存快速笔记。它将标记添加到 SQLite 数据库中,并在我们在 Wear 设备上打开应用程序时将标记附加到地图上:

从应用中检索笔记和位置信息:

图片

在此对话框中,用户可以输入他或她想要保存在当前位置的数据:

图片

到目前为止,我们已经探讨了如何将地图集成到穿戴设备上,并清楚地了解了如何获取地图 API 密钥。我们使用GoogleApiclient来获取位置服务。我们正在检查 GPS 硬件的可用性:

以下步骤简要介绍了如何编写自定义标记。

  • 通过实现GoogleMap.InfoWindowAdapter探索了自定义InfoWindow适配器。

  • 使用boxinsetlayout为 Wear 兼容性创建了自定义的dialogFragment

  • Geocoder类获取GeoSpatial数据

  • SQLite 及其与地图数据的集成

现在,是时候了解关于穿戴设备上的地图的更多信息了。

独立地图应用与移动同步地图应用之间的区别

面向 Wear 2.0 平台的手表应用可以通过板载 Wi-Fi 传输器连接到 Wi-Fi。我们可以缓存地图等更多内容,但它仍然缺乏移动地图应用程序的舒适性。通常,对于独立的 Wear 应用,目标 API 级别是 25,并带有安全操作运行时权限。在本章中,我们添加了处理运行时权限的代码。

将应用标识为独立应用

Wear 2.0 要求手表应用的 Android Manifest文件中有一个新的元数据元素,作为<application>元素的子元素。新元数据元素的名称是com.google.android.wearable.standalone,值必须是 true 或 false:

 <meta-data
    android:name="com.google.android.wearable.standalone"
    android:value="true" />

由于独立应用是独立或半独立的,因此 iPhone 用户和缺少 Play 商店的 Android 手机(如 BlackBerry android 分叉操作系统和诺基亚定制 Android 手机)可以安装它们。

如果手表应用依赖于手机应用,请将前一个元数据元素的值设置为 false。

即使值是 false,手表应用也可以在相应的手机应用安装之前安装。因此,如果手表应用检测到配套手机缺少必要的手机应用,手表应用应提示用户安装手机应用。

在手表应用和手机应用之间共享数据

手表应用和手机应用之间可以共享数据,或者共享特定于应用的数据。你可以使用标准的 Android 存储 API 在本地存储数据。例如,你可以使用SharedPreferences APIs,SQLite,或者内部存储(就像在手机上一样)。消息传递 API 的手表应用可以与对应的手机应用通信。

从另一台设备上检测你的应用

CapabilityAPI中,你的 Wear 应用可以检测到与 Wear 应用对应的手机应用。Wear 设备可以自发地以及静态地向配对的设备广播它们的事件。要检查配对 Wear 设备宣传的功能,更多信息请查看此链接:developer.android.com/training/wearables/data-layer/messages.html#AdvertiseCapabilities

请注意,并非所有手机都支持 Play 商店(如 iPhone 等)。本节描述了这些情况的最佳实践:你的独立手表应用需要你的手机应用,而你的手机应用也需要你的独立手表应用。

指定功能名称以检测你的应用

对于每种设备类型(手表或手机)对应的应用,请在res/values/wear.xml文件中为功能名称指定一个唯一的字符串。例如,在你的移动模块中,wear.xml文件可能包含以下代码,在 Wear 和移动模块中:

<resources>
    <string-array name="android_wear_capabilities">
        <item>verify_remote_example_phone_app</item>
    </string-array>
</resources>

检测并引导用户安装相应的手机应用

Wear 2.0 引入了独立应用程序。Wear 应用足够强大,可以在没有移动应用支持的情况下运行。在必须要有移动应用的紧急情况下,Wear 应用可以指导用户安装移动支持应用和相应的 Wear 应用,通过以下步骤:

  • 使用CapabilityApi检查你的手机应用是否已安装在配对的手机上。更多信息,请查看谷歌提供的这个示例:github.com/googlesamples/android-WearVerifyRemoteApp.

  • 如果你的手机应用没有安装在手机上,使用PlayStoreAvailability.getPlayStoreAvailabilityOnPhone()来检查它是什么类型的手机。

  • 如果返回PlayStoreAvailability.PLAY_STORE_ON_PHONE_AVAILABLEtrue,表示手机中已安装 Play 商店。

  • 在 Wear 设备上调用RemoteIntent.startRemoteActivity(),使用市场 URI(market://details?id=com.example.android.wearable.wear.finddevices)在手机上打开 Play 商店。

  • 如果返回PlayStoreAvailability.PLAY_STORE_ON_PHONE_UNAVAILABLE,这意味着该手机很可能是 iOS 手机(没有 Play 商店)。通过在 Wear 设备上调用RemoteIntent.startRemoteActivity()并使用此 URI 打开 iPhone 上的 App Store:itunes.apple.com/us/app/yourappname。也请参阅从手表打开 URL。在 iPhone 上,从 Android Wear,你无法编程确定你的手机应用是否已安装。作为最佳实践,为用户提供一种机制(例如,一个按钮),以手动触发打开 App Store。

若要更详细地了解独立应用,请查看以下链接:独立应用介绍

在 Wear 设备上保持应用活跃

当我们为不同的使用场景编写应用时,需要做出一些调整。我们知道,在不使用应用时,应该让应用在 Wear 设备上进入休眠状态,以获得更好的电池性能;但是,当我们为地图构建应用时,有必要让地图对用户可见且处于活跃状态。

Android 为此提供了一个简单的配置:一个激活环境模式的几行代码方法:

//oncreate Method
setAmbientEnabled();

这将在地图上启动环境模式。当用户不再积极使用应用时,API 切换到非交互式和低色彩渲染的地图:

@Override
public void onEnterAmbient(Bundle ambientDetails) {
    super.onEnterAmbient(ambientDetails);
    mMapFragment.onEnterAmbient(ambientDetails);
}

下面的代码在 WearMap 上退出了环境模式。当用户开始积极使用应用时,API 切换到地图的正常渲染:

@Override
public void onEnterAmbient(Bundle ambientDetails) {
    super.onEnterAmbient(ambientDetails);
    mMapFragment.onEnterAmbient(ambientDetails);
}

为你的应用配置 WAKE_LOCK

当一些 Wear 应用始终可见时,它们非常有用。让应用始终可见会影响电池寿命,因此在你添加此功能到应用时,应仔细考虑这一影响。

在清单文件中添加 WAKE_LOCK 权限:

<uses-permission android:name="android.permission.WAKE_LOCK" />

WAKE_LOCK mechanism:
 // Schedule a new alarm
    if (isAmbient()) {
        // Calculate the next trigger time
        long delayMs = AMBIENT_INTERVAL_MS - (timeMs % 
        AMBIENT_INTERVAL_MS);
        long triggerTimeMs = timeMs + delayMs;

        mAmbientStateAlarmManager.setExact(
AlarmManager.RTC_WAKEUP,
            triggerTimeMs,
            mAmbientStatePendingIntent);

    } else {
        // Calculate the next trigger time for interactive mode
    }

用户可以使用语音输入,而不是使用输入法框架读取输入,这需要在你的 Wear 设备上保持网络活跃:

private static final int SPEECH_REQUEST_CODE = 0;

// 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, SPEECH_REQUEST_CODE);
}

// 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 == SPEECH_REQUEST_CODE && resultCode == RESULT_OK) 
    {
        List<String> results = data.getStringArrayListExtra(
                RecognizerIntent.EXTRA_RESULTS);
        String spokenText = results.get(0);
        // Do something with spokenText
    }
    super.onActivityResult(requestCode, resultCode, data);
}

了解完全交互模式和精简模式

Google Maps 安卓 API 可以作为精简模式地图提供静态图片。

为 Android 地图添加精简模式与配置正常地图类似,因为它将使用相同的类和接口。我们可以通过以下两种方式设置 Google 地图为精简模式:

  • 作为 MapViewMapFrgament 的 XML 属性

  • 使用 GoogleMapOptions 对象

<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:map="http://schemas.android.com/apk/res-auto"
    android:name="com.google.android.gms.maps.MapFragment"
    android:id="@+id/map"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    map:cameraZoom="13"
    map:mapType="normal"
    map:liteMode="true"/>

或者,按照以下方式使用 GoogleMapOptions 对象:

GoogleMapOptions options = new GoogleMapOptions().liteMode(true);

交互模式允许应用使用所有的生命周期方法,包括 onCreate()onDestroy()onResume()onPause(),以及所有 Google API 功能,使应用完全交互。相应的代价是会有网络依赖。

有关交互模式和精简模式的更多信息,请查看以下链接:交互与精简模式

概述

我们已经来到了章节的末尾,期待着对 WearMapDiary 应用进行改进。现在,我们了解了如何创建一个MapsActivity,设置地图和 Google API 密钥,配置 Wear 模拟器中的 Google Play 服务,运行时权限检查,检查 GPS 硬件,以及使用geocoder类获取位置名称。我们已经理解了地图的交互模式和 Lite 模式的概念。在下一章中,让我们进一步了解 Wear 和地图用户界面控件以及其他 Google 地图技术,例如街景,更改地图类型等等。

第七章:任意地点的导航方式 - 用户界面控制及更多功能

现在你已经学会了如何为 Android Wear 应用程序带来 Google Maps 的生命,并探索 SQLite 集成,我们需要用户界面控制和更多增强功能。在本章中,让我们专注于通过添加功能,如移动地图上的标记和更改地图类型,使地图应用程序更具功能性和直观性。你将在本章中学习以下主题:

  • 标记控制

  • 地图类型

  • 地图缩放控制

  • 在穿戴设备上的街景视图

  • 最佳实践

标记不仅仅是地图上表示坐标的符号。通过将标记的默认符号替换为有意义的图像描绘,标记被用来传达这是什么类型的地点;例如,如果是加油站,标记符号可以类似于加油枪符号或医院。

更改标记颜色和自定义

使用MarkerOptions类,我们可以更改标记的颜色和图标。以下代码解释了更改标记的图标和颜色。

要更改标记的颜色,请参考以下代码:

private void addMarker(Memory memory) {
    Marker marker = mMap.addMarker(new MarkerOptions()
          .draggable(true).icon(BitmapDescriptorFactory.defaultMarker
          (BitmapDescriptorFactory.HUE_CYAN)).alpha(0.7f)
            .position(new LatLng(memory.latitude, memory.longitude)));

    mMemories.put(marker.getId(), memory);
}

我们现在可以看到,标记的颜色已从红色变为带有透明度的青色。如果你希望移除透明度,可以移除传递给标记选项的.alpha()值:

改变了标记的颜色

要将标记更改为drawable目录中的图标,请检查以下代码:

private void addMarker(Memory memory) {
    Marker marker = mMap.addMarker(new MarkerOptions()
            .draggable(true).icon(BitmapDescriptorFactory.fromResource
            (R.drawable.ic_edit_location)).alpha(0.7f)
            .position(new LatLng(memory.latitude, memory.longitude)));

    mMemories.put(marker.getId(), memory);
}

这将替换默认的标记图标,使用我们从 drawable 目录中传递的自定义图像。我们需要确保图标大小不过大,它具有最佳的大小 72x72:

使用自定义图像更改标记图像

之前的代码片段将帮助更改标记的颜色或图标,但对于更复杂的情况,我们可以动态构建标记视觉资产并将其添加到地图中。

使用简单的 Java 代码创建我们自己的自定义设计的标记怎么样?我们将创建一个在图像顶部绘制简单文本的标记。以下代码解释了如何使用Bitmap类和Canvas类在图像上绘制文本:

 private void addMarker(Memory memory) {

        Bitmap.Config conf = Bitmap.Config.ARGB_8888;
        Bitmap bmp = Bitmap.createBitmap(80, 80, conf);
        Canvas canvas1 = new Canvas(bmp);

        // paint defines the text color, stroke width and size
        Paint color = new Paint();
        color.setTextSize(15);
        color.setColor(Color.BLACK);

        // modify canvas
        canvas1.drawBitmap(BitmapFactory.decodeResource(getResources(),
                R.drawable.ic_edit_location), 0,0, color);
        canvas1.drawText("Notes", 30, 35, color);

        // add marker to Map

        Marker marker = mMap.addMarker(new MarkerOptions()
                .draggable(true).icon(BitmapDescriptorFactory
                .fromBitmap(bmp)).alpha(0.7f)
                .position(new LatLng(memory.latitude, 
                memory.longitude)));

        mMemories.put(marker.getId(), memory);
    }

下面的截图显示了使用位图和画布绘制的带有注释的标记:

动态添加来自 realmdb 信息的标记。

拖动标记并更新位置

MapActivity中实现GoogleMap.OnMarkerDragListener接口,并实现OnMarkerDragListener接口中的所有回调方法:

@Override
public void onMarkerDragStart(Marker marker) {

}

@Override
public void onMarkerDrag(Marker marker) {

}

@Override
public void onMarkerDragEnd(Marker marker) {

}

实现了接口中的这三个方法后,在第三个回调onMarkerDragEnd中,我们可以用更新后的位置详情更新内存。我们还可以在onMapReady回调中注册draglistner

@Override
public void onMapReady(GoogleMap googleMap) {

mMap.setOnMarkerDragListener(this);
...
}

然后,用以下代码更新onMarkerDragEnd方法:

@Override
public void onMarkerDragEnd(Marker marker) {

    Memory memory = mMemories.get(marker.getId());
    updateMemoryPosition(memory, marker.getPosition());
    mDataSource.updateMemory(memory);

}

之前的代码片段在标记被拖动时更新位置。

InfoWindow 点击事件

当用户点击InfoWindow时,它允许用户删除标记。为了监听Infowindow的点击事件,我们需要实现GoogleMap.OnInfoWindowClickListener及其回调方法onInfoWindowClick(..)

onMapready回调中注册infoWindoClicklistner,如下所示:

mMap.setOnInfoWindowClickListener(this);

在回调方法内部,当用户点击时,让我们设计一个警告对话框。它应该允许用户删除标记:

@Override
public void onInfoWindowClick(final Marker marker) {
    final Memory memory = mMemories.get(marker.getId());
    String[] actions  = {"Delete"};
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle(memory.city+", "+memory.country)
            .setItems(actions, new DialogInterface.OnClickListener() {
                @Override
                public void onClick
                (DialogInterface dialog, int which) {
                    if (which == 0){
                        marker.remove();
                        mDataSource.deleteMemory(memory);
                    }
                }
            });

    builder.create().show();
}

用户界面控件

对于可穿戴设备,用户界面控件(如缩放和位置控件)是禁用的。我们可以使用UISettings类启用它们。UISettings类扩展了 Google Map 用户界面对象的设置。要获取此接口,请调用getUiSettings()

下面的布尔方法返回组件的启用或禁用状态:

  • public boolean isCompassEnabled ():获取指南针是否启用/禁用

  • public boolean isMyLocationButtonEnabled ():获取我的位置按钮是否启用/禁用

  • public boolean isZoomControlsEnabled ():获取缩放控件是否启用

  • public boolean isZoomGesturesEnabled ():获取是否启用缩放手势

  • public boolean isTiltGesturesEnabled ():获取是否启用倾斜手势

  • public boolean isRotateGesturesEnabled ():获取是否启用旋转手势

  • public boolean isScrollGesturesEnabled ():获取是否启用/禁用滚动手势

    这些方法将返回组件的状态。

    为了在应用程序中启用这些组件,getUiSettings()将提供适当的设置方法,如下所示:

  • public void setCompassEnabled (boolean enabled):启用或禁用指南针

  • public void setIndoorLevelPickerEnabled (boolean enabled):设置当启用室内模式时,室内楼层选择器是否启用

  • public void setMyLocationButtonEnabled (boolean enabled):启用或禁用我的位置按钮

  • public void setRotateGesturesEnabled (boolean enabled):设置是否启用旋转手势的偏好设置

  • public void setZoomControlsEnabled (boolean enabled):启用或禁用缩放控件

让我们通过WearMapdiary应用程序来看看这个功能。我们将为应用程序启用缩放控件。在OnMapready方法中,为mMap对象添加以下代码行:

mMap.getUiSettings().setZoomControlsEnabled(true);

同样,我们可以设置其他用户界面控件。可穿戴设备上的所有这些控件都有一定的限制,例如,setIndoorLevelPickerEnabled在可穿戴设备上不起作用。

地图类型

地图类型决定了地图的整体表现形式。例如,地图集通常包含关注显示边界的政治地图,而道路地图显示一个城市或地区的所有道路。Google Maps Android API 提供了四种类型的地图,以及一个不显示地图的选项。让我们更详细地看看这些选项:

  • 普通: 典型的道路地图。显示道路、人类建造的一些特征和重要的自然特征,如河流。道路和特征标签也可见。

  • 混合型(Hybrid):卫星照片数据加上道路地图。道路和特征标签也可见。

  • 卫星: 卫星照片数据。道路和特征标签不可见。

  • 地形: 地形数据。地图包括颜色、等高线、标签和透视阴影。一些道路和标签也可见。

  • 无(None):无瓦片。地图将被渲染为没有加载瓦片的空网格。

让我们来看看WearMapdiary应用程序中的这个功能。我们将更改应用程序的地图类型。在 OnMapready 方法中,在 mMap 对象内添加以下代码行,将地图类型更改为混合型(Hybrid):

mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);

地图是混合型,即卫星图像和标签。现在,若要将地图类型更改为地形,请在 mMap 中插入以下代码:

object'mMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN);

地形类型地图的外观如前图所示。现在,若要将地图类型更改为 NONE,请在 mMap 对象中插入以下代码:

mMap.setMapType(GoogleMap.MAP_TYPE_NONE);

当您选择没有地图时,它看起来如前一个屏幕截图所示。现在,若要将地图类型更改为卫星,请在 mMap 对象中插入以下代码:

mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE)

穿戴应用中的 Streetview

Google Street View 提供了其覆盖区域内指定道路的全景 360 度视图。Streetview 是可视化用户目的地或任何地址的好方法。添加 Streetview 为应用程序增添了现实世界的视觉元素,并为用户提供有意义的上下文。用户可以与 Streetview 互动;用户将喜欢在 Streetview 中平移和扫描位置。

要创建街景地图,我们将创建一个新的片段或活动,并可以启动活动或附加片段。在这个例子中,让我们使用 SupportStreetViewPanoramaFragment 类创建一个新活动,并在 onMapLongclick 回调中启动活动:

Lets create a new activity with the following layout and java code. 
//Java class
public class StreetView extends AppCompatActivity {

    private static final LatLng SYDNEY = new LatLng(-33.87365, 
    151.20689);

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_street_view);

        SupportStreetViewPanoramaFragment streetViewPanoramaFragment =
                (SupportStreetViewPanoramaFragment)
                        getSupportFragmentManager()
                        .findFragmentById(R.id.Streetviewpanorama);

        streetViewPanoramaFragment.getStreetViewPanoramaAsync(
                new OnStreetViewPanoramaReadyCallback() {
                    @Override
                    public void onStreetViewPanoramaReady
                    (StreetViewPanorama panorama) {
                        // Only set the panorama to SYDNEY on startup 
                        (when no panoramas have been
                        // loaded which is when the savedInstanceState 
                        is null).
                        if (savedInstanceState == null) {
                            panorama.setPosition(SYDNEY);
                        }
                    }
                });
    }
}

在新的布局资源中添加以下代码,并将文件命名为 activity_street_view


<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent">

<fragment
 android:id="@+id/Streetviewpanorama"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 class="com.google.android.gms.maps.SupportStreetViewPanoramaFragment" />
</FrameLayout>

现在,在 MapActivity 中启动此活动 onMapLongclicklistner。在启动活动之前,请确保您已将应用程序或活动主题更改为 Theme.AppCompat.Light.NoActionBar

android:theme="@style/Theme.AppCompat.Light.NoActionBar"
@Override
    public void onMapLongClick(LatLng latLng) {
        // Display the dismiss overlay with a button to exit this 
        activity.

        //        mDismissOverlay.show();

        Intent street = new Intent(MapsActivity.this, 
        StreetView.class);
        startActivity(street);

现在,我们拥有了一个完整、可运行的基本 Streetview 穿戴应用,你可以平移和旋转 360 度。

  • 多段线(Polylines):多段线扩展到对象类。多段线是一系列点的列表,在这些连续点之间绘制线段。多段线具有以下属性:

    • 点: 线的顶点。在连续点之间绘制线段。多段线需要起点和终点来绘制线条。
    • 宽度: 线段宽度以屏幕像素为单位。宽度是常数,与相机缩放无关。

    • 颜色: 线段颜色采用 ARGB 格式;与 Color 使用的格式相同。

    • 起始/结束端帽: 定义了在折线开始或结束时使用的形状。支持的端帽类型:ButtCap, SquareCap, RoundCap(适用于实线描边模式)和 CustomCap(适用于任何描边模式)。默认情况下,起始和结束都是 ButtCap

    • 连接类型: 连接类型定义了在折线除起始和结束顶点外的所有顶点处连接相邻线段时要使用的形状。

    • 描边模式: 沿着线条重复的实线或模式项序列。选择包括以下内容:

      • 间隙

      • 虚线

    • Z-Index: 此图块覆盖与其他覆盖层相比的绘制顺序。

    • 可见性: 指示线段的可见性,或者告诉线段是否被绘制。

    • 测地线状态: 指示折线的段是否应被绘制为测地线,而不是在墨卡托投影上的直线。测地线是地球上两点之间的最短路径。构建测地线曲线时假定地球是一个球体。

    • 可点击性: 当你希望用户点击折线时触发一个事件。它与通过 setOnPolylineClickListener(GoogleMap.OnPolylineClickListener) 注册的 GoogleMap.OnPolylineClickListener 一起工作。

    • 标签: 与折线关联的对象。例如,该对象可以包含关于折线表示内容的数据。这比存储一个分离的 Map<Polyline, Object> 要简单。另一个例子,你可以关联一个与数据集中的 ID 对应的字符串 ID。

onMapready 回调中添加以下代码,并将其附加到地图实例上:

Polyline line = mMap.addPolyline(new PolylineOptions()
        .add(new LatLng(-34, 151), new LatLng(-37, 74.0))
        .width(5)
        .color(Color.WHITE));

最佳实践

Android Wear 对于快速和一目了然的信息非常有用。在最新的 Google Play 服务中,穿戴设备上最被请求的功能是地图,Google Maps 的更新也来到了 Android Wear,这意味着你可以理想地开发地图应用程序,就像我们为移动应用程序开发一样,开发穿戴地图应用程序的过程没有变化。这意味着只需几行代码和配置就能获得最佳的类开发体验。

让我们讨论一下 Android Wear 地图应用程序的一些常见用例以及如何实现最佳的地图应用程序体验:

  • 最常见的用例之一是简单地显示地图;由于穿戴设备显示较小,我们可能需要全屏显示整个地图。

    你的应用程序可能需要显示一个标记来指代地标。需要允许用户在地图上平移并找到地图上的地点。

  • 安卓穿戴保留了从左向右滑动来关闭当前应用程序的手势。如果你不需要你的地图平移,这将继续有效。但是,如果你需要你的地图应用程序在地图上移动和平移,我们需要重写这个特定的关闭手势以减少混淆,并让用户退出应用程序。为此,我们可以实现dismissoverlay视图,并将其附加到长按事件上。该视图将处理关闭动作。

  • 另一个常见的用例是在地图上选择位置,这样你可以与朋友分享位置。为了实现这一点,我们可以将标记放置在屏幕中央,并让用户在地图上平移并选择最近的平移latlong值,这表示在地图片段组件内选择的位置。然后,使用地图oncamerachange监听器来检测用户是否在地图上进行了平移。我们可以通过cameraposition.target.letlong值访问新位置。

  • 释放我们不使用的组件是一个好习惯;例如,当我们初始化它时,释放 Google API 客户端。我们应在活动生命周期回调中释放它。

有关实现最佳穿戴地图应用程序的更多信息,请点击此链接:developers.google.com/maps/documentation/android-api/wear.

总结

在本章中,你学习了如何添加 UI 控件,如缩放、地图类型等。

使用 Google Maps Android API,你了解了用户如何与以下关键项的穿戴地图应用程序互动:

添加 UI 控件: UI 控件帮助用户以更个性化的方式控制地图。

拖动标记并更新位置标签: 当用户想要修改地图上的标记放置时,拖动同一标记是一个很好的方法。

自定义标记: 我们知道标记标识地图上的位置。自定义标记可以帮助用户了解位置类型。自定义标记传达更多关于位置的信息;例如,位置处的燃油图标表示该位置是加油站。

不同的地图类型: 不同的地图类型帮助用户以个性化的方式体验地图。

信息窗口点击事件: 信息窗口是一种特殊的覆盖层,用于在地图上的给定位置显示内容(通常是文本或图像)的弹出气球。InfoWindow点击事件有助于执行某些操作。对于 WearMapDiary 应用程序的范围,我们正在附加dialogfragment以更新片段区域中的文本。

多段线: 多段线指定一系列坐标作为 LatLng 对象的数组。这表示地图上的一个图形路径。

街景视图: Google 街景提供了从其覆盖区域内的指定道路上的全景 360 度视图。

现在,除了wearmapdiary之外,我们还可以利用所有这些与地图相关的想法,打造出最能帮助用户的应用程序。

第八章:让我们以智能方式进行聊天 - 消息 API 及更多

创新时代赋予我们挖掘众多新兴智能主题的能力。社交媒体现在已成为一种强大的沟通媒介。观察在线社交网络与技术的发展趋势,我们可以认为社交媒体的理念已经进步,消除了很多沟通的难题。大约几十年前,通信媒介是书信。几个世纪前,是训练有素的鸽子。如果我们继续回顾,无疑会有更多故事来理解那时人们的沟通方式。现在,我们生活在物联网、可穿戴智能设备和智能手机的时代,通信在地球的每个角落以秒计的速度发生。不详细讨论通信,让我们构建一个移动应用和穿戴应用,展示谷歌穿戴消息 API 的强大功能,以帮助构建聊天应用,并有一个穿戴设备伴侣应用来管理和响应收到的消息。为了支持聊天过程,我们将在本章使用谷歌自家技术 Firebase。我们不会深入探讨 Firebase 技术,但一定会了解在移动平台上使用 Firebase 的基础知识以及与穿戴技术的工作方式。Firebase 实时数据库在其哈希表结构中反映数据更新。本质上,这些是 Firebase 处理的关键-值对流。数据在最小的互联网带宽要求下即时更新。

为了支持聊天过程,我们将在本章使用谷歌的自家技术 Firebase。我们将理解移动平台的通用注册和登录过程,并为所有注册会员提供空间,使他们每个人都能通过从列表中选择一个用户来进行独家聊天。

在本章中,我们将探讨以下内容:

  • 将 Firebase 配置到您的移动应用中

  • 创建用户界面

  • 使用GoogleApiClient工作

  • 理解消息 API

  • 处理事件

  • 构建一个穿戴模块

现在,让我们了解如何将 Firebase 设置到我们的项目中。在使用项目中的 Firebase 技术之前,我们需要执行几个步骤。首先,我们需要应用 Firebase 插件,然后是我们项目中使用的依赖项。

安装 Firebase

为了安装 Firebase,请执行以下步骤:

  1. 访问 Firebase 控制台 console.firebase.google.com

  1. 在控制台中选择添加项目,并填写有关项目的必要信息。项目成功添加后,您将看到以下屏幕:

  1. “开始使用”页面帮助您为不同的平台设置项目。让我们选择第二个选项,它说“将 Firebase 添加到您的 Android 应用中”:

  1. 添加项目包名,出于进一步的安全考虑,你可以添加 SHA-1 指纹,但这是可选的。现在注册应用:

  1. 下载配置文件。google-services.json文件将包含应用的所有重要配置,并将其放置在你的项目结构中的 app 目录下。

现在,让我们启动 Android Studio 并创建项目:

确保包名与 Firebase 控制台提到的相同。

让我们选择目标是手机和穿戴的平台:

现在,向移动活动选择器中添加空活动:

在穿戴活动选择器中选择空白穿戴活动,通过 Android Studio 模板生成空白穿戴活动代码:

现在,为你的类和 XML 文件命名,并完成项目,以便 Android Studio 为你的移动和穿戴模块生成模板代码。使用文件资源管理器或查找器,进入目录结构,并复制粘贴google-services.json文件:

由于我们同时构建移动和穿戴应用,且app目录名称对于移动项目是 mobile,对于穿戴项目是 wear,我们应该将配置文件(google-services.json)复制到移动目录内。

添加配置文件后,是时候添加插件类路径依赖项了:

classpath 'com.google.gms:google-services:3.0.0'

现在,在移动 Gradle 模块依赖项中,将插件应用到所有标签范围内的底部,如下截图所示:

为了帮助 Gradle 管理依赖项和构建项目的顺序,我们应在 Gradle 文件的底部添加 Google Play 服务依赖项。然而,它也将避免与其他 Google 依赖项的冲突。

成功同步后,Firebase SDKs 被集成到我们的项目中。现在,我们可以开始使用我们感兴趣的功能。在这个项目中,为了聊天功能范围,我们将使用 Firebase 实时数据库。让我们将依赖项添加到同一 gradle 文件的依赖项中。我们将使用 volley 网络库从 Firebase 用户节点获取用户列表。我们需要添加设计支持库以支持材料设计:

compile 'com.firebase:firebase-client-android:2.5.2+'
compile 'com.android.volley:volley:1.0.0'
compile 'com.android.support:design:25.1.1'
compile 'com.android.support:cardview-v7:25.1.1'

如果你在 gradle 中遇到错误,请在依赖项部分添加以下包装。

packagingOptions {

exclude 'META-INF/DEPENDENCIES.txt'

exclude 'META-INF/LICENSE.txt'

exclude 'META-INF/NOTICE.txt'

exclude 'META-INF/NOTICE'

exclude 'META-INF/LICENSE'

exclude 'META-INF/DEPENDENCIES'

exclude 'META-INF/notice.txt'

exclude 'META-INF/license.txt'

exclude 'META-INF/dependencies.txt'

exclude 'META-INF/LGPL2.1'

}

在完成所有必要的项目设置后,让我们来构思我们将要构建的聊天应用程序。

一个基本的聊天应用程序需要有一个注册过程,为了避免匿名聊天,或者至少要知道我们正在和谁聊天,我们需要发送者和接收者的名字。第一个界面将是登录界面,包含用户名和密码字段,让已经注册的用户可以开始与其他用户聊天。然后,我们有注册界面,同样包含用户名和密码字段。一旦用户成功注册,我们将要求用户输入凭据,并允许他们访问用户列表界面,在那里可以选择他们想与之聊天的人。

概念化聊天应用程序

用户输入凭据的登录界面将如下所示:

输入字段的注册界面将如下所示:

下面的截图展示了显示已注册用户列表的用户界面:

实际聊天消息的聊天界面将如下所示:

在圆形屏幕上,可穿戴聊天应用程序将如下所示:

当一条消息进入手持设备时,它应该通知可穿戴设备,并且用户应该能够从可穿戴设备发送回复。在本章中,我们将看到一个工作的移动设备和可穿戴设备聊天应用程序。

理解数据层

可穿戴数据层 API 是谷歌 Play 服务的一部分,它建立了与手持设备应用和可穿戴应用的通信通道。使用GoogleApiClient类,我们可以访问数据层。数据层主要在可穿戴应用中使用,与手持设备通信,但建议不要用它来连接网络。当我们使用构建器模式创建GoogleAPIClient类时,我们将Wearable.API附加到addAPI方法中。当我们在GoogleApiclient中添加多个 API 时,客户端实例有可能在onConnection失败回调中失败。通过addApiIfAvailable()添加 API 是一个好的方法。这将处理大部分繁重的工作;如果 API 可用,它将添加 API。使用addConnectionCallbacks添加所有这些之后,我们可以处理数据层事件。我们需要通过调用connect()方法来启动客户端实例的连接。成功连接后,我们可以使用数据层 API。

数据层事件

事件允许开发者监听通信通道中发生的事情。成功的通信通道将能够在调用完成时发送调用状态。这些事件将允许开发者监控无线通信通道中的所有状态变化和数据变化。数据层 API 在未完成的交易上返回待定结果,例如putdataitem()。当交易未完成时,待定结果将在后台自动排队,如果我们不处理它,这个操作将在后台完成。然而,待定结果需要被处理;待定结果将等待结果状态,并且有两种方法同步和异步地等待结果。

如果数据层代码在 UI 线程中运行,我们应避免对数据层 API 进行阻塞调用。使用pendingresult对象的异步回调,我们将能够检查状态和其他重要信息:

pendingResult.setResultCallback(new ResultCallback<DataItemResult>() {
    @Override
    public void onResult(final DataItemResult result) {
        if(result.getStatus().isSuccess()) {
            Log.d(TAG, "Data item set: " + 
            result.getDataItem().getUri());
        }
    }
});

如果数据层代码在后台服务中的独立线程中运行,例如wearableListenerService,那么阻塞调用是可以的,你可以在pendingresult对象上调用await()方法:

DataItemResult result = pendingResult.await();
if(result.getStatus().isSuccess()) {
    Log.d(TAG, "Data item set: " + result.getDataItem().getUri());
}

数据层事件可以通过两种方式监控:

  • 创建一个扩展了WearableListenerService的类。

  • 实现DataApi.DataListener的 Activity

在这两个设施中,我们重写方法以处理数据事件。通常,我们需要在可穿戴设备和手持应用中都创建实例。我们可以根据应用场景的需要重写方法。本质上,WearableListenerService具有以下事件:

  • onDataChanged(): 每当创建、删除或更新时,系统都会触发这个方法。

  • onMessageReceived(): 从一个节点发送的消息会在目标节点触发这个事件。

  • onCapabilityChanged(): 当实例广告的某个能力在网络上可用时,会触发这个事件。我们可以通过调用isnearby()来检查附近的节点。

这些方法是在后台线程中执行的。

要创建WearableListenerService,我们需要创建一个扩展了WearableListenerService的类。监听你感兴趣的事件,比如onDataChanged()。在你的 Android 清单中声明一个intent过滤器,以通知系统你的WearableListenerService

public class DataLayerListenerService extends WearableListenerService {

    private static final String TAG = "DataLayerSample";
    private static final String START_ACTIVITY_PATH = "/start-
    activity";
    private static final String DATA_ITEM_RECEIVED_PATH = "/data-item-
    received";

    @Override
    public void onDataChanged(DataEventBuffer dataEvents) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "onDataChanged: " + dataEvents);
        }

        GoogleApiClient googleApiClient = new 
        GoogleApiClient.Builder(this)
                .addApi(Wearable.API)
                .build();

        ConnectionResult connectionResult =
        googleApiClient.blockingConnect(30, TimeUnit.SECONDS);

        if (!connectionResult.isSuccess()) {
            Log.e(TAG, "Failed to connect to GoogleApiClient.");
            return;
        }

        // Loop through the events and send a message
        // to the node that created the data item.
        for (DataEvent event : dataEvents) {
            Uri uri = event.getDataItem().getUri();

            // Get the node id from the host value of the URI
            String nodeId = uri.getHost();
            // Set the data of the message to be the bytes of the URI
            byte[] payload = uri.toString().getBytes();

            // Send the RPC
            Wearable.MessageApi.sendMessage(googleApiClient, nodeId,
            DATA_ITEM_RECEIVED_PATH, payload);
        }
    }
}

并在清单中如下注册服务:

<service android:name=".DataLayerListenerService">
  <intent-filter>
      <action 
      android:name="com.google.android.gms.wearable.DATA_CHANGED" />
      <data android:scheme="wear" android:host="*"
               android:path="/start-activity" />
  </intent-filter>
</service>

DATA_CHANGED动作替换了之前推荐的BIND_LISTENER动作,以便只有特定事件通过该路径。在本章中,当我们实际操作项目时,我们会进一步了解。

能力 API

这个 API 有助于广告穿戴网络中节点所提供的功能。功能对应用程序是本地的。利用数据层和消息 API,我们可以与节点通信。为了发现目标节点是否擅长执行某些操作,我们必须利用能力 API,例如如果我们需要从穿戴设备启动一个活动。

要将能力 API 初始化到您的应用程序中,请执行以下步骤:

  1. res/values目录中创建一个 XML 配置文件。

  2. 添加一个名为android_wear_capabilities的资源。

  3. 定义设备所提供的能力:

<resources>
    <string-array name="android_wear_capabilities">
        <item>voice_transcription</item>
    </string-array>
</resources>

voice_transcription的 Java 程序:

private static final String
        VOICE_TRANSCRIPTION_CAPABILITY_NAME = "voice_transcription";

private GoogleApiClient mGoogleApiClient;

...

private void setupVoiceTranscription() {
    CapabilityApi.GetCapabilityResult result =
            Wearable.CapabilityApi.getCapability(
                    mGoogleApiClient, 
                    VOICE_TRANSCRIPTION_CAPABILITY_NAME,
                    CapabilityApi.FILTER_REACHABLE).await();

    updateTranscriptionCapability(result.getCapability());
}

既然我们已经有了实施聊天应用程序的所有设置和设计,那我们就开始吧。

移动应用实现

聊天应用程序的移动端应用使用了谷歌的 Firebase 实时数据库。每当用户发送消息时,它都会实时反映在 Firebase 控制台上。故事先放一边,既然我们已经准备好了所有屏幕,那就开始编写代码吧。

既然我们已经知道将要使用的颜色,让我们在res目录下的 colors 值 XML 文件中声明颜色:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#129793</color>
    <color name="colorPrimaryDark">#006865</color>
    <color name="colorAccent">#ffaf40</color>
    <color name="white">#fff</color>
</resources>

根据设计,我们有一个带有蓝绿色背景的曲线边缘按钮。要制作类似的按钮,我们需要在drawable目录中创建一个 XML 资源,并将其命名为buttonbg.xml,它基本上是一个selector标签,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

 <!-- When item pressed this item will be triggered -->    <item android:state_pressed="true">
        <shape android:shape="rectangle">
        <corners android:radius="25dp" />
        <solid android:color="@color/colorPrimaryDark" />
    </shape>
   </item>

 <!-- By default the background will be this item -->    <item>
        <shape android:shape="rectangle">
        <corners android:radius="25dp" />
        <solid android:color="@color/colorPrimary" />
    </shape>
</item>

</selector>

selector标签内,我们有一个item标签,它传达了状态;任何普通的按钮都会有状态,比如点击、释放和默认。这里,我们采用了默认的按钮背景和按下状态,并使用item属性标签,如形状和圆角,来雕刻按钮,正如设计所示。

为了避免多次更改,我们不会将MainActivity重构为LoginActivity,而是将MainActivity视为LoginActivity。现在,在activity_main.xml中,让我们添加以下代码到登录屏幕设计。为了使屏幕动态,我们将在scrollview下添加代码:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    android:fitsSystemWindows="true">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

       <!-- Your design code goes here -->

   </RelativeLayout>
</ScrollView>

现在,为了完成登录设计,我们需要两个输入字段,一个按钮实例和一个可点击链接实例。完成的登录屏幕代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    android:fitsSystemWindows="true">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="25dp"
            android:orientation="vertical">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginBottom="24dp"

                android:gravity="center|center_horizontal
                |center_vertical"
                android:text="Welcome to Packt Smartchat"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="25sp"
                android:textStyle="bold" />

            <!--  Email Label -->

            <android.support.v7.widget.CardView
                android:layout_margin="10dp"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

                <LinearLayout
                    android:padding="5dp"
                    android:orientation="vertical"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                <android.support.design.widget.TextInputLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="8dp"
                    android:layout_marginTop="8dp">

                    <EditText
                        android:id="@+id/input_email"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="Username"
                        android:inputType="textEmailAddress"
                        android:windowSoftInputMode="stateHidden" />
                </android.support.design.widget.TextInputLayout>

                <!--  Password Label -->
                <android.support.design.widget.TextInputLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="8dp"
                    android:layout_marginTop="8dp"
                    app:passwordToggleEnabled="true">

                    <EditText
                        android:id="@+id/input_password"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="Password"
                        android:inputType="textPassword"
                        android:windowSoftInputMode="stateHidden" />
                </android.support.design.widget.TextInputLayout>
                </LinearLayout>
            </android.support.v7.widget.CardView>

            <LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:paddingBottom="5dp"
                android:paddingTop="8dp">

                <TextView
                    android:id="@+id/register"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:gravity="end"
                    android:padding="5dp"
                    android:text="No Account? Register"
                    android:textSize="14sp" />
            </LinearLayout>

            <Button
                android:id="@+id/btn_login"
                android:layout_width="150dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginBottom="24dp"
                android:layout_marginTop="24dp"
                android:background="@drawable/buttonbg"
                android:textColor="@color/white"
                android:textStyle="bold"
                android:padding="12dp"
                android:text="Login" />

        </LinearLayout>
    </RelativeLayout>

</ScrollView>

让我们创建另一个活动,并将其称为RegistrationActivity,它具有与登录活动类似的组件要求,两个输入字段和一个按钮。XML 布局的完整代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    android:fitsSystemWindows="true">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="25dp"
            android:orientation="vertical">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginBottom="24dp"
                android:gravity="center
                |center_horizontal|center_vertical"
                android:text="Register"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="25sp"
                android:textStyle="bold" />

            <!--  Email Label -->

            <android.support.v7.widget.CardView
                android:layout_margin="10dp"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

                <LinearLayout
                    android:padding="5dp"
                    android:orientation="vertical"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                    <android.support.design.widget.TextInputLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginBottom="8dp"
                        android:layout_marginTop="8dp">

                        <EditText
                            android:id="@+id/input_email"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:hint="Username"
                            android:inputType="textEmailAddress"
                            android:windowSoftInputMode="stateHidden" 
                        />
                    </android.support.design.widget.TextInputLayout>

                    <!--  Password Label -->
                    <android.support.design.widget.TextInputLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginBottom="8dp"
                        android:layout_marginTop="8dp"
                        app:passwordToggleEnabled="true">

                        <EditText
                            android:id="@+id/input_password"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:hint="Password"
                            android:inputType="textPassword"
                            android:windowSoftInputMode="stateHidden" 
                         />
                    </android.support.design.widget.TextInputLayout>
                </LinearLayout>
            </android.support.v7.widget.CardView>

            <LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:paddingBottom="5dp"
                android:paddingTop="8dp">

            </LinearLayout>

            <Button
                android:id="@+id/btn_submit"
                android:layout_width="150dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginBottom="24dp"
                android:layout_marginTop="24dp"
                android:background="@drawable/buttonbg"
                android:textColor="@color/white"
                android:textStyle="bold"
                android:padding="12dp"
                android:text="Submit" />

        </LinearLayout>
    </RelativeLayout>

</ScrollView>

现在,让我们创建一个用户活动列表,其中将包含用户列表。将其称为UsersList活动,它将有一个简单的ListView和一个TextView用来处理空列表。UsersListActivity的完整 XML 代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/noUsersText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="No users found!"
        android:visibility="gone" />

    <ListView
        android:id="@+id/usersList"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

为聊天屏幕创建另一个活动。我们将它称为ChatActivity。在活动的 XML 文件中添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical"
    tools:context="com.packt.smartchat.MainActivity">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_weight="20"
        android:layout_height="wrap_content"
        android:id="@+id/scrollView">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:id="@+id/layout1">
        </LinearLayout>

    </ScrollView>

    <include
        layout="@layout/message_area"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="bottom"
        android:layout_marginTop="5dp"/>
</LinearLayout>

我们需要包含一个编辑消息的布局。在layout目录中创建另一个名为message_area.xml的 XML 文件,并添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimaryDark"
    android:gravity="bottom"
    android:orientation="horizontal">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textColorHint="#CFD8DC"
        android:textColor="#CFD8DC"
        android:singleLine="true"
        android:hint="Write a message..."
        android:id="@+id/messageArea"
        android:maxHeight="80dp" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="4"
        android:padding="4dp"
        android:src="img/ic_menu_send"
        android:id="@+id/sendButton"/>
</LinearLayout>

现在,我们所有的视觉元素都已就位,可以开始编写我们的编程逻辑了。

在我们开始处理活动 Java 文件之前,在清单文件中添加以下权限:

 <uses-permission android:name="android.permission.WAKE_LOCK" />
 <uses-permission android:name="android.permission.INTERNET" />

MainActivity文件中,让我们创建所有实例,并将它们映射到我们在activity_main.xml中放置的 XML ID。在MainActivity类的全局范围内,声明以下实例:

private TextView mRegister;
private EditText mUsername, mPassword;
private Button mLoginButton;
public String mUserStr, mPassStr;

现在,让我们使用onCreate方法内的findViewById()方法将这些实例连接到它们的 XML 视觉元素,如下所示:

mRegister = (TextView)findViewById(R.id.register);
mUsername = (EditText)findViewById(R.id.input_email);
mPassword = (EditText)findViewById(R.id.input_password);
mLoginButton = (Button)findViewById(R.id.btn_login);

现在,当用户点击注册链接时,它应该将用户带到注册活动。使用intent,我们将实现它:

mRegister.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View v) {
        startActivity(new Intent(MainActivity.this, 
        RegistrationActivity.class));
    }
});

点击登录按钮时,它应该进行网络调用,检查 Firebase 中是否存在用户,并在成功时显示适当的操作。在我们编写登录逻辑之前,让我们先编写注册逻辑。

在注册活动中,使用findViewById()方法在 Java 文件中连接所有组件:

//In Global scope of registration activity 
private EditText mUsername, mPassword;
private Button mSubmitButton;
public String mUserStr, mPassStr;

// Inside the oncreate method
Firebase.setAndroidContext(this);
mUsername = (EditText)findViewById(R.id.input_email);
mPassword = (EditText)findViewById(R.id.input_password);
mSubmitButton = (Button)findViewById(R.id.btn_submit);

mSubmit上附加一个点击监听器,并在onClick监听器中获取输入,以确保我们没有传递空字符串:

mSubmitButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      // Input fields
      // Validation logics
    }
});

简单的验证检查将使应用程序在容易出错的情况下变得强大。以下是来自输入字段的验证和获取输入:

mSubmitButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mUserStr = mUsername.getText().toString();
        mPassStr = mPassword.getText().toString();

        // Validation
        if(mUserStr.equals("")){
            mUsername.setError("can't be blank");
        }
        else if(mPassStr.equals("")){
            mPassword.setError("can't be blank");
        }
        else if(!mUserStr.matches("[A-Za-z0-9]+")){
            mUsername.setError("only alphabet or number allowed");
        }
        else if(mUserStr.length()<5){
            mUsername.setError("at least 5 characters long");
        }
        else if(mPassStr.length()<5){
            mPassword.setError("at least 5 characters long");
        }
    }
});

现在我们需要访问 Firebase 以注册用户。在我们继续之前,请登录 Firebase 控制台,console.firebase.google.com,然后转到我们之前创建的项目。

现在,在左侧菜单中,我们将看到数据库选项并选择它。在规则选项卡中,默认情况下,读写授权设置为null。建议您将其更改为true,但在编写生产应用程序时不建议这样做:

{
 "rules": {
 ".read": true,
 ".write": true
 }
}

当我们将读写权限设置为 true 时,实际上我们是在告诉 Firebase,只要他们有端点 URL,任何人都可以读写。

了解到将 URL 公开的复杂性,我们将在项目中使用它。现在,在mSubmit点击监听器中,我们将检查一些验证,并获取用户名和密码。

我们应该完成mSubmit点击监听器的代码。在密码关键字段的else if实例之后,让我们为执行所有 Firebase 网络操作创建一个 else 情况。我们将制作 Firebase 参考 URL,推送子值,并利用volley网络库。我们将检查用户名是否存在,如果存在的话,我们将允许用户使用应用程序。

本项目的 Firebase 端点 URL 是packt-wear.firebaseio.com,节点名称可以是我们要为用户添加的任何名称。让我们添加packt-wear.firebaseio.com/users;代码如下所示:

else {
        final ProgressDialog pd = new 
        ProgressDialog(RegistrationActivity.this);
        pd.setMessage("Loading...");
        pd.show();

        String url = "https://packt-wear.firebaseio.com/users.json";

        StringRequest request = new StringRequest(Request.Method.GET, 
        url, new Response.Listener<String>(){
            @Override
            public void onResponse(String s) {
                Firebase reference = new Firebase("https://packt-
                wear.firebaseio.com/users");
                if(s.equals("null")) {
                    reference.child(mUserStr)
                    .child("password").setValue(mPassStr);
                    Toast.makeText(RegistrationActivity.this, 
                    "registration successful", 
                    Toast.LENGTH_LONG).show();
                }
                else {
                    try {
                        JSONObject obj = new JSONObject(s);

                        if (!obj.has(mUserStr)) {
                            reference.child(mUserStr)
                            .child("password").setValue(mPassStr);
                            Toast.makeText(RegistrationActivity.this,   
                            "registration successful", 
                            Toast.LENGTH_LONG).show();
                        } else {
                            Toast.makeText(RegistrationActivity.this, 
                            "username already exists", 
                            Toast.LENGTH_LONG).show();
                        }

                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }

                pd.dismiss();
            }

        },new Response.ErrorListener(){
            @Override
            public void onErrorResponse(VolleyError volleyError) {
                System.out.println("" + volleyError );
                pd.dismiss();
            }
        });

        RequestQueue rQueue = 
        Volley.newRequestQueue(RegistrationActivity.this);
        rQueue.add(request);
    }
}

使用volley,我们可以添加请求队列并以非常高效的方式处理网络请求。

现在,完整的注册活动类如下所示:

public class RegistrationActivity extends AppCompatActivity {

    private EditText mUsername, mPassword;
    private Button mSubmitButton;
    public String mUserStr, mPassStr;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_registration);

        mUsername = (EditText)findViewById(R.id.input_email);
        mPassword = (EditText)findViewById(R.id.input_password);
        mSubmitButton = (Button)findViewById(R.id.btn_submit);

        Firebase.setAndroidContext(this);

        mSubmitButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mUserStr = mUsername.getText().toString();
                mPassStr = mPassword.getText().toString();

                // Validation
                if(mUserStr.equals("")){
                    mUsername.setError("can't be blank");
                }
                else if(mPassStr.equals("")){
                    mPassword.setError("can't be blank");
                }
                else if(!mUserStr.matches("[A-Za-z0-9]+")){
                    mUsername.setError("only alphabet or number 
                    allowed");
                }
                else if(mUserStr.length()<5){
                    mUsername.setError("at least 5 characters long");
                }
                else if(mPassStr.length()<5){
                    mPassword.setError("at least 5 characters long");
                }else {
                    final ProgressDialog pd = new 
                    ProgressDialog(RegistrationActivity.this);
                    pd.setMessage("Loading...");
                    pd.show();

                    String url = "https://packt-
                    wear.firebaseio.com/users.json";

                    StringRequest request = new StringRequest
                    (Request.Method.GET, url, 
                    new Response.Listener<String>(){
                        @Override
                        public void onResponse(String s) {
                            Firebase reference = new Firebase
                        ("https://packt-wear.firebaseio.com/users");
                            if(s.equals("null")) {
                                reference.child(mUserStr)
                                .child("password").setValue(mPassStr);
                                Toast.makeText
                                (RegistrationActivity.this, 
                                "registration successful", 
                                Toast.LENGTH_LONG).show();
                            }
                            else {
                                try {
                                    JSONObject obj = new JSONObject(s);

                                    if (!obj.has(mUserStr)) {
                                        reference.child(mUserStr)
                                        .child("password")
                                        .setValue(mPassStr);
                                        Toast.makeText
                                        (RegistrationActivity.this, 
                                        "registration successful", 
                                        Toast.LENGTH_LONG).show();
                                    } else {
                                        Toast.makeText
                                        (RegistrationActivity.this, 
                                        "username already exists", 
                                        Toast.LENGTH_LONG).show();
                                    }

                                } catch (JSONException e) {
                                    e.printStackTrace();
                                }
                            }

                            pd.dismiss();
                        }

                    },new Response.ErrorListener(){
                        @Override
                        public void onErrorResponse(VolleyError    
                        volleyError) {
                            System.out.println("" + volleyError );
                            pd.dismiss();
                        }
                    });

                    RequestQueue rQueue = 
                    Volley.newRequestQueue
                    (RegistrationActivity.this);
                    rQueue.add(request);
                }
            }
        });

    }
}

现在,让我们跳转到MainActivity处理用户登录逻辑。

在我们继续之前,让我们创建一个带有静态实例的类,如下所示:

public class User {
    static String username = "";
    static String password = "";
    static String chatWith = "";
}

现在,正如我们在注册屏幕上看到的,让我们在登录屏幕中使用volley库进行验证,让我们检查用户名是否存在。如果有效的用户使用有效的密码登录,我们将不得不允许用户进入聊天屏幕。以下代码放入登录点击监听器中:

   mUserStr = mUsername.getText().toString();
    mPassStr = mPassword.getText().toString();

    if(mUserStr.equals("")){
        mUsername.setError("Please enter your username");
    }
    else if(mPassStr.equals("")){
        mPassword.setError("can't be blank");
    }
    else{
        String url = "https://packt-wear.firebaseio.com/users.json";
        final ProgressDialog pd = new 
        ProgressDialog(MainActivity.this);
        pd.setMessage("Loading...");
        pd.show();

        StringRequest request = new StringRequest(Request.Method.GET, 
        url, new Response.Listener<String>(){
            @Override
            public void onResponse(String s) {
                if(s.equals("null")){
                    Toast.makeText(MainActivity.this, "user not found", 
                    Toast.LENGTH_LONG).show();
                }
                else{
                    try {
                        JSONObject obj = new JSONObject(s);

                        if(!obj.has(mUserStr)){
                            Toast.makeText(MainActivity.this, "user not 
                            found", Toast.LENGTH_LONG).show();
                        }
                        else if(obj.getJSONObject(mUserStr)
                        .getString("password").equals(mPassStr)){
                            User.username = mUserStr;
                            User.password = mPassStr;
                            startActivity(new Intent(MainActivity.this, 
                            UsersListActivity.class));
                        }
                        else {
                            Toast.makeText(MainActivity.this, 
                            "incorrect password", Toast
                           .LENGTH_LONG).show();
                        }
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }

                pd.dismiss();
            }
        },new Response.ErrorListener(){
            @Override
            public void onErrorResponse(VolleyError volleyError) {
                System.out.println("" + volleyError);
                pd.dismiss();
            }
        });

        RequestQueue rQueue = 
        Volley.newRequestQueue(MainActivity.this);
        rQueue.add(request);
    }
}

完整的类将如下所示:

public class MainActivity extends AppCompatActivity {

    private TextView mRegister;
    private EditText mUsername, mPassword;
    private Button mLoginButton;
    public String mUserStr, mPassStr;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mRegister = (TextView)findViewById(R.id.register);
        mUsername = (EditText)findViewById(R.id.input_email);
        mPassword = (EditText)findViewById(R.id.input_password);
        mLoginButton = (Button)findViewById(R.id.btn_login);

        mRegister.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, 
                RegistrationActivity.class));
            }
        });

        mLoginButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mUserStr = mUsername.getText().toString();
                mPassStr = mPassword.getText().toString();

                if(mUserStr.equals("")){
                    mUsername.setError("Please enter your username");
                }
                else if(mPassStr.equals("")){
                    mPassword.setError("can't be blank");
                }
                else{
                    String url = "https://packt-
                    wear.firebaseio.com/users.json";
                    final ProgressDialog pd = new                 
                    ProgressDialog(MainActivity.this);
                    pd.setMessage("Loading...");
                    pd.show();

                    StringRequest request = new StringRequest
                    (Request.Method.GET, url, 
                    new Response.Listener<String>(){
                        @Override
                        public void onResponse(String s) {
                            if(s.equals("null")){
                                Toast.makeText(MainActivity.this, "user 
                                not found", Toast.LENGTH_LONG).show();
                            }
                            else{
                                try {
                                    JSONObject obj = new JSONObject(s);

                                    if(!obj.has(mUserStr)){

                                    Toast.makeText(MainActivity.this, 
                                    "user not found", 
                                    Toast.LENGTH_LONG).show();
                                    }
                                    else if(obj.getJSONObject(mUserStr)
                                    .getString("password")
                                    .equals(mPassStr)){
                                        User.username = mUserStr;
                                        User.password = mPassStr;
                                        startActivity(new 
                                        Intent(MainActivity.this,    
                                        UsersListActivity.class));
                                    }
                                    else {

                                    Toast.makeText(MainActivity.this, 
                                    "incorrect password", 
                                    Toast.LENGTH_LONG).show();
                                    }
                                } catch (JSONException e) {
                                    e.printStackTrace();
                                }
                            }

                            pd.dismiss();
                        }
                    },new Response.ErrorListener(){
                        @Override
                        public void onErrorResponse(VolleyError 
                        volleyError) {
                            System.out.println("" + volleyError);
                            pd.dismiss();
                        }
                    });

                    RequestQueue rQueue = 
                    Volley.newRequestQueue(MainActivity.this);
                    rQueue.add(request);
                }
            }
        });

    }
}

现在,允许用户成功登录后,我们需要显示用户列表,忽略登录的那个。但用户应该能够看到其他用户列表。现在,让我们处理ListView中的用户列表。让我们连接组件:

//Instances 
private ListView mUsersList;
private TextView mNoUsersText;
private ArrayList<String> mArraylist = new ArrayList<>();
private int totalUsers = 0;
private ProgressDialog mProgressDialog;

//inside onCreate method 
mUsersList = (ListView)findViewById(R.id.usersList);
mNoUsersText = (TextView)findViewById(R.id.noUsersText);
mProgressDialog = new ProgressDialog(UsersListActivity.this);

mProgressDialog.setMessage("Loading...");
mProgressDialog.show();

现在,在onCreate方法中,我们将初始化volley并获取用户列表,如下所示:

String url = "https://packt-wear.firebaseio.com/users.json";
    StringRequest request = new StringRequest(Request.Method.GET, url, 
    new Response.Listener<String>(){
        @Override
        public void onResponse(String s) {
 doOnSuccess(s);
        }
    },new Response.ErrorListener(){
        @Override
        public void onErrorResponse(VolleyError volleyError) {
            System.out.println("" + volleyError);
        }
    });

    RequestQueue rQueue = 
    Volley.newRequestQueue(UsersListActivity.this);
    rQueue.add(request);

    mUsersList.setOnItemClickListener(new 
    AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int 
        position, long id) {
            User.chatWith = mArraylist.get(position);
            startActivity(new Intent(UsersListActivity.this, 
            ChatActivity.class));
        }
    });

}

当我们将 Firebase 端点 URL 公开时,任何拥有该 URL 的人都可以读取和写入端点。我只是使用 URL 并添加.json作为扩展名,这样它会返回 JSON 结果。现在,我们需要编写最后一个用于管理成功结果的方法:

public void doOnSuccess(String s){
    try {
        JSONObject obj = new JSONObject(s);

        Iterator i = obj.keys();
        String key = "";

        while(i.hasNext()){
            key = i.next().toString();

            if(!key.equals(User.username)) {
                mArraylist.add(key);
            }

            totalUsers++;
        }

    } catch (JSONException e) {
        e.printStackTrace();
    }

    if(totalUsers <=1){
        mNoUsersText.setVisibility(View.VISIBLE);
        mUsersList.setVisibility(View.GONE);
    }
    else{
        mNoUsersText.setVisibility(View.GONE);
        mUsersList.setVisibility(View.VISIBLE);
        mUsersList.setAdapter(new ArrayAdapter<String>(this, 
        android.R.layout.simple_list_item_1, mArraylist));
    }

    mProgressDialog.dismiss();
}

完整的类将如下所示:

public class UsersListActivity extends AppCompatActivity {

    private ListView mUsersList;
    private TextView mNoUsersText;
    private ArrayList<String> mArraylist = new ArrayList<>();
    private int totalUsers = 0;
    private ProgressDialog mProgressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_users_list);
        mUsersList = (ListView)findViewById(R.id.usersList);
        mNoUsersText = (TextView)findViewById(R.id.noUsersText);

        mProgressDialog = new ProgressDialog(UsersListActivity.this);
        mProgressDialog.setMessage("Loading...");
        mProgressDialog.show();

        String url = "https://packt-wear.firebaseio.com/users.json";
        StringRequest request = new StringRequest(Request.Method.GET, 
        url, new Response.Listener<String>(){
            @Override
            public void onResponse(String s) {
                doOnSuccess(s);
            }
        },new Response.ErrorListener(){
            @Override
            public void onErrorResponse(VolleyError volleyError) {
                System.out.println("" + volleyError);
            }
        });

        RequestQueue rQueue = 
        Volley.newRequestQueue(UsersListActivity.this);
        rQueue.add(request);

        mUsersList.setOnItemClickListener(new 
        AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, 
            int position, long id) {
                User.chatWith = mArraylist.get(position);
                startActivity(new Intent(UsersListActivity.this, 
                ChatActivity.class));
            }
        });

    }

    public void doOnSuccess(String s){
        try {
            JSONObject obj = new JSONObject(s);

            Iterator i = obj.keys();
            String key = "";

            while(i.hasNext()){
                key = i.next().toString();

                if(!key.equals(User.username)) {
                    mArraylist.add(key);
                }

                totalUsers++;
            }

        } catch (JSONException e) {
            e.printStackTrace();
        }

        if(totalUsers <=1){
            mNoUsersText.setVisibility(View.VISIBLE);
            mUsersList.setVisibility(View.GONE);
        }
        else{
            mNoUsersText.setVisibility(View.GONE);
            mUsersList.setVisibility(View.VISIBLE);
            mUsersList.setAdapter(new ArrayAdapter<String>(this, 
            android.R.layout.simple_list_item_1, mArraylist));
        }

        mProgressDialog.dismiss();
    }
}

现在,我们已经完成了一个流程,即引导用户并显示可聊天的用户列表。现在是时候处理实际的聊天逻辑了。让我们开始处理ChatActivity

对于消息背景,我们将添加两个可绘制资源文件:rounded_corner1.xmlrounded_corner2.xml。让我们为可绘制资源文件添加 XML 代码:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#dddddd" />
    <stroke
        android:width="0dip"
        android:color="#dddddd" />
    <corners android:radius="10dip" />
    <padding
        android:bottom="5dip"
        android:left="5dip"
        android:right="5dip"
        android:top="5dip" />
</shape>

对于rounded_corner2.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#f0f0f0" />
    <stroke
        android:width="0dip"
        android:color="#f0f0f0" />
    <corners android:radius="10dip" />
    <padding
        android:bottom="5dip"
        android:left="5dip"
        android:right="5dip"
        android:top="5dip" />
</shape>

现在,让我们为 Firebase 聊天活动声明必要的实例:

//Global instances
LinearLayout mLinearlayout;
ImageView mSendButton;
EditText mMessageArea;
ScrollView mScrollview;
Firebase reference1, reference2;

//inside onCreate method 
mLinearlayout = (LinearLayout)findViewById(R.id.layout1);
mSendButton = (ImageView)findViewById(R.id.sendButton);
mMessageArea = (EditText)findViewById(R.id.messageArea);
mScrollview = (ScrollView)findViewById(R.id.scrollView);

Firebase.setAndroidContext(this);
reference1 = new Firebase("https://packt-wear.firebaseio.com/messages/" + User.username + "_" + User.chatWith);
reference2 = new Firebase("https://packt-wear.firebaseio.com/messages/" + User.chatWith + "_" + User.username);

点击发送按钮时,使用push()方法,我们可以将用户名和发送的消息更新到 Firebase:

mSendButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        String messageText = mMessageArea.getText().toString();

        if(!messageText.equals("")){
            Map<String, String> map = new HashMap<String, String>();
            map.put("message", messageText);
 map.put("user", User.username);
            reference1.push().setValue(map);
 reference2.push().setValue(map);
        }
    }
});

我们需要从 Firebase 的addChildEventListener()实现回调。在onChildAdded方法中,我们可以显示添加的消息。以下代码完成了 Firebase 的添加,并为消息添加了背景:

reference1.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String s) {
        Map map = dataSnapshot.getValue(Map.class);
        String message = map.get("message").toString();
        String userName = map.get("user").toString();

        if(userName.equals(User.username)){
 addMessageBox("You:-\n" + message, 1);
        }
        else{
 addMessageBox(User.chatWith + ":-\n" + message, 2);
        }
    }

    @Override
    public void onChildChanged(DataSnapshot dataSnapshot, String s) {

    }

    @Override
    public void onChildRemoved(DataSnapshot dataSnapshot) {

    }

    @Override
    public void onChildMoved(DataSnapshot dataSnapshot, String s) {

    }

    @Override
    public void onCancelled(FirebaseError firebaseError) {

    }
});

addMessageBox方法改变了发送者和接收者消息的背景:

public void addMessageBox(String message, int type){
TextView textView = new TextView(ChatActivity.this);
textView.setText(message);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.setMargins(0, 0, 0, 10);
textView.setLayoutParams(lp);

    if(type == 1) {
textView.setBackgroundResource(R.drawable.rounded_corner1);
}
    else{
textView.setBackgroundResource(R.drawable.rounded_corner2);
}

    mLinearlayout.addView(textView);
    mScrollview.fullScroll(View.FOCUS_DOWN);
}

ChatActivity的完整代码如下:

public class ChatActivity extends AppCompatActivity {

    LinearLayout mLinearlayout;
    ImageView mSendButton;
    EditText mMessageArea;
    ScrollView mScrollview;
    Firebase reference1, reference2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat);

        mLinearlayout = (LinearLayout)findViewById(R.id.layout1);
        mSendButton = (ImageView)findViewById(R.id.sendButton);
        mMessageArea = (EditText)findViewById(R.id.messageArea);
        mScrollview = (ScrollView)findViewById(R.id.scrollView);

        Firebase.setAndroidContext(this);
        reference1 = new Firebase("https://packt-
        wear.firebaseio.com/messages/" + User.username + "_" + 
        User.chatWith);
        reference2 = new Firebase("https://packt-  
        wear.firebaseio.com/messages/" + User.chatWith + "_" + 
        User.username);

        mSendButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String messageText = mMessageArea.getText().toString();

                if(!messageText.equals("")){
                    Map<String, String> map = new HashMap<String, 
                    String>();
                    map.put("message", messageText);
                    map.put("user", User.username);
                    reference1.push().setValue(map);
                    reference2.push().setValue(map);
                }
            }
        });

        reference1.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, 
            String s) {
                Map map = dataSnapshot.getValue(Map.class);
                String message = map.get("message").toString();
                String userName = map.get("user").toString();

                if(userName.equals(User.username)){
                    addMessageBox("You:-\n" + message, 1);
                }
                else{
                    addMessageBox(User.chatWith + ":-\n" + message, 2);
                }
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, 
            String s) {

            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {

            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, 
            String s) {

            }

            @Override
            public void onCancelled(FirebaseError firebaseError) {

            }
        });
    }

    public void addMessageBox(String message, int type){
        TextView textView = new TextView(ChatActivity.this);
        textView.setText(message);
        LinearLayout.LayoutParams lp = new LinearLayout
        .LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 
        ViewGroup.LayoutParams.WRAP_CONTENT);
        lp.setMargins(0, 0, 0, 10);
        textView.setLayoutParams(lp);

        if(type == 1) {
            textView.setBackgroundResource(R.drawable.rounded_corner1);
        }
        else{
            textView.setBackgroundResource(R.drawable.rounded_corner2);
        }

        mLinearlayout.addView(textView);
        mScrollview.fullScroll(View.FOCUS_DOWN);
    }
}

我们已经为移动应用程序完成了完整的聊天模块。现在,让我们为聊天应用程序编写 Wear 模块。

Wear 应用程序实现

Wear 模块的目标是,穿戴设备应接收新消息并在应用中显示,用户应能够从穿戴设备回复这些消息。在这里,我们将了解 Wear 和移动应用之间建立通信的类和 API。

现在,Android Studio 为计时器应用生成了样板代码。我们需要做的是删除所有代码,只保留 onCreate() 方法。稍后,在 activity_main.xml 文件中,让我们添加一个帮助用户聊天的用户界面。这里,我将使用 EdittextTextview,以及一个将消息发送到移动设备的按钮。让我们添加以下 XML 代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.packt.smartchat.MainActivity"
    tools:deviceIds="wear">

    <LinearLayout
        android:layout_gravity="center|center_horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="Message"
            android:layout_margin="30dp"
            app:layout_box="all" />

        <EditText
            android:id="@+id/message"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <Button
            android:id="@+id/send"
            android:layout_gravity="center"
            android:text="Send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>

</android.support.wearable.view.BoxInsetLayout>

现在,用户界面已准备就绪。当我们希望接收和发送消息到移动设备时,我们需要编写一个扩展了 WearableListenerService 的服务类,并且需要在清单文件中注册该服务类:

public class ListenerService extends WearableListenerService {

    @Override
    public void onMessageReceived(MessageEvent messageEvent) {

        if (messageEvent.getPath().equals("/message_path")) {
            final String message = new String(messageEvent.getData());
            Log.v("myTag", "Message path received on watch is: " + 
            messageEvent.getPath());
            Log.v("myTag", "Message received on watch is: " + message);

            // Broadcast message to wearable activity for display
            Intent messageIntent = new Intent();
            messageIntent.setAction(Intent.ACTION_SEND);
            messageIntent.putExtra("message", message);
            LocalBroadcastManager
            .getInstance(this).sendBroadcast(messageIntent);
        }
        else {
            super.onMessageReceived(messageEvent);
        }
    }
}

现在,在清单文件的 application 标签范围内以下面的方式注册 service 类:

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@android:style/Theme.DeviceDefault">

<activity...></activity>

<service android:name=".ListenerService">
 <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="/message_path" />
 </intent-filter>
</service>

</application>

我们使用最新标准注册了 wear 服务。之前,我们不得不使用 BIND_LISTENER API 来注册服务。由于其低效,它已被弃用。我们必须使用之前的 DATA_CHANGEDMESSAGE_RECEIVED API,因为它们允许应用监听特定路径。而 BIND_LISTENER API 监听广泛范围的系统消息,这是性能和电池电量的缺点。以下代码展示了已弃用的 BIND_LISTENER 注册方法:

 <service android:name=".ListenerService">
 <intent-filter>
 <action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
 </intent-filter>
 </service>

在清单文件中注册服务后,我们可以在 Wear 模块中的 MainActivity 直接进行操作。在开始之前,请确保你已将 MainActivity 中的所有样板代码删除,只留下一个 onCreate 方法,如下所示:

public class MainActivity extends WearableActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setAmbientEnabled();

    }
}

MainActivity 中连接所有 XML 组件:

mTextView = (TextView)findViewById(R.id.text);
send = (Button) findViewById(R.id.send);
message = (EditText)findViewById(R.id.message);

让我们实现 GoogleApiClient 的接口,以帮助查找已连接的节点和处理失败的场景:

public class MainActivity extends WearableActivity implements GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener  {

                 .......

}

实现 ConnectionCallbacksOnConnectionFailedListener 之后,我们必须从这些接口重写一些方法,即 onConnectedonConnectionSuspendedonConnectionFailed 方法。我们大部分逻辑将在 onConnected 方法中编写。现在,在 MainActivity 范围内,我们需要编写一个扩展了 BroadcastReciever 的类,并重写 onReceive 方法以监听消息:

public class MessageReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String message = intent.getStringExtra("message");
        Log.v("packtchat", "Main activity received message: " + 
        message);
        // Display message in UI
        mTextView.setText(message);
    }
}

onCreate 方法中注册本地广播接收器,如下所示:

// Register the local broadcast receiver
IntentFilter messageFilter = new IntentFilter(Intent.ACTION_SEND);
MessageReceiver messageReceiver = new MessageReceiver();
LocalBroadcastManager.getInstance(this).registerReceiver(messageReceiver, messageFilter);

声明 GoogleApiClient 和 Node 实例,以及用于从穿戴设备发送消息的 WEAR_PATH,移动应用将监听这些消息:

private Node mNode;
private GoogleApiClient mGoogleApiClient;
private static final String WEAR_PATH = "/from-wear";

onCreate 方法中初始化 mGoogleApiclient

//Initialize mGoogleApiClient
mGoogleApiClient = new GoogleApiClient.Builder(this)
        .addApi(Wearable.API)
        .addConnectionCallbacks(this)
        .addOnConnectionFailedListener(this)
        .build();

onConnected 方法中,我们将使用可穿戴节点 API 来获取所有已连接的节点。它可以是已配对的穿戴设备或移动设备。使用以下代码,我们将知道哪些穿戴设备已配对:

@Override
public void onConnected(@Nullable Bundle bundle) {
    Wearable.NodeApi.getConnectedNodes(mGoogleApiClient)
            .setResultCallback(new 
            ResultCallback<NodeApi.GetConnectedNodesResult>() {
                @Override
                public void onResult(NodeApi.GetConnectedNodes
                Result nodes) {
                    for (Node node : nodes.getNodes()) {
                        if (node != null && node.isNearby()) {
                            mNode = node;
                            Log.d("packtchat", "Connected to " + 
                            mNode.getDisplayName());
                        }
                    }
                    if (mNode == null) {
                        Log.d("packtchat", "Not connected!");
                    }
                }
            });
}

现在,我们需要为send按钮实例添加clicklistener,以从edittext获取值并将其传递给发送消息到移动设备的方法:

send.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mMsgStr = message.getText().toString();
        sendMessage(mMsgStr);
    }
});

sendMessage方法接受一个字符串参数,并将相同的字符串消息以字节的形式发送到已连接的节点。使用MessageAPI,我们将消息发送到移动设备。以下代码解释了如何实现这一点:

private void sendMessage(String city) {
    if (mNode != null && mGoogleApiClient != null) {
        Wearable.MessageApi.sendMessage(mGoogleApiClient, 
        mNode.getId(), WEAR_PATH, city.getBytes())
                .setResultCallback(new 
                ResultCallback<MessageApi.SendMessageResult>() {
                    @Override
                    public void onResult(MessageApi.SendMessageResult 
                    sendMessageResult) {
                        if (!sendMessageResult.getStatus().isSuccess()) 
                        {
                            Log.d("packtchat", "Failed message: " +

                            sendMessageResult.getStatus()
                            .getStatusCode());
                        } else {
                            Log.d("packtchat", "Message succeeded");
                        }
                    }
                });
    }
}

让我们重写onstartonstop方法,以便在连接和断开GoogleAPIClient时寻求帮助:

@Override
protected void onStart() {
    super.onStart();
    mGoogleApiClient.connect();
}

@Override
protected void onStop() {
    super.onStop();
    mGoogleApiClient.disconnect();
}

完整的穿戴模块MainActivity代码如下:

public class MainActivity extends WearableActivity implements GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener  {

    private TextView mTextView;
    Button send;
    EditText message;
    String mMsgStr;

    private Node mNode;
    private GoogleApiClient mGoogleApiClient;
    private static final String WEAR_PATH = "/from-wear";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = (TextView)findViewById(R.id.text);
        send = (Button) findViewById(R.id.send);
        message = (EditText)findViewById(R.id.message);
        setAmbientEnabled();

        //Initialize mGoogleApiClient
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addApi(Wearable.API)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .build();

        // Register the local broadcast receiver
        IntentFilter messageFilter = new 
        IntentFilter(Intent.ACTION_SEND);
        MessageReceiver messageReceiver = new MessageReceiver();
        LocalBroadcastManager.getInstance(this)
        .registerReceiver(messageReceiver, messageFilter);

        send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mMsgStr = message.getText().toString();
                sendMessage(mMsgStr);
            }
        });

    }

    private void sendMessage(String message) {
        if (mNode != null && mGoogleApiClient != null) {
            Wearable.MessageApi.sendMessage(mGoogleApiClient, 
            mNode.getId(), WEAR_PATH, message.getBytes())
                    .setResultCallback(new 
                    ResultCallback<MessageApi.SendMessageResult>() {
                        @Override
                        public void onResult(MessageApi
                        .SendMessageResult sendMessageResult) {
                            if (!sendMessageResult.getStatus()
                            .isSuccess()) {
                                Log.d("packtchat", "Failed message: " +

                                sendMessageResult.getStatus()
                                .getStatusCode());
                            } else {
                                Log.d("packtchat", "Message 
                                succeeded");
                            }
                        }
                    });
        }
    }

    public class MessageReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String message = intent.getStringExtra("message");
            Log.v("packtchat", "Main activity received message: " + 
            message);
            // Display message in UI
            mTextView.setText(message);

        }
    }

    @Override
    public void onConnected(@Nullable Bundle bundle) {
        Wearable.NodeApi.getConnectedNodes(mGoogleApiClient)
                .setResultCallback(new 
                 ResultCallback<NodeApi.GetConnectedNodesResult>() {
                    @Override
                    public void 
                    onResult(NodeApi.GetConnectedNodesResult nodes) {
                        for (Node node : nodes.getNodes()) {
                            if (node != null && node.isNearby()) {
                                mNode = node;
                                Log.d("packtchat", "Connected to " + 
                                mNode.getDisplayName());
                            }
                        }
                        if (mNode == null) {
                            Log.d("packtchat", "Not connected!");
                        }
                    }
                });
    }

    @Override
    protected void onStart() {
        super.onStart();
        mGoogleApiClient.connect();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mGoogleApiClient.disconnect();
    }

    @Override
    public void onConnectionSuspended(int i) {

    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult 
    connectionResult) {

    }
}

穿戴模块已准备好接收和发送消息到连接的设备。现在,我们需要升级我们的移动模块以接收和发送消息到穿戴设备。

切换到移动模块,并创建一个服务类WearListner,然后重写onMessageReceived方法,如下所示:

public class WearListner extends WearableListenerService {

    @Override
    public void onMessageReceived(MessageEvent messageEvent) {
        if (messageEvent.getPath().equals("/from-wear")) {
            final String message = new String(messageEvent.getData());
            Log.v("pactchat", "Message path received on watch is: " + 
            messageEvent.getPath());
            Log.v("packtchat", "Message received on watch is: " + 
            message);

            // Broadcast message to wearable activity for display
            Intent messageIntent = new Intent();
            messageIntent.setAction(Intent.ACTION_SEND);
            messageIntent.putExtra("message", message);
            LocalBroadcastManager
            .getInstance(this).sendBroadcast(messageIntent);
        }
        else {
            super.onMessageReceived(messageEvent);
        }
    }
}

现在,在清单文件的 application 标签范围内注册这个WearListner类:

<service android:name=".WearListner">
    <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="/from-wear" />
    </intent-filter>
</service>

现在,让我们将工作范围切换到移动模块,并添加以下更改以实现与穿戴设备和移动设备的深度链接。

ChatActivity中实现GoogleApiClient类中的ConnectionCallbacksOnConnectionFailedListener接口:

public class ChatActivity extends AppCompatActivity implements
        GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener {
...
}

重写ConnectionCallbacksOnConnectionFailedListener接口中的方法,类似于我们对穿戴设备的MainActivity所做的那样:

onCreate方法中初始化GoogleApiClient,如下所示:

googleClient = new GoogleApiClient.Builder(this)
        .addApi(Wearable.API)
        .addConnectionCallbacks(this)
        .addOnConnectionFailedListener(this)
        .build();

ChatActivity中编写广播接收器类以及我们收到的字符串信息。我们需要在已经编写的addMessageBox方法中传递它:

public class MessageReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String message = intent.getStringExtra("message");
        Log.v("myTag", "Main activity received message: " + message);
        // Displaysage in UI
        addMessageBox("You:-\n" + message, 1);
        if(!message.equals("")){
            Map<String, String> map = new HashMap<String, String>();
            map.put("message", message);
            map.put("user", User.username);
            reference1.push().setValue(map);
        }
        new SendToDataLayerThread("/message_path","You:-\n" + 
        message).start();
    }
}

onCreate方法中注册MessageReciever,如下所示:

// Register the local broadcast receiver
IntentFilter messageFilter = new IntentFilter(Intent.ACTION_SEND);
MessageReceiver messageReceiver = new MessageReceiver();
LocalBroadcastManager.getInstance(this).registerReceiver(messageReceiver, messageFilter);

注册广播接收器后,编写SendToDataLayerThread类,该类扩展了Thread类,在单独的线程中处理所有负载,但仍在 UI 线程中。在void run方法中,我们将检查所有已连接的节点并遍历已连接的节点。一旦建立连接,我们将使用MessageAPI发送消息,如代码所示。Message APIsendMessage方法会查找一些参数,例如googleclient和已连接节点 ID 路径,正是我们在穿戴设备清单中注册的内容以及实际的消息字节。使用SendMessageResult实例,我们开发者可以确保从设备发出的消息是否成功到达节点:

class SendToDataLayerThread extends Thread {
    String path;
    String message;

    // Constructor to send a message to the data layer
    SendToDataLayerThread(String p, String msg) {
        path = p;
        message = msg;
    }

    public void run() {
        NodeApi.GetConnectedNodesResult nodes = 
        Wearable.NodeApi.getConnectedNodes(googleClient).await();
        for (Node node : nodes.getNodes()) {
            MessageApi.SendMessageResult result = Wearable.MessageApi
           .sendMessage(googleClient, node.getId(), path, 
           message.getBytes()).await();
            if (result.getStatus().isSuccess()) {
                Log.v("myTag", "Message: {" + message + "} sent to: " + 
                node.getDisplayName());
            } else {
                // Log an error
                Log.v("myTag", "ERROR: failed to send Message");
            }
        }
    }
}

我们需要在chatActivity的几个方法中初始化sendtoDatalayer线程:

@Override
public void onConnected(@Nullable Bundle bundle) {
    String message = "Conected";
    //Requires a new thread to avoid blocking the UI
    new SendToDataLayerThread("/message_path", message).start();
}

reference1更新了某些子事件时,我们需要将消息添加到sendToDatalayer线程中:

reference1.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String s) {
        Map map = dataSnapshot.getValue(Map.class);
        String message = map.get("message").toString();
        String userName = map.get("user").toString();
        mMessageArea.setText("");
        if(userName.equals(User.username)){
            addMessageBox("You:-\n" + message, 1);
            new SendToDataLayerThread("/message_path","You:-\n" + 
            message).start();

        }
        else{
            addMessageBox(User.chatWith + ":-\n" + message, 2);
            new SendToDataLayerThread("/message_path", User.chatWith + 
            ":-\n" + message).start();

        }
    }

为连接和断开GoogleApiClient添加以下回调,在ChatActivity中添加回调:

@Override
protected void onStart() {
    super.onStart();
    googleClient.connect();
}

@Override
protected void onStop() {
    if (null != googleClient && googleClient.isConnected()) {
        googleClient.disconnect();
    }
    super.onStop();
}

聊天应用程序通过与穿戴设备和移动设备的交互而完整。ChatActivity收到的每条消息都会发送到穿戴设备,并且穿戴设备的回复会更新到移动设备。

显示两个用户之间基本对话的聊天界面如下所示:

在圆形表盘手表上,可穿戴应用界面将如下所示:

概述

在本章中,我们了解了如何利用 Firebase 实时数据库作为聊天媒介。我们构建了一个简单的消息应用,可以从可穿戴设备发送回复。这个项目有很大的扩展空间,可以增强项目的各个元素。我们看到了聊天应用如何在可穿戴设备和手持设备之间发送和接收消息。我们从零开始构建了一个聊天应用,并为节点设置了数据层事件,以便它们之间进行通信。消息传递 API 的基本思想是加强我们对可穿戴设备通信的理解。同时,GoogleApiClient 类在 Play 服务中扮演了重要的角色。

在下一章中,我们将了解通知、Firebase 功能,以及如何使用 Firebase 函数触发推送通知。

第九章:让我们以智能的方式进行聊天 - 通知及其他

前一章帮助我们构建了一个对话式消息应用,但穿戴设备的应用界面非常普通,没有任何通知功能。通知是消息应用非常重要的一个方面,但实现它需要复杂的基础架构来处理通知。当发送者向接收者发送消息时,接收者应该收到一个通知,传达一些信息,比如发送者的名字以及一条快速消息预览。

通知是 Android 中我们可以用来显示信息的组件。在消息应用的情况下,接收者应该收到推送通知来实例化通知组件。所以,每当 Firebase 中的实时数据库更新时,手持设备和穿戴设备都应该收到通知。幸运的是,我们不需要服务器来处理通知;Firebase 会为你的消息应用处理推送通知。当实时数据库有更新时,我们需要触发推送通知。

在本章中,我们将探讨以下内容:

  • Firebase 函数

  • 通知

  • 材料设计穿戴应用

Firebase 函数

Firebase 函数是监控数据库触发器以及许多服务器相关执行的最智能解决方案。我们不需要托管一个服务器来监听 Firebase 数据库的变化,然后再触发推送通知,我们可以利用 Firebase 的一项技术来完成这项任务。Firebase 函数对所有 Firebase 技术都有有效的控制,比如 Firebase 认证、存储、分析等。Firebase 函数可以在各种方面使用;例如,当你的分析群组达到某个里程碑时,你可以发送有针对性的通知、邀请等。任何你可能在 Firebase 系统中实现的服务器级别的业务逻辑都可以用 Firebase 函数来完成。我们将使用 Firebase 函数在数据库触发时发送推送通知。完成这项任务我们需要具备入门级的 JavaScript 知识。

要开始使用 Firebase 函数,我们需要 Node.js 环境,你可以从 nodejs.org/ 安装它。安装 Node.js 时,它还会安装 node 包管理器 (npm)。它有助于安装 JavaScript 框架、插件等。安装 Node.js 后,打开终端或命令行。

输入 $node --version 检查是否安装了 node。如果 CLI 返回最新的版本号,那么你就可以开始了:

//Install Firebase CLI 
$ npm install -g firebase-tools

如果你遇到任何错误,你应该以超级用户模式执行命令:

sudo su
npm install -g firebase-tools

导航到你想保存 Firebase 函数程序的目录,并使用以下命令进行身份验证:

//CLI authentication
$ firebase login

成功认证后,你可以初始化 Firebase 函数:

//Initialise Firebase functions
$ firebase init functions

CLI 工具将在您初始化的目录中生成 Firebase 函数和必要的代码。用您喜欢的编辑器打开 index.js。我们使用 functions.database 创建一个用于处理实时数据库触发器的 Realtime 数据库事件的 Firebase 函数。我们将调用 ref(path),以从函数中达到特定的数据库 pathonwrite() 方法,该方法将在数据库更新时发送通知。

现在,为了构建我们的通知负载,让我们了解我们的实时数据库结构:

我们可以看到,消息有一个名为 ashok_geetha 的子节点,这意味着这两个用户的对话存储在其中,具有唯一的 Firebase ID。对于此实现,我们将选择 ashok_geetha 以推送通知。

现在,在 index.js 文件中,添加以下代码:

//import firebase functions modules
const functions = require('firebase-functions');
//import admin module
        const admin = require('firebase-admin');
        admin.initializeApp(functions.config().firebase);

// Listens for new messages added to messages/:pushId
        exports.pushNotification = functions.database
        .ref('/messages/ashok_geetha/{pushId}').onWrite( event => {

        console.log('Push notification event triggered');

        //  Grab the current value of what was written to the Realtime 
        Database.
        var valueObject = event.data.val();

        if(valueObject.photoUrl != null) {
        valueObject.photoUrl= "Sent you a lot of love!";
        }

        // Create a notification
        const payload = {
        notification: {
        title:valueObject.message,
        body: valueObject.user || valueObject.photoUrl,
        sound: "default"
        },
        };

        //Create an options object that contains the time to live for 
        the notification and the priority
        const options = {
        priority: "high",
        timeToLive: 60 * 60 * 24
        };

        return admin.messaging().sendToTopic("pushNotifications", 
        payload, options);
        });

当我们拥有不同的 Firebase 结构时,通过使用 url-id 配置,我们可以触发 Firebase 函数,以使推送通知能够与所有用户一起工作。我们只需在 url 中进行以下更改即可。 /messages/{ChatID}/{pushId}

现在,在终端中,使用 $ firebase deploy 命令完成 Firebase 函数的部署:

之前的 Node.js 设置使用 Firebase 函数触发推送通知。现在,我们需要一种机制,以便在我们的本地移动和穿戴应用程序中接收来自 Firebase 的消息。

切换到 Android Studio,并将以下依赖项添加到您的移动项目 gradle 模块文件中:

//Add the dependency
dependencies {
     compile 'com.google.firebase:firebase-messaging:10.2.4'
}

添加依赖项后,创建一个名为 MessagingService 的类,该类扩展自 FirebaseMessagingService 类。FirebaseMessaging 类扩展自 com.google.firebase.iid.zzb,而 zzb 类扩展自 Android Service 类。这个类将帮助 Firebase 消息传递和 Android 应用之间的通信过程。它还可以自动显示通知。让我们创建一个类并将其扩展到 FirebaseMessagingService 类,如下所示:

public class MessagingService extends FirebaseMessagingService {

    //Override methods
}

是时候添加 onMessageReceived 重写方法了。当应用在前台或后台时,该方法接收通知,我们可以使用 getnotification() 方法检索所有通知参数:

@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
    super.onMessageReceived(remoteMessage);
}

RemoteMessage 对象将包含我们从 Firebase 函数中的通知负载中发送的所有数据。在方法内部,添加以下代码以获取标题和消息内容。我们在 Firebase 函数上发送标题参数中的消息;您可以根据自己的用例进行自定义:

String notificationTitle = null, notificationBody = null;
// Check if message contains a notification payload.
if (remoteMessage.getNotification() != null) {
    Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
    notificationTitle = remoteMessage.getNotification().getTitle();
    notificationBody = remoteMessage.getNotification().getBody();
}

为了构建通知,我们将使用 NotificationCompat.Builder,当用户点击通知时,我们会将他带到 MainActivity

private void sendNotification(String notificationTitle, String notificationBody) {
    Intent intent = new Intent(this, MainActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, 
    intent,
            PendingIntent.FLAG_ONE_SHOT);

    Uri defaultSoundUri= RingtoneManager.getDefaultUri
    (RingtoneManager.TYPE_NOTIFICATION);
    NotificationCompat.Builder notificationBuilder = 
    (NotificationCompat.Builder) new NotificationCompat.Builder(this)
            .setAutoCancel(true)   //Automatically delete the 
                                   notification
            .setSmallIcon(R.mipmap.ic_launcher) //Notification icon
            .setContentIntent(pendingIntent)
            .setContentTitle(notificationTitle)
            .setContentText(notificationBody)
            .setSound(defaultSoundUri);

    NotificationManager notificationManager = (NotificationManager) 
    getSystemService(Context.NOTIFICATION_SERVICE);

    notificationManager.notify(0, notificationBuilder.build());
}

onMessageReceived 内部调用该方法,并将内容传递给 sendNotification 方法:

sendNotification(notificationTitle, notificationBody);

完整的类代码如下所示:

public class MessagingService extends FirebaseMessagingService {

    private static final String TAG = "MessagingService";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        String notificationTitle = null, notificationBody = null;
        // Check if message contains a notification payload.
        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification Body: " + 
            remoteMessage.getNotification().getBody());
            notificationTitle = 
            remoteMessage.getNotification().getTitle();
            notificationBody = 
            remoteMessage.getNotification().getBody();

            sendNotification(notificationTitle, notificationBody);

        }
    }

    private void sendNotification(String notificationTitle, String 
    notificationBody) {
        Intent intent = new Intent(this, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 
       0, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri= RingtoneManager.getDefaultUri
        (RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder =     
        (NotificationCompat.Builder) 
        new NotificationCompat.Builder(this)
                .setAutoCancel(true)   //Automatically delete the 
                                  notification
                .setSmallIcon(R.mipmap.ic_launcher) //Notification icon
                .setContentIntent(pendingIntent)
                .setContentTitle(notificationTitle)
                .setContentText(notificationBody)
                .setSound(defaultSoundUri);

        NotificationManager notificationManager = (NotificationManager) 
       getSystemService(Context.NOTIFICATION_SERVICE);

        notificationManager.notify(0, notificationBuilder.build());
    }
}

我们现在可以在清单中注册之前的服务:

<service
    android:name=".MessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
</service>

毕竟,我们现在有一个用于监听 pushNotification 的服务,但我们需要监听我们发送的特定字符串。我们可以将字符串添加到一些常量中或 XML 文件中,但是当我们要求 Firebase 发送特定频道的通知时,我们需要订阅一个称为主题的频道。在 ChatActivity 中添加以下代码,并在方法内部:

FirebaseMessaging.getInstance().subscribeToTopic("pushNotifications");

为了使之前的操作全局化,创建一个扩展 Application 类的类。在 onCreate 方法内部,我们可以按照以下方式订阅主题:

public class PacktApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Firebase.setAndroidContext(this);
        FirebaseMessaging.getInstance()
        .subscribeToTopic("pushNotifications");
    }
}

现在,我们需要在清单中注册应用程序类。应用程序类控制着应用程序生命周期的 onCreate 方法,这将有助于维护应用程序的生命状态:

<application
    android:name=".PacktApp"
    ...>

</application>

恭喜!我们已经成功配置了推送通知,并在我们的手机上收到了通知:

在已连接的 Wear 设备中,当我们收到通知时,我们将能够看到以下通知。默认情况下,NotificationCompat.Builder 类可以帮助 Wear 设备接收通知,如果我们想要自定义它,可以通过以下部分进行操作。

从 Wear 通知组件,我们可以直接从 Wear 设备接收回复到移动应用程序。为了实现这一点,我们将使用 NotificationCompat 类中的 WearExtender 组件。使用此设置,用户将能够使用语音输入、输入法框架IMF)和表情符号回复通知:

 Notification notification = new NotificationCompat.Builder(mContext)
         .setContentTitle("New mail from " + sender.toString())
         .setContentText(subject)
         .setSmallIcon(R.drawable.new_mail)
         .extend(new NotificationCompat.WearableExtender()
                 .setContentIcon(R.drawable.new_mail))
         .build();
 NotificationManagerCompat.from(mContext).notify(0, notification);

将会有很多用例,我们需要发送带有已经存储的回复的快速响应,或者快速输入功能。在这种情况下,我们可以使用 Action.WearableExtender

/Android Wear requires a hint to display the reply action inline.
Action.WearableExtender actionExtender =
    new Action.WearableExtender()
        .setHintLaunchesActivity(true)
        .setHintDisplayActionInline(true);
wearableExtender.addAction(actionBuilder.extend(actionExtender).build());

现在,在项目中,让我们更新我们的消息服务类,当后台服务接收到推送通知时,我们会触发通知组件。

将以下实例添加到类的全局作用域中:

private static final String TAG = "MessagingService";
public static final String EXTRA_VOICE_REPLY = "extra_voice_reply";
public static final String REPLY_ACTION =
        "com.packt.smartcha.ACTION_MESSAGE_REPLY";

当我们从 Wear 设备收到回复时,我们将保留该引用并将其传递给通知处理程序:

// Creates an Intent that will be triggered when a reply is received.
private Intent getMessageReplyIntent(int conversationId) {
    return new Intent().setAction(REPLY_ACTION).putExtra("1223", 
    conversationId);
}

Wear 通知随着每个新的 Android 版本的发布而不断发展,在 NotificationCompat.Builder 中,我们有所有可以使您的移动应用程序与 Wear 设备交互的功能。当您有一个移动应用程序,并且它有来自 Wear 设备的交互,如通知等,即使没有 Wear 伴侣应用程序,您也可以获得文本回复。

消息服务类

MessagingService 类中,我们有一个名为 sendNotification 的方法,该方法向 Wear 和移动设备发送通知。让我们用以下代码更改更新该方法:

private void sendNotification(String notificationTitle, String notificationBody) {
    // Wear 2.0 allows for in-line actions, which will be used for 
    "reply".
    NotificationCompat.Action.WearableExtender inlineActionForWear2 =
            new NotificationCompat.Action.WearableExtender()
                    .setHintDisplayActionInline(true)
                    .setHintLaunchesActivity(false);

    RemoteInput remoteInput = new 
    RemoteInput.Builder("extra_voice_reply").build();

    // Building a Pending Intent for the reply action to trigger.
    PendingIntent replyIntent = PendingIntent.getBroadcast(
            getApplicationContext(),
            0,
            getMessageReplyIntent(1),
            PendingIntent.FLAG_UPDATE_CURRENT);

    // Add an action to allow replies.
    NotificationCompat.Action replyAction =
            new NotificationCompat.Action.Builder(
                    R.drawable.googleg_standard_color_18,
                    "Notification",
                    replyIntent)

                    /// TODO: Add better wear support.
                    .addRemoteInput(remoteInput)
                    .extend(inlineActionForWear2)
                    .build();

    Intent intent = new Intent(this, ChatActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, 
    intent,
            PendingIntent.FLAG_ONE_SHOT);

    Uri defaultSoundUri = RingtoneManager.getDefaultUri
    (RingtoneManager.TYPE_NOTIFICATION);
    NotificationCompat.Builder notificationBuilder =      
    (NotificationCompat.Builder) 
    new NotificationCompat.Builder(this)
            .setAutoCancel(true)   //Automatically delete the     
            notification
            .setSmallIcon(R.mipmap.ic_launcher) //Notification icon
            .setContentIntent(pendingIntent)
            .addAction(replyAction)
            .setContentTitle(notificationTitle)
            .setContentText(notificationBody)
            .setSound(defaultSoundUri);

    NotificationManagerCompat notificationManager = 
    NotificationManagerCompat.from(this);
    notificationManager.notify(0, notificationBuilder.build());
}

前一个方法在 Wear 设备上具有 IMF 输入、语音回复和绘制表情符号的功能。修改代码后完整的类如下所示:

package com.packt.smartchat;

/**
 * Created by ashok.kumar on 02/06/17.
 */

public class MessagingService extends FirebaseMessagingService {

    private static final String TAG = "MessagingService";
    public static final String EXTRA_VOICE_REPLY = "extra_voice_reply";
    public static final String REPLY_ACTION =
            "com.packt.smartcha.ACTION_MESSAGE_REPLY";
            public static final String SEND_MESSAGE_ACTION =
            "com.packt.smartchat.ACTION_SEND_MESSAGE";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        String notificationTitle = null, notificationBody = null;
        // Check if message contains a notification payload.
        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification Body: " + 
            remoteMessage.getNotification().getBody());
            notificationTitle = 
            remoteMessage.getNotification().getTitle();
            notificationBody = 
            remoteMessage.getNotification().getBody();

            sendNotification(notificationTitle, notificationBody);

        }
    }

    // Creates an intent that will be triggered when a message is read.
    private Intent getMessageReadIntent(int id) {
        return new Intent().setAction("1").putExtra("1482", id);
    }

    // Creates an Intent that will be triggered when a reply is 
    received.
    private Intent getMessageReplyIntent(int conversationId) {
        return new Intent().setAction(REPLY_ACTION).putExtra("1223", 
        conversationId);
    }

    private void sendNotification(String notificationTitle, String 
    notificationBody) {
        // Wear 2.0 allows for in-line actions, which will be used for 
        "reply".
        NotificationCompat.Action.WearableExtender 
        inlineActionForWear2 =
                new NotificationCompat.Action.WearableExtender()
                        .setHintDisplayActionInline(true)
                        .setHintLaunchesActivity(false);

        RemoteInput remoteInput = new 
        RemoteInput.Builder("extra_voice_reply").build();

        // Building a Pending Intent for the reply action to trigger.
        PendingIntent replyIntent = PendingIntent.getBroadcast(
                getApplicationContext(),
                0,
                getMessageReplyIntent(1),
                PendingIntent.FLAG_UPDATE_CURRENT);

        // Add an action to allow replies.
        NotificationCompat.Action replyAction =
                new NotificationCompat.Action.Builder(
                        R.drawable.googleg_standard_color_18,
                        "Notification",
                        replyIntent)

                        /// TODO: Add better wear support.
                        .addRemoteInput(remoteInput)
                        .extend(inlineActionForWear2)
                        .build();

        Intent intent = new Intent(this, ChatActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 
        0, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri = RingtoneManager.getDefaultUri
         (RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder = 
        (NotificationCompat.Builder) new 
         NotificationCompat.Builder(this)
                .setAutoCancel(true)   //Automatically delete the 
                notification
                .setSmallIcon(R.mipmap.ic_launcher) //Notification icon
                .setContentIntent(pendingIntent)
                .addAction(replyAction)
                .setContentTitle(notificationTitle)
                .setContentText(notificationBody)
                .setSound(defaultSoundUri);

        NotificationManagerCompat notificationManager = 
        NotificationManagerCompat.from(this);
        notificationManager.notify(0, notificationBuilder.build());
    }
}

收到的通知将如下所示:

当用户点击通知时,他将或她将得到以下三个选项:

在 Wear 设备上收到通知后,用户可以通过文本或语音输入,或者借助表情符号来回复他们的想法。为了处理这种情况,我们需要编写一个广播接收器。让我们创建一个名为MessageReplyReceiver的类,继承自BroadcastReceiver类,并重写onReceive方法。当你收到回复时,只需按照以下方式使用conversationId更新意图:

@Override
public void onReceive(Context context, Intent intent) {
    if (MessagingService.REPLY_ACTION.equals(intent.getAction())) {
        int conversationId = intent.getIntExtra("reply", -1);
        CharSequence reply = getMessageText(intent);
        if (conversationId != -1) {
            Log.d(TAG, "Got reply (" + reply + ") for ConversationId " 
            + conversationId);
        }
        // Tell the Service to send another message.
        Intent serviceIntent = new Intent(context, 
        MessagingService.class);
        serviceIntent.setAction(MessagingService.SEND_MESSAGE_ACTION);
        context.startService(serviceIntent);
    }
}

remoteIntent对象中,为了接收数据并将意图数据转换为文本,使用以下方法:

private CharSequence getMessageText(Intent intent) {
    Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
    if (remoteInput != null) {
        return 
        remoteInput.getCharSequence
        (MessagingService.EXTRA_VOICE_REPLY);
    }
    return null;
}

MessageReplyReceiver类的完整代码如下:

public class MessageReplyReceiver extends BroadcastReceiver {

    private static final String TAG = 
    MessageReplyReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {
        if (MessagingService.REPLY_ACTION.equals(intent.getAction())) {
            int conversationId = intent.getIntExtra("reply", -1);
            CharSequence reply = getMessageText(intent);
            if (conversationId != -1) {
                Log.d(TAG, "Got reply (" + reply + ") for 
                ConversationId " + conversationId);
            }
            // Tell the Service to send another message.
            Intent serviceIntent = new Intent(context,    
            MessagingService.class);
            serviceIntent.setAction
           (MessagingService.SEND_MESSAGE_ACTION);
            context.startService(serviceIntent);
        }
    }

    private CharSequence getMessageText(Intent intent) {
        Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
        if (remoteInput != null) {
            return remoteInput.getCharSequence
            (MessagingService.EXTRA_VOICE_REPLY);
        }
        return null;
    }
}

之后,在清单文件中如下注册broadcastreceiver

<receiver android:name=".MessageReplyReceiver">
    <intent-filter>
        <action 
        android:name="com.packt.smartchat.ACTION_MESSAGE_REPLY"/>
    </intent-filter>
</receiver>

现在,我们已经完全准备好接收来自 Wear 通知组件到本地应用程序的数据。

从 Wear 通知组件的语音输入屏幕将如下所示:

在这个屏幕上使用绘图表情,Android 会预测你绘制了什么:

IMF 可以用来通过键入输入进行回复:

总结

在本章中,你已经学习了如何使用 Firebase 函数发送推送通知,以及如何使用来自 Wear 支持库的通知组件。通知是智能设备中的核心组件;它们通过提醒用户发挥着至关重要的作用。我们已经理解了NotificationCompat.Builder类和WearableExtender类。我们还探索了输入方法框架以及使用多种回复机制(如表情符号、语音支持等)进行回复的最简单方式。

第十章:只为时间而生 - 手表表盘与服务

表盘,也称为表盘,是显示固定数字和移动指针的时间的部分。表盘的外观可以通过各种艺术手法和创新来设计。设计传统手表表盘是一门优美的艺术;手表表盘设计师会知道雕刻和设计传统可穿戴手表表盘需要什么。在安卓穿戴中,这个过程非常相似,除了作为表盘制作者,你手中没有任何工具,但需要知道你需要扩展哪个服务以及哪段代码可以帮助你自定义表盘的外观和感觉。表盘将显示时间和日期。在安卓穿戴中,表盘可以是模拟的,也可以是数字的。

安卓穿戴的表盘是包装在可穿戴应用内的服务。当用户选择可用的某个表盘时,可穿戴设备会显示表盘并调用其服务回调方法。自定义表盘使用动态的数字画布,可以融入色彩、活动和相关数据。在安卓穿戴中安装可穿戴表盘应用后,我们可以通过表盘选择器在不同的表盘之间切换。用户可以通过手机上的谷歌应用商店中的配套应用程序,在他们的手表上安装各种表盘。在本章中,你将学习以下主题:

  • CanvasWatchFaceService 类和注册你的手表表盘

  • CanvasWatchFaceService.Engine 和回调方法

  • 编写表盘和处理手势和点击事件

  • 理解表盘元素并初始化它们

  • 常见问题

CanvasWatchFaceService 类和注册你的手表表盘

表盘是具有绘图和视觉渲染能力的服务;所有表盘都将扩展CanvasWatchFaceService类。CanvasWatchFaceService类从WallpaperSeviceWallpaperService.Engine类中提取其功能。Engine类及其回调方法帮助表盘处理其生命周期。如果你需要为 Android Wear 制作一个表盘,你应该使用CanvasWatchfaceService类,而不是普通的旧WallpaperService。表盘服务与壁纸服务一样,只需实现onCreateEngine()方法。表盘引擎需要实现onTimeTick()方法以刷新时间并更新视图,以及onAmbientModeChanged(boolean)方法以在不同版本的表盘之间切换,例如灰色模式和彩色表盘。表盘引擎同样实现onInterruptionFilterChanged(int)以根据用户查询的信息量更新视图。对于在环境模式下发生的更新,将持有wake_lock,这样设备在表盘完成绘图过程之前不会进入休眠状态。在应用程序中注册表盘与注册壁纸非常相似,但需要几个额外的步骤。然而,表盘需要wake_lock权限,如下所示:

 <uses-permission android:name="android.permission.WAKE_LOCK" />

之后,你的表盘服务声明需要预览元数据:

 <meta-data
     android:name="com.google.android.wearable.watchface.preview"
     android:resource="@drawable/preview_face" />
 <meta-data
     android:name=
     "com.google.android.wearable.watchface.preview_circular"
     android:resource="@drawable/preview_face_circular" />

最后,我们需要添加一个特殊的意图过滤器,以便手表能够识别。

 <intent-filter>
     <action android:name="android.service.wallpaper.WallpaperService" 
     />
     <category
         android:name=
         "com.google.android.wearable.watchface.category.WATCH_FACE" />
 </intent-filter>

CanvasWatchFaceService.Engine

CanvasWatchFaceService.Engine类扩展了WatchFaceService.Engine类。在这里,可以在画布上绘制表盘的实际实现可以完成。我们应该实现onCreateEngine()以恢复你的具体引擎实现。CanvasWatchFaceService.Engine有一个公共构造函数和几个过程,使我们能够实现表盘。让我们探讨一下我们将在本章后半部分实现的一些方法:

  • void invalidate ():计划调用onDraw(Canvas, Rect)以绘制下一帧。这必须在主线程上处理。

  • void onDestroy ():在这个回调中,我们可以释放用于完成表盘的硬件和其他资源。

  • void onDraw(Canvas canvas, Rect bounds):绘制表盘、所有视觉组件以及时钟更新逻辑,此方法中完成其他时钟排列。

  • void onSurfaceChanged():这个方法包含四个参数,void onSurfaceChanged (SurfaceHolder holder, int organise, int width, int stature)SurfaceHolder参数允许你控制表面大小和不同的排列方式。

  • void postInvalidate():发布一个消息以安排调用onDraw(Canvas, Rect)以绘制下一帧。此外,这个方法是线程安全的。我们可以从任何线程调用这个方法。

这些方法在规划您的表盘时起着重要的作用。让我们开始制作表盘。在以下练习中,我们将了解如何制作数字表盘。

编写自己的表盘

Android Studio 是我们应该使用的主要工具,用于编写 Wear 应用,原因有很多;由于我们已经为 Wear 2.0 开发配置了开发环境,因此这不应成为挑战。让我们启动 Android Studio 并创建一个 Wear 项目。

在活动选择器中,选择添加无活动。由于表盘是一个服务,我们不需要活动:

我们在前一节中读到,我们将扩展类到CanvasWatchFaceService,在画布上绘制表盘,另一个类是CanvasWatchFaceService.Engine在那里我们将处理表盘的实际实现以及我们讨论的更重要方法。这将帮助我们实现表盘的必要设置。

现在,在包中创建一个名为PacktWatchFace的类文件。

PacktWatchFace类将扩展到CanvasWatchFaceService类:

创建类文件后,将其扩展到CanvasWatchFaceService类;这是我们将在清单中注册的服务类。在这个类内部,我们需要为 Wear 设计实现创建一个子类。之后,在同一个类中,我们需要覆盖onCreateEngine()方法。

以下代码是 Wear 表盘设计的入口点设置:

public class PacktWatchFace extends CanvasWatchFaceService{

@Override
public Engine onCreateEngine() {
    return new Engine();
}

private class Engine extends CanvasWatchFaceService.Engine{

  }
}

PacktWatchFace类仅实现一个方法onCreateEngine(),该方法返回CanvasWatchFaceService.Engine的实际实现。现在,是时候在清单文件中注册watchFace服务类了。

在清单注册的应用范围内,添加PacktWatchFace类:

<service
    android:name=".PacktWatchFace"
    android:label="PacktWatchface Wear"
    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="@mipmap/ic_launcher" />
    <meta-data
        android:name=
        "com.google.android.wearable.watchface.preview_circular"
        android:resource="@mipmap/ic_launcher" />

    <intent-filter>
        <action android:name=
        "android.service.wallpaper.WallpaperService" />

        <category android:name=
        "com.google.android.wearable.watchface.category.WATCH_FACE" />
    </intent-filter>
</service>

res目录下的xml文件夹中创建一个文件,并将其命名为watch_face.xml。在内部,添加如下wallpaper XML 标签:

<?xml version="1.0" encoding="UTF-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"/>

表盘服务声明需要预览元数据,如前所示。在可穿戴设备的表盘选择器预览中也会使用相同的元数据。这些元素将指定表盘服务的元数据。表盘将使用我们在元数据标签中提供的预览图片和其他信息。

在您的清单中添加以下权限:

<uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

让我们设置图形元素和年代实例的全局基本实例:

//Essential instances
private static Paint textPaint, boldTextPaint, backGround, whiteBackground, darkText;
private static Calendar calendar;
private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
private static final int MSG_UPDATE_TIME = 0;

onDraw方法中,我们可以绘制需要在表盘上显示的内容。绘制的视觉将保持静态;仅通过在画布上绘制,我们不能使其动态。时间显示的实现起着重要的作用。

表盘需要显示的信息和其他外观由表盘设计师决定。现在,让我们在onCreate方法中初始化所有实例:

backGround = new Paint() {{ setARGB(255, 120, 190, 0); }};
textPaint = createPaint(false, 40);
boldTextPaint = createPaint(true, 40);
whiteBackground = createPaint(false, 0);
darkText = new Paint() {{ setARGB(255, 50, 50, 50); setTextSize(18); }};

接下来,我们将编写一个单独的方法createPaint(),用于返回所有调用的值:

private Paint createPaint(final boolean bold, final int fontSize) {
    final Typeface typeface = (bold) ? Typeface.DEFAULT_BOLD : 
    Typeface.DEFAULT;

    return new Paint()
    {{
        setARGB(255, 255, 255, 255);
        setTextSize(fontSize);
        setTypeface(typeface);
        setAntiAlias(true);
    }};
}

处理点击事件和手势

在表盘上,用户可以进行交互,但CanvasWatchService.Engine类仅提供了一个交互方法,即单次点击。如果我们想要有其他的交互方式,需要重写onTapCommand方法。我们还需要在onCreate方法中通过更改 Wear 应用的风格来请求tapevents

setWatchFaceStyle(new WatchFaceStyle.Builder(PacktWatchFace.this)
    .setAcceptsTapEvents(true)
    .build());

之后,我们可以重写onTapCommand()方法来处理点击事件,并且可以重写函数,以便在用户点击应用时提供功能和服务。

以下代码展示了当用户点击表盘时显示的提示信息:

@Override
public void onTapCommand(
        @TapType int tapType, int x, int y, long eventTime) {
    switch (tapType) {
        case WatchFaceService.TAP_TYPE_TAP:
            // Handle the tap
            Toast.makeText(PacktWatchFace.this, "Tapped", 
            Toast.LENGTH_SHORT).show();
            break;

        // There are other cases, not mentioned here. <a 
        href="https://developer.android.com/training/wearables/watch-
        faces/interacting.html">Read Android guide</a>
        default:
            super.onTapCommand(tapType, x, y, eventTime);
            break;
    }
}

这样,我们可以自定义点击功能。默认的函数签名提供了两个坐标,xy;通过使用这些坐标,我们可以确定用户点击的位置,这有助于表盘设计师相应地自定义手势和点击事件。

支持不同的形态因素

Android Wear 设备有方形和矩形设计。让表盘在这两种形态上看起来一样是表盘开发者的责任。大多数为矩形显示屏设计的 UI 布局在圆形显示屏上会失败,反之亦然。为了解决这个问题,WallpaperService Engine 有一个名为onApplyWindowInsets的函数。onApplyWindowInsets方法帮助检查设备是否为圆形;通过确定这一点,我们可以绘制圆形或方形表盘:

@Override
public void onApplyWindowInsets(WindowInsets insets) {
    super.onApplyWindowInsets(insets);
    boolean isRound = insets.isRound();

    if(isRound) {
        // Render the Face in round mode.
    } else {
        // Render the Face in square (or rectangular) mode.
    }
}

现在,让我们编写一个能够定时更新表盘的完整方法:

  @Override
        public void onDraw(Canvas canvas, Rect bounds) {
            calendar = Calendar.getInstance();

            canvas.drawRect(0, 0, bounds.width(), bounds.height(), 
            whiteBackground); // Entire background Canvas
            canvas.drawRect(0, 60, bounds.width(), 240, backGround);

            canvas.drawText(new SimpleDateFormat("cccc")
            .format(calendar.getTime()), 130, 120, textPaint);

            // String time = String.format
            ("%02d:%02d", mTime.hour, mTime.minute);
            String time = new SimpleDateFormat
            ("hh:mm a").format(calendar.getTime());
            canvas.drawText(time, 130, 170, boldTextPaint);

            String date = new SimpleDateFormat
            ("MMMM dd, yyyy").format(calendar.getTime());
            canvas.drawText(date, 150, 200, darkText);
        }

onVisibilityChanged方法帮助注册和解注册告知表盘时间的接收器:

@Override
public void onVisibilityChanged(boolean visible) {
    super.onVisibilityChanged(visible);

    if (visible) {
        registerReceiver();
        // Update time zone in case it changed while we weren't 
        visible.
        calendar = Calendar.getInstance();
    } else {
        unregisterReceiver();
    }

    // Whether the timer should be running depends on whether we're 
    visible (as well as
    // whether we're in ambient mode), so we may need to start or stop 
    the timer.
    updateTimer();
}

private void registerReceiver() {
    if (mRegisteredTimeZoneReceiver) {
        return;
    }
    mRegisteredTimeZoneReceiver = true;
    IntentFilter filter = new 
    IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
    PacktWatchFace.this.registerReceiver(mTimeZoneReceiver, filter);
}

private void unregisterReceiver() {
    if (!mRegisteredTimeZoneReceiver) {
        return;
    }
    mRegisteredTimeZoneReceiver = false;
    PacktWatchFace.this.unregisterReceiver(mTimeZoneReceiver);
}

/**
 * Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
 * or stops it if it shouldn't be running but currently is.
 */
private void updateTimer() {
    mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
    if (shouldTimerBeRunning()) {
        mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
    }
}

为了确保计时器只在表盘可见时运行,我们将设置以下配置:

private boolean shouldTimerBeRunning() {
    return isVisible() && !isInAmbientMode();
}

要在表盘中定期更新时间,请执行以下操作:

 private void handleUpdateTimeMessage() {
        invalidate();
        if (shouldTimerBeRunning()) {
            long timeMs = System.currentTimeMillis();
            long delayMs = INTERACTIVE_UPDATE_RATE_MS
                    - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
            mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, 
            delayMs);
        }
    }
}

现在,让我们用WeakReference类的实现来完善代码。弱引用对象将允许引用对象被最终确定,并且可以在之后访问。弱引用将使所有之前弱可达的对象被最终确定。最后,它将把那些最近清除的、注册在引用队列中的弱引用入队:

private static class EngineHandler extends Handler {
    private final WeakReference<Engine> mWeakReference;

    public EngineHandler(PacktWatchFace.Engine reference) {
        mWeakReference = new WeakReference<>(reference);
    }

    @Override
    public void handleMessage(Message msg) {
        PacktWatchFace.Engine engine = mWeakReference.get();
        if (engine != null) {
            switch (msg.what) {
                case MSG_UPDATE_TIME:
                    engine.handleUpdateTimeMessage();
                    break;
            }
        }
    }
}

要添加一个可绘制对象,我们可以使用BitmapFactory类:

bitmapObj = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);

// And set the bitmap object in onDraw methods canvas            canvas.drawBitmap(bob, 0, 40, null);

既然完整的逻辑定义已经完成,让我们看看最终定稿的表盘类:

/**
 * Created by ashok.kumar on 27/04/17.
 */

public class PacktWatchFace extends CanvasWatchFaceService{

    //Essential instances
    private static Paint textPaint, boldTextPaint, backGround, 
    whiteBackground, darkText;
    private static Calendar calendar;
    private static final long INTERACTIVE_UPDATE_RATE_MS = 
    TimeUnit.SECONDS.toMillis(1);
    private static final int MSG_UPDATE_TIME = 0;

    @Override
    public Engine onCreateEngine() {
        return new Engine();
    }

    private class Engine extends CanvasWatchFaceService.Engine {

        final Handler mUpdateTimeHandler = new EngineHandler(this);

        final BroadcastReceiver mTimeZoneReceiver = new 
        BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                calendar = Calendar.getInstance();
            }
        };
        boolean mRegisteredTimeZoneReceiver = false;

        boolean mLowBitAmbient;

        @Override
        public void onCreate(SurfaceHolder holder) {
            super.onCreate(holder);

            setWatchFaceStyle(new WatchFaceStyle.Builder
            (PacktWatchFace.this)
                    .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
                    .setBackgroundVisibility
                    (WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
                    .setShowSystemUiTime(false)
                    .build());

            backGround = new Paint() {{ setARGB(255, 120, 190, 0); }};
            textPaint = createPaint(false, 40);
            boldTextPaint = createPaint(true, 40);
            whiteBackground = createPaint(false, 0);
            darkText = new Paint() 
            {{ setARGB(255, 50, 50, 50); setTextSize(18); }};

            setWatchFaceStyle(new WatchFaceStyle.Builder
            (PacktWatchFace.this)
                    .setAcceptsTapEvents(true)
                    .build());

            calendar = Calendar.getInstance();
        }

        private Paint createPaint
        (final boolean bold, final int fontSize) {
            final Typeface typeface = 
            (bold) ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;

            return new Paint()
            {{
                setARGB(255, 255, 255, 255);
                setTextSize(fontSize);
                setTypeface(typeface);
                setAntiAlias(true);
            }};
        }

        @Override
        public void onTapCommand(
                @TapType int tapType, int x, int y, long eventTime) {
            switch (tapType) {
                case WatchFaceService.TAP_TYPE_TAP:
                    // Handle the tap
                    Toast.makeText(PacktWatchFace.this, 
                    "Tapped", Toast.LENGTH_SHORT).show();
                    break;

                default:
                    super.onTapCommand(tapType, x, y, eventTime);
                    break;
            }
        }

        @Override
        public void onDestroy() {
            mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
            super.onDestroy();
        }

        @Override
        public void onPropertiesChanged(Bundle properties) {
            super.onPropertiesChanged(properties);
            mLowBitAmbient = properties.getBoolean
            (PROPERTY_LOW_BIT_AMBIENT, false);
        }

        @Override
        public void onTimeTick() {
            super.onTimeTick();
            invalidate();
        }

        @Override
        public void onAmbientModeChanged(boolean inAmbientMode) {
            super.onAmbientModeChanged(inAmbientMode);
            if (inAmbientMode) {
                if (mLowBitAmbient) {
                }
                invalidate();
            }

            updateTimer();
        }

        @Override
        public void onDraw(Canvas canvas, Rect bounds) {
            calendar = Calendar.getInstance();

            canvas.drawRect(0, 0, bounds.width(), bounds.height(), 
            whiteBackground); // Entire background Canvas
            canvas.drawRect(0, 60, bounds.width(), 240, backGround);

            canvas.drawText(new SimpleDateFormat("cccc")
            .format(calendar.getTime()), 130, 120, textPaint);

            // String time = String.format("%02d:%02d", mTime.hour, 
            mTime.minute);
            String time = new SimpleDateFormat
            ("hh:mm a").format(calendar.getTime());
            canvas.drawText(time, 130, 170, boldTextPaint);

            String date = new SimpleDateFormat
            ("MMMM dd, yyyy").format(calendar.getTime());
            canvas.drawText(date, 150, 200, darkText);
        }

        @Override
        public void onVisibilityChanged(boolean visible) {
            super.onVisibilityChanged(visible);

            if (visible) {
                registerReceiver();
                calendar = Calendar.getInstance();
            } else {
                unregisterReceiver();
            }

            updateTimer();
        }

        private void registerReceiver() {
            if (mRegisteredTimeZoneReceiver) {
                return;
            }
            mRegisteredTimeZoneReceiver = true;
            IntentFilter filter = new IntentFilter
            (Intent.ACTION_TIMEZONE_CHANGED);
            PacktWatchFace.this.registerReceiver
            (mTimeZoneReceiver, filter);
        }

        private void unregisterReceiver() {
            if (!mRegisteredTimeZoneReceiver) {
                return;
            }
            mRegisteredTimeZoneReceiver = false;
            PacktWatchFace.this.unregisterReceiver(mTimeZoneReceiver);
        }

        private void updateTimer() {
            mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
            if (shouldTimerBeRunning()) {
                mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
            }
        }

        private boolean shouldTimerBeRunning() {
            return isVisible() && !isInAmbientMode();
        }

        private void handleUpdateTimeMessage() {
            invalidate();
            if (shouldTimerBeRunning()) {
                long timeMs = System.currentTimeMillis();
                long delayMs = INTERACTIVE_UPDATE_RATE_MS
                        - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
                mUpdateTimeHandler
                .sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
            }
        }
    }

    private static class EngineHandler extends Handler {
        private final WeakReference<Engine> mWeakReference;

        public EngineHandler(PacktWatchFace.Engine reference) {
            mWeakReference = new WeakReference<>(reference);
        }

        @Override
        public void handleMessage(Message msg) {
            PacktWatchFace.Engine engine = mWeakReference.get();
            if (engine != null) {
                switch (msg.what) {
                    case MSG_UPDATE_TIME:
                        engine.handleUpdateTimeMessage();
                        break;
                }
            }
        }
    }
}

最终编译的表盘将出现在你的 Wear 设备的表盘选择器中:

恭喜你构建了你的第一个表盘。

理解表盘元素并初始化它们

根据我们正在开发的表盘类型,我们需要规划表盘的某些元素。我们已经了解了构建数字表盘需要什么,但要构建模拟表盘,我们需要了解一些有助于构建表盘的表盘元素。

通常,模拟表盘是由以下三个基本组件组成的:

  • HOUR_STROKE

  • MINUTE_STROKE

  • SECOND_TICK_STROKE

现在,要构建一个模拟表盘,我们需要这三个组件,其余的事情与构建数字表盘几乎相同。在这里,我们需要在动画化笔画上投入更多的努力。

首先,我们需要设计Strokes,如下代码所示:

 mHourPaint = new Paint();
 mHourPaint.setColor(mWatchHandColor);
 mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH);
 mHourPaint.setAntiAlias(true);
 mHourPaint.setStrokeCap(Paint.Cap.ROUND);
 mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);

 mMinutePaint = new Paint();
 mMinutePaint.setColor(mWatchHandColor);
 mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH);
 mMinutePaint.setAntiAlias(true);
 mMinutePaint.setStrokeCap(Paint.Cap.ROUND);
 mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, 
 mWatchHandShadowColor);

 mSecondPaint = new Paint();
 mSecondPaint.setColor(mWatchHandHighlightColor);
 mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
 mSecondPaint.setAntiAlias(true);
 mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
 mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, 
 mWatchHandShadowColor);

 mTickAndCirclePaint = new Paint();
 mTickAndCirclePaint.setColor(mWatchHandColor);
 mTickAndCirclePaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
 mTickAndCirclePaint.setAntiAlias(true);
 mTickAndCirclePaint.setStyle(Paint.Style.STROKE);
 mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, 
 mWatchHandShadowColor);

现在,使用之前设计的Strokes,我们可以按照自己的想法设计和定制表盘,还可以在画布上添加不同的背景以及其他装饰元素,使你的模拟表盘变得特别。

对于数字表盘,你需要为文本以及表盘中使用的其他图形元素提供一个参考。

常见问题

Wear 表盘应用与 Wear 应用不同。表盘应用遇到的最常见问题是不同的形态因素,如方形和圆形表盘下巴。为解决这个问题,程序员需要在表盘执行前检测 Wear 的形态因素。正如我们已经讨论的,CanvasWatchFaceService.EngineonApplyWindowInsets()方法可以帮助找到 Wear 的形状。

表盘应用始终在运行;实际上,表盘服务扩展了壁纸服务。当我们有大量从网络 API 调用获取数据的服务时,电池可能会很快耗尽。这些服务可能包括:

  • 不同的形态因素

  • 电池效率

  • 用户界面适应

  • 过多的动画

  • 我们用来构建 Wear 表盘的资源

  • 依赖硬件的表盘

用户界面适应是表盘制作者面临的另一个挑战;当我们启用表盘样式中的setHotwordIndicator()时,Android 系统应该能够在我们构建的表盘上方发布和叠加通知卡片。我们构建的模拟表盘必须考虑这种情况,因为模拟表盘在调整大小方面有些不情愿,并且在连续的笔画动画中,它不会与系统通知协调。表盘中过多的动画不是一个好主意。有许多动画会导致 CPU 和 GPU 性能问题。当我们在表盘中使用动画时需要考虑以下事项:

  • 减少动画的帧率

  • 让 CPU 在动画之间休眠

  • 减小所使用的位图资源的大小

  • 绘制缩放位图时禁用抗锯齿

  • 将昂贵的操作移出绘图方法

当你的表盘依赖于硬件来显示数据时,你应该确保周期性地访问硬件并释放它。例如,当我们使用 GPS 来显示当前位置,并且表盘不断监听 GPS 时,不仅会耗尽电池电量,垃圾回收器还可能会抛出错误。

交互式表盘

每当 Wear 2.0 更新到来时,趋势都会发生变化,带来新的互动表盘,这些表盘也可以具有独特的互动和风格表达。所有为 Wear 开发表盘的开发者可能需要开始考虑互动表盘。

什么是一个真正的互动表盘?

这个想法是让用户通过及时提供令人愉悦和有用的信息来喜欢和爱上表盘,这改变了用户对表盘的体验。

谷歌提出以下方法来实现互动表盘:

  • 创意视觉

  • 不同的形态因素

  • 显示模式

  • 系统 UI 元素

  • 集成数据的表盘

Android Wear 以非常高效的方式表达时间提供了一个数字画布。Android Wear 还提供了在表盘上集成数据,以实现更高层次的个性化。表盘和设计需要一目了然,并且应向表盘用户传达优先信息。

我们知道,Android Wear 的不同形态因素是表盘开发者的实现挑战。表盘在设计语言上应保持一致,这包括一套通用的颜色、线宽、阴影和其他设计元素。

在 Wear 2.0 中,有两种显示模式:

  • 活跃模式

  • 常亮模式

活跃模式是当用户扭动手腕或触摸显示屏来查看时间时。Wear 将点亮显示屏并激活表盘。在这个模式下,表盘可以使用丰富多彩的动画和流畅的设计语言来表达时间和其他信息。

常亮模式有助于节省电池电量,当 Wear 设备进入常亮模式时,显示能力仅限于黑、白和灰色。我们需要仔细设计在常亮模式下显示的内容,使其与表盘设计相似,但颜色和动画较少。

系统 UI 元素指示 Wear 设备的状态;例如,电池水平和其他系统 UI 元素。表盘应允许这些指示器显示在 Wear 设备的特定位置。

集成数据的表盘可以帮助表盘用户一目了然地查看所选信息,例如,步数、天气预报等可以显示在表盘上。

总结

在本章中,我们探讨了设计表盘的基本理解,并构建了一个数字表盘。我们了解了CanvasWatchFaceService类如何在构建表盘时提供帮助,并且也了解了以下与表盘相关的概念:

  • CanvasWatchFaceService

  • canvasWatchFaceService.Engine方法

  • 在 Wear 模块清单中注册表盘

  • 处理轻触手势

  • 不同的形态因素

  • 向表盘添加位图图像

  • 表盘元素

  • 常见问题

  • 互动表盘

制作手表表盘是一项卓越的艺术工程技术,包括我们应该在表盘上表达哪些数据,以及时间和日期数据是如何被展示的。ComplicationsAPI是 Wear 2.0 中的新特性。我们将在下一章讨论这个内容,同时也会涉及一些高级概念。

第十一章:关于 Wear 2.0 的更多信息

Android Wear 2.0 是一个重要的更新,包含了许多新功能,包括 Google 助手、独立应用程序、新手表表盘以及支持第三方复杂功能。在前面的章节中,我们探讨了如何编写不同类型的 Wear 应用。Wear 2.0 根据当前市场研究提供了更多功能,谷歌正在与合作伙伴公司合作,为 Wear 构建强大的生态系统。

在本章中,让我们了解如何通过以下概念将我们的现有技能向前推进:

  • 独立应用程序

  • 曲面布局和更多的 UI 组件

  • 复杂功能 API

  • 不同的导航和动作

  • 手腕手势

  • 输入法框架

  • 将 Wear 应用分发到 Play 商店

独立应用程序

在 Wear 2.0 中,独立应用程序是穿戴生态系统的强大功能。在没有手机的情况下使用穿戴应用是多么酷啊!有许多场景,Wear 设备曾经需要依赖手机,例如,接收新电子邮件通知,Wear 需要连接到手机以使用互联网服务。现在,穿戴设备可以独立连接 Wi-Fi,并且可以同步所有应用进行新更新。用户现在可以在没有配对手机的情况下,使用穿戴应用完成更多任务。

将应用标识为独立应用

独立应用程序的理念是穿戴平台的一个伟大特性。Wear 2.0 通过 Android 清单文件中的元数据元素区分独立应用。

<application>应用标签内部,</application>元数据元素与com.google.android.wearable.standalone一起放置,值为 true 或 false。新的元数据元素指示穿戴应用是否为独立应用,不需要配对手机即可操作。当元数据元素设置为 true 时,应用也可以适用于与 iPhone 合作的穿戴设备。通常,手表应用可以分为以下几类:

  • 完全独立于手机应用

  • 半独立(没有手机配对,穿戴应用的功能将有限)

  • 依赖于手机应用

要使 Wear 应用完全独立或半独立,请将元数据的值设置为 true,如下所示:

<application>
...
  <meta-data
    android:name="com.google.android.wearable.standalone"
    android:value="true" />
...
</application>

现在,任何平台,比如没有 Play 商店的 iPhone 或 Android 手机,也可以使用穿戴应用,直接从穿戴设备中的 Play 商店下载。通过将元数据值设置为 false,我们告诉 Android Wear 这个穿戴应用依赖于带有 Play 商店应用的手机。

注意:不管值可能是错误的,手表应用可以在相应的手机应用安装之前安装。这样,如果手表应用识别到配套手机没有必要的手机应用,手表应用应该提示用户安装手机应用。

独立应用存储

你可以使用标准的 Android 存储 API 在本地存储数据。例如,你可以使用 SharedPreference API、SQLite 或内部存储。到目前为止,我们已经探讨了如何将 ORM 库(如 Realm)集成到穿戴应用中,不仅仅是为了存储数据,同时也为了在穿戴应用和手机应用之间共享代码。另一方面,特定于形状组件和外形尺寸的代码可以放在不同的模块中。

在另一台设备上检测穿戴应用。

Android 穿戴应用和 Android 手机应用可以使用 Capability API 识别支持的应用。手机和穿戴应用可以静态和动态地向配对的设备广播。当应用在用户的穿戴网络中的节点上时,Capability API使另一个应用能够识别相应安装的应用。

广告功能。

要从穿戴设备在手持设备上启动活动,请使用MessageAPI类发送请求。多个穿戴设备可以与手持 Android 设备关联;穿戴应用需要确定关联的节点是否适合从手持设备应用启动活动。要广告手持应用的功能,请执行以下步骤:

  1. 在项目的res/values/目录中创建一个 XML 配置文件,并将其命名为wear.xml

  2. wear.xml中添加一个名为android_wear_capabilities的资源。

  3. 定义设备提供的功能。

注意:功能是自定义字符串,由你定义,并且在你的应用中应该是唯一的。

下面的示例展示了如何向wear.xml添加一个名为voice_transcription的功能:

<resources>
    <string-array name="android_wear_capabilities">
        <item>voice_transcription</item>
    </string-array>
</resources>

获取具有所需功能的节点。

最初,我们可以通过调用CapabilityAPI.getCapability()方法来检测有能力的节点。以下示例展示了如何手动检索具有voice_transcription功能的可达节点的结果:

private static final String
        VOICE_TRANSCRIPTION_CAPABILITY_NAME = "voice_transcription";

private GoogleApiClient mGoogleApiClient;

...

private void setupVoiceTranscription() {
    CapabilityApi.GetCapabilityResult result =
            Wearable.CapabilityApi.getCapability(
                    mGoogleApiClient, 
                    VOICE_TRANSCRIPTION_CAPABILITY_NAME,
                    CapabilityApi.FILTER_REACHABLE).await();

    updateTranscriptionCapability(result.getCapability());
}

要在穿戴设备连接时检测到有能力的节点,请将CapabilityAPI.capabilityListner()实例注册到googleAPIclient。以下示例展示了如何注册监听器并检索具有voice_transcription功能的可达节点的结果:

private void setupVoiceTranscription() {
    ...

    CapabilityApi.CapabilityListener capabilityListener =
            new CapabilityApi.CapabilityListener() {
                @Override
                public void onCapabilityChanged(CapabilityInfo 
                capabilityInfo) {
                    updateTranscriptionCapability(capabilityInfo);
                }
            };

    Wearable.CapabilityApi.addCapabilityListener(
            mGoogleApiClient,
            capabilityListener,
            VOICE_TRANSCRIPTION_CAPABILITY_NAME);
}

注意:如果你创建了一个扩展WearableListenerService的服务来识别功能变化,你可能需要重写onConnectedNodes()方法,以监听更细粒度的连接细节,例如,当穿戴设备从 Wi-Fi 更改为与手机通过蓝牙连接时。有关如何监听重要事件的信息,请阅读数据层事件

在识别出有能力的节点后,确定消息发送的位置。你应该选择一个与你的可穿戴设备接近的节点,以减少通过多个节点路由消息。一个附近的节点被定义为直接与设备连接的节点。要确定一个节点是否在附近,请调用Node.isNearby()方法。

检测并指导用户安装手机应用

现在,我们知道如何使用 Capability API 检测穿戴和手机应用。现在是引导用户从 Play 商店安装相应应用的时候了。

使用CapabilityApi检查你的手机应用是否已安装在配对的手机上。有关更多信息,请参见 Google 示例。如果你的手机应用没有安装在手机上,使用PlayStoreAvailability.getPlayStoreAvailabilityOnPhone()检查它是什么类型的手机。

如果返回PlayStoreAvailability.PLAY_STORE_ON_PHONE_AVAILABLE有效,这意味着手机是一部安装了 Play 商店的 Android 手机。在穿戴设备上调用RemoteIntent.startRemoteActivity(),在手机上打开 Play 商店。使用你的电话应用的市场 URI(可能与你的手机 URI 不同)。例如,使用市场 URI:market://details?id=com.example.android.wearable.wear.finddevices

如果返回PlayStoreAvailability.PLAY_STORE_ON_PHONE_UNAVAILABLE,这意味着手机很可能是 iOS 手机(无法访问 Play 商店)。通过在穿戴设备上调用RemoteIntent.startRemoteActivity(),在 iPhone 上打开 App Store。你可以指定你的应用在 iTunes 的 URL,例如,itunes.apple.com/us/application/yourappname.同样,注意从手表打开 URL。在 iPhone 上,从 Android Wear,你不能编程确定你的手机应用是否已安装。作为最佳实践,为用户提供一种机制(例如,一个按钮)手动触发打开 App Store。

使用之前描述的remoteIntent API,你可以确定任何 URL 都可以从穿戴设备在手机上打开,而不需要手机应用。

只获取重要的信息

在大多数用例中,当我们从互联网获取数据时,我们只获取必要的信息。超出这个范围,我们可能会遇到不必要的延迟、内存使用和电池消耗。

当穿戴设备与蓝牙 LE 连接关联时,穿戴应用可能只能访问每秒 4 千字节的数据传输能力。根据穿戴设备的不同,建议采取以下步骤:

  1. 审查你的网络请求和响应,以获取更多信息,这是针对手机应用的。

  2. 在通过网络发送到手表之前缩小大图片

云消息传递

对于通知任务,应用可以直接在手表应用中使用 Firebase 云消息传递FCM);在 2.0 版本的手表中不支持 Google 云消息传递。

没有特定的 FCM API 用于手表应用;它遵循与移动应用通知相似的配置:FCM 与手表以及休眠模式下的工作良好。推荐使用 FCM 来发送和接收手表设备的通知。

从服务器接收通知的过程是,应用需要将设备的唯一 Firebase registration_id 发送到服务器。然后服务器可以识别 FCM_REST 端点并发送通知。FCM 消息采用 JSON 格式,并可以包含以下任一负载:

  • 通知负载:通用通知数据;当通知到达手表时,应用可以检查通知,用户可以启动接收通知的应用。

  • 数据负载:负载将包含自定义的键值对。该负载将被作为数据传输给手表应用。

在开发特定于手表设备的应用时,手表应用包含了许多关注点,比如获取高带宽网络和根据手表标准降低图片质量等。此外,还有 UI 设计和保持后台服务等。在开发应用时牢记这些,将使它们在众多应用中脱颖而出。

复杂功能 API

复杂功能对手表来说肯定不是什么新鲜事物。互联网上说,第一个带有复杂功能的怀表是在十六世纪被展示的。智能手表是我们考虑复杂功能组件的理想之地。在 Android Wear 中,手表表盘显示的不仅仅是时间和日期,还包括步数计数器、天气预报等。到目前为止,这些复杂功能的工作方式有一个主要限制。到目前为止,每个自定义手表表盘应用都需要执行自己的逻辑来获取显示信息。例如,如果两个表盘都有获取步数并显示相关信息的功能,那么这将是一种浪费。Android Wear 2.0 旨在通过新的复杂功能 API 解决这个问题。

在复杂功能的情况下,手表表盘通信数据提供者扮演着主要角色。它包含获取信息的逻辑。手表表盘不会直接访问数据提供者;当有选择复杂功能的其他数据时,它会得到回调。另一方面,数据提供者不会知道数据将如何被使用;这取决于手表表盘。

下列描述讨论了手表表盘如何从数据提供者获取复杂功能数据:

复杂功能数据提供者

新的并发症 API 具有巨大的潜力;它可以访问电池电量、气候、步数等。并发症数据提供商是一个服务,扩展了 ComplicationProviderService。这个基类有一组回调,目的是为了知道提供商何时被选为当前表盘的数据源:

  • (onComplicationActivated): 当并发症被激活时,会调用此回调方法。

  • (onComplicationDeactivated): 当并发症被停用时,会调用此回调方法。

  • (onComplicationUpdate): 当特定并发症 ID 的并发症更新信息时,会调用此回调。

ComplicationProviderService 类是一个抽象类,扩展到了一个服务。提供商服务必须实现 onComplicationUpdate(整型,整型和 ComplicationManager)以响应并发症系统对更新的请求。此服务的清单声明必须包含对 ACTION_COMPLICATION_UPDATE_REQUEST 的意图过滤器。如果需要,还应该包含确定支持的类型、刷新周期和配置动作的元数据:(METADATA_KEY_SUPPORTED_TYPES, METADATA_KEY_UPDATE_PERIOD_SECONDS, 和 METADATA_KEY_PROVIDER_CONFIG_ACTION)。

服务的清单条目还应包含一个 android:Icon 属性。那里给出的图标应该是一个单色的白色图标,代表提供商。此图标将出现在提供商选择器界面中,并且可能还会被包含在 ComplicationProviderInfo 中,提供给表盘在它们的配置活动中显示。

下面的代码演示了使用 ComplicationsData 的构建器模式将 ComplicationData 填充为短文本类型,并带有下一个事件日期和可选图标:

ComplicationData.Builder(ComplicationData.*TYPE_SHORT_TEXT*)
    .setShortText(ComplicationText.*plainText*(formatShortDate(date)))
    .setIcon(Icon.*createWithResource*(context, R.drawable.*ic_event*))
    .setTapAction(createContactEventsActivityIntent())
    .build();

向手表表盘添加并发症

Android Wear 2.0 为您的智能手表带来了许多新组件。然而,更引人注目的是在表盘上增加了可适应的并发症。并发症是一个两部分的系统;手表表盘工程师可以设计他们的表盘以拥有并发症的开放插槽,应用程序开发人员可以将应用程序的一部分作为并发症整合进来。手表表盘应用可以接收并发症数据,并允许用户选择数据提供商。Android Wear 提供了一个数据源的用户界面。我们可以向某些表盘添加并发症,或来自应用程序的数据。您的 Wear 2.0 表盘上的并发症可以显示电池寿命和日期,这只是开始。您还可以包含一些第三方应用程序的并发症。

接收数据和渲染并发症

要开始接收并发症数据,表盘会在 WatchFaceService.Engine 类中调用 setActiveComplications,并传入表盘并发症 ID 列表。表盘创建这些 ID 以便显著标识出并发症可以出现的位置,并将它们传递给 createProviderChooserIntent 方法,使用户能够选择哪个并发症应出现在哪个位置。并发症数据通过 onComplicationDataUpdateWatchFaceService.Engine 的回调)来传达。

允许用户选择数据提供商

Android Wear 提供了一个 UI(通过一个活动),使用户能够为特定并发症选择提供商。表盘可以调用 createProviderChooserIntent 方法来获取一个可用于展示选择器界面的意图。这个意图必须与 startActivityForResult 一起使用。当表盘调用 createProviderChooserIntent 时,表盘提供一个表盘并发症 ID 和支持的类型列表。

用户与并发症的交互

提供商可以指定用户点击并发症时发生的动作,因此大多数并发症应该是可以被点击的。这个动作将被指定为 PendingIntent,包含在 ComplicationData 对象中。表盘负责检测并发症上的点击,并在点击发生时触发挂起意图。

并发数据权限

Wear 应用必须拥有以下权限才能接收并发症数据并打开提供商选择器:

com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA

打开提供商选择器

如果表盘没有获得前面的权限,将无法启动提供商选择器。为了更容易请求权限并启动选择器,Wearable Support Library 提供了 ComplicationHelperActivity 类。在几乎所有情况下,应使用此类别代替 ProviderChooserIntent 来启动选择器。要使用 ComplicationHelperActivity,请在清单文件中将它添加到表盘:

<activity android:name="android.support.wearable.complications.ComplicationHelperActivity"/>

要启动提供商选择器,请调用 ComplicationHelperActivity.createProviderChooserHelperIntent 方法来获取意图。新的意图可以与 startActivitystartActivityForResult 一起使用来启动选择器:

startActivityForResult(
  ComplicationHelperActivity.createProviderChooserHelperIntent(
     getActivity(),
     watchFace,
     complicationId,
     ComplicationData.TYPE_LARGE_IMAGE),
  PROVIDER_CHOOSER_REQUEST_CODE);

当帮助活动启动时,它会检查权限是否已授予。如果未授予权限,帮助活动会提出运行时权限请求。如果权限请求被接受(或不必要),则会显示提供商选择器。

对于表盘来说,有许多场景需要考虑。在您的表盘实现并发症之前,请检查所有这些场景。您是如何接收并发症数据的?是来自提供商、远程服务器还是 REST 服务?提供商和表盘是否来自同一个应用?您还应该检查是否缺少适当的权限等。

了解 Wear 的不同导航方式

安卓穿戴在各个方面都在演进。在穿戴 1.0 中,屏幕之间的切换曾经让用户感到繁琐和困惑。现在,谷歌引入了材料设计和交互式抽屉,例如:

  • 导航抽屉:导航抽屉与移动应用导航抽屉类似的组件。它将允许用户在视图之间切换。用户可以通过在内容区域的顶部向下轻扫来在穿戴设备上访问导航抽屉。我们可以通过将setShouldOnlyOpenWhenAtTop()方法设置为 false,允许在滚动父内容内的任何位置打开抽屉,并且可以通过设置为 true 来限制它。

  • 单页导航抽屉:穿戴应用可以在单页和多页上向用户展示视图。新的导航抽屉组件通过将app:navigation_style设置为single_page,允许内容保持在单页上。

  • 操作抽屉:有一些每个类别应用都会进行的通用操作。操作抽屉为穿戴应用提供了访问所有这些操作的途径。通常,操作抽屉位于穿戴应用的底部区域,它可以帮助提供与手机应用中的操作栏类似的上下文相关用户操作。开发者可以选择将操作抽屉定位在底部或顶部,并且当用户滚动内容时可以触发操作抽屉。

下图是穿戴 2.0 导航抽屉的快速预览:

下面的例子展示了在消息应用中使用操作执行的动作回复:

实现

要在您的应用中使用新引入的组件,请使用WearableDrawerLayout对象作为您布局的根视图来声明用户界面。在WearableDrawerLayout内,添加一个实现NestedScrollingChild的视图来包含主要内容,以及额外的视图来包含抽屉的内容。

下面的 XML 代码展示了我们如何为WearableDrawerLayout赋予生命:

<android.support.wearable.view.drawer.WearableDrawerLayout
    android:id="@+id/drawer_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:deviceIds="wear">

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <LinearLayout
            android:id="@+id/linear_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical" />

    </android.support.v4.widget.NestedScrollView>

    <android.support.wearable.view.drawer.WearableNavigationDrawer
        android:id="@+id/top_navigation_drawer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <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_drawer_menu"/>

</android.support.wearable.view.drawer.WearableDrawerLayout>

单页导航抽屉

单页导航抽屉在穿戴应用中更快、更流畅地切换不同视图。要创建单页导航抽屉,请在抽屉上应用navigation_style="single_page"属性。例如:

 <android.support.wearable.view.drawer.WearableNavigationDrawer
        android:id="@+id/top_drawer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_red_light"
        app:navigation_style="single_page"/>

现在,下一个主要任务是填充抽屉布局上的数据。我们可以在 XML 布局中通过抽屉布局的app:using_menu属性以及从菜单目录加载 XML 文件来完成这个任务。

使用WearableDrawerView,我们可以设计自己的自定义抽屉布局。下面的代码展示了自定义抽屉布局:

<android.support.wearable.view.drawer.WearableDrawerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="top"
        android:background="@color/red"
        app:drawer_content="@+id/drawer_content"
        app:peek_view="@+id/peek_view">
        <FrameLayout
            android:id="@id/drawer_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <!-- Drawer content goes here.  -->
        </FrameLayout>
        <LinearLayout
            android:id="@id/peek_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:paddingTop="8dp"
            android:paddingBottom="8dp"
            android:orientation="horizontal">
            <!-- Peek view content goes here.  -->
        <LinearLayout>
    </android.support.wearable.view.drawer.WearableDrawerView>

有主要的抽屉事件,如onDrawerOpened()onDrawerClosed()onDrawerStateChanged()。我们也可以创建自定义事件;默认情况下,我们可以使用早期的一组回调来监听抽屉活动。

穿戴 2.0 的通知

Wear 2.0 更新了通知的视觉风格和交互范式。Wear 2.0 引入了可扩展通知,提供了更多内容区域和动作,以提供最佳体验。视觉更新包括材料设计、通知的触摸目标、深色背景以及通知的水平滑动手势。

内联动作

内联动作允许用户在通知流卡片内执行特定于上下文的操作。如果通知配置了内联动作,它将显示在通知的底部区域。内联动作是可选的;谷歌推荐在不同使用场景中使用,例如用户在查看通知后需要执行某个操作,如短信回复和停止健身活动。通知只能有一个内联动作,要启用它,我们需要将setHintDisplayActionInline()设置为 true。

要向通知添加内联动作,请执行以下步骤:

  1. 按如下方式创建一个RemoteInput.Builder的实例:
String[] choices = context.getResources().getStringArray(R.array.notification_reply_choices);     choices = WearUtil.addEmojisToCannedResponse(choices);   
RemoteInput remoteInput = new RemoteInput.
Builder(Intent.EXTRA_TEXT)         
.setLabel(context.getString
      (R.string.notification_prompt_reply))      
     .setChoices(choices)    
     .build();

  1. 使用addRemoteInput()方法,我们可以附加RemoteInput对象:
NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action.Builder(
        R.drawable.ic_full_reply, R.string.notification_reply, 
        replyPendingIntent);
    actionBuilder.addRemoteInput(remoteInput);
    actionBuilder.setAllowGeneratedReplies(true);

  1. 最后,添加一个提示以显示内联动作,并使用添加动作方法将动作添加到通知中:
// Android Wear 2.0 requires a hint to display the reply action inline.
    Action.WearableExtender actionExtender =
        new Action.WearableExtender()
            .setHintLaunchesActivity(true)
            .setHintDisplayActionInline(true);
    wearableExtender.addAction
    (actionBuilder.extend(actionExtender).build());

扩展通知

Wear 2.0 引入了可扩展通知,能够为每个通知显示大量内容和动作。扩展通知遵循材料设计标准,当我们向通知附加额外内容页面时,它们将在扩展通知内可用,用户在检查通知中的动作和内容时将获得应用内体验。

扩展通知的最佳实践

何时使用扩展通知:

  1. 配对手机的通知应使用扩展通知。

  2. 当应用通知在本地运行且仅通过点击启动应用时,我们不应使用扩展通知。

通知的桥接模式

桥接模式指的是穿戴设备和伴随应用之间共享通知的系统。独立应用和伴随应用可以获得复制通知。Android 穿戴整合了处理复制通知问题的组件。

开发者可以如下更改通知的行为:

  • 在清单文件中指定桥接配置

  • 在运行时指定桥接配置

  • 设置一个消除 ID,以便通知消除在设备间同步

在清单文件中的桥接配置:

<application>
...
  <meta-data
    android:name="com.google.android.wearable.notificationBridgeMode"
    android:value="NO_BRIDGING" />
...
</application>

在运行时进行桥接配置(使用BridgingManager类):

BridgingManager.fromContext(context).setConfig(
  new BridgingConfig.Builder(context, false)
    .build());

使用消除 ID 同步通知消除:

NotificationCompat.WearableExtender wearableExtender =
  new NotificationCompat.WearableExtender().setDismissalId("abc123");
Notification notification = new NotificationCompat.Builder(context)
// ... set other fields ...
  .extend(wearableExtender)
  .build();

通知是吸引用户在穿戴设备上使用应用的重要组件。Android Wear 2.0 提供了更多智能回复、消息样式等,并将继续提供更多功能。

Wear 2.0 输入方法框架

我们在前面的章节中构建的应用程序中已经看到了穿戴设备的输入机制。Wear 2.0 通过将 Android 的输入法框架IMF)扩展到 Android Wear,支持除语音之外的其他输入方式。IMF 允许使用虚拟的、屏幕上的键盘和其他输入方法进行文本输入。尽管由于屏幕尺寸的限制,使用方式略有不同,但用于穿戴设备的 IMF API 与其他设备形态的 API 是相同的。Wear 2.0 带来了系统默认的输入法编辑器IME),并为第三方开发者开放了 IMF API,以便为 Wear 创建自定义输入方法。

调用穿戴设备的 IMF

要调用穿戴设备的 IMF,您的平台 API 级别应为 23 或更高。在包含 EditText 字段的 Android Wear 应用中:触摸文本字段会将光标置于该字段,并在获得焦点时自动显示 IMF。

手腕手势

Wear 2.0 支持手腕手势。当您无法使用穿戴设备的触摸屏时,可以利用手腕手势进行快速的单手操作,例如,当用户在慢跑时,他想要使用手腕手势执行特定上下文的操作。有一些手势不适用于应用,例如,按下手腕、抬起手腕和摇动手腕。每个手腕手势都映射到按键事件类中的一个整型常量:

手势 KeyEvent 描述
向外挥动手腕 KEYCODE_NAVIGATE_NEXT 此按键代码跳转到下一个项目。
向内挥动手腕 KEYCODE_NAVIGATE_PREVIOUS 此按键代码返回上一个项目。

使用应用中手势的最佳实践

以下是使用应用中手势的最佳实践:

  • 查阅 KeyEventKeyEvent.Callback 页面,了解将按键事件传递到您的视图的相关信息

  • 为手势提供触摸平行支持

  • 提供视觉反馈

  • 不要将重复的手势重新解释为您的自定义新手势。它可能与系统的摇动手腕手势发生冲突。

  • 小心使用 requestFocus()clearFocus()

认证协议

随着独立手表的出现,穿戴应用现在可以在不依赖伴随应用的情况下完全在手表上运行。这一新功能也意味着,当应用需要从云端访问数据时,Android Wear 独立应用需要自行管理认证。Android Wear 支持多种认证方法,以使独立穿戴应用能够获取用户认证凭据。现在,穿戴支持以下功能:

  • Google 登录

  • OAuth 2.0 支持

  • 通过数据层传递令牌

  • 自定义代码认证

所有这些协议遵循与移动应用编程相同的标准;在穿戴设备上集成 Google 登录或其他协议时没有大的变化,但这些协议有助于授权用户。

应用分发

我们现在知道如何为 Wear 2.0 开发应用,并且在过去的经验中,我们可能已经将一个 Android 应用发布到了 Play 商店。那么,通过谷歌开发者控制台将一个独立的可穿戴应用或一般的可穿戴应用发布到 Play 商店需要什么呢?

Wear 2.0 捆绑了 Play 商店应用;用户可以搜索特定的可穿戴应用,并在连接到互联网时直接在可穿戴设备上安装它们。通常,Play 商店中的 Wear 2.0 应用需要在清单文件中至少和目标 API 级别 25 或更高。

发布你的第一款可穿戴应用

要让你的应用在手表上的 Play 商店中显示,请生成一个已签名的可穿戴 APK。如果这是一个独立的可穿戴应用,发布应用的过程将类似于发布移动应用。如果不是独立应用,且需要上传多个 APK,请遵循developer.android.com/google/play/publishing/multiple-apks.html

让我们在 Play 商店中发布 Wear-Note 应用。这是谷歌为开发者提供的专用仪表板,让你可以管理 Play 商店中的应用。谷歌有一次性的 25 美元注册费用,你需要在上传应用之前支付。收取费用的原因是防止虚假、重复账户,从而避免不必要的低质量应用充斥 Play 商店。

以下步骤展示了我们如何将可穿戴应用发布到 Play 商店的过程:

  1. 访问play.google.com/apps/publish/

  1. 点击创建应用并为你的应用起一个标题。

  2. 之后,你将看到一个表格,需要填写描述和其他详细信息,包括应用的屏幕截图和图标以及促销图形。

  3. 在商店列表中,填写有关应用的所有正确信息。

  4. 现在,上传已签名的可穿戴 APK。

  5. 填写内容评级问卷,获得评级,并将评级应用到你的应用中。

  6. 在定价和分发方面,你需要拥有一个商家账户才能在定价模式下分发你的应用。现在,可穿戴笔记应用是一款免费的 Wear 应用,并允许你选择免费。

  7. 选择列表中的所有国家并选择可穿戴设备 APK:

  1. 谷歌将审查可穿戴应用的二进制文件,并在准备好后批准其在 Wear Play 商店中分发。

  2. 恭喜你!现在,你的应用已经准备好发布了:

总结

在本章中,我们已经了解了独立应用程序和 Complications API。我们看到了如何使用 Capability API 检测伴随应用,并且对独立应用程序以及发布可穿戴应用有了清晰的认识。

本章节探讨了如何加强我们对 Wear 2.0 及其组件的理解,包括对独立应用、曲线布局以及更多用户界面组件的详尽了解,以及使用导航抽屉和操作抽屉构建可穿戴应用。同时,本章还提供了关于手腕手势及其在可穿戴应用中的使用、输入法框架的使用,以及将可穿戴应用分发到 Google Play 商店的简要了解。

posted @ 2024-05-22 15:10  绝不原创的飞龙  阅读(8)  评论(0编辑  收藏  举报