安卓穿戴项目(四)
原文:
zh.annas-archive.org/md5/C54AD11200D923EEEED7A76F711AA69C译者:飞龙
第七章:任意地点的导航方式 - 用户界面控制及更多功能
现在你已经学会了如何为 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,请执行以下步骤:
- 访问 Firebase 控制台
console.firebase.google.com:
- 在控制台中选择添加项目,并填写有关项目的必要信息。项目成功添加后,您将看到以下屏幕:
- “开始使用”页面帮助您为不同的平台设置项目。让我们选择第二个选项,它说“将 Firebase 添加到您的 Android 应用中”:
- 添加项目包名,出于进一步的安全考虑,你可以添加 SHA-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 初始化到您的应用程序中,请执行以下步骤:
-
在
res/values目录中创建一个 XML 配置文件。 -
添加一个名为
android_wear_capabilities的资源。 -
定义设备所提供的能力:
<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.xml和rounded_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 文件中,让我们添加一个帮助用户聊天的用户界面。这里,我将使用 Edittext 和 Textview,以及一个将消息发送到移动设备的按钮。让我们添加以下 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_CHANGED 和 MESSAGE_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 {
.......
}
实现 ConnectionCallbacks 和 OnConnectionFailedListener 之后,我们必须从这些接口重写一些方法,即 onConnected、onConnectionSuspended 和 onConnectionFailed 方法。我们大部分逻辑将在 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");
}
}
});
}
}
让我们重写onstart和onstop方法,以便在连接和断开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类中的ConnectionCallbacks和OnConnectionFailedListener接口:
public class ChatActivity extends AppCompatActivity implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
}
重写ConnectionCallbacks和OnConnectionFailedListener接口中的方法,类似于我们对穿戴设备的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 API的sendMessage方法会查找一些参数,例如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 函数触发推送通知。