2015.12.23 - 2016.01.13
个人英文阅读练习笔记。原文地址:http://developer.android.com/training/building-wearables.html。

12.23
这些节描述如何构建在手持应用程序中会自动同步到可穿戴设备上的通知(Notifications),同时描述如何构建运行在可穿戴设备上的应用程序。

注:关于用在笔记中会使用到的APIs的信息,见 Wear API reference documentation。

如果您优先选择教学视频,在网上想办法看关于Android 可穿戴开发视频,开始视频课程。

12.24

1. 添加可穿戴特性到Notifications

如何在可穿戴设备上构建会被同步的并且看起来不错的手持通知。

当安卓手持设备(电话或平板)和安卓的可穿戴设备连接时,手持设备会自动跟可穿戴设备分享通知。在可穿戴设备上,每个通知在上下文流中以新卡片的形式出现。

然而,欲给用户最佳的体验,应该添加可穿戴设备指定的功能到已经创建的通知中。后续小节将描述如何同时创建被手持设备和可穿戴设备创建的通知。


图1. 相同的通知在手持设备和可穿戴设备上的展示

1.1 为可穿戴设备创建Notification

学习如何用Android支持库所支持的可穿戴特性创建通知。

欲构建手持设备上同时也会发送给可穿戴设备的通知,使用NotificationCompat.Builder。当用此类来构建通知时,系统会自动在合适的时间里展示通知,不管是在手持设备还是在可穿戴设备之上。

注:通知使用RemoteViews被自定义布局剥夺且可穿戴设备只会展示文本和图标。然而,可以创建运行在可穿戴设备上的应用程序从而创建使用自定义卡片布局的”创建自定义通知“。

(1) 导入必要的类

欲导入必要的包,在build.gradle文件中添加以下语句:

compile "com.android.support:support-v4:20.0.+"

然后,应用程序就能够访问必要的包了,从支持库中导入必要使用的类:

import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.app.NotificationCompat.WearableExtender;

(2) 用Notification Builder创建Notifications

v4支持库允许使用最新的通知特性(诸如动作按钮和大图标)来创建通知,且仍旧兼容Android 1.6(API level 4)以及更高的版本。

欲用支持库创建通知,需要创建NotificationCompat.Builder的实例并通过将通知传递给notify()来发布通知。如下例:

int notificationId = 001;
// Build intent for notification content
Intent viewIntent = new Intent(this, ViewEventActivity.class);
viewIntent.putExtra(EXTRA_EVENT_ID, eventId);
PendingIntent viewPendingIntent =PendingIntent.getActivity(this, 0, viewIntent, 0);NotificationCompat.Builder notificationBuilder =new NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_event).setContentTitle(eventTitle).setContentText(eventLocation).setContentIntent(viewPendingIntent);// Get an instance of the NotificationManager service
NotificationManagerCompat notificationManager =NotificationManagerCompat.from(this);// Build the notification and issues it with notification manager.
notificationManager.notify(notificationId, notificationBuilder.build());

当此通知出现在手持设备上时,用户可以通过触碰该通知从而调用setContentIntent()方法指定的PendingIntent。当该通知出现在Android可穿戴设备上时,用户可以将通知移到左边来揭示”打开”动作,这会调用手持设备上的intent。

(3) 增加动作按钮

除了原来被setContentIntent()定义的内容动作外,还可以通过传递PendingIntent给addAction()方法来增加其他的动作。

以下代码片段创建了跟前一个相同类型的通知,但增加了一个在地图上查看事件位置的动作。

// Build an intent for an action to view a map
Intent mapIntent = new Intent(Intent.ACTION_VIEW);
Uri geoUri = Uri.parse("geo:0,0?q=" + Uri.encode(location));
mapIntent.setData(geoUri);
PendingIntent mapPendingIntent =PendingIntent.getActivity(this, 0, mapIntent, 0);NotificationCompat.Builder notificationBuilder =new NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_event).setContentTitle(eventTitle).setContentText(eventLocation).setContentIntent(viewPendingIntent).addAction(R.drawable.ic_map,getString(R.string.map), mapPendingIntent);

在手持设备上,此动作以附在通知上以按钮的形式出现。在可穿戴中,当用户将通知划到左边时动作以大按钮的形式出现。当用户按下该按钮,相关联的intent将会在手持设备上被调用。

提示:如果通知包含了“回应”动作(如信息应用程序),可以在Android可穿戴设备上开启语音输入响应来增强行为表现。更多信息读“从一个Notification接收语音输入”。

(4) 指定可穿戴式动作

如果欲想让在可穿戴设备上的动作不同于手持设备上的动作,使用WearableExtender.addAction()来实现。一旦用该方法增加动作,可穿戴设备不会展示用NotificationCompat.Builder.addAction()添加的任何动作。也就是说,只有用WearableExtender.addAction()添加的动作才会出现在可穿戴设备上,且这些动作在手持设备上不会显示。

// Create an intent for the reply action
Intent actionIntent = new Intent(this, ActionActivity.class);
PendingIntent actionPendingIntent =PendingIntent.getActivity(this, 0, actionIntent,PendingIntent.FLAG_UPDATE_CURRENT);// Create the action
NotificationCompat.Action action =new NotificationCompat.Action.Builder(R.drawable.ic_action,getString(R.string.label), actionPendingIntent).build();// Build the notification and add the action via WearableExtender
Notification notification =new NotificationCompat.Builder(mContext).setSmallIcon(R.drawable.ic_message).setContentTitle(getString(R.string.title)).setContentText(getString(R.string.content)).extend(new WearableExtender().addAction(action)).build();

(5) 增加一个大视图

可以通过增加一个“大视图”风格到通知的方式插入扩展的文本内容到通知中。在手机上,用户可以通过展开通知的方式看到大视图。在可穿戴设备上,大视图内容默认可见。

欲增加扩展内容到通知中,用NotificationCompat.Builder对象调用setStyle(),给该方法传递一个BigTextStyle或InboxStyle的实例。

举例,为包含完整的事件描述(比setContentText()提供的空间更大),以下代码片段增加了NotificationCompat.BigTextStyle到事件通知中。

// Specify the 'big view' content to display the long
// event description that may not fit the normal content text.
BigTextStyle bigStyle = new NotificationCompat.BigTextStyle();
bigStyle.bigText(eventDescription);NotificationCompat.Builder notificationBuilder =new NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_event).setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notif_background)).setContentTitle(eventTitle).setContentText(eventLocation).setContentIntent(viewPendingIntent).addAction(R.drawable.ic_map,getString(R.string.map), mapPendingIntent).setStyle(bigStyle);

可以通过setLargeIcon()方法增加一个大图标到任何通知中。然而,这些图标在可穿戴设备上以大背景图片的形式出现且因它们被扩大来适应可穿戴设备的屏幕会显得不那么好看。欲添加可穿戴指定的背景图片到通知中,见“为通知增加可穿戴特性”。更多关于为通知设计大图片的信息,见“Android可穿戴的设计原则”。

(6) 为Notification添加可穿戴特性

如果曾需要为通知增加可穿戴-指定选项,诸如指定额外的页或让用户决定用语音响应文本,就可以使用NotificationCompat.WearableExtender类来指定选项。欲使用该API:
[1] 创建WearableExtender类的实例,为通知设置可穿戴-自定选项。
[2] 创建NotificationCompat.Builder实例,设置通知需要的特性(如前些节中描述)。
[3] 调用通知的extent()方法并传递WearableExtender。这就将可穿戴选项应用到了通知上。
[4] 调用build()构建通知。

举例,以下代码片段调用setHintHideIcon()方法来从通知卡片汇中移除应用程序图标。

// Create a WearableExtender to add functionality for wearables
NotificationCompat.WearableExtender wearableExtender =new NotificationCompat.WearableExtender().setHintHideIcon(true).setBackground(mBitmap);// Create a NotificationCompat.Builder to build a standard notification
// then extend it with the WearableExtender
Notification notif = new NotificationCompat.Builder(mContext).setContentTitle("New mail from " + sender).setContentText(subject).setSmallIcon(R.drawable.new_mail).extend(wearableExtender).build();

setHintHideIcon()和setBackground()方法只是NotificationCompat.WearableExtender中可用的两个通知特性的例子而已。

注:setBackground()方法所使用的位图应该具有400x400的分辨率(对于无滑条的背景),应该使用640x400分辨率的图片来支持滑条。将这些位图图片放置到res/drawable-nodpi目录下。放置可穿戴通知的其它非位图资源到res/drawable-hdpi目录下(如被setContentIcon()方法用到的资源)。

如果曾需在后续事件读可穿戴-指定选项,使用响应的获取方法来获取这些选项。此例调用getHintHideIcon()方法来获取通知是否隐藏图标:

NotificationCompat.WearableExtender wearableExtender =new NotificationCompat.WearableExtender(notif);
boolean hintHideIcon = wearableExtender.getHintHideIcon();

(7) 传递Notification

当需要传递通知时,常使用NotificationManagerCompat API而不是NotificationManager:

// Get an instance of the NotificationManager service
NotificationManagerCompat notificationManager =NotificationManagerCompat.from(mContext);// Issue the notification with notification manager.
notificationManager.notify(notificationId, notif);

如果使用框架的NotificationManager,一些来自NotificationCompat.WearableExtender不会正常工作,所以需要确保是使用的NotificationCompat。

12.25

1.2 在Notification中接收语音输入

学习如何增加动作到可穿戴设备的通知中以让其接收来自用户的语音输入并传递转录的信息给手机上的应用程序。

假设在手持设备上拥有了包含输入文本动作的通知,如回复邮件,这通常会在手持设备上启动一个活动来输入文本。然而,当通知出现在可穿戴设备上时,并没有键盘输入,所以可以用RemoteInput来让用户口述提前已定义好的文本信息来回应。

当用户用语音或选择可用的信息回应时,系统会将文本信息附在为通知动作指定的Intent中并将Intent发送到手持设备上。

注:Android模拟器不支持语音输入。当使用模拟器来模拟可穿戴设备时,在AVD设置里开启硬件键盘,这样就可以用键盘回应来代替语音回应。

(1) 定义语音输入

欲创建支持语音输入的动作,创建可以添加到通知动作中的RmoteInput.Builder的实例。此类的构造函数接收一个字符串(系统用此字符串作为语音输入的键),此字符串也会用来检索在手持设备应用程序中的文本输入。

例如,以下例子创建了RemoteInput对象(提供语音输入的自定义标签):

// Key for the string that's delivered in the action's intent
private static final String EXTRA_VOICE_REPLY = "extra_voice_reply";String replyLabel = getResources().getString(R.string.reply_label);RemoteInput remoteInput = new RemoteInput.Builder(EXTRA_VOICE_REPLY).setLabel(replyLabel).build();

添加预定义文本响应
除了允许语音输入外,还么一提供到5个文本响应(用户可快速选择其中之一来回复)。调用setChoices()并将预定义文本作为数组参数传给该方法。

如可以在资源数组中定义一些响应:
res/values/strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><string-array name="reply_choices"><item>Yes</item><item>No</item><item>Maybe</item></string-array>
</resources>

然后,使用该字符串数组并将其添加到RemoteInput中:

public static final String EXTRA_VOICE_REPLY = "extra_voice_reply";
...
String replyLabel = getResources().getString(R.string.reply_label);
String[] replyChoices = getResources().getStringArray(R.array.reply_choices);RemoteInput remoteInput = new RemoteInput.Builder(EXTRA_VOICE_REPLY).setLabel(replyLabel).setChoices(replyChoices).build();

(2) 添加语音输入作为通知动作

欲设置语音输入、附加RemoteInput对象到动作上,使用addRemoteInput()。然后就可以应用动作到通知上了。如下例:

// Create an intent for the reply action
Intent replyIntent = new Intent(this, ReplyActivity.class);
PendingIntent replyPendingIntent =PendingIntent.getActivity(this, 0, replyIntent,PendingIntent.FLAG_UPDATE_CURRENT);// Create the reply action and add the remote input
NotificationCompat.Action action =new NotificationCompat.Action.Builder(R.drawable.ic_reply_icon,getString(R.string.label), replyPendingIntent).addRemoteInput(remoteInput).build();// Build the notification and add the action via WearableExtender
Notification notification =new NotificationCompat.Builder(mContext).setSmallIcon(R.drawable.ic_message).setContentTitle(getString(R.string.title)).setContentText(getString(R.string.content)).extend(new WearableExtender().addAction(action)).build();// Issue the notification
NotificationManagerCompat notificationManager =NotificationManagerCompat.from(mContext);
notificationManager.notify(notificationId, notification);

当发布该通知后,用户通过将其划到左边的方式就可以看到“回应”动作按钮。

(3) 以字符串的形式接收语音输入

欲接收用户在活动中的描述的回应动作intent中转录的信息,调用getResultsFromIntent(),并传递“回应”动作intent。此方法返回一个包含文本响应的Bundle。可以查询该Bundle来获取响应。

注:不要使用Intent.getExtras()来获取语音结果,因为语音输入被存储为ClipData。getResultsFromIntent()方法提供了一个方便的方式接受字符串序列(不用额外来处理ClipData)。

以下代码展示接受intent并返回语音响应的方法,该方法被EXTRA_VOICE_REPLY键引用(该见被之前的例子使用):

/*** Obtain the intent that started this activity by calling* Activity.getIntent() and pass it into this method to* get the associated voice input string.*/private CharSequence getMessageText(Intent intent) {Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);if (remoteInput != null) {return remoteInput.getCharSequence(EXTRA_VOICE_REPLY);}return null;
}

1.3 添加页到Notification中

学习如何增加额外的信息页,当用户往左边划屏幕时这些页面程可见状态。

当需要在不用打开手持设备上的应用程序就能提供更多的信息时,可以增加一个或更多页面到可穿戴设备上的通知中。额外的页将立刻出现在主通知卡片的右边。

欲创建含多个页的通知:
[1] 用NotificationCompat.Bulder创建主通知(第一个页面),以喜欢的方式让通知出现在手持设备上。
[2] 用NotificationCompat.Builder创建其余页。
[3] 用addPage()方法应用页到主通知中或用addPage()方法在Collection中添加多页。

举例,以下代码添加另外一个页面到通知中:

// Create builder for the main notification
NotificationCompat.Builder notificationBuilder =new NotificationCompat.Builder(this).setSmallIcon(R.drawable.new_message).setContentTitle("Page 1").setContentText("Short message").setContentIntent(viewPendingIntent);// Create a big text style for the second page
BigTextStyle secondPageStyle = new NotificationCompat.BigTextStyle();
secondPageStyle.setBigContentTitle("Page 2").bigText("A lot of text...");// Create second page notification
Notification secondPageNotification =new NotificationCompat.Builder(this).setStyle(secondPageStyle).build();// Extend the notification builder with the second page
Notification notification = notificationBuilder.extend(new NotificationCompat.WearableExtender().addPage(secondPageNotification)).build();// Issue the notification
notificationManager =NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, notification);

12.26

1.4 Notifications栈

学习如何将应用程序中所有相似的通知放置到栈中,允许用户在不添加卡片到卡片系统的情况下能够私自查看每一个通知。

当在为手持设备创建通知时,应该合计相似的通知到单个汇总通知里。例如应用程序创建了接收信息的通知,不应该再手持设备上展示多于一个的通知 - 当接收到多个信息时,用单个通知提供一个诸如“2条新信息”的汇总。

然而在Android可穿戴设备上,汇总通知具有更少的用途,因为用户不能读取可穿戴设备上相关信息的详细内容(必须打开手持设备上的应用程序以查看更多的信息)。所以对于可穿戴设备,应该将通知以组的形式存入栈中。栈中的通知以单个卡片的形式存在,用户可独立的扩展每条通知的详细内容。setGroup()方法允许在手持设备上提供单个汇总通知让实现上述功能成为可能。

(1) 添加每个通知到组中

欲创建栈,为想让进入栈中的每个每个通知调用setGroup()方法并为其制定一个组键。然后调用notify()将其发送到可穿戴设备上。

final static String GROUP_KEY_EMAILS = "group_key_emails";// Build the notification, setting the group appropriately
Notification notif = new NotificationCompat.Builder(mContext).setContentTitle("New mail from " + sender1).setContentText(subject1).setSmallIcon(R.drawable.new_mail).setGroup(GROUP_KEY_EMAILS).build();// Issue the notification
NotificationManagerCompat notificationManager =NotificationManagerCompat.from(this);
notificationManager.notify(notificationId1, notif);

若在以后欲想创建另外一个通知,指定相同的组键。当调用notify()时,此通知出现在相同的栈中作为之前的一个通知,而不是作为一个新卡片:

Notification notif2 = new NotificationCompat.Builder(mContext).setContentTitle("New mail from " + sender2).setContentText(subject2).setSmallIcon(R.drawable.new_mail).setGroup(GROUP_KEY_EMAILS).build();notificationManager.notify(notificationId2, notif2);

默认情况下,通知以被添加的先后顺序依次出现(最近在顶部的可见通知)。可以通过调用setSortKey()来第通知进行排序。

(2) 添加汇总通知

提供一个在手持设备上出现的汇总通知犹然重要。所以,除了添加每个独特的通知到相同的栈组外,也要增加汇总通知并在汇总通知中调用setGroupSummary()。

此通知不会再可穿戴设备上的通知栈中出现,但它以在手持设备上的唯一通知的形式出现。

Bitmap largeIcon = BitmapFactory.decodeResource(getResources(),R.drawable.ic_large_icon);// Create an InboxStyle notification
Notification summaryNotification = new NotificationCompat.Builder(mContext).setContentTitle("2 new messages").setSmallIcon(R.drawable.ic_small_icon).setLargeIcon(largeIcon).setStyle(new NotificationCompat.InboxStyle().addLine("Alex Faaborg   Check this out").addLine("Jeff Chang   Launch Party").setBigContentTitle("2 new messages").setSummaryText("johndoe@gmail.com")).setGroup(GROUP_KEY_EMAILS).setGroupSummary(true).build();notificationManager.notify(notificationId3, summaryNotification);

此通知使用了NotificationCompat.InboxStyle,此种形式提供了一种简单的方式为邮件或信息应用程序创建通知。可以使用此种风格,也可以使用定义在NotificationCompat中定义的另外一种风格,也可以不为汇总通知使用任何风格。


提示:欲使用截图中的文本风格,见”Styling with HTML markup“和”Style with Spannables“。

汇总通知同时也能够影响在可穿戴设备上没有被展示的通知。当创建汇总通知时,可以使用NotificationCompat.WearableExtender类并调用setBackground或addAction()来设置背景图片或应用在可穿戴设备上的整个栈的动作。以下代码是为整个通知栈设置背景的例子:

Bitmap background = BitmapFactory.decodeResource(getResources(),R.drawable.ic_background);NotificationCompat.WearableExtender wearableExtender =new NotificationCompat.WearableExtender().setBackground(background);// Create an InboxStyle notification
Notification summaryNotificationWithBackground =new NotificationCompat.Builder(mContext).setContentTitle("2 new messages")....extend(wearableExtender).setGroup(GROUP_KEY_EMAILS).setGroupSummary(true).build();

2. 创建可穿戴设备应用程序

如何构建直接运行在可穿戴设备上的应用程序。

让可穿戴应用程序直接运行在设备上,就能够访问到诸如传感器和GPU这样的硬件。它们的基础部分跟在其它设备上用Android SDK构建应用程序是一样的,但是在设计、使用量以及被提供的功能方面却有很大区别。以下是手持设备和可穿戴设备的应用程序的主要区别:

  • 与手持设备应用程序相比,可穿戴应用程序的尺寸和功能都相对要小。在可穿戴设备上的应用程序只包含有意义的部分,它们通常是与手持设备中相关联应用程序中的某个子功能。通常来讲,当可能时应该在手持设备上实施尽可能多的操作并将结果发送给可穿戴设备的应用程序。
  • 用户不用直接下载应用程序到可穿戴设备上。相反,应该将可穿戴应用程序捆在手持设备应用程序中。当用户安装手持设备应用程序时,系统会自动安装可穿戴应用程序。然而,以开发角度出发,仍可将可穿戴应用程序直接安装到可穿戴设备上。
  • 可穿戴应用程序可以访问很多标准的Android APIs,但不支持以下APIs:
    • android.webkit
    • android.print
    • andaoid.app.backup
    • android.appwidget
    • android.hardware.usb
      在尝试着使用API之前可通过调用hasSystemFeature()方法来检查可穿戴设备是否支持该API。

欲保护可穿戴设备上的电量,可以为可穿戴应用程序开启轻松氛围环境模式。当用户将活动置闲或用户将屏幕遮住时,将设备由交互模式转换到轻松氛围环境模式。能够转变到轻松氛围环境模式的可穿戴应用程序被称为“永远在线”应用程序。以下内容描述永远在线应用程序的两个模式:
交互性
在此种模式下,使用全色彩流动动画。应用程序同时响应相应的输入。

阴影色(轻松氛围)
用灰度图像渲染屏幕且不再呈现任何的输入线索。此种展示模式在android 5.1及更高的版本中才支持。

对于运行低于android 5.1的设备或不支持阴影色模式的应用程序,当用户闲置某个活动或用手掌覆盖屏幕时,可穿戴应用程序的主屏幕会被呈现。如果需要在android 5.1之前的版本中展示持久的内容,在上下文流中创建通知。

注:推荐使用Android Studio来进行android可穿戴开发,因为它提供工程设置、库包含和打包的快捷方式。后续小节都以android studio为平台进行描述。

12.28

2.1 创建和运行可穿戴设备应用程序

学习如何创建同时包含手持设备和可穿戴设备应用程序模块的工程以及如何在设备或模拟器中运行程序。

直接运行在可穿戴设备上的可穿戴应用程序,能够访问可穿戴设备的诸如传感器、活动、服务及更多的底层硬件。

一个在手持设备上包含可穿戴应用程序的同伴应用程序也是需要的,若要将程序发布到Google Play存储上。可穿戴不支持Google Play存储,所以用户是下载同伴手持应用程序,它会自动将可穿戴应用程序安装到可穿戴设备上。手持应用程序在用来执行重处理时同样有用,如网络操作或其它工作并将结果发送给可穿戴设备。

此小节将描述如何设置设备或模拟器并创建一个包含可穿戴应用程序和手持应用程序的工程。

(1) 更新SDK

在构建可穿戴应用程序之前,必须先完成以下工作:

  • 更新SDK工具至23.0.0或更高版本。更新的SDK工具能够让开发者构建和测试可穿戴应用程序。
  • 跟新SDK[包含android 4.4W.2(API 20)或更高版本]。更新的平台版本为可穿戴应用程序提供新的APIs。

欲更新包含这些组件的SDK,见“获取最新的SDK工具”。

(2) 设置android可穿戴模拟器或设备

推荐在真实的设备上开发可穿戴应用程序以能够更好的评估到用户体验。但模拟器也有它的优点,它提供不同类型的屏幕尺寸,这有利于测试。

[1] 设置android 可穿戴虚拟机

欲设置android Wear真机:

  1. 点击 Tools > Android > AVD Manager.
  2. 点击Create Virtual Device…
    1. 在Category列表中点击Wear
    2. 选择Android Wear Square或Android Wear Round
    3. 点击Next
    4. 选择发布名(如KetKat Wear)
    5. 点击Next
    6. (可选择)更改虚拟机的个人偏好
    7. 点击Finish
  3. 开启模拟器
    1. 选择刚创建的虚拟机
    2. 点击Play按钮
    3. 等待模拟器初始化至显示android Wear主页屏幕
  4. 将手持设备和模拟器匹配
    1. 在手持设备上,从Google Play上安装Android Wear应用程序
    2. 用USB连接手持设备到电脑
    3. Forward AVD的通讯端口连接手持设备(必须确保手持设备的稳定连接状态) - adb -d forward tcp:5601 tcp 5601
    4. 开启手持设备上的Android Wear 应用程序并间接到模拟器
    5. 点击android Wear应用程序右上角的菜单并选择Demo Cards
    6. 所选择的卡片将会以通知的形式出现在模拟器的home屏幕中

[2] 设置Android Wear 真机

欲设置android Wear真机,按照以下步骤:
1. 在手持设备上安装在Google Play上可用的Android Wear 应用程序
2. 跟随应用程序的指令完成手持设备和可穿戴设备的匹配。这能够实现同步测试手持设备的通知,如果正构建它们。
3. 离开在手机上打开的Android Wear应用程序
4. 在Android Wear设备上开启adb调试
1. 点击Settings > About
2. 按Build number 7次
3. 回到Settings菜单中
4. 到屏幕底部的Developer options中
5. 按ADB Debugging 开启adb
5. 通过USB连接可穿戴设备到电脑,这样在开发过程中就可以直接将应用程序直接安装到可穿戴设备上。一条信息同时出现在可穿戴设备和手持设备上时就表明可允许调试。注:如果可穿戴设备连接电脑失败,可以尝试“通过蓝牙连接”。
6. 在Android Wear应用程序中,检查总是允许此台电脑并点击确定

Android Studio中的Android 工具窗口会显示来自可穿戴设备的系统日志。当运行“adb devices”命令时,可穿戴设备应该也会被列出来。

(3) 创建工程

创建一个同时包含可穿戴和手持的应用模块工程来开始开发。在Android Studio中,点击 File > New Project并按照“工程向导”步骤(在“创建工程”一节中描述)。当按照向导步骤时,输入以下信息:

  1. 在Configure your Project窗口中,输入应用程序名和包名
  2. 在Form Factors窗口中:
    1. 在Minimum SDK下选择Phoe and Tablet并选择API 9:Android 2.3(Gingerbread)
    2. 在Minimum SDK下选择Wear并选择API 20:Android 4.4(KitKat Wear)
  3. 在第一个Add an Activity窗口中,为移动应用程序添加空活动
  4. 在第二个Add an Activity窗口中,为可穿戴应用程序添加空活动。

当完成向导时,Android Studio会创建有移动和可穿戴(mobile和wear)的工程。现在就具备了拥有手持和可穿戴应用程序的工程了,可在此基础上自定义活动、服务以及布局。手持应用程序做任务较重的工作,诸如网络通信、密集处理或需要大量用户交互需求的任务。当应用程序完成这些操作后,应用程序应该通过通知或者同步并发送数据给可穿戴应用程序的方式通知可穿戴应用程序。

注:wear模块也包含一个使用WatchViewStub显示”Hello World”的活动。此小节填充一个基于设备屏幕为圆还是方的布局。WatchViewStub类是一个由wearable support library提供的一个用户界面窗体部件。

(4) 安装可穿戴应用程序

在开发过程中,使用adb install命令或Android Studio上的Play按钮直接安装应用程序到可穿戴设备中(就像手持应用程序那样)。

当准备发布应用程序给用户时,将可穿戴应用程序嵌入到手持设备应用程序中。当用户从Google Play安装手持设备应用程序时,连接的可穿戴设备会自动接收到可穿戴应用程序。

注:可穿戴应用程序只在发布情况(release key)才会自动安装。关于如何打包可穿戴应用程序的信息见 Packaging Wearable Apps。

欲安装”Hello World”应用程序到可穿戴设备上时,从Run/Debug configuration下拉菜单中选择wear并点击Play按钮。活动会在可穿戴设备上显示时就会打印出”Hello world!”。

(5) 包含正确的库

作为工程向导的一部分,正确的依赖在build.gradle文件中合适的模块中被导入。然而,这些库不需要,所以阅读以下描述找到需要与否的库:
通知(Notification)
Android v4 support library(或 v13,其包含v4)包含了已存在于手持设备上来支持可穿戴设备的通知的APIs。

对于出现在可穿戴设备上的通知(被运行在可穿戴设备上的某应用程序发布),可以在可穿戴设备上使用标准框架APIs(API Level 20)并在工程的mobile模块中移除库依赖。

可穿戴数据层(Wearable Data Layer)
欲使用可穿戴数据层APIs在可穿戴设备和手持设备间同步和发送数据,需要更新Google Play services版本。若没有使用这些APIs,将两个模块的依赖都移除掉。

可穿戴设备用户界面支持库(Wearable UI support library)
这是一个非官方包含UI widgets designed for wearables的库。鼓励在工程中使用这些库,因为他们简化了最佳实践,而且它们能够在任何时候被改变。然而,如果库被更新,应用程序不会被打断因为他们已经变异到了程序中。欲获取更新库的新特性,需要惊天连接新版本库来相应的更新到应用程序。此库只使用于所创建的可穿戴应用程序。

在下一节中,将会描述如何为可穿戴应用程序创建布局设计以及如何使用平台支持的多样的语音动作。

12.30

2.2 创建自定义布局

学习如何为通知和活动创建和呈现自定义布局。

为可穿戴应用程序创建布局跟手持设备应用程序一样,除了不得不在可穿戴设备应用中设计布局的尺寸和扫视能力外。除了好的用户体验外不需要端口功能和来自手持应用程序的用户界面。只在必要时创建自定义布局。阅读如何设计优秀可穿戴应用程序的设计手册。

(1) 创建自定义通知

通常,应该在手持设备上创建通知并自动让它们同步到可穿戴设备上。这就让通知只被创建一次然而能被多个设备利用(不只是可穿戴设备,甚至连TV都可以),不用在每个设备上都创建一种通知。

如果标准通知风格不能工作(如NotificationCompat.BigTexStyle或NotificationCompat.Inboxstyle),可以展示任何有自定义布局的活动。只可在可穿戴设备上定义和发布自定义通知,系统不会同步这些通知到手持设备上。

注:当在可穿戴设备上创建自定义通知时,可以使用标准的通知APIs(API Level 20)代替支持库。

欲创建自定义通知:

  1. 创建一个布局并设置它作为欲展示活动的内容示图。
public void onCreate(Bundle bundle){...setContentView(R.layout.notification_activity);
}
  1. 在android清单文件中为活动定义必要的特性以允许活动能被展示在可穿戴程序的上下文流进程中。需要声明活动的可被导出性、可嵌入性以及有相关联的空任务。同时推荐推荐将主题设置为Theme.DeviceDefault.Light。如下例:
<activity android:name="com.example.MyDisplayActivity"android:exported="true"android:allowEmbedded="true"android:taskAffinity=""android:theme="@android:style/Theme.DeviceDefault.Light" />
  1. 为欲展示的活动创建PendingIntent。如下例:
Intent notificationIntent = new Intent(this, NotificationActivity.class);
PendingIntent notificationPendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
  1. 构建Notification并调用setDisplayIntent()提供PendingIntent。当用户查看通知时,系统使用该PendingIntent来启动活动。
  2. 用notify()方法来发布通知。
    注:当通知微露在主屏幕上时,系统将用由通知的语义数据产生标准的模板来展示通知。此模板在所有的表面都会工作良好。当用户划开通知时,为通知自定义的活动将会出现。

(2) 用可穿戴用户界面库创建布局

用Android Studio工程向导创建可穿戴应用程序时可穿戴用户界面库已被自动包含在工程中。也可以通过在build.gradle文件中通过以下的依赖声明将库添加到工程中:

dependencies {compile fileTree(dir: 'libs', include: ['*.jar'])compile 'com.google.android.support:wearable:+'compile 'com.google.android.gms:play-services-wearable:+'
}

此库帮助构建为可穿戴设备设计的用户界面。更多信息见“为可穿戴设备创建自定义用户界面”。

在可穿戴用户界面库中有以下主要类:
BoxInsetLayout
一个能知道屏幕形状并能够将其子元素放置到原型屏幕中间的FrameLayout对象。

CardFragment
一个呈现可扩展、可垂直滚动卡片内容的Fragment。

CircleImageView
被圆圈环绕的图片视图。

ConfirmationActivity
在用户完成动作后展示信息动画的活动。

CrossFadeDrawable
一个可拖拽的包含两个可拖拽并提供方法来直接调整两者的混合度。

DelayedConfirmationView
提供循环向下计数的视图,尤其被用于在一个短暂延时过后自动确认操作。

DimissOverlayView
用于实现long-press-to-dismiss的视图。

GridPageAdapter
应用页到GridViewPager对象的适配器。

FragmentGridPageAdapter
代表以fragment形式的页的GridPagerAdapter实例的实现。

DotsPageIndicator
为GridViewPager实现的页标识符,在所有相关页中标识当前页。

WatchViewStub
基于设备屏幕形状用来加载指定布局的类。

WearableListView
是ListView的一个可选的版本,在可穿戴设备上的小屏幕上优化了缓用。它展示垂直滑轮列表条目,并自动停留在最近的条目当用户停止滑动时。

(3) 可穿戴用户界面库API参考

参考文档详细解释如何使用每个用户界面窗体部件。浏览“可穿戴API参考文档”来获取以上类的使用细节。

注:推荐使用Android Studio来开发Android 可穿戴应用程序,因为它提供了工程设置、库包含和打包。

2.3 保持应用程序可见

学习如何为应用程序开启阴影(ambient)模式,这样在节约电量的同时还能够保持可见性。

一些可穿戴应用程序的时刻可见性时最重要的。如用户外出跑步时可瞟到可穿戴上所记录的用户所跑过的距离,或在可穿戴设备上记录一些需要购买的东西的列表,当用户在超市购物时可从可穿戴设备上看到自己所要买的东西的列表。制作一个时刻可见的应用程序对电池寿命有影响,所以当添加此特性到应用程序中时要认真考虑它的这一影响。

运行Android 版本5.1或更高版本的Android Wear设备可以在保留电量的情况下允许应用程序仍在前台。当设备处于低电量阴影模式下时,Android Wear应用程序可以控制展示在屏幕上的具体内容。能同时运行在阴影状态和交互模式下的可穿戴应用程序被称为一直开着(always-on)的应用程序。

此节描述如何开启可穿戴应用程序处于一直开着状态,当处于阴影状态时更新屏幕,并保持向后兼容。

(1) 开启可穿戴应用程序的阴影模式

对于新的和已存在的工程,可以通过更新开发工程配置来添加阴影模式到可穿戴应用程序中。在完成工程配置后,扩展WearableActivity类,此类提供开启应用程序阴影模式的所有方法。以下各小节详细的描述这些步骤。

[1] 配置开发工程

欲在可穿戴应用程序中支持阴影模式,必须更新Android SDK并配置开发工程。跟随以下步骤来做一些必要的改变:

  • 更新SDK来包含Android 5.1(API 22)或更高的平台,它提供APIs来允许活动进入阴影模式。更多关于如何更新SDK的信息见“添加SDK包”。
  • 创建工程或更改已存在的工程的目标为Android 5.1或更高。这意味着必须设置清单文件中的targetSdkVersion为22或更高的版本值。
  • 设置清单文件的minSdkVersion为20或更高的版本值,如果欲让设备支持Android 5.1之前的版本。更多关于像后兼容见“维持向后兼容”。
  • 添加或更新以下依赖到build.gradle文件中:
dependencies {...compile 'com.google.android.support:wearable:1.2.0'provided 'com.google.android.wearable:wearable:1.0.0'
}

注:provided依赖确保了类在运行时被载入来支持阴影模式,在编译时也可以。
- 添加可穿戴分享库入口到可穿戴设备应用程序的清单文件中:

<application><uses-library android:name="com.google.android.wearable"android:required="false" />...
</application>
  • 添加WAKE_LOCK权限到手持和可穿戴应用程序清单文件中:
<uses-permission android:name="android.permission.WAKE_LOCK" />

[2] 创建支持阴影模式的活动

欲在活动中开启阴影模式,使用WearableActivity类和其方法。

  1. 创建扩展于WearableActivity的活动。
  2. 在活动的onCreate()方法中,调用setAmbientEnabled()方法。

在活动中按照如下方式开启阴影模式:

public class MainActivity extends WearableActivity {@Override
public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setAmbientEnabled();...
}

[3] 处理模式间的转换

若在活动被呈现的某段时间内用户都没有和活动进行交互,或用户用手掌覆盖了屏幕,系统会转换活动进入阴影模式。在应用程序转变为阴影模式后,更新活动用户界面到一个更简单的布局以减少电的消耗。此时,应该使用一个带最少白色图片和文本的黑色背景。以缓解用户从交互模式进入阴影模式的感觉,尝试维持部件在屏幕中的位置不变。更多关于在阴影模式下呈现内容的信息见“ Watch Faces for Android Wear”设计手册。

注:在阴影模式下,关闭屏幕中任意的交互元素,如按钮。更多关于如何设计和用户交互的always-on应用程序的信息,见“ App Structure for Android Wear”设计手册。

当活动转变到阴影模式时,系统会在可穿戴活动中调用onEnterAmbient()方法。以下代码展示在系统转变到阴影模式后如何改变文字的颜色为白色并关闭图形保真:

@Override
public void onEnterAmbient(Bundle ambientDetails) {super.onEnterAmbient(ambientDetails);mStateTextView.setTextColor(Color.WHITE);mStateTextView.getPaint().setAntiAlias(false);
}

当用户点击屏幕或举起他们的手腕时,活动将从阴影模式转变为交互模式。系统将会调用onExitAmbient()方法。重写该方法来更新用户界面布局以让应用程序展示全色、交互的状态。

以下代码片段展示如何在系统转变为交互模式时如何改变文字的颜色为绿色并开启图形保真:

@Override
public void onExitAmbient() {super.onExitAmbient();mStateTextView.setTextColor(Color.GREEN);mStateTextView.getPaint().setAntiAlias(true);
}

(2) 更新阴影模式的内容

阴影模式允许用新信息为用户更新屏幕内容,但必须平衡更新显示与电池寿命。重写onUpdateAmbient()方法来更新屏幕以几分钟的方式需要被强烈的考虑。如果应用程序需要更高屏幕的更新,考虑电池寿命和更新频率之间的权衡。要实现电量保留,更新频率应该不要超过10s。然而在实际中,更新应用程序应要少于这个频率。

12.31

[1] 1分钟更新一次

为预留电量,大多数的可穿戴应用程序处于阴影模式中时都不会频繁的更新屏幕。推荐在此种模式下设计应用程序每1分钟更新一次屏幕的显示。系统提供回调方法onUpdateAmbient()来允许以推荐的频率更新屏幕。

欲更新应用程序内容,在可穿戴活动中重写onUpdateAmbient()方法:

@Override
public void onUpdateAmbient() {super.onUpdateAmbient();// Update the content
}

[2] 更高频率更新

对于需要更高频率更新屏幕的应用程序,如在健身、时间保持以及旅游信息应用程序,使用AlarmManager对象来唤醒处理器并以更高频率更新屏幕。

欲实现在阴影模式下以更高频率更新内容的闹铃,跟随以下步骤:

  1. 准备闹铃管理器。
  2. 设置更新频率。
  3. 在活动转变到阴影模式或正在阴影模式中时计划下一次更新。
  4. 当活动转变到交互模式或活动停止时取消闹铃。
    注:在被触发后,闹铃管理器可能会在活动中创建新的实例。欲阻止此情况,确保在清单文件中被声明的活动有android:launchMode=”singleInstance”参数。

以下小节详细描述这些步骤。

准备闹铃管理器
闹铃管理器启动挂起的用来更新屏幕和计划下一个闹铃的意图。以下例子展示如何在活动的onCreate()方法中声明闹铃管理器以及挂起意图:

private AlarmManager mAmbientStateAlarmManager;
private PendingIntent mAmbientStatePendingIntent;@Override
public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setAmbientEnabled();mAmbientStateAlarmManager =(AlarmManager) getSystemService(Context.ALARM_SERVICE);Intent ambientStateIntent =new Intent(getApplicationContext(), MainActivity.class);mAmbientStatePendingIntent = PendingIntent.getActivity(getApplicationContext(),0,ambientStateIntent,PendingIntent.FLAG_UPDATE_CURRENT);...
}

当闹铃触发并启动挂起意图时,通过重写onNewIntent()方法来更新屏幕并计划下一个闹铃:

@Override
public void onNewIntent(Intent intent) {super.onNewIntent(intent);setIntent(intent);// Described in the following sectionrefreshDisplayAndSetNextUpdate();
}

更新屏幕并计划数据更新
在此例的活动中,在阴影模式下每20s闹铃管理器会触发一次。当计时器开始计时时,闹铃触发意图更新屏幕然后设置下一次更新的延迟时间。

以下样例展示如何更新屏幕的信息以及设置下一次更新的闹铃:

// Milliseconds between waking processor/screen for updates
private static final long AMBIENT_INTERVAL_MS = TimeUnit.SECONDS.toMillis(20);private void refreshDisplayAndSetNextUpdate() {if (isAmbient()) {// Implement data retrieval and update the screen for ambient mode} else {// Implement data retrieval and update the screen for interactive mode}long timeMs = System.currentTimeMillis();// Schedule a new alarmif (isAmbient()) {// Calculate the next trigger timelong 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}
}

计划下一次闹铃
当活动正进入阴影模式或活动已经在阴影模式中时分别重写onEnterAmbient()和onUpdateAmbient()方法来计划闹铃更新屏幕:

@Override
public void onEnterAmbient(Bundle ambientDetails) {super.onEnterAmbient(ambientDetails);refreshDisplayAndSetNextUpdate();
}@Override
public void onUpdateAmbient() {super.onUpdateAmbient();refreshDisplayAndSetNextUpdate();
}

注:在此例中,refreshDisplayAndSetNextUpdate()方法在屏幕需要更新时被调用。更过关于何时调用此方法的例子,见AlwaysOn样例。

取消闹铃
当设备转换到交互模式时,在onExitAmbient()方法中取消闹铃:

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

当用户退出或停止活动时,在活动的onDestroy()方法中取消闹铃:

@Override
public void onDestroy() {mAmbientStateAlarmManager.cancel(mAmbientStatePendingIntent);super.onDestroy();
}

(3) 维持向后兼容

自动支持阴影模式的活动运行在跑着Android 5.1版本(API level 22)以及更早的系统的设备上时会自动变回通常的活动。不需要特别的代码来支持这一特性。但设备转变到阴影模式时,设备返回到home屏幕并退出活动。

如果应用程序没有被安装或更新到跑着Android 5.1以及之前版本的设备上,用以下内容更新清单文件的内容:

<uses-library android:name="com.google.android.wearable" android:required="true" />

2016.01.01

2.4 增加语音功能

学习如何用语音动作启动活动以及如何启动系统语音辨识应用程序来获取无固定格式的语音输入。

语音是可穿戴应用程序体验中重要的一部分。它们能够让用户不用手操作且很快的实现操作。可穿戴设备提供两种类型的语音动作:
系统提供(System-provided)
这些语音动作都是基于任务且是被构建在可穿戴平台之上的。当语音动作开始后,在活动宏过滤想要的语音。如包括“Take a note”或“Set an alarm”。

应用程序提供(App-provided)
这些语音动作都是基于应用程序的,声明它们就像声明一个启动图标一样。用户说“开始”来使用语音动作然后被指定的活动就会被开启。

(1) 声明系统提供的语音动作

Android Wear平台提供了几种基于用户动作(如“Take a not”、或“Set an alarm”)的语音意图。这就允许用户说出他们想做的并然系统推敲处最合理的活动来给予启动。

当用户说出语音动作时,应用程序能够过滤掉不匹配的意图并启动一个活动。如果欲开启服务在后台做些什么,以可见线索显示活动并启动活动中的服务。确保在接触可见线索是调用finish()。

如,对于”Take a note“命令,声明意图过滤器来启动名为MyNoteActivity的活动:

  <activity android:name="MyNoteActivity"><intent-filter><action android:name="android.intent.action.SEND" /><category android:name="com.google.android.voicesearch.SELF_NOTE" /></intent-filter></activity>

以下是Wear平台支持的语音意图列表:

名称 短语举例 意图
Call a car/taxi “OK Google, get me a taxi”
”OK Google, call me a car”
动作(Action):com.google.android.gms.actions.RESERVE_TAXI_RESERVATION
Take a note “OK Google, take a note”
”OK Google, note to self”
动作(Action):android.intent.action.SEND
类别(Category):com.google.android.voicesearch.SELF_NOTE
额外(Extras):android.content.Intent.EXTRA_TEXT - 一个拥笔记本身的字符串
Set alarm “OK Google, set an alarm for 8 AM”
”OK Google, wake me up at 6 tomorrow”
动作(Action):android.intent.action.SET_ALARM
额外(Extras):android.provider.AlarmClock.EXTRA_HOUR - 一个关于闹钟的时的整数;andorid.provider.AlarmClock.EXTRA_MINUTES - 一个关于闹钟分的整数(这两个额外的数据是可选的,两个都选或两个都不选都可以)
Set timer “Ok Google, set a timer for 10 minutes” 动作(Action):android.intent.action.SET_TIMER
额外(Extras):android.provider.AlarmClock.EXTRA_LENGTH - 代表计时器长度的整数(范围在1 ~ 86400 - 24小时对应的秒数)
Start stopwatch “Ok Google, start stopwatch” 动作(Action):com.google.android.wearable.action.STOPWATCH
Start/Stop a bike ride “Ok Google, start cycling”
”Ok Google, stop cycling”
动作(Action):vnd.google.fitness.TRACK
Mime类型:vnd.google.fitness.activity/biking
额外(Extras):actionStatus - 开始时值为ActiveActionStatus、结束时值为CompleteActionStatus的字符串。
Start/Stop a run “OK Google, track my run”
”OK Google, start running”
动作(Action):vnd.google.fitness.TRACK
Mime类型:vnd.google.fitness.activity/running
额外(Extras):actionStatus -开始为ActiveActionStatus结束时为CompleteActionStatus的字符串。
Start/Stop a workout “OK Google, start a workout”
”OK google, track my workout”
”OK Google, stop workout”
动作(Action):vnd.google.fitness.TRACK
MIME 类型:vnd.google.fitness.activity/other
额外(Extras):actionStatus - 开始值为ActiveActionStatus结束值为CompletedActionStatus的字符串。
Show heart rate “OK Google, what’s my heart rate?”
”OK Google, what’s my bpm?”
动作(Action):vnd.google.fitness.VIEW
MIME 类型:vnd.google.fitness.data_type/com.google.heart_rate.bmp
Show step count “OK Google, how many steps have i taken?”
”OK Google, what’s my step count?”
动作(Action):vnd.google.fitness.VIEW
MIME 类型:vnd.google.fitness.data_type/com.google.step_count.cumulative

关于注册平台意图和访问其额外信息的文挡,见Common intents。

(2) 声明应用程序提供的语音动作

如果平台没有提供语音意图,那么可以直接用”开启 活动名“的语音动作。

注册”开启“动作跟注册手持设备上的启动图标一样。即在应用程序中用请求动作的语句代替请求启动图标的语句即可。

欲指定在说”启动“之后的文本,为想要启动的活动指定一个label属性。举例,意图过滤器识别”开启 MyRunningApp“语音动作并启动StartRunActivity。

<application><activity android:name="StartRunActivity" android:label="MyRunningApp"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity>
</application>

(3) 获取自由形式的语音输入

除了使用语音动作来启动活动外,也可以调用系统的内建语音识别器活动来获取用户的语音输入。这对获取用户语音输入并处理很有用,如根据用户语音进行查找或发送该语音。

在应用程序中,使用ACTION_RECOGNIZE_SPEECH动作来调用startActivityForResult()。这会启动语音辨别活动,并且可以在onActivityResult()中处理结果。

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 textstartActivityForResult(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);
}

01.03

2.5 包装可穿戴式应用程序

学习如何在一个手持设备应用程序中包装一个可穿戴应用程序。这样,系统会在用户安装来自Google Play存储的相应的手持设备应用程序的同时安装可穿戴应用程序。

当发布给用户时,必须将可穿戴应用程序打包在手持设备应用程序中,因为用户不能在可穿戴设备上直接浏览和安装应用程序。若打包正确,当用户下载手持设备应用程序时,系统会自动的将可穿戴设备应用程序推进到与手持设备配对的可穿戴设备中。

注:当在开发时用调试键(debug key)签署应用时此特性不可用。当在开发时,需要用adb install命令或Android Studio直接安装到可穿戴设备之上。

(1) 用Android Studio打包

欲在Android Studio中打包可穿戴设备应用程序:

  1. 在手持设备应用程序的清单文件中包含可穿戴应用程序清单文件中的所有权限。例如,若为可穿戴应用程序指定了VIBRATE权限,必须也要将此权限添加到手持设备应用程序中。
  2. 确保手持设备应用程序和可穿戴应用程序模块拥有相同的包名和版本号。
  3. 在手持设备应用程序的build.gradle文件中声明指向可穿戴设备应用程序的Gradle依赖:
dependencies {compile 'com.google.android.gms:play-services:5.0.+@aar'compile 'com.android.support:support-v4:20.0.+''wearApp project(':wearable')
}
  1. 点击Build > Generate Signed APK…并根据在屏幕上的指示指定发布密钥库并签署应用程序。Android Studio会自动导出嵌入可穿戴应用程序的手持设备应用程序到工程的根目录下。

可选的,可以通过使用Gradle wrapper的命令行来进行应用程序(两者)签署。两个应用程序都必须被签署为能够自动让可穿戴设备应用程序自动工作的模式。

存储密钥文件并在环境变量中认证,最后向如下命令运行Gradle wrapper:

./gradlew assembleRelease \-Pandroid.injected.signing.store.file=$KEYFILE \-Pandroid.injected.signing.store.password=$STORE_PASSWORD \-Pandroid.injected.signing.key.alias=$KEY_ALIAS \-Pandroid.injected.signing.key.password=$KEY_PASSWORD

分别注册可穿戴设备和手持设备应用程序
如果构建可穿戴设备应用的过程需要与手持设备应用程序分开注册,可以在手持设备应用程序模块的build.gradle文件中声明以下Gradle规则以嵌入之前的可穿戴应用程序:

dependencies {...wearApp files('/path/to/wearable_app.apk')
}

然后就可以以自己喜欢的任何方式签署手持设备应用程序了(用Android Studio Build > Generate Signed APK…菜单条目或用Gradle的signingConfig规则)。

(2) 手动打包

若使用其它的IDE或其它的构建方法还可以手动将可穿戴设备应用程序打包到手持设备应用程序中。

  1. 在手持设备应用程序的清单文件中包含可穿戴应用程序清单文件中的所有权限。如,若在可穿戴设备应用程序中包含了VIBRATE权限,那么也必须将此权限添加到移动应用程序的权限中。
  2. 确保可穿戴应用程序和移动APKs拥有相同的报名和版本号。
  3. 复制经签署的可穿戴应用程序到手持设备应用工程下的res/raw目录下。我们将此APK关联为wearable_app.apk。
  4. 创建包含可穿戴应用程序版本号和路径信息的res/xml/ewarable_app.xml文件。示例如下:
<wearableApp package="wearable.app.package.name"><versionCode>1</versionCode><versionName>1.0</versionName><rawPathResId>wearable_app</rawPathResId>
</wearableApp>

在可穿戴应用程序的AndroidManifest.xml文件中,package、versionCode以及versionName拥有相同的值。rawPathResId是一个APK资源的静态变量名。如,对于wearable_app.apk,静态变量名为wearable_app。
5. 添加meta-data标签到手持设备应用程序的application标签下以关联wearable_app_desc.xml文件。

 <meta-data android:name="com.google.android.wearable.beta.app"android:resource="@xml/wearable_app_desc"/>
  1. 构建并签署手持设备应用程序。

(3) 关闭Asset Compression

许多构建工具会自动压缩文件并添加到Android应用程序的res/raw目录下。因为可穿戴APK已经被打包过了(zipped),所以,若构建工具再打包可穿戴设备APK一次将会导致可穿戴应用程序安装器不能识别可穿戴应用程序。

当此发生时,安装就会失败。在手持设备应用程序中,PackageUpdateService日志会输出错误:“打不开此文件描述符对应的文件;它已经被正确打包”。

Android Studio不会自动压缩APK,但若使用其它的构建工具,确保没有对可穿戴应用程序进行压缩。

2.6 蓝牙调试

学习如何通过蓝牙来调试可穿戴应用程序(而不是通过USB连接调试)。

通过将调试输出发送到与开发计算机相连接的手持设备上可以通过蓝牙来调试可穿戴应用程序。

(1) 设置设备来调试

  1. 开启手持设备的USB调试:

    • 打开设置应用程序并滑到底部。
    • 如果没有开发选择设置,按关于电话(或关于平板),滑到底部,点击构建数7次。
    • 返回并按开发选项。
    • 开启USB调试。
  2. 开启可穿戴设备上的蓝牙调试“
    1. 点击屏幕两次以唤起Wear菜单。
    2. 滑到底部并按设置。
    3. 滑到底部,如果没有开发选项条目,按关于,然后按构建数7次。
    4. 按开发选项条目。
    5. 开启蓝牙调试。

(2) 设置调试会话

  1. 在手持设备上,打开Android Wear相应的应用程序。
  2. 按右上角的菜单并选择设置。
  3. 开启蓝牙调试。可以在选项下看到出现的微小的状态总结
Host: disconnected
Target: connected
  1. 通过USB连接手持设备到电脑并运行:
adb forward tcp:4444 localabstract:/adb-hub
adb connect localhost:4444

注:可以使用可以访问的可用的端口。
在Android Wear相应的手持设备应用程序中,可以看到状态改变为:

Host: connected
Target: connected

(3) 调试应用程序

当运行adb devices命令时可穿戴设备上会出现localhost:444。欲运行任意的adb命令,使用以下格式:

adb -s localhost:4444 <command>

如果没有其它的设备连接到TCP/IP(即模拟器),可以缩短命令为:

adb -e <command>

如下例:

adb -e logcat
adb -e shell
adb -e bugreport

01.04

3. 为Wear设备创建自定义UIs

如何为可穿戴设备应用程序创建自定义的用户界面。

可穿戴应用程序的用户界面与手持设备应用程序的用户界面有着很大的不同。可穿戴应用程序应该遵循Android Wear 设计原则并实现推荐的UI模式,这就能够确保优化的可穿戴应用程序有用一致的用户体验。

此节描述如何为可穿戴设备应用程序和自定义通知(在任何的Android Wear设备上看起来很好看,拥有Ixia用户界面模式)创建自定义用户界面:

  • 卡片
  • 倒计时和确认
  • 2D采摘
  • 选择列表

可穿戴用户界面库是Android SDK中的一部分Google Repository,提供类来帮助实现这些模式并创建能够在圆形或方形的Android Wear设备上运行的布局。

注:推荐使用Android Studio来作为Android Wear应用程序的开发平台,因为它提供了工程设置,库包含以及打包便利工具。此节内容以Android Studio为载体描述相关内容。

3.1 定义布局

学习如何创建看起来很好看且能够运行在方形或圆形的Android Wear设备上的布局。

可穿戴设备使用与手持设备上相同的布局技术,但它的设计也受到特殊的约束。它无端口功能且手持设备应用程序的用户界面需要一个好的用户体验。更多关于如何设计伟大的可穿戴应用程序的信息,见 Android Wear Design Guidelines。

当为Android Wear应用程序创建布局时,需要考虑设备是方形还是圆形的屏幕。任何放置在圆形Android Wear设备平面镜角落的内容可能会被剪切掉,所以为方形屏幕设计的布局在圆形屏幕上就不适用。更多关于此类型问题的例子,见视频 Full Screen Apps for Android Wear。

举例,图1展示了同一个布局在方形和圆形屏幕上的显示:

图1. 为方形屏幕设计的布局不适合圆形屏幕的验证

<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"><TextView        android:id="@+id/text"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/hello_square" />
</LinearLayout>

文本在圆形屏幕上显示不正确。

可穿戴用户界面库提供了两种不同的方法来解决这个问题:

  • 为方形和圆形屏幕分别定义不同的布局。应用程序在运行时检测设备屏幕后再将正确的布局导入屏幕中。
  • 使用包含在库中的同时使用于方形和圆形设备的布局。此布局基于不同形状的屏幕应用不同的窗口插页。

在不同形状屏幕显示不同的布局时可使用第一种方法。在不同形状屏幕显示相似布局时可使用第二种方法。

(1) 添加可穿戴 UI 库

在使用Android Studio的工程向导后,Android Studio将可穿戴UI库包含在wear模块中。欲用此库编译工程,确保Extras > Google Repository包被安卓在Android SDK管理器中以及以下依赖被写在了wear模块的build.gradle文件中了:

dependencies {compile fileTree(dir: 'libs', include: ['*.jar'])compile 'com.google.android.support:wearable:+'compile 'com.google.android.gms:play-services-wearable:+'
}

com.google.android.support:wearable依赖是实现后续小节描述的布局所需要的依赖。

浏览API reference documentation获取更多可穿戴用户库的类。

(2) 分别为方形和圆形屏幕指定不同的布局

在可穿戴UI库中的WatchViewStub类允许为方形或圆形的屏幕指定不同的布局定义。此类在运行时检测屏幕形状并将对应的布局导入屏幕中。

欲在应用程序中用此类来处理不同形状的屏幕:

  1. 添加WatchViewStub作为活动布局的主元素。
  2. 为方形屏幕的rectLayout属性指定一个布局定义文件。
  3. 为圆形屏幕的roundLayout属性指定布一个局定义文件。

活动的布局定义示例如下:

<android.support.wearable.view.WatchViewStubxmlns: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/watch_view_stub"android:layout_width="match_parent"android:layout_height="match_parent"app:rectLayout="@layout/rect_activity_wear"app:roundLayout="@layout/round_activity_wear">
</android.support.wearable.view.WatchViewStub>

将此布局填充到活动中:

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

然后为方形和圆形屏幕创建不同的布局定义文件。在此例中,需要创建res/layout/rect_activity_wear.xml和res/layout/round_activity_wear.xml文件。创建这些布局文件跟创建手持设备应用程序的方法一样,但要考虑可穿戴应用程序的约束。系统将会根据屏幕形状将正确的布局导入。

访问布局视图
为方形或圆形屏幕指定的布局在WatchViewStub检测到屏幕的形状之前不会被导入,所以应用程序不会立即访问这些视图。欲访问这些视图,在当形状指定布局被导入时会被通知的活动中设置一个监听器:

@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_wear);WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {@Override public void onLayoutInflated(WatchViewStub stub) {// Now you can access your viewsTextView tv = (TextView) stub.findViewById(R.id.text);...}});
}

(3) 使用自动感知形状布局

在可穿戴UI库中的扩展于FrameLayout的BoxInsetLayout类可以定义一个布局文件就能够运行于方形或圆形的屏幕上。此类根据屏幕的形状应用于所需的窗口插页中且允许视图居中对齐或靠某边缘对齐。


图2. 在圆形屏幕上的窗口插页

图2中的灰色方框展示了BoxInsetLayout在应用所需的窗口插页后在圆形屏幕上自动放置其子视图的区域。欲在此区域内展示,子视图需要用以下这些值来指定layout_box属性:

  • top, bottom, left以及right的组合。举例,”left | top”将子视图置于图2中灰色区域的靠左且顶部位置。
  • all值将所有的子视图内容置入图2中的灰色区域中。

在方形屏幕上,窗口插页为0且layout_box属性会忽略。

图3. 能够同时运行在方形和圆形屏幕的布局定义

在图3中的布局使用了BoxInsetLayout元素以让该布局文件能够运行于方形和圆形的屏幕之上:

<android.support.wearable.view.BoxInsetLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:background="@drawable/robot_background"android:layout_height="match_parent"android:layout_width="match_parent"android:padding="15dp"><FrameLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:padding="5dp"app:layout_box="all"><TextViewandroid:gravity="center"android:layout_height="wrap_content"android:layout_width="match_parent"android:text="@string/sometext"android:textColor="@color/black" /><ImageButtonandroid:background="@null"android:layout_gravity="bottom|left"android:layout_height="50dp"android:layout_width="50dp"android:src="@drawable/ok" /><ImageButtonandroid:background="@null"android:layout_gravity="bottom|right"android:layout_height="50dp"android:layout_width="50dp"android:src="@drawable/cancel" /></FrameLayout>
</android.support.wearable.view.BoxInsetLayout>

注意布局文件中被加粗的部分:

  • android:padding=”15dp” - 此行为BoxInsetLayout元素分配填充空间。因为在圆形设备上窗体插页比15dp要大,所以此填充空间值应用于方形屏幕。
  • android:padding=”15dp” - 此行为内部的FrameLayout元素分配填充空间。此填充空间能够运行在方形和圆形的屏幕上。在按钮和窗口插页的总的填充空间(方形屏幕为20dp,圆形屏幕为5dp)。
  • app:layout_box=”all” - 此行确保FrameLayout元素及其子元素都被包含在了在圆形屏幕上定义的窗口插页的区域中。此行对方形设备没有影响。

01.05

3.2 创建卡片

学习如何用自定义布局创建卡片。

卡片能在不同的应用程序中给用户连续的外观和感觉。此节描述如何在Android Wear 应用程序中创建卡片。

可穿戴UI库中专门为可穿戴设备提供了实现特殊UI的类。此库包含CardFrame类,它以白色为背景、圆形角落以及灯光降落阴影中的卡片风格框架中包含了视图。一个CardFrame实例只能包含一个直接子视图,通常是一个布局管理器,它可以添加其它视图来自定义卡片中的视图内容。

可以用两种方式添加卡片到应用程序中:

  • 使用或扩展CardFragment类。
  • 在布局中的CardScrollView实例中添加卡片。

注:此节展示如何添加卡片到Android Wear活动中。在可穿戴设备上的Android通知也被展示在卡片上。更多信息见 Adding Wearable Features to Notifications。

(1) 创建卡片碎片(Fragment)

CardFragment类提供一个含有标题、描述以及图标的默认卡片。若图1中展示的卡片布局满足需要就使用此方法添加卡片到应用程序中。

图1. 默认的CardFragment布局

欲添加一个CardFragment实例到应用程序中:

  1. 在布局中,为包含卡片的元素分配一个ID。
  2. 在活动中创建一个CardFragment实例。
  3. 使用碎片管理器添加CardFragment实例到其容器中。

以下样例代码实现图1中展示的功能:

<android.support.wearable.view.BoxInsetLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/robot_background"
android:layout_height="match_parent"
android:layout_width="match_parent"><FrameLayoutandroid:id="@+id/frame_layout"android:layout_width="match_parent"android:layout_height="match_parent"app:layout_box="bottom"></FrameLayout>
</android.support.wearable.view.BoxInsetLayout>

以下代码添加CardFragment实例到图1所示的活动中:

protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_wear_activity2);FragmentManager fragmentManager = getFragmentManager();FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();CardFragment cardFragment = CardFragment.create(getString(R.string.cftitle),getString(R.string.cfdesc),R.drawable.p);fragmentTransaction.add(R.id.frame_layout, cardFragment);fragmentTransaction.commit();
}

欲使用CardFragment类来创建拥有自定义布局的卡片,扩展此类并重写onCreateContentView方法。

(2) 添加卡片到布局中

也可以直接添加卡片到布局定义中,效果如图2所示。当想在布局定义文件内的卡片自定义布局时使用该方法。

图2. 添加CardFrame到布局中

以下布局代码样例演示了用两个元素来进行垂直线性布局的个过程。可以创建更复杂的布局来满足应用程序的需要。

<android.support.wearable.view.BoxInsetLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/robot_background"
android:layout_height="match_parent"
android:layout_width="match_parent"><android.support.wearable.view.CardScrollViewandroid:id="@+id/card_scroll_view"android:layout_height="match_parent"android:layout_width="match_parent"app:layout_box="bottom"><android.support.wearable.view.CardFrameandroid:layout_height="wrap_content"android:layout_width="fill_parent"><LinearLayoutandroid:layout_height="wrap_content"android:layout_width="match_parent"android:orientation="vertical"android:paddingLeft="5dp"><TextViewandroid:fontFamily="sans-serif-light"android:layout_height="wrap_content"android:layout_width="match_parent"android:text="@string/custom_card"android:textColor="@color/black"android:textSize="20sp"/><TextViewandroid:fontFamily="sans-serif-light"android:layout_height="wrap_content"android:layout_width="match_parent"android:text="@string/description"android:textColor="@color/black"android:textSize="14sp"/></LinearLayout></android.support.wearable.view.CardFrame></android.support.wearable.view.CardScrollView>
</android.support.wearable.view.BoxInsetLayout>

CardScrollView元素检测屏幕的形状并在圆形或方形的屏幕上展示不同的卡片,在圆形屏幕上使用更宽边缘的卡片。然而,将CardScrollView元素放置到BoxInsetLayout中并使用layout_box=”bottom”属性对圆形屏幕的卡片对齐底部有用(不会截弃卡片的任何内容)。

01.06

3.3 创建列表

学习如何创建为可穿戴设备优化的列表。

在可穿戴设备上的列表允许用户从一套选择中简单的选择一个条目。此节描述如何在Android Wear 应用程序中创建列表。

可穿戴UI库包含WearableListView类,该类是为可穿戴设备优化的用来实现列表的类。

欲在Android Wear应用程序中创建列表:

  1. 添加WearableListView元素到活动的布局定义中。
  2. 为列表条目创建自定义布局实现。
  3. 使用该实现为列表条目创建布局定义文件。
  4. 创建适配器填充列表。
  5. 分配适配器到WearableListView元素。

    图1. 在android Wear上的列表视图

这些步骤将在后续板块中详细描述。

(1) 添加列表视图

以下布局使用BoxInsetLayout元素添加了列表视图到活动中,所以该列表在圆形或方形设备上都能够正确的被展示。

<android.support.wearable.view.BoxInsetLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:background="@drawable/robot_background"android:layout_height="match_parent"android:layout_width="match_parent"><FrameLayoutandroid:id="@+id/frame_layout"android:layout_height="match_parent"android:layout_width="match_parent"app:layout_box="left|bottom|right"><android.support.wearable.view.WearableListViewandroid:id="@+id/wearable_list"android:layout_height="match_parent"android:layout_width="match_parent"></android.support.wearable.view.WearableListView></FrameLayout>
</android.support.wearable.view.BoxInsetLayout>

(2) 为列表条目创建布局实现

在许多情况下,每个列表条目由一个图标和一个描述组成。Notifications例中实现了继承于LinearLayout包含这两个元素在每个列表条目的自定义布局。此布局也实现了WearableListView.OnCenterProximityListener接口中的方法以在用户滑过列表时改变条目图标的颜色和响应WearableListView元素事件时消退文字。

public class WearableListItemLayout extends LinearLayoutimplements WearableListView.OnCenterProximityListener {private ImageView mCircle;private TextView mName;private final float mFadedTextAlpha;private final int mFadedCircleColor;private final int mChosenCircleColor;public WearableListItemLayout(Context context) {this(context, null);}public WearableListItemLayout(Context context, AttributeSet attrs) {this(context, attrs, 0);}public WearableListItemLayout(Context context, AttributeSet attrs,int defStyle) {super(context, attrs, defStyle);mFadedTextAlpha = getResources().getInteger(R.integer.action_text_faded_alpha) / 100f;mFadedCircleColor = getResources().getColor(R.color.grey);mChosenCircleColor = getResources().getColor(R.color.blue);}// Get references to the icon and text in the item layout definition@Overrideprotected void onFinishInflate() {super.onFinishInflate();// These are defined in the layout file for list items// (see next section)mCircle = (ImageView) findViewById(R.id.circle);mName = (TextView) findViewById(R.id.name);}@Overridepublic void onCenterPosition(boolean animate) {mName.setAlpha(1f);((GradientDrawable) mCircle.getDrawable()).setColor(mChosenCircleColor);}@Overridepublic void onNonCenterPosition(boolean animate) {((GradientDrawable) mCircle.getDrawable()).setColor(mFadedCircleColor);mName.setAlpha(mFadedTextAlpha);}
}

也可以创建动画对象来扩大列表中间条目的图标。可以使用WearableListView.OnCenterProximityLisstener接口中的onCenterPosition()和onNonCenterPosition()方法来管理动画。更多关于动画的信息见Animating with ObjectAnimator。

(3) 为条目创建布局定义

为列表条目实现自定义布局后,应该为列表条目内中的每个组件提供一个指定布局参数布局定义文件。以下布局定义使用来自前一节的自定义布局实现并定义一个图标和文本视图(它的IDs匹配在布局实现中的类):
res/layout/list_item.xml

<com.example.android.support.wearable.notifications.WearableListItemLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:gravity="center_vertical"android:layout_width="match_parent"android:layout_height="80dp"><ImageViewandroid:id="@+id/circle"android:layout_height="20dp"android:layout_margin="16dp"android:layout_width="20dp"android:src="@drawable/wl_circle"/><TextViewandroid:id="@+id/name"android:gravity="center_vertical|left"android:layout_width="wrap_content"android:layout_marginRight="16dp"android:layout_height="match_parent"android:fontFamily="sans-serif-condensed-light"android:lineSpacingExtra="-4sp"android:textColor="@color/text_color"android:textSize="16sp"/>
</com.example.android.support.wearable.notifications.WearableListItemLayout>

(4) 创建适配器填充列表

适配器用其内容填充WearableListView.OnCenterProximityListener元素。以下的简单的适配器用基于字符串数组的元素填充列表。

private static final class Adapter extends WearableListView.Adapter {private String[] mDataset;private final Context mContext;private final LayoutInflater mInflater;// Provide a suitable constructor (depends on the kind of dataset)public Adapter(Context context, String[] dataset) {mContext = context;mInflater = LayoutInflater.from(context);mDataset = dataset;}// Provide a reference to the type of views you're usingpublic static class ItemViewHolder extends WearableListView.ViewHolder {private TextView textView;public ItemViewHolder(View itemView) {super(itemView);// find the text view within the custom item's layouttextView = (TextView) itemView.findViewById(R.id.name);}}// Create new views for list items// (invoked by the WearableListView's layout manager)@Overridepublic WearableListView.ViewHolder onCreateViewHolder(ViewGroup parent,int viewType) {// Inflate our custom layout for list itemsreturn new ItemViewHolder(mInflater.inflate(R.layout.list_item, null));}// Replace the contents of a list item// Instead of creating new views, the list tries to recycle existing ones// (invoked by the WearableListView's layout manager)@Overridepublic void onBindViewHolder(WearableListView.ViewHolder holder,int position) {// retrieve the text viewItemViewHolder itemHolder = (ItemViewHolder) holder;TextView view = itemHolder.textView;// replace text contentsview.setText(mDataset[position]);// replace list item's metadataholder.itemView.setTag(position);}// Return the size of your dataset// (invoked by the WearableListView's layout manager)@Overridepublic int getItemCount() {return mDataset.length;}
}

(5) 关联适配器并设置点击监听器

在活动中,从布局中获取WearableListView.OnCenterProximitListener元素的引用,分配一个适配器实例填充列表,并设置一个点击监听器来完成动作(当用户选择一个列表条目时)。

public class WearActivity extends Activityimplements WearableListView.ClickListener {// Sample dataset for the listString[] elements = { "List Item 1", "List Item 2", ... };@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.my_list_activity);// Get the list component from the layout of the activityWearableListView listView =(WearableListView) findViewById(R.id.wearable_list);// Assign an adapter to the listlistView.setAdapter(new Adapter(this, elements));// Set a click listenerlistView.setClickListener(this);}// WearableListView click listener@Overridepublic void onClick(WearableListView.ViewHolder v) {Integer tag = (Integer) v.itemView.getTag();// use this data to complete some action ...}@Overridepublic void onTopEmptyRegionClick() {}
}

3.4 创建2D采集(2D Picker)

学习如何实现2D采集用户模式以页导航数据。

Android Wear中的2D 采集模式允许用户导航和选择一套条目时以页的方式进行。使用可穿戴UI库中的页网格可以很简单的实现此模式,页网格是一个允许用户垂直滑动和水平浏览页数据的布局管理器。

欲实现该模式,添加GridViewPager元素到活动的布局中并实现一个通过扩展FragmentGridPagerAdapter类提供一套页的适配器。

(1) 添加页网格

按照如下方式添加GridViewPager元素到布局定义中:

<android.support.wearable.view.GridViewPagerxmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/pager"android:layout_width="match_parent"android:layout_height="match_parent" />

可以使用在”定义布局“中的任何技术来确保2D采集可以在方形或圆形的屏幕上运行。

(2) 实现页适配器

页适配器提供一套页填充GridViewPager组件。欲实现该适配器,扩展可穿戴UI库中的FragmentGridPagerAdapter类。

以下代码片段展示如何提供一套静态的含自定义背景图片的卡片:

public class SampleGridPagerAdapter extends FragmentGridPagerAdapter {private final Context mContext;private List mRows;public SampleGridPagerAdapter(Context ctx, FragmentManager fm) {super(fm);mContext = ctx;}static final int[] BG_IMAGES = new int[] {R.drawable.debug_background_1, ...R.drawable.debug_background_5};// A simple container for static data in each pageprivate static class Page {// static resourcesint titleRes;int textRes;int iconRes;...}// Create a static set of pages in a 2D arrayprivate final Page[][] PAGES = { ... };// Override methods in FragmentGridPagerAdapter...
}

适配器调用getFragment()和getBackgroundForRow()检索内容并展示每一行:

// Obtain the UI fragment at the specified position
@Override
public Fragment getFragment(int row, int col) {Page page = PAGES[row][col];String title =page.titleRes != 0 ? mContext.getString(page.titleRes) : null;String text =page.textRes != 0 ? mContext.getString(page.textRes) : null;CardFragment fragment = CardFragment.create(title, text, page.iconRes);// Advanced settings (card gravity, card expansion/scrolling)fragment.setCardGravity(page.cardGravity);fragment.setExpansionEnabled(page.expansionEnabled);fragment.setExpansionDirection(page.expansionDirection);fragment.setExpansionFactor(page.expansionFactor);return fragment;
}// Obtain the background image for the row
@Override
public Drawable getBackgroundForRow(int row) {return mContext.getResources().getDrawable((BG_IMAGES[row % BG_IMAGES.length]), null);
}

以下样例展示如何检索背景并展示在网格中指定的页:

// Obtain the background image for the specific page
@Override
public Drawable getBackgroundForPage(int row, int column) {if( row == 2 && column == 1) {// Place image at specified positionreturn mContext.getResources().getDrawable(R.drawable.bugdroid_large, null);} else {// Default to background image for rowreturn GridPagerAdapter.BACKGROUND_NONE;}
}

getRowCount()方法告知适配器内容有多少可用的行,getColumnCount()方法告知适配器内容每一行中有多少可用的列:

// Obtain the number of pages (vertical)
@Override
public int getRowCount() {return PAGES.length;
}// Obtain the number of pages (horizontal)
@Override
public int getColumnCount(int rowNum) {return PAGES[rowNum].length;
}

适配器根据指定的一套页实现细节。由适配器提供的每一页时Fragment类型。在此例中,每一页时使用的其中一种卡片布局的CardFragment实例。然而,可以在相同的3D采集中结合不同类型的页,如卡片、动作图标以及根据使用情况自定义的布局。

不是所有的行都需要相同数量的页。注意在此例中,不同的行的列的数目。也可以使用GridViewPager组件只用一行或只一列实现一个1D采集。

GridViewPager类提供对卡片内容超过屏幕的滚动。此例按照需要扩展了每一个卡片,所以用户可以通过滚动查看卡片的内容。当用户到达卡片的末端时,滑动的相同方向显示了下一页是否可用。

可以为每个含getBackgroundForPage()方法的页指定自定义背景。当用户通过页来导航时,GridViewPager类在背景间自动应用视觉差和淡入淡出的效果。

图1. GridViewPager样例

为页网格分配适配器实例
在活动中,给GridViewPager组件分配适配器实例实现:

public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);...final GridViewPager pager = (GridViewPager) findViewById(R.id.pager);pager.setAdapter(new SampleGridPagerAdapter(this, getFragmentManager()));}
}

01.07

3.5 显示确认

学习当用户完成动作时如何展示确认动画。

在Android Wear应用程序中的Confirmations使用整个屏幕或比在手持设备应用程序中使用更大比例的屏幕。这就能够确保用户能够更准确的看到这些确认(瞟一眼就基本可以看到)且拥有更大的触碰空间来取消动作。

可穿戴UI库能够在Android Wear应用程序中显示确认动画和计时器:
确认计时器 - 自动确认计时器为用户显示一个动画计时器(当计时超时后自动帮用户取消某操作)
确认动画 - 在用户完成一个动作时确认动画给用户一个视觉回馈。

以下小版块将描述如何实现这些模式。

(1) 使用自动确认计时器

自动确认计时器帮助用户自动取消某操作。当用户执行某操作时,应用程序展会一个含动画计时的按钮来取消该操作。在计时超时前用户可以取消该操作。当用户取消操作或计时器超时后应用程序会收到通知。

图1. 确认计时器

欲在用户完成某操作后在应用程序中显示一个确认计时器:

  1. 添加DelayedConfirmationView元素到布局中。
  2. 在活动中实现DelayedConfirmationListener接口。
  3. 设置计时器超时的时间段并在用户完成动作后开始计时。

按照以下方式将DelayedConfirmationView元素添加到布局中:

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

可以用android:src属性为圆圈内部的展示分配一个可拖拽资源并直接在布局定义中配置圆圈的参数。

在计时完成或用户点击按钮后收到通知,在活动中实现相应监听器方法:

public class WearActivity extends Activity implementsDelayedConfirmationView.DelayedConfirmationListener {private DelayedConfirmationView mDelayedView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_wear_activity);mDelayedView =(DelayedConfirmationView) findViewById(R.id.delayed_confirm);mDelayedView.setListener(this);}@Overridepublic void onTimerFinished(View view) {// User didn't cancel, perform the action}@Overridepublic void onTimerSelected(View view) {// User canceled, abort the action}
}

欲开始计时器,将以下代码片段添加到活动中用户选择动作的地方:

// Two seconds to cancel the action
mDelayedView.setTotalTimeMs(2000);
// Start the timer
mDelayedView.start();

(2) 展示确认动画

欲在用户完成应用程序中的动作后展示确认动画,在其中一个活动中创建一个意图来启动ConfirmationActivity。可以为其中一个动画的EXTRA_ANIMATION_TYPE意图额外数据:

  • SUCCESS_ANIMATION
  • FAILURE_ANIMATION
  • OPEN_ON_PHONE_ANIMATION

也可以添加出现在确认图标下的信息。

图2. 确认动画

欲在应用程序中使用ConfirmationActivity,首先在将该活动生命在清单文件中:

<manifest><application>...<activityandroid:name="android.support.wearable.activity.ConfirmationActivity"></activity></application>
</manifest>

然后判断用户的动作的结果并用意图启动该活动:

Intent intent = new Intent(this, ConfirmationActivity.class);
intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE,ConfirmationActivity.SUCCESS_ANIMATION);
intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,getString(R.string.msg_sent));
startActivity(intent);

在显示确认动画后,ConfirmationActivity结束且活动恢复。

3.6 退出全屏活动

学习如何实现long-press-to-dismiss用户界面模式退出全屏活动。

默认下,用户通过从左向右的滑动退出Android Wear活动。如果应用程序包含水平的滚动内容,用户需要首先滑到内容的边缘并再从左向右滑动一次时退出应用程序。

更多关于身临其境的体验,如能够向任何方向滚动的的应用程序,可以关闭通过滑动手势退出应用程序的方法。然而,如果不关闭它,必须用可穿戴库中的DismissOverlayView类来实现long-press-to-dismiss用户界面模式来让用户退出应用程序。同时也必须在用户第一次使用该应用程序时通知它们长按可退出应用程序。

关于退出Android Wear活动的设计手册,见 Breaking out of the card。

(1) 关闭滑动退出手势

若应用程序的交互模式干扰滑动退出手势(Swipe-To-Dismiss),可以在应用程序中关闭它。欲在应用程序中关闭该功能,扩展默认的主题并设置android:windowSwipeToDismiss属性为false:

<style name="AppTheme" parent="Theme.DeviceDefault"><item name="android:windowSwipeToDismiss">false</item>
</style>

如果关闭了该手势,必须实现长按退出UI模式来让用户退出应用程序,这将在下一小节中进行描述。

(2) 实现长按退出模式

欲在活动中使用DismissOverlayView类,添加该元素到布局定义中,它将覆盖整个屏幕并被置放在所有视图的上面。

以下例子展示如何添加DismissOverlayView元素:

<FrameLayout    xmlns:android="http://schemas.android.com/apk/res/android"android:layout_height="match_parent"android:layout_width="match_parent"><!-- other views go here --><android.support.wearable.view.DismissOverlayView        android:id="@+id/dismiss_overlay"android:layout_height="match_parent"android:layout_width="match_parent"/>
<FrameLayout>

在活动中,获取DismissOverlayView元素并设置一些介绍性的文字。此文字在用户第一次使用该应用程序时被展示给用户以提示它们可以通过长按操作退出应用程序。然后使用GestureDetector来检测长按操作:

public class WearActivity extends Activity {private DismissOverlayView mDismissOverlay;private GestureDetector mDetector;public void onCreate(Bundle savedState) {super.onCreate(savedState);setContentView(R.layout.wear_activity);// Obtain the DismissOverlayView elementmDismissOverlay = (DismissOverlayView) findViewById(R.id.dismiss_overlay);mDismissOverlay.setIntroText(R.string.long_press_intro);mDismissOverlay.showIntroIfNecessary();// Configure a gesture detectormDetector = new GestureDetector(this, new SimpleOnGestureListener() {public void onLongPress(MotionEvent ev) {mDismissOverlay.show();}});}// Capture long presses@Overridepublic boolean onTouchEvent(MotionEvent ev) {return mDetector.onTouchEvent(ev) || super.onTouchEvent(ev);}
}

当系统检测到长按手势时,DismissOverlayView元素将显示一个退出按钮,若用户点击该按钮就会随之终止活动。

01.08

4. 发送或同步数据

如何在手持设备和可穿戴设备之间同步数据。

可穿戴数据层API时Google Play服务的一部分,它为手持和可穿戴应用程序之间提供了通讯通道。API由系统可发送、有线可同步以及监听数据层重要的事件并通知应用程序的数据对象组成:
数据项
DataItem在手持和可穿戴设备之间以自动同步的方式的方式提供数据存储。

信息
MessageApi类能够发送信息且利于远程调用(RPC),如在可穿戴设备上收集手持设备的媒体播放数据或在手持设备上开启可穿戴设备之上的意图。信息对单向请求或请求/响应通信模式同样很适用。若手持和可穿戴设备相连,系统将信息依次传送并返回成功结果码。如果设备间没有连接,错误码将被返回。成功的结果码并不意味着信息传递成功,因为在接收到结果码后设备可能会断开连接。

资产(Asset)
Asset对象用来发送二进制数据,如图片。将assets附在数据项中后,系统会自动传输这些数据,通过缓存大的assets保存蓝牙带宽以避免重新传输。

WearableListenerService(for service)
扩展WearableListenerService能够监听服务中的重要的数据层事件。系统管理WearableListenerService的生命周期,当它需要发送数据项或信息时就将其绑定到服务,当无数据发送时就解除该绑定。

DataListener(前台活动)
当活动运行于前台时在活动中实现DataListener能够监听重要的数据层事件。使用该类替换WearableListenerService能够只在用户使用该应用程序时监听变化。

通道
可以使用ChannelApi类类传输大的数据项,如音乐或电影文件,从手持设备传送到可穿戴设备中。数据传输的通道API有以下优点:

  • 在两个或多个连接的设备间传送大数据文件时,当使用Asset对象附加到DataItem对象时无提供的自动同步。通道API不像DataApi类那样保存磁盘空间,它会在同步连接设备之间在本地设备上创建一份assets的拷贝。
  • 可以使用MessageApi类来发送一个在尺寸上很大的文件。
  • 传输流数据,如从网络服务拉进来的音乐或从耳机来的语音数据。

警告:因为这些APIs是为手持设备和可穿戴设备的通信而设计的,所以它们只能被用于设置设备间的通信。例,不要尝试打开底层的套接字来创建通信通道。

Android Wear支持多个可穿戴设备欲手持设备相连。如当用户保存笔记到手持设备上,它会自动出现在用户的可穿戴设备中。欲同步设备间的数据,Google 的服务用设备网络的云节点实现。系统同步数据到直接相连的设备、云节点以及通过Wi-Fi连接到云节点的可穿戴设备中。

图1. 手持设备和可穿戴设备网络节点示例

4.1 访问可穿戴设备数据层

此节描述如何创建客户端访问数据层APIs。

欲调用数据层API,创建GoogleApiClient类的实例,它是Google Play服务APIs的主入口点。

GoogleApiClient提供能够让其轻松创建客户端实例的构建器。一个小型的GoogApiClent如下代码所示(注:现在,此小型的客户端足够作为开始。然而,见Accessing Google Play service APIs获取更多关于创建GoogleApiClient、实现其回调函数以及处理错误情况的信息):

GoogleApiClient mGoogleApiClient = new GoogleApiClient.Builder(this).addConnectionCallbacks(new ConnectionCallbacks() {@Overridepublic void onConnected(Bundle connectionHint) {Log.d(TAG, "onConnected: " + connectionHint);// Now you can use the Data Layer API}@Overridepublic void onConnectionSuspended(int cause) {Log.d(TAG, "onConnectionSuspended: " + cause);}}).addOnConnectionFailedListener(new OnConnectionFailedListener() {@Overridepublic void onConnectionFailed(ConnectionResult result) {Log.d(TAG, "onConnectionFailed: " + result);}})// Request access only to the Wearable API.addApi(Wearable.API).build();

重要:若添加多个APIs到GoogleApiClient,可以在没有安装Android Wear应用程序的设备上运行客户端连接错误。欲避免连接错误,调用addApiIfAvailable()方法并传递到Wearable API来表明客户端应该优雅的处理丢失API。更多信息见Access the Wearable API.

使用数据层API之前,在客户端中通过调用connect()方法开启连接,正如Start a Connection一节中所描述的那样。当系统调用为客户端调用onConnected()回调方法时,准备使用数据层API。

4.2 同步数据项

数据项是被存储在一个可被复制数据存储会在手持设备和可穿戴设备间同步的对象。

DataItem定义系统用来同步手持设备和可穿戴设备数据的数据接口。一个DataItem通常由以下项组成:

  • Payload - 一个字节数组,可以用任何数据设置它,允许做序列化或非序列化的对象。有效负荷的尺寸被限制成了100KB。
  • Path - 一个唯一的字符串,必须以前斜线开始(如”/path/to/data”)。

通常不需要直接实现DataItem。相反,您需要:

  1. 创建putDataRequest对象,指定字符串路径来唯一表示某项。
  2. 调用setData()设置有效负载。
  3. 调用DataApi.putDataItem()请求系统创建数据项。
  4. 当请求数据项时,系统返回适合实现DataItem接口的对象。

    然而,一般不使用setData()来使用原始的字节,我们推荐使用data map,它以简单的使用方式暴漏数据项如Bundle接口。

(1) 用Data Map同步数据

当有可能时,使用DataMap类。此方法允许以Android Bundle的格式与数据项工作,所以序列化/非序列化的对象已被完成,可以通过键-值对的方式操作数据。

欲使用数据映射(data map):

  1. 创建putDataMapRequest对象,设置数据项的路径。
    注:路径字符串是标识数据项的唯一标识符,通过它能在相连的某个设备中访问它。路径必须以前斜线开始。如果在应用程序中使用层次数据,应该创建一个匹配该数据结构的路径方案。
  2. 调用putDataMapRequest.getDataMap()获取可以在其上设置值的数据映射。
  3. 使用诸如putString()的put…()方法为数据映射设置应有的值。
  4. 调用putDataMapRequest.asPutDataRequest()获取PutDataRequest对象。
  5. 调用DataApi.putDataItem()请求系统创建数据项。
    注:若手持设备和可穿戴设备断开了连接,数据被缓存且当重新建立连接时缓存的数据将会继续被同步。

以下样例中的increaseCounter()方法展示如何创建数据映射并往其中放置数据:

public class MainActivity extends Activity implementsDataApi.DataListener,GoogleApiClient.ConnectionCallbacks,GoogleApiClient.OnConnectionFailedListener {private static final String COUNT_KEY = "com.example.key.count";private GoogleApiClient mGoogleApiClient;private int count = 0;...// Create a data map and put data in itprivate void increaseCounter() {PutDataMapRequest putDataMapReq = PutDataMapRequest.create("/count");putDataMapReq.getDataMap().putInt(COUNT_KEY, count++);PutDataRequest putDataReq = putDataMapReq.asPutDataRequest();PendingResult<DataApi.DataItemResult> pendingResult =Wearable.DataApi.putDataItem(mGoogleApiClient, putDataReq);}...
}

更多关于处理PendingResult对象的信息见Wait for the Status of Data Layer Calls。

(2) 监听数据项事件

若数据层连接的其中一边改变了数据项,此时可能需要通知连接的另一边该改变。可以为数据项事件实现一个监听器来实现该功能。

以下代码片段在定义在前一个样例中的计数器有改变时通知应用程序:

public class MainActivity extends Activity implementsDataApi.DataListener,GoogleApiClient.ConnectionCallbacks,GoogleApiClient.OnConnectionFailedListener {private static final String COUNT_KEY = "com.example.key.count";private GoogleApiClient mGoogleApiClient;private int count = 0;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mGoogleApiClient = new GoogleApiClient.Builder(this).addApi(Wearable.API).addConnectionCallbacks(this).addOnConnectionFailedListener(this).build();}@Overrideprotected void onResume() {super.onResume();mGoogleApiClient.connect();}@Overridepublic void onConnected(Bundle bundle) {Wearable.DataApi.addListener(mGoogleApiClient, this);}@Overrideprotected void onPause() {super.onPause();Wearable.DataApi.removeListener(mGoogleApiClient, this);mGoogleApiClient.disconnect();}@Overridepublic void onDataChanged(DataEventBuffer dataEvents) {for (DataEvent event : dataEvents) {if (event.getType() == DataEvent.TYPE_CHANGED) {// DataItem changedDataItem item = event.getDataItem();if (item.getUri().getPath().compareTo("/count") == 0) {DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap();updateCount(dataMap.getInt(COUNT_KEY));}} else if (event.getType() == DataEvent.TYPE_DELETED) {// DataItem deleted}}}// Our method to update the countprivate void updateCount(int c) { ... }...
}

此活动实现了DataItem.DataListener接口。该活动在onConnected()方法中添加自身作为数据项事件的监听器并在onPause()方法中移除监听器。

也可以实现以服务的形式实现监听器。更多信息见Listen for Data Layer Events。

4.3 转移资产(Assets)

Assets是二进制组成的数据,典型的如图片或媒体数据。

欲通过蓝牙发送大量的诸如图片二进制数据,在数据项中附加Asset并将数据项放置到可复制的存储空间中。

Assets自动处理数据缓存来组织重新传输和保存蓝牙带宽。手持设备应用程序下载图片的通常用的模式是将图片缩减到一个合适可穿戴设备展示的尺寸,并以asset的形式传输给可穿戴设备。以下样例将演示该模式。

注:尽管数据项的被限制到了100KB,assets依旧能够满足需要的大小。然而,在许多情况下传输大的assets会影响用户体验,所以当传输大assets时确保程序执行良好。

(1) 传输Asset

在Asset类中使用create…()方法创建asset。这里,我们转换一个位图为字节流然后调用createFromBytes()创建asset。

private static Asset createAssetFromBitmap(Bitmap bitmap) {final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream);return Asset.createFromBytes(byteStream.toByteArray());
}

当创建asset后,用DataMap或PutDataRequest中的putAsset()方法将其添加到数据项中,然后用putDataItem()将数据项添加到数据存储中。

使用PutDataRequest

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);
Asset asset = createAssetFromBitmap(bitmap);
PutDataRequest request = PutDataRequest.create("/image");
request.putAsset("profileImage", asset);
Wearable.DataApi.putDataItem(mGoogleApiClient, request);

使用 PutDataMapRequest

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);
Asset asset = createAssetFromBitmap(bitmap);
PutDataMapRequest dataMap = PutDataMapRequest.create("/image");
dataMap.getDataMap().putAsset("profileImage", asset)
PutDataRequest request = dataMap.asPutDataRequest();
PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi.putDataItem(mGoogleApiClient, request);

(2) 接收Asset

当asset创建后,在连接的另一边可能需要读并提取该asset。以下代码样例演示如何实现回调方法检测asset改变并提取asset:

@Override
public void onDataChanged(DataEventBuffer dataEvents) {for (DataEvent event : dataEvents) {if (event.getType() == DataEvent.TYPE_CHANGED &&event.getDataItem().getUri().getPath().equals("/image")) {DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());Asset profileAsset = dataMapItem.getDataMap().getAsset("profileImage");Bitmap bitmap = loadBitmapFromAsset(profileAsset);// Do something with the bitmap}}
}public Bitmap loadBitmapFromAsset(Asset asset) {if (asset == null) {throw new IllegalArgumentException("Asset must be non-null");}ConnectionResult result =mGoogleApiClient.blockingConnect(TIMEOUT_MS, TimeUnit.MILLISECONDS);if (!result.isSuccess()) {return null;}// convert asset into a file descriptor and block until it's readyInputStream assetInputStream = Wearable.DataApi.getFdForAsset(mGoogleApiClient, asset).await().getInputStream();mGoogleApiClient.disconnect();if (assetInputStream == null) {Log.w(TAG, "Requested an unknown Asset.");return null;}// decode the stream into a bitmapreturn BitmapFactory.decodeStream(assetInputStream);
}

4.4 发送和接收信息

信息是为fire-and-forget信息设计的,这样就可以在可穿戴设备和手持设备之间发送自如。

使用MessageApi并附加以下项到信息后发送信息:

  • 任意的有效负荷(可选的)
  • 唯一标识信息动作的路径
    不像数据项,这里并无手持和可穿戴应用程序之间的同步。单向通信信息的机制对远程调用(ROC)来说比较方便,如发送信息到可穿戴设备上以开启一个活动。

多个可穿戴设备可以连接到用户的手持设备上。在网络中的每个相连的设备被当成一个节点。在有多个设备相连的情况下,必须要考虑哪一个节点接收信息。如,一个语音转录应用程序应该接收可穿戴设备的语音数据,应该发送信息到拥有电量的节点上处理请求,如手持设备。

注:对于早于7.3.0版本的Google Play服务,在同一时间中只能有一个可穿戴设备可以连接到手持设备上。若考虑多个节点相连则应更新已经存在的代码。如果不实现这些改变,信息可能不会被传输到目的设备中。

(1) 发送信息

一个可穿戴应用程序可以给用户提供诸如语音转录的功能。用户可以通过可穿戴设备的麦克风说于可穿戴设备,并将其转录保存为一个笔记。由于可穿戴设备不具有处理语音转录活动的电量和效率,应用程序应该卸载该工作并留给其它连接的设备。

以下小节描述如何广播可以处理活动请求的设备节点,发现能够满足需要的节点并发送信息到这些节点上。

[1] 宣扬(Advertise)能力

欲从可穿戴设备上启动手持设备上的活动,使用MessageApi类发送该请求。由于多个可穿戴设备科连接到手持设备上,可穿戴应用程序需要判断连接的节点具有启动活动的能力。在手持应用程序中,宣扬它所运行的具有该能力的节点。

欲宣扬手持应用程序中能力:

  1. 在工程的res/values/目录下创建一个名为wear.xml的XML配置文件。
  2. 添加名为android_wear_capabilities的资源到wear.xml中。
  3. 定义设备提供的能力。
    注:标识能力的自定义字符串必须在应用程序中唯一。

以下代码展示如何添加名为voice_transcription的能力到wear.xml中:

<resources><string-array name="android_wear_capabilities"><item>voice_transcription</item></string-array>
</resources>

[2] 检索拥有所需性能的节点

首先,需要调用CapabilityApi.getCapability()方法检测具某能力的节点。以下代码样例展示如何手动检索拥有voice_transcription能力的节点:

private static final StringVOICE_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.CapailityListener()实例到GoogleApiClient中。以下代码样例展示如何注册监听器并检索具有voice_transcription能力的节点:

private void setupVoiceTranscription() {...CapabilityApi.CapabilityListener capabilityListener =new CapabilityApi.CapabilityListener() {@Overridepublic void onCapabilityChanged(CapabilityInfo capabilityInfo) {updateTranscriptionCapability(capabilityInfo);}};Wearable.CapabilityApi.addCapabilityListener(mGoogleApiClient,capabilityListener,VOICE_TRANSCRIPTION_CAPABILITY_NAME);
}

注:若创建扩展于WearbleListenerService的服务来检测能力的改变,可能需要重写onConnectedNode()方法监听连接转变的细节,如当可穿戴设备由Wi-Fi转变为蓝牙连接到手持设备时。一个实现的例子,见FindMyPhoe例子中的DisconnectListenerService类。更多关于如何监听重要事件的信息,见Listen for Data Layer Events。

在检测到拥有某能力的节点后,判断将信息发送给哪一个节点。应该选择离自己最近的的节点。欲判断附近的节点,调用Node.isNearby()方法。

以下代码样例展示如何判断最佳的的节点使用:

private String transcriptionNodeId = null;private void updateTranscriptionCapability(CapabilityInfo capabilityInfo) {Set<Node> connectedNodes = capabilityInfo.getNodes();transcriptionNodeId = pickBestNodeId(connectedNodes);
}private String pickBestNodeId(Set<Node> nodes) {String bestNodeId = null;// Find a nearby node or pick one arbitrarilyfor (Node node : nodes) {if (node.isNearby()) {return node.getId();}bestNodeId = node.getId();}return bestNodeId;
}

[3] 传递信息

一旦决定了使用最佳的节点之后,用MessageApi类将发送信息。

以下代码样例展示如何由可穿戴设备发送信息给具有转录功能的节点。在尝试发信息之前确认节点可用。该调用是同步的且是块处理的,直到系统排队数据发送。

注:成功的结果码并不能保证信息传递成功。若应用程序需要数据可用性,使用DataItem对象或ChanneApi类在设备间发送数据。


public static final String VOICE_TRANSCRIPTION_MESSAGE_PATH = "/voice_transcription";private void requestTranscription(byte[] voiceData) {if (transcriptionNodeId != null) {Wearable.MessageApi.sendMessage(googleApiClient, transcriptionNodeId,VOICE_TRANSCRIPTION_MESSAGE_PATH, voiceData).setResultCallback(new ResultCallback() {@Overridepublic void onResult(SendMessageResult sendMessageResult) {if (!sendMessageResult.getStatus().isSuccess()) {// Failed to send message}}});} else {// Unable to retrieve node with transcription capability}
}

注:欲学更多关于到Google Play服务的异步和同步的调用并要使用它们时,见Communicate with Google Play Services。

(2) 接收信息

欲在接收信息后被通知,实现MessageListener接口为信息事件提供监听器。然后,用MessageApi.addListener()方法注册监听器。此样例展示监听器可能的实现来检查VOICE_TRANSCRIPTION_MESSAGE_PATH。若此条件为true,开启活动来处理语音数据。

@Override
public void onMessageReceived(MessageEvent messageEvent) {if (messageEvent.getPath().equals(VOICE_TRANSCRIPTION_MESSAGE_PATH)) {Intent startIntent = new Intent(this, MainActivity.class);startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);startIntent.putExtra("VOICE_DATA", messageEvent.getData());startActivity(startIntent);}
}

此代码片段需要更多的实现细节。学习如何实现一个完整的监听服务获活动见Listening for Data Layer Events。

4.5 处理数据层事件

通知变化或事件到数据层。

当调用数据层API时,可以接收调用完成后的状态也可以用监听器监听调用的变化。

(1) 等待数据层调用的状态

数据层的诸如putDataItem()的APIs有时会返回PendingResult。一旦PendingResult被创建,操作在后台以被排列。如果在此后不做任何处理,操作最终会静静的完成。然而,在接收到此返回且在操作完成前可以做一些操作,所以PendingResult会让等待结果状态,不管是同步还是异步。

[1] 异步调用

如果代码运行在主用户界面线程中,不要在数据层API中做阻碍的调用。可以通过添加回调方法到PendingResult对象中的方式来运行异步调用,当操作完成后该操作会被取消:

pendingResult.setResultCallback(new ResultCallback<DataItemResult>() {@Overridepublic void onResult(final DataItemResult result) {if(result.getStatus().isSuccess()) {Log.d(TAG, "Data item set: " + result.getDataItem().getUri());}}
});

[2] 同步调用

若代码运行在后台服务(在WearableListenerService的情况中)中的某个单独的处理线程中,若不在意阻碍。在这种情况下可以在PendingResult对象中调用await(),在请求完成并返回Result对象之前会一直阻碍:

DataItemResult result = pendingResult.await();
if(result.getStatus().isSuccess()) {Log.d(TAG, "Data item set: " + result.getDataItem().getUri());
}

(2) 监听数据层事件

因为数据层在手持设备和可穿戴设备间同步和发送数据,所以通常需要监听重要的事件,如数据项被创建、信息被收到或可穿戴设备和手持设备连接时。

欲监听数据层事件,有以下两个选择:

  • 创建扩展于WearableListenerService的服务。
  • 创建活动来实现DataApi.DataListener。

[1] 用WearableListenerService

典型地在可穿戴和手持设备应用程序中创建服务实例。若对其中一个应用程序中的数据事件并不感兴趣,然后就不用在这个应用程序中实现该服务。

举例,可以在手持设备应用程序中设置和获取数据项对象并在可穿戴应用程序中监听这些更新来更新其用户界面。可穿戴设备应用程序从不更新任何的数据项,所以手持应用程序不会监听任何来自可穿戴应用程序的数据事件。

可以用WearableListenerService监听以下事件:

  • onDataChanged() - 当数据项对象被创建、改变或被删除时被调用。连接的一个设备发生其中一个事件后将触发两个设备上的该回调方法。
  • onMessageReceived() - 连接的一个设备发送信息时触发另一个设备上的该回调方法。
  • onPeerConnected()和onPeerDisconnected() - 当连接到手持设备或可穿戴设备或断开时该函数被调用。连接状态的改变将会触发连接的所有设备上的该回调方法。

欲创建WearableListenerService:

  1. 创建扩展于WearableListenerService的类。
  2. 监听感兴趣的事件,如onDataChanged()。
  3. 在Android清单文件中声明意图过滤器以通知系统关于WearableListenerService。这允许系统绑定所需的服务。

以下代码展示如何实现一个简单的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";@Overridepublic void onDataChanged(DataEventBuffer dataEvents) {if (Log.isLoggable(TAG, Log.DEBUG)) {Log.d(TAG, "onDataChanged: " + dataEvents);}final List events = FreezableUtils.freezeIterable(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 : events) {Uri uri = event.getDataItem().getUri();// Get the node id from the host value of the URIString nodeId = uri.getHost();// Set the data of the message to be the bytes of the URIbyte[] payload = uri.toString().getBytes();// Send the RPCWearable.MessageApi.sendMessage(googleApiClient, nodeId,DATA_ITEM_RECEIVED_PATH, payload);}}
}

以下代码在Android 清单文件中声明意图过滤器:

<service android:name=".DataLayerListenerService"><intent-filter><action android:name="com.google.android.gms.wearable.BIND_LISTENER" /></intent-filter>
</service>

在数据层回调方法中的权限
欲为应用程序的数据层事件传递回调方法,Google Play服务绑定WearableListenerService,并通过IPC调用回调方法。这将拥有回调方法继承于调用进程的结果。

若尝试在回调方法中执行特殊的操作,安全检查会失败因为回调方法以调用进程标识在运行,而不是标识应用程序的进程。

欲解决该问题,在IPC绑定后调用clearCallingIdentity()重启标识,然后在完成特殊操作时使用restoreCallingIdentity()存储标识:

long token = Binder.clearCallingIdentity();
try {performOperationRequiringPermissions();
} finally {Binder.restoreCallingIdentity(token);
}

[2] 用监听器活动

若应用程序在用户和应用程序交互是指关心数据层事件并不需要长期运行服务来处理每个数据改变,可以通过实现一个或多个以下接口在活动中监听事件:

  • DataApi.DataListener
  • MessageApi.MessageListener
  • NodeApi.NodeListener

欲创建活动来监听数据事件:

  1. 实现所需的接口。
  2. 在onCreate(Bundle)中创建GoogleApiClient实例和数据层API一起工作。
  3. 在onStart()中调用connect()将客户端连接到Google Play服务。
  4. 当与Google Play服务建立连接后,系统会调用onConnected()。此时可以调用DataApi.addListener(), MessageApi.addListener()或者NodeApi.addListener()通知Google Play服务活动对监听数据层事件感兴趣。
  5. 在onStop()中用DataApi.removeListener(), MessageApi.removeListener()或NodeApi.removeListener()注销所有的监听器。
  6. 根据所实现的接口实现onDataChanged(), onMessageReceived(), onPeerConnected()和onPeerDisconnect()。

以下例子实现DataAp.DataListener:

public class MainActivity extends Activity implementsDataApi.DataListener, ConnectionCallbacks, OnConnectionFailedListener {private GoogleApiClient mGoogleApiClient;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);mGoogleApiClient = new GoogleApiClient.Builder(this).addApi(Wearable.API).addConnectionCallbacks(this).addOnConnectionFailedListener(this).build();}@Overrideprotected void onStart() {super.onStart();if (!mResolvingError) {mGoogleApiClient.connect();}}@Overridepublic void onConnected(Bundle connectionHint) {if (Log.isLoggable(TAG, Log.DEBUG)) {Log.d(TAG, "Connected to Google Api Service");}Wearable.DataApi.addListener(mGoogleApiClient, this);}@Overrideprotected void onStop() {if (null != mGoogleApiClient && mGoogleApiClient.isConnected()) {Wearable.DataApi.removeListener(mGoogleApiClient, this);mGoogleApiClient.disconnect();}super.onStop();}@Overridepublic void onDataChanged(DataEventBuffer dataEvents) {for (DataEvent event : dataEvents) {if (event.getType() == DataEvent.TYPE_DELETED) {Log.d(TAG, "DataItem deleted: " + event.getDataItem().getUri());} else if (event.getType() == DataEvent.TYPE_CHANGED) {Log.d(TAG, "DataItem changed: " + event.getDataItem().getUri());}}}
}

01.09

5. 创建Watch faces

如何为可穿戴设备创建表面(Watch faces)。

Android Wear的表面呈现一个动态的数字画面,用颜色、动画以及相关的上下文信息来告知时间。Android Wear companion app提供了不同风格和形状的表面。当用户在可穿戴应用程序或同伴应用程序中选择了一个可用的表面,可穿戴设备预览该表面并允许用户设置配置选择。

Android Wear能够为Wear设备创建自定义表面。当用户安装包含拥有表面的可穿戴应用程序时,它们在可穿戴设备的同伴程序中也可用。

此节描述实现自定义表面和将它们包装在可穿戴应用程序中。此节也描述设计思想和设计实现点以确保设计集成了系统UI元素且是高效率的。

注:推荐使用Android Studio来开发Android Wear,因为它提供了工程设置、库包含以及方便打包。此节内容基于Android Studio。

5.1 设计表面

学习如何设计能运行于任何Android Wear设备的表面。

类似设计传统表面的处理,为Android Wear创建表面时一个清晰的可视化动画。Android Wear设备提供了高级的性能来供表面的设计,诸如丰富的颜色、动态的背景、动画以及数据集成。然而,可能还有其它的设计思想需要考虑。

此小节提供设计表面和开始设计的常见原则的总结。更多信息见Watch Faces for Android Wear设计手册。

(1) 遵循设计原则

当计划了表面的外观以及呈现给用户的内容后,考虑一下设计原则:
为方形和圆形设备考虑
设计应该能同时运行于方形和圆形的Android Wear设备上,包括有insets on the bottom of the screen 的设备。

支持所有的展示模式
设计的表面应该支持有颜色显示的阴影模式和用全色彩和动画的交互模式。

优化特殊屏幕的技术
在阴影模式下,表面应该保持大量像素都为黑。基于屏幕技术,可能需要避免大量的白色像素,只使用黑色或白色并关闭图形保真。

适应系统UI元素
设计应该确保系统显示仍旧可见且当卡片出现在屏幕上时用户仍旧可以看到时间。

集成数据
在同伴移动设备上表面能够支持传感器和手机的连接以显示用户相关的上下文数据,诸如每天的天气或下一个日历事件。

提供配置选择
可以让用户配置一些在可穿戴设备或Android Wear同步应用程序中的设计(如颜色和尺寸)。

更多关于为Android Wear设计表面的信息见Watch Faces for Android Wear设计手册。

(2) 创建实现策略

在完成表面的设计后,需要决定如何在可穿戴设备上获取必要的数据和绘制表面。大多数实现由以下组件组成:

  • 一个或多个背景图片。
  • 检索需要数据的应用程序代码。
  • 在背景图片上绘制文字和形状的应用程序代码。

在交互模式下使用一张背景图片并在阴影模式下使用一张不同的背景图片是一种典型的设计。阴影模式下的背景图片通常是黑色的。Android Wear设备的背景图片要跟其屏幕(方形或圆形)的密度320像素匹配。在圆形设备上,背景图片的角不可见。在代码中,可以检测设备屏幕尺寸并在设备的分辨率低于图片时就缩小背景图片。欲提升性能,应该只缩小图片一次并将它存储在结果位图中。

应只在需要时代码才去检索上下文数据并将结果存储到可重复使用来绘制表面的地方。如并不需要没分钟提取天气更新数据。

欲延续电池寿命,在阴影模式下绘制表面的代码应该相对来说更简单。常可以使用简单的颜色绘制此种模式下的形状框架。在交互模式下,可以使用全色彩、复杂的形状、梯度以及动画来绘制表面。

后续小节将细节描述如何实现表面。

5.2 构建表面服务

学习在表面的生命周期内如何响应重要的事件。

Android Wear中的表面以服务形式实现并被打包在可穿戴应用程序中。当用户安装包含拥有表面的可穿戴程序的手持设备应用程序时,在手持设备和可穿戴设备的表面采集中的Android Wear companion app中表面都可用。当用户选择一个可用的表面时,可穿戴设备显示表面并按照要求调用它的服务回调方法。

此小节描述如何配置Android 工程来包含表面并实现表面服务。

(1) 创建和配置工程

欲在Android Studio中为表面创建Android 工程:

  1. 开启Android Studio。
  2. 创建新工程 - 若没有打开的工程,在Welcome界面点击New Project;若有打开的工程,从File菜单中选择New Project
  3. 提供应用程序名并点击Next
  4. 在代理中选择Phone and Tablet
  5. Minimum SDK下选择API 18。
  6. 从代理中选择Wear
  7. Minimum SDK下,选择API 21并点击Next
  8. 在后续的两个界面中选择Add No Activity并点击Next
  9. 点击Finish
  10. 在IDE窗口中点击View > Tool Windows > Project

Android Studio用wear和mobile模块创建工程,更多信息见Create a Project。

[1] 依赖

Wearable Support library提供了必要的用于扩展来创建表面的实现类。Google Play服务客户端库(play-service和play-services-wearable)是在同伴设备和可穿戴数据层API同步数据所需要的。

当用以上步骤创建工程后,Android Studio将自动的添加所需要的条目到build.gradle文件中。

[2] 可穿戴支持库API类参考

参考文档提供了关于使用来创建表面的类的细节信息。浏览可穿戴支持库的类参考文档:API reference documentation。

[3] 声明权限

表面需要PROVIDE_BACKGROUND和WAKE_LOCK权限。添加以下权限到手持和可穿戴应用程序的清单文件中:

<manifest ...><uses-permissionandroid:name="com.google.android.permission.PROVIDE_BACKGROUND" /><uses-permissionandroid:name="android.permission.WAKE_LOCK" />...
</manifest>

警示:手持设备应用程序必须包含可穿戴应用程序中的所有权限。

(2) 实现服务回调方法

Android Wear中的表面以服务的形式实现。当表面处于活跃状态时,当时间改变或重要的事件发生(如转换到阴影模式或接收到新的通知)时系统调用服务中的方法。然后服务用更新的时间和其它相关数据在屏幕上绘制表面。

欲实现表面,需要扩展CanvasWathcFaceService和CanvasWatchFaceService.Engine类,然后重写CanvasWatchFaceService.Engine类中的方法。这些类都含在Wearable Support Library中。

以下代码片段勾画除了需要实现的主要方法:

public class AnalogWatchFaceService extends CanvasWatchFaceService {@Overridepublic Engine onCreateEngine() {/* provide your watch face implementation */return new Engine();}/* implement service callback methods */private class Engine extends CanvasWatchFaceService.Engine {@Overridepublic void onCreate(SurfaceHolder holder) {super.onCreate(holder);/* initialize your watch face */}@Overridepublic void onPropertiesChanged(Bundle properties) {super.onPropertiesChanged(properties);/* get device features (burn-in, low-bit ambient) */}@Overridepublic void onTimeTick() {super.onTimeTick();/* the time changed */}@Overridepublic void onAmbientModeChanged(boolean inAmbientMode) {super.onAmbientModeChanged(inAmbientMode);/* the wearable switched between modes */}@Overridepublic void onDraw(Canvas canvas, Rect bounds) {/* draw your watch face */}@Overridepublic void onVisibilityChanged(boolean visible) {super.onVisibilityChanged(visible);/* the watch face became visible or invisible */}}
}

CanvasWatchFaceService类提供类似于View.invalidate()方法的无效机制。当想系统重绘制表面时通过实现可以调用invalidate()方法。可以只在主UI线程中使用invalidate()方法。欲让在其它线程中无效canvas,调用postInvalidate()方法。

(3) 注册表面服务

在实现表面以后,在可穿戴应用程序中的清单文件中注册该实现。当用户安装该应用程序时,系统使用该服务的信息让表面在Android Wear companion app和可穿戴设备的表面采集器中可用。

以下代码片段展示如何在application元素下注册表面实现:

<service    android:name=".AnalogWatchFaceService"android:label="@string/analog_name"android:allowEmbedded="true"android:taskAffinity=""android:permission="android.permission.BIND_WALLPAPER" ><meta-data        android:name="android.service.wallpaper"android:resource="@xml/watch_face" /><meta-data        android:name="com.google.android.wearable.watchface.preview"android:resource="@drawable/preview_analog" /><meta-data        android:name="com.google.android.wearable.watchface.preview_circular"android:resource="@drawable/preview_analog_circular" /><intent-filter><action android:name="android.service.wallpaper.WallpaperService" /><category            android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /></intent-filter>
</service>

Android Wear companion app和可穿戴设备的表面采集器在使用安装的表面呈现给用户时将定义在com.google.android.wearable.watchface.previe元素条目中的图片作为预览图片入口。欲获取该可拖拽图片,在Android Wear设备或模拟器设备中运行表面并截图。在拥有hdpi屏幕的Android Wears设备上,预览图片典型为320x320像素(大小)。

在圆形设备上看起来不同的表面能提供圆形和方形的预览图片。欲指定圆形的预览图片,使用com.google.android.wearable.watchface.preview_circular元数据入口。如果表面包含预览图片,同伴应用和可穿戴设备的表面采集器将根据表面的形状展示合适的一张。如果圆形预览图片没有被包含,方形预览图片会被用于方形和圆形设备。对于圆形设备,方形图片被圆形截圆形预览图片。

android.service.wallpaper元数据入口指定watch_face.xml资源文件,该文件包含了wallpaper元素:

<?xml version="1.0" encoding="UTF-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android" />

可穿戴应用程序可以包含多于一个的表面。必须为每个表面实现添加服务入口到可穿戴应用程序的清单文件中。

01.10

5.3 绘制表面

歇息日结早Wear设备屏幕上绘制表面。

在完成工程配置并添加实现表面服务的类后,可以开始编写代码初始化和绘制自定义表面了。

此节包含来自WatchFace样例的例子以显示系统时如何使用表面服务的。服务实现的许多方面在其中的描述(如初始化和设备特性检测)可适用任何表面,所以可以在您的表面工程中重新使用其中的一些代码。

图1. 在WatchFace样例中的模拟和数字表面

(1) 初始化表面

当系统载入服务时,应该分配和初始化表面所需的大量资源,包括载入位图资源、创建运行自定义动画的时钟对象、配置绘制的风格以及一些其它的计算。通常可以只执行这些操作一次并重复使用这些操作的结果。这种方式提升了表面的性能并给维持代码带来了更简单性。

欲初始化表面,遵循以下步骤:

  1. 为自定义时钟声明变量、图像对象以及其它元素。
  2. 在Engine.onCreate()方法中初始化表面元素。
  3. 在Engine.onVisibilityChanged()方法中初始化自定义时钟。

以下各小节详细描述各步骤。

[1] 声明变量

当系统载入服务时所初始化的资源要能够在所实现的不同地方被访问到,所以要确保它们可被重复使用。可通过在WatchFaceService.Engine实现中声明变量的方式实现这个过程。

为以下元素声明变量:
图形对象 - 大多数的表面至少包含一种被作为背景的位图图片,就如Create an implementation Strategy中所描述。可以使用代表时钟或表面的其它元素的位图图片。

周期定时器 - 系统每一分钟通知表面时间的改变,但是一些表面以自定义时间间隔运行动画。在这种情况下,需要提供所需频率的时钟来更新表面的显示。

时区变化接收器 - 当用户旅游时他们可以调整时区,且系统需要广播时区变化事件。服务实现必须要注册一个在时区变化时能够接收该事件的广播事件并及时的更新表面。

以下代码片段展示如何定义这些变量:

private class Engine extends CanvasWatchFaceService.Engine {static final int MSG_UPDATE_TIME = 0;Calendar mCalendar;// device featuresboolean mLowBitAmbient;// graphic objectsBitmap mBackgroundBitmap;Bitmap mBackgroundScaledBitmap;Paint mHourPaint;Paint mMinutePaint;...// handler to update the time once a second in interactive modefinal Handler mUpdateTimeHandler = new Handler() {@Overridepublic void handleMessage(Message message) {switch (message.what) {case MSG_UPDATE_TIME:invalidate();if (shouldTimerBeRunning()) {long timeMs = System.currentTimeMillis();long delayMs = INTERACTIVE_UPDATE_RATE_MS- (timeMs % INTERACTIVE_UPDATE_RATE_MS);mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);}break;}}};// receiver to update the time zonefinal BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {mCalendar.setTimeZone(TimeZone.getDefault());invalidate();}};// service methods (see other sections)...
}

在以上的代码中,自定义时钟以用线程信息队列发送和处理延迟信息的Handler实例的形式被实现。对于这个特殊的表面,系统时钟每秒滴答一次。当系统时钟滴答时,handler调用invalidate()方法和系统会调用onDraw()方法重新绘制表面。

[2] 初始化表面元素

在为位图资源、绘制风格以及每次会用来重新绘制表面的其它元素声明成员变量后,当系统载入服务时初始化它们。只初始化它们一次并重用它们,这样就能够提升程序性能和节约电量。

在Engine.onCreate()方法中,初始化以下元素:

  • 载入背景图片。
  • 创建风格和颜色绘制图像对象。
  • 分配对象计算时间。
  • 配置系统UI。

以下代码片段展示如何初始化这些元素:

@Override
public void onCreate(SurfaceHolder holder) {super.onCreate(holder);// configure the system UI (see next section)...// load the background imageResources resources = AnalogWatchFaceService.this.getResources();Drawable backgroundDrawable = resources.getDrawable(R.drawable.bg, null);mBackgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();// create graphic stylesmHourPaint = new Paint();mHourPaint.setARGB(255, 200, 200, 200);mHourPaint.setStrokeWidth(5.0f);mHourPaint.setAntiAlias(true);mHourPaint.setStrokeCap(Paint.Cap.ROUND);...// allocate a Calendar to calculate local time using the UTC time and time zonemCalendar = Calendar.getInstance();
}

当系统初始化表面时背景图片只被载入一次。图像风格时Paint类的实例。使用这些风格在Engine.onDraw()方法中绘制表面的元素,正如第五小节“绘制表面”描述的那样。

[3] 初始化自定义时钟

作为一个表面的开发者,当设备处于交互模式下时需要决定用含所需频率的自定义时钟多久更新一次表面。这决定了创建自定义动画和其它的视觉效果。

注:在阴影模式下,系统不会调用自定义时钟。欲在阴影模式下更新表面,见Update the watch face in ambient mode。

来自AnalogWatchFaceService类的时钟定义每秒滴答一次并显示在声明变量中。在Engine.onVisibilityChanged()方法中,开启自定义定时器,若满足以下俩条件:

  • 表面可见。
  • 设备在交互模式下。

AnalogWatchFaceService类将计划下一个时钟若对其配置如下:

private void updateTimer() {mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);if (shouldTimerBeRunning()) {mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);}
}private boolean shouldTimerBeRunning() {return isVisible() && !isInAmbientMode();
}

自定义时钟每1s滴答一次。

在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.mCalendar.setTimeZone(TimeZone.getDefault());} else {unregisterReceiver();}// Whether the timer should be running depends on whether we're visible and// whether we're in ambient mode, so we may need to start or stop the timerupdateTimer();
}

当表面可见时,onVisibilityChanged()方法为时区改变事件注册接收器。若设备处于交互模式,该方法也开启自定义时钟。当表面不可见时,该方法停止自定义时钟并注销时区改变的事件。registerReceiver()和unregisterReceiver()方法实现如下:

private void registerReceiver() {if (mRegisteredTimeZoneReceiver) {return;}mRegisteredTimeZoneReceiver = true;IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);AnalogWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
}private void unregisterReceiver() {if (!mRegisteredTimeZoneReceiver) {return;}mRegisteredTimeZoneReceiver = false;AnalogWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
}

[4] 在阴影模式下更新表面

在阴影模式下,系统每分钟调用一次Engine.onTimeTick()。在该模式下每分钟更新一次表面已算高效。欲在交互模式下更新表面,必须提供自定义计时器。

在阴影模式下,大多数表面实现简单的让canvas无效以在Engine.onTimeTick()方法中重新绘制表面:

@Override
public void onTimeTick() {super.onTimeTick();invalidate();
}

(2) 配置系统UI

表面不应干预系统UI元素,如Accimmodate System UI Elements一节中描述。如果表面有一个浅的背景或在屏幕底部附近显示信息,可能就需要配置通知卡片的尺寸或开启背景保护。

当表面活跃时,Android Wear允许配置系统UI元素以下几个方面:

  • 指定看到第一个通知卡片与屏幕的距离。
  • 指定系统是否绘制实现在表面之上。
  • 在阴影模式下显示或隐藏卡片。
  • 用图形背景图片保护系统显示。
  • 指定系统显示的位置。

欲配置系统UI的这些方面,创建WatchFaceStyle实例并将其传递到Engine.setWatchFaceStyle()方法中。

AnalogWatchFaceServic类按照如下代码配置系统UI:

@Override
public void onCreate(SurfaceHolder holder) {super.onCreate(holder);// configure the system UIsetWatchFaceStyle(new WatchFaceStyle.Builder(AnalogWatchFaceService.this).setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT).setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE).setShowSystemUiTime(false).build());...
}

以上代码片段以单行形式配置了卡片。卡片的背景只为中断的通知简单的显示,系统事件不会显示(因为表面绘制其自己的时间)。

可以在表面实现中的任何位置配置系统UI的风格。如,若用户选择白色背景,可以为系统显示添加屏幕保护。

更多关于配置系统UI的细节,见Wear API reference documentation.

(3) 获取关于设备屏幕的信息

当系统检测到设备屏幕属性后将调用Engine.onPropertiesChanged()方法,诸如设备用户是否低层(low-bit)了阴影模式或者屏幕是否需要burn-in的保护。

以下代码片段展示如何获取这些属性:

@Override
public void onPropertiesChanged(Bundle properties) {super.onPropertiesChanged(properties);mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION,false);
}

在绘制表面时应该考虑设备的这些属性:

  • 对于被low-bit了阴影模式的设备,在阴影模式下屏幕的颜色支持更少的位,所以当设备转换到阴影模式后应该关闭图形保真并进行位图过来。
  • 对于需要burn-in保护的设备,在阴影模式下避免使用大块的白色像素且不要在屏幕的边缘放置10像素以内的内容,因为屏幕会周期的变换这些内容以避免像素内建。

更多关于low-bit阴影模式和burn-in保护,见Optimize for Special Screens。关于如何关闭位图过滤的信息,见BitMap Filtering。

(4) 响应不同模式间的切换

当设备在阴影模式和交互模式间转换时,系统调用Engine.onAmbientModeChanged()方法。在实现中要根据具体情况在模式之间做相应的切换并调用invalidate()方法来让系统重新绘制表面。

以下代码片段展示如何实现该方法:

@Override
public void onAmbientModeChanged(boolean inAmbientMode) {super.onAmbientModeChanged(inAmbientMode);if (mLowBitAmbient) {boolean antiAlias = !inAmbientMode;mHourPaint.setAntiAlias(antiAlias);mMinutePaint.setAntiAlias(antiAlias);mSecondPaint.setAntiAlias(antiAlias);mTickPaint.setAntiAlias(antiAlias);}invalidate();updateTimer();
}

该样例在图形庚哥和无效canvas方面做了调整以让系统能够重新绘制表面。

(5) 绘制表面

欲绘制自定义表面,系统调用以Canvas实例为参数的Engine.onDraw()方法并绑定所需绘制的表面。绑定考虑任何插图区域,诸如一些圆形设备底部的“下巴”。可以直接使用canvas按照如下步骤绘制表面:

  1. 重写onSurfaceChanged()方法调整背景以在视图改变的任何时间里适合设备。
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {if (mBackgroundScaledBitmap == null|| mBackgroundScaledBitmap.getWidth() != width|| mBackgroundScaledBitmap.getHeight() != height) {mBackgroundScaledBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,width, height, true /* filter */);}super.onSurfaceChanged(holder, format, width, height);
}
  1. 检查设备处于阴影模式还是交互模式。
  2. 执行所需的图形计算。
  3. 在canvas中绘制背景位图。
  4. 在Canvas类中使用方法绘制表面。

以下代码片段展示如何实现onDraw()方法:

@Override
public void onDraw(Canvas canvas, Rect bounds) {// Update the timemCalendar.setTimeInMillis(System.currentTimeMillis());// Constant to help calculate clock hand rotationsfinal float TWO_PI = (float) Math.PI * 2f;int width = bounds.width();int height = bounds.height();canvas.drawBitmap(mBackgroundScaledBitmap, 0, 0, null);// Find the center. Ignore the window insets so that, on round watches// with a "chin", the watch face is centered on the entire screen, not// just the usable portion.float centerX = width / 2f;float centerY = height / 2f;// Compute rotations and lengths for the clock hands.float seconds = mCalendar.get(Calendar.SECOND) +mCalendar.get(Calendar.MILLISECOND) / 1000f;float secRot = seconds / 60f * TWO_PI;float minutes = mCalendar.get(Calendar.MINUTE) + seconds / 60f;float minRot = minutes / 60f * TWO_PI;float hours = mCalendar.get(Calendar.HOUR) + minutes / 60f;float hrRot = hours / 12f * TWO_PI;float secLength = centerX - 20;float minLength = centerX - 40;float hrLength = centerX - 80;// Only draw the second hand in interactive mode.if (!isInAmbientMode()) {float secX = (float) Math.sin(secRot) * secLength;float secY = (float) -Math.cos(secRot) * secLength;canvas.drawLine(centerX, centerY, centerX + secX, centerY +secY, mSecondPaint);}// Draw the minute and hour hands.float minX = (float) Math.sin(minRot) * minLength;float minY = (float) -Math.cos(minRot) * minLength;canvas.drawLine(centerX, centerY, centerX + minX, centerY + minY,mMinutePaint);float hrX = (float) Math.sin(hrRot) * hrLength;float hrY = (float) -Math.cos(hrRot) * hrLength;canvas.drawLine(centerX, centerY, centerX + hrX, centerY + hrY,mHourPaint);
}

该方法根据当前时钟计算时钟所需的位置并使用在onCreate()方法中初始化的图形风格将它们绘制在背景位图的顶部。只在交互模式下绘制不再阴影模式下绘制。

更多关于绘制Canvas实例的信息见Canvas and Drawables。

WatchFace样例包含额外一些额外的表面样例,可以通过此样例知道如何实现onDraw()方法。

5.4 在表面中显示信息

学习如何嵌入上下文信息到表面中。

除了告知时间外,Android Wear设备给用户提供了上下文相关的卡片形式的信息、通知以及其它的可穿戴应用程序。创建自定义表面不仅能以视觉方式告知时间,还能够为用户显示相关的信息。

跟其它可穿戴应用程序一样,使用Wearable Data Layer API表面也能够和运行在手持设备上的引用程序通讯。在一些情况下,需要在从网络或用户文件中检索数据的工程中的手持应用程序的模块中创建活动然后分享给表面。

图1. 含集成数据的表面例子

(1) 创建令人折服的体验

在设计和实现上下文意识的表面前,回答以下几个问题:

  • 欲嵌入什么类型的数据?
  • 在哪里获取这些数据?
  • 数据多久改变一次较有意义?
  • 怎么呈现数据才能让用户看一眼就能明白?

Android Wear设备常与一个有GPS传感器和手机连接的设备配对,这样就能够拥有无限的能力继承不同种类的数据到表面中,诸如位置、日历事件、社交媒体动态、图片种子、股票市场行情、新闻事件、运动得分等等。然而,不是所有的数据都适合在表面中显示,所以应该考虑与用户最相关的数据。在可穿戴设备不再欲同伴设备相连时表面应该能够优雅的处理这些数据。

Android Wear设备应用程序中的活跃的表面运行持续,所以必须以电池高效的方式检索数据。举例,可以获取以每十分钟获取当前数据并将结果存储在本地,而不是每分钟都更新一次。也当设备由阴影模式进入交互模式时也可以刷新上下文数据,因为用户当这种模式转变时用户很可能会瞄一眼表面。

应该在表面中总结上下文信息,因为在屏幕上只有有限的空间且用户的眼神只会在表面上停留一两秒的时间。有时候传达上下文信息最好的方式是用图形和颜色响应它。如表面可以根据当前天气改变它的背景图片。

(2) 添加数据到表面

WatchFace例子演示了在CalendarWathcFaceService类中如何从用户的配置文件中获取日历数据并显示在接下来的24小时中有哪些会议。

欲实现嵌入上下文数据的表面,遵循以下步骤:

  1. 提供检索数据的任务。
  2. 创建自定义时钟来周期的调用任务,或在外部数据改变时通知表面服务。
  3. 用更新的数据重新绘制表面。


图2. 日历表面

后续小节详细描述每一个步骤。

[1] 提供检索数据的任务

在CanvasWatchFaceService.Engine实现中创建扩展于AsyncTask的类并添加检索所感兴趣数据的代码。

CalendarWatchFaceService类获取接下来一天的回忆数量的代码如下:

/* Asynchronous task to load the meetings from the content provider and* report the number of meetings back using onMeetingsLoaded() */
private class LoadMeetingsTask extends AsyncTask<Void, Void, Integer> {@Overrideprotected Integer doInBackground(Void... voids) {long begin = System.currentTimeMillis();Uri.Builder builder =WearableCalendarContract.Instances.CONTENT_URI.buildUpon();ContentUris.appendId(builder, begin);ContentUris.appendId(builder, begin + DateUtils.DAY_IN_MILLIS);final Cursor cursor = getContentResolver() .query(builder.build(),null, null, null, null);int numMeetings = cursor.getCount();if (Log.isLoggable(TAG, Log.VERBOSE)) {Log.v(TAG, "Num meetings: " + numMeetings);}return numMeetings;}@Overrideprotected void onPostExecute(Integer result) {/* get the number of meetings and set the next timer tick */onMeetingsLoaded(result);}
}

Wearable支持库中的WearableCalendarContract类提供直接用户同伴设备日历事件的方式。

当任务完成检索数据时,代码调用回调方法,以下小节描述如何实现回调方法。

更多关于从日历获取数据的信息见Calendar Provider API手册。

[2] 创建自定义时钟

可以实现自定义时钟周期的更新数据。CalendarWatchFaceService类使用发送和处理延迟信息的Handler实例使用线程信息队列:

private class Engine extends CanvasWatchFaceService.Engine {...int mNumMeetings;private AsyncTask<Void, Void, Integer> mLoadMeetingsTask;/* Handler to load the meetings once a minute in interactive mode. */final Handler mLoadMeetingsHandler = new Handler() {@Overridepublic void handleMessage(Message message) {switch (message.what) {case MSG_LOAD_MEETINGS:cancelLoadMeetingTask();mLoadMeetingsTask = new LoadMeetingsTask();mLoadMeetingsTask.execute();break;}}};...
}

当表面变得可见该方法初始化时钟:

@Override
public void onVisibilityChanged(boolean visible) {super.onVisibilityChanged(visible);if (visible) {mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);} else {mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS);cancelLoadMeetingTask();}
}

下一个时钟在onMeetingsLoaded()方法中设置,下一节将会描述这一点。

[3] 用更新的数据重新绘制表面

当任务检索数据结束后,调用invalidate()方法让系统重新绘制表面。存储数据到Engine类的成员变量中,这样就可以在onDraw()方法中访问到它。

CalendarWatchFaceService类为任务完数据检索后提供了可被调用的方法:

private void onMeetingsLoaded(Integer result) {if (result != null) {mNumMeetings = result;invalidate();}if (isVisible()) {mLoadMeetingsHandler.sendEmptyMessageDelayed(MSG_LOAD_MEETINGS, LOAD_MEETINGS_DELAY_MS);}
}

回调方法存储结果到成员变量中、无效视图以及计划下一个时钟以运行任务。

01.11

5.5 创建可交互的表面

学习如何让用户和表面交互。

表可不仅可展示一个用户可交互的漂亮的表面。如,用户可以点击表面去了解当前正播放的音乐,或者查看当天的日程表。Android Wear允许Android Wear表面在给定位置接收表面的单击手势,只要没有另外的UI元素也响应这个手势。

此节描述在首先构建表面风格时如何实现可交互的表面,然后实现手势处理。

注:在开始开发交互表面工作之前,应该阅读一下Watch Faces for Android Wear设计手册。

(1) 处理Tab事件

当构建交互表面风格时,应用程序首先必须做的事情时告知系统表面要接收tab 事件。一下代码展示如何完成这个工作:

setWatchFaceStyle(new WatchFaceStyle.Builder(mService).setAcceptsTapEvents(true)// other style customizations.build());

当系统检测到表面的tap时,它将触发WatchFaceService.Engine.onTapCommand()方法。在WatchFaceService.Engine的实现中重写该方法来初始化希望执行的动作,诸如显示详细的步骤或改变表面的主题。在Handle Gestures样例中的代码片段展示了如何实现这样的一个实现。

(2) 处理手势

欲提供一致的用户体验,系统为UI元素诸如拖和长按的手势。因此,系统不会发送原始的触摸事件给表面。相反,系统发送特殊的命令给onTapCommand()方法中。

当用户初始化触摸屏幕时,系统发送第一个命令TAP_TYPE_TOUCH。该事件视觉上的回馈给用户。当该事件触发时应用程序不应该启动UI。启动UI将组织拖事件打开应用程序启动、设置阴影以及通知流。

在发送下一个命令前,系统判断交互是否是单个tap,这是仅允许的手势。如果用户立刻放开手指,系统将判断单个tap发生,然后发送TAP_TYPE_TAP事件。如果用户没有立刻移开手指,系统将发送TAP_TYPE_TOUCH_CANCEL事件。一旦用户触发了TAP_TYPE_TOUCH_CANCEL事件,直到跟屏幕做新的交互之前都不能触发TAP_TYPE_TAP事件。

以下代码样例展示在表面上如何实现tap事件:

@Override
public void onTapCommand(@TapType int tapType, int x, int y, long eventTime) {switch (tapType) {case WatchFaceService.TAP_TYPE_TAP:hideTapHighlight();if (withinTapRegion(x, y)) {// Implement the tap action// (e.g. show detailed step count)onWatchFaceTap();}break;case WatchFaceService.TAP_TYPE_TOUCH:if (withinTapRegion(x, y)) {// Provide visual feedback of touch eventstartTapHighlight(x, y, eventTime);}break;case WatchFaceService.TAP_TYPE_TOUCH_CANCEL:hideTapHighlight();break;default:super.onTapCommand(tapType, x, y, eventTime);break;}
}

在该样例中,应用程序判断事件发生了类型,然后响应地响应。如果事件来自用户首次交互,应用程序将给一个视觉上的回馈。如果是一个在触碰后立即放开的事件,在用户tap后就立即执行动作。如果是手指触碰一段时间的事件,应用程序将不做任何事情。

5.6 提供配置活动

学习如何创建拥有配置参数的表面。

当用户安装包含拥有表面的可穿戴应用程序的手持应用程序时,表面在Android Wear同伴设备上的应用程序和可穿戴上的表面采集器上都可以用。用户可以在同伴应用程序中或使用可穿戴设备上的表面采集为可穿戴设备选择一个表面。

一些表面支持参数的配置以让用户自定义表面的外观和行为。例如,一些表面让用户选择自定义背景颜色,能够显示两个不同时区的时间的表面让用户选择他们感兴趣的时区。

表面支持参数配置能够让用户在可穿戴设备/手持设备上自定义表面所需的活动。用户可以可穿戴设备上开始可穿戴配置活动,然后在Android Wear同伴应用程序中开启同伴的活动配置。

WatchFace中的数字表面演示了如何实现手持和可穿戴配置活动以及如何更新表面以响应配置的改变。

(1) 为配置活动指定Intent

若表面包含配置活动,添加以下元数据条目到可穿戴应用程序的清单文件中的服务的声明中:

<service    android:name=".DigitalWatchFaceService" ... /><!-- companion configuration activity --><meta-data        android:name="com.google.android.wearable.watchface.companionConfigurationAction"android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" /><!-- wearable configuration activity --><meta-data        android:name="com.google.android.wearable.watchface.wearableConfigurationAction"android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />...
</service>

用应用程序的包名为这些条目提供值。配置活动为该意图注册意图过滤器,在用户欲配置表面时系统将揭开该意图。

若表面只包含了同伴或可穿戴配置活动,以上程序只需要包含相关的元数据条目。

(2) 创建可穿戴配置活动

可穿戴配置活动为表面提供了有限的自定义选择,因为负责的菜单在小屏幕上较难导航。可穿戴配置活动应该提供两个选择并只有几个选择来自定义表面的几个主要方面。

欲创建可穿戴配置活动,添加一个新的活动到可穿戴应用程序模块中并在可穿戴应用程序的清单文件中声明一下意图过滤器:

<activity    android:name=".DigitalWatchFaceWearableConfigActivity"android:label="@string/digital_config_name"><intent-filter><action android:name="com.example.android.wearable.watchface.CONFIG_DIGITAL" /><category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" /><category android:name="android.intent.category.DEFAULT" /></intent-filter>
</activity>

在该意图过滤器中的动作的名字不洗和定义在”为配置活动指定意图“中的意图名相同。

在配置活动中,构建一个简单的为用户自定义表面提供可选择条目的UI。当用户做选择时,使用Wearable Data Layer API来与配置改变和表面活动通信。

更多细节见WatchFace例子中的DigitalWatchFaceWearbleconfigActivity和DigitalWatchFaceUtil类。

(3) 创建同伴配置活动

同伴配置活动给用户访问为表面提供的全部配置选择,因为在手持设备上的大屏幕中较容易交互复杂的菜单。如手持设备上的一个配置活动允许为用户呈现精细制作的颜色采集器以供用户选择表面的背景颜色。

欲创建同伴配置活动,在手持应用程序模块中添加一个新的活动并在手持应用程序中的清单文件中声明意图过滤器:

<activity    android:name=".DigitalWatchFaceCompanionConfigActivity"android:label="@string/app_name"><intent-filter><action android:name="com.example.android.wearable.watchface.CONFIG_DIGITAL" /><category android:name="com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION" /><category android:name="android.intent.category.DEFAULT" /></intent-filter>
</activity>

在配置活动中,构建UI提供选择来自定义表面的配置元素。当用户选择时,使用Wearable Data Layer API来和配置改变欲表面活动的通信。

更多信息见WatchFace例子中的DidgitalWatchFaceCompanionConfigActivity类。

(4) 在可穿戴应用程序中创建监听服务

欲接收配置活动中的更新的配置参数,在可穿戴应用程序中创建从Wearable Data Layer API中的WearableListenerService接口实现的服务。当配置参数改变后,表面实现可以重新绘制表面。

更多详细信息,见WatchFace例子中的DigitalWatchFaceConfigListenerService和DigitalWatchFaceService类。

5.7 解决常见问题

学习在开发表面时如何解决常见问题。

为Android Wear创建自定义表面基本不同于创建通知和指定可穿戴活动。此节描述如何解决在第一次开发表面时可能会遇到的一些问题。

(1) 检测屏幕形状

一些Android Wear设备有方形的屏幕,也有一些设备是圆形的屏幕。拥有圆形屏幕的底部包含一个镶边(或“下巴”)。表面应该调整以利用该形状的这种特性,正如desig guidelines中描述的一样。

Android Wear能够在运行时判断屏幕的形状。欲检测屏幕是圆形还是方形,将CanvasWatchFaceService.Engine类中的onApplyWindowInsets()方法重写如下:

private class Engine extends CanvasWatchFaceService.Engine {boolean mIsRound;int mChinSize;@Overridepublic void onApplyWindowInsets(WindowInsets insets) {super.onApplyWindowInsets(insets);mIsRound = insets.isRound();mChinSize = insets.getSystemWindowInsetBottom();}...
}

当绘制表面时欲适应设计,检查mIsRound和mChinSize两个成员变量的值。

(2) Accomodate Peek Cards

当用户收到通知时,通知卡片可能会基于系统UI风格覆盖屏幕的一部分。当通知卡片出现时应该通过确定用户仍旧可以看到时间来调整显示。

当通知卡片出现时模拟表面可以做调整,就像将表面往下调整以调整屏幕不会被卡片覆盖。屏幕中呈现时间的数字表面不会被卡片覆盖。欲判断表面的调整的空间,使用WatchFaceService.Engine.getPeekCardPosition()方法。

在阴影模式下,peek卡片有一个透明的背景。如果表面在阴影模式下在卡片附近有详细信息,考虑绘制黑色的矩形在它们上面。

图1. 当通知卡片出现时模拟时钟表面需要调整

(3) 配置系统指标

欲确保系统指标仍然可见,可以配置它们在屏幕上的位置以及当创建WatchFaceStyle实例时它们是否需要屏保:

  • 使用setStatusBarGravity()设置状态条的位置。
  • 使用setHotwordIndicatorGravity()方法设置热字的位置。
  • 使用setViewProtection()方法设置半透明灰色背景保护状态条和热字。若表面有一个浅色背景则这通常是有必要的,因为系统指标通常是白色的。

更多关于系统指标的信息见Configure the System UI并阅读design guidelines。

(4) 使用相对测量方法

来自不同制造商的Android Wears设备的屏幕有各种各样的尺寸和分辨率。表面应该使用相关的测量来适应这些变化。

当绘制表面时,用Canvas.getWidth()和Canvas.getHeight()方法获取canvas的尺寸并使用屏幕尺寸检测的部分值来设置图形元素的位置。如果重新设置表面元素的大小来响应peek卡片,使用以上提供的剩余空间的部分值来重新绘制表面。

5.8 优化程序性能和电池寿命

学习如何提升动画帧速率以及如何节约电量。

除了容纳通知卡片和系统指标外,还需要确保表面中的动画能够流畅的运行以及不让服务做不必要的计算。Android Wear中的表面在设备上持续的运行,所以高效的使用电量显得很有必要。

此节提供一些加速动画和侧脸并保存电量的有效方式。

(1) 减少位图Assets的尺寸

许多表面有背景和其它的转换的和重叠在背景图片顶部的图形Asset组成,诸如时钟指针和其它设计来移动时间的元素。典型地,这些图像元素在Engine.onDraw()方法中是可旋转的(每分钟,有时是可缩放的),就像Draw Your Watch Face中所描述。

较大图形assets会耗更多的计算来转换它们。在Engine.onDraw()方法中转换较大图形会降低框架速率(当系统运行动画时)。

欲提升表面的性能:

  • 不适用比需要大的图形元素。
  • 移动额外的透明像素到边缘周围。

指针的示例图片能够被缩小97%,如下图。

图1. 时钟指针能够被截图额外的像素

减少位图assets尺寸不仅能提升动画的性能,还可以节约电量。

(2) 结合位图Assets

若位图会被经常绘制到一起,考虑将它们结合到相同的图形asset中。可以结合背景图片在交互模式中使用滴答标志避免每分钟绘制两个屏幕位图。

(3) 当绘制缩略位图时关闭Anti-Aliasing

当在Canvas对象中使用Canvas.drawBitmap()方法绘制缩放位图时,可以提供Paint实例配置几个选项。欲提升性能,使用setAntiAlias()方法关闭anti-aliasing,因为该选项在位图上不会有任何效果。

使用位图过滤器
对于绘制在其它元素上面的位图assets,使用setFilterBitmap()方法在同一个Paint实例上开启位图过滤器。图2展示了通过位图过滤和没有使用位图过滤的方法的视图。

图2. 关闭位图过滤器(左)和开启位图过滤(右)的例子

注:在low-bit阴影模式中,系统不会渲染位图过滤的图片颜色。当阴影模式活跃时,关闭位图过滤。

(4) 将耗时的操作移到绘制方法之外

每次重新绘制表面时,系统调用Engine.onDraw()方法,所以应该在该方法中包含更新只需要的操作以提升程序性能。

当可能时,在Engine.onDraw()方法中避免以下操作:

  • 载入图片以及其它资源。
  • 重新调整图片的尺寸。
  • 分配对象。
  • 执行在帧中不会改变的计算。

可以在Engine.onCreate()方法中执行这些操作。可以提前在Engine.onSurfaceChanged()重新调整图片尺寸,该方法提供canvas的尺寸。

欲分析表明的性能,使用Android Device Monitor。特别地,确保Engine.onDraw()中的实现的执行时间是很短和持续调用的。更多信息见Using DDMS。

(5) 遵循节约电量的最佳步骤

除了前几小节中所描述的技术外,遵循本节中描述的遵循节约电量消耗的最佳步骤。

减少动画帧速率
动画常会消耗昂贵的计算并会耗大量的电量。大多数动画每秒30帧看起来就流利了,所以应该避免动画运行在更高的帧速率上。

让CPU睡眠
表面的动画和内容小的改变会唤醒CPU。在动画期间应该让CPU睡眠。如在交互模式下可以使用每秒的动画短的爆然后让CPU睡眠到下一秒。让CPU经常睡眠,即使很简单,能够较大程度节约电量的消耗。

欲最大化电池寿命,节制的使用动画。即使是一个闪烁的福报都会唤醒CPU并会耗电。

监控电量消耗
Android Wear companion app让开发者和用户在可穿戴设备的Setting >>Watch battery下查看电池的不同消耗情况。

更多关于Android 5.0的帮助提示电池寿命的新特性,见Project Volta。

01.12

6. 在Android Wear上检测位置

在Android可穿戴设备上如何检测位置数据。

可穿戴设备上的位置意识能够创建应用程序来让用户了解他们所处的地理位置、移动以及他们所处的周围的环境。使用可穿戴设备小型格式的代理和可瞄自然,可以构建记录和响应位置数据的应用程序。

一些包含GPS传感器的可穿戴设备在没有与其它设备相连的情况下也能欧检索地理数据。然而,当在可穿戴应用程序中请求位置数据时,不需要担心位置数据的来源;系统使用最高效的方法检索位置更新。应用程序应该能够处理位置数据丢失的情况(可穿戴设备与配对设备断开了连接并没有内建的GPS传感器的情况)。

此文档展示如何检查设备上是否有传感器、接收位置数据并监控数据连接。

注:标题假设您已经知道了如何使用Google Play服务API检索位置数据。更多信息见Making Your App Location-Aware。

(1) 连接Google Play 服务

可穿戴设备上的位置数据通过Google Play服务位置APIs获取。使用FusedLocationProviderApi和其伴随的类获取该数据。欲访问位置服务,创建GoogleApiClient类的实例,该类是Google Play服务APIs的主要入口点。

警示:不要在Android 框架中使用存在的Location APIs。最佳检索位置更新的方法是通过Google Play服务API(见本标题所列举的提纲)。

欲连接Google Play服务,配置应用程序创建GoogleApiClient的实例:

  1. 创建活动指向ConnectionCallbacks、OnConnectionFailedListener和LocationListener接口实现。
  2. 在活动的onCreate()方法中,创建GoogleApiClient实例并添加位置服务。
  3. 欲优雅地管理连接的生命周期,在onResume()方法中调用connect()并在onPause()方法中调用disconnect()方法。

以下代码展示包含实现LocationListener接口的活动的实现:

public class WearableMainActivity extends Activity implementsGoogleApiClient.ConnectionCallbacks,GoogleApiClient.OnConnectionFailedListener,LocationListener {private GoogleApiClient mGoogleApiClient;...@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);...mGoogleApiClient = new GoogleApiClient.Builder(this).addApi(LocationServices.API).addApi(Wearable.API)  // used for data layer API.addConnectionCallbacks(this).addOnConnectionFailedListener(this).build();}@Overrideprotected void onResume() {super.onResume();mGoogleApiClient.connect();...}@Overrideprotected void onPause() {super.onPause();...mGoogleApiClient.disconnect();}
}

更多关于连接Google Play服务的信息,见Accessing Google APIs。

(2) 请求位置更新

在应用程序连接到Google Play服务 API后,就应该准备开始接收位置更新。当系统为客户端调用onConnected()时,应该按照如下步骤构建位置数据:

  1. 创建LocationRequest对象并使用像setPriority()方法设置一些选项。
  2. 使用requestLocationUpdates()请求位置更新。
  3. 在onPause()方法中使用removeLocationUpdates()方法移除位置更新。

以下代码样例展示如何检索和移除位置更新:

@Override
public void onConnected(Bundle bundle) {LocationRequest locationRequest = LocationRequest.create().setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY).setInterval(UPDATE_INTERVAL_MS).setFastestInterval(FASTEST_INTERVAL_MS);LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, locationRequest, this).setResultCallback(new ResultCallback() {@Overridepublic void onResult(Status status) {if (status.getStatus().isSuccess()) {if (Log.isLoggable(TAG, Log.DEBUG)) {Log.d(TAG, "Successfully requested location updates");}} else {Log.e(TAG,"Failed in requesting location updates, "+ "status code: "+ status.getStatusCode()+ ", message: "+ status.getStatusMessage());}}});
}@Override
protected void onPause() {super.onPause();if (mGoogleApiClient.isConnected()) {LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);}mGoogleApiClient.disconnect();
}@Override
public void onConnectionSuspended(int i) {if (Log.isLoggable(TAG, Log.DEBUG)) {Log.d(TAG, "connection to location client suspended");}
}

现在已经开启了位置更新,系统会以在setInterval()中指定的间隔用更新的位置调用onLocationChanged()方法。

(3) 检测板上GPS

不是所有的可穿戴设备都有GPS传感器。如果用户出去跑步并将手机留在了家里,可穿戴应用程序不能通过连接接收到位置数据。如果可穿戴设备的确没有传感器,应该能检测这种情况并警示用户位置功能已不可用。

欲检测Android Wear设备是否有一个内建的GPS传感器,使用hasSystemFeature()方法。以下代码片段在开启活动时检测设备是否有内建的GPS:

protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main_activity);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.}...
}private boolean hasGps() {return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS);
}

(4) 处理断开连接事件

依赖线路连接来获取位置数据的可穿戴设备可能会突然失去位置数据。若可穿戴因公程序希望数据流正常,必须处理数据流突然断开或位置数据不可用的情况。在无板上GPS传感器的可穿戴设备上,当断开有线数据连接时位置数据会丢失。

在可穿戴设备无GPS传感器且用有线连接手机来获得位置数据的情况下,应该检测连接是否断开并警示用户,然后优雅的更新应用程序功能。

欲检测有线数据连接的断开:

  1. 扩展WearableListenerService来监听重要的数据层事件。
  2. 在Android清单文件中声明意图过滤器来通知系统WearableListenerService。此过滤器允许系统按照需要绑定服务。
<service android:name=".NodeListenerService"><intent-filter><action android:name="com.google.android.gms.wearable.BIND_LISTENER" /></intent-filter>
</service>
  1. 实现onPeerDisconnected()方法并处理设备是否有GPS的情况。
public class NodeListenerService extends WearableListenerService {private static final String TAG = "NodeListenerService";@Overridepublic void onPeerDisconnected(Node peer) {Log.d(TAG, "You have been disconnected.");if(!hasGPS()) {// Notify user to bring tethered handset// Fall back to functionality that does not use location}}...
}

更多信息见Listen for Data Layer Events手册。

(5) 处理找不到位置

当GPS信号丢失时,可以用getLastLocation()检索最新的可知位置。此方法在GPS没能够启动时或可穿戴设备无内建的GPS并与手机失去连接时的情况下很有用。

以下代码使用getLastLocation()方法检索可用的最新的位置:

Location location = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);

(6) 同步数据

若可穿戴应用程序使用内建的GPS记录数据,可以将位置数据同步到手机上。使用Locationlistener,当位置改变时实现onLocationChanged()方法检测并记录位置数据。

当位置改变时以下可穿戴应用程序的代码检测位置并使用数据层API为稍有手机应用程序的检索存储数据:

@Override
public void onLocationChanged(Location location) {...addLocationEntry(location.getLatitude(), location.getLongitude());}private void addLocationEntry(double latitude, double longitude) {if (!mSaveGpsLocation || !mGoogleApiClient.isConnected()) {return;}mCalendar.setTimeInMillis(System.currentTimeMillis());// Set the path of the data mapString path = Constants.PATH + "/" + mCalendar.getTimeInMillis();PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(path);// Set the location values in the data mapputDataMapRequest.getDataMap().putDouble(Constants.KEY_LATITUDE, latitude);putDataMapRequest.getDataMap().putDouble(Constants.KEY_LONGITUDE, longitude);putDataMapRequest.getDataMap().putLong(Constants.KEY_TIME, mCalendar.getTimeInMillis());// Prepare the data map for the requestPutDataRequest request = putDataMapRequest.asPutDataRequest();// Request the system to create the data itemWearable.DataApi.putDataItem(mGoogleApiClient, request).setResultCallback(new ResultCallback() {@Overridepublic void onResult(DataApi.DataItemResult dataItemResult) {if (!dataItemResult.getStatus().isSuccess()) {Log.e(TAG, "Failed to set the data, "+ "status: " + dataItemResult.getStatus().getStatusCode());}}});
}

关于如何使用数据层API的更多信息见Sending and Syncing Data手册。

7. 请求Android Wear上的权限

如何在Android可穿戴设备上请求权限。

Android 6.0(API level 23)介绍新的权限模式,一些改变使用一些特定的可穿戴设备,一些改变适用于所有的可穿戴设备。

用户必须为可穿戴应用程序和手持应用程序分别授予权限。之前,当用户安装可穿戴应用程序时,它会自动将用户已经授予的权限继承到手持应用程序中。然而,从Android 6.0开始,可穿戴应用程序不再继承这些权限。因此,用户可能需要授予手持应用程序权限以使用位置数据,然后在授予相同权限给可穿戴应用程序。

对于可穿戴和手持应用程序来说,Android 6.0权限模式也是流线应用程序安装并通过用户提前授予每个权限的方式消除需求。相反,应用程序不请求权限指导它们实际需要它们的时候。

注:对于使用新权限模式的应用程序,必须为uses-sdk-element和compileSdkVersion指定23。

本文档剩余内容讨论当开发Android Wear应用程序时如何使用Android 6.0(API level 23)权限模式。

(1) 权限场景

从广义上讲,当在Android Wear删请求dangerous permisions时可能会遇到以下四个场景:

  • Wear应用程序为运行在可穿戴设备上的应用程序请求权限。
  • Wear应用程序为运行在手持设备上的应用程序请求权限。
  • 手持应用程序为运行在可穿戴设备上的应用程序请求权限。
  • 可穿戴应用程序与对应的手持设备应用程序使用不同的权限模型。

此节剩余部分将解释以上的每一个场景。更多关于请求权限的细节信息见Requesting Permissions。

[1] Wear应用程序为运行在可穿戴设备上的应用程序请求权限

当Wear应用程序为运行在可穿戴设备上的应用程序请求权限时,系统展示一个该权限的对话框给用户。应用程序或服务只能在一个活动中调用requestPermissions()方法。若用户通过服务和应用程序交互,诸如表面,服务在请求权限前必须打开活动。

当根据给定的操作弄清楚为何需要权限时应用程序在上下文中请求权限。若应用程序请求确定权限很明显,应用程序可以尝试在启动的时候请求它们。如果不是那么明显,可以选择在尝试一个权限前提供额外的教程。

若应用程序或表面一次请求一个或更多的权限,权限请求一个连接一个出现。

图1. 权限屏幕连续出现

注:从Android 6.0(API level 23)开始,Android Wear自动同步日历、联系人以及位置数据到Wear设备上。作为结果,当Wear请求数据时此场景可应用。

[2] Wear应用程序请求手持设备权限

当Wear 应用程序请求手持设备权限时,Wear应用程序必须发送用户到手持设备以接收该权限。手持应用程序可以通过活动提供额外的教程给用户。活动应该包含两个按钮:一个用于授权,另一个用于否认授权。

图2. 发送用户到手持设备上授权

[3] 手持应用程序请求可穿戴权限

当用户在手持应用程序中为可穿戴请求权限时,手持应用程序必须发送用户到可穿戴设备上以接受该权限。手持应用程序使用可穿戴设备上的requestPermissions()方法来触发系统权限对话框。

图3. 发送用户到可穿戴设备上授权

[4] 可穿戴和手持应用程序的不匹配权限

若手持应用程序开始使用Android 6.0(API level 23)模式但可穿戴应用程序并没有,系统将下载Wear应用程序,但是不会安装它。在用户首次启动该应用程序时,系统尝试授权所有挂起的权限。一段这样做了,他将安装该应用程序。如果应用程序(举例如表面),没有启动器,系统展示通知流来让用户启动应用程序需要的权限。

(2) 请求权限模式

对于用户来说有几种不同的权限请求模式。根据优先级来排列它们:

  • Ask in context - 当权限对某特定功能来说明显需要时,但对应用程序的安装却不是必需的。
  • Educate in context - 当请求权限的原因不是很明显时,权限对于应用程序的运行也不是必需。
  • Ask up front - 当权限的需求明显且权限的需要时为了应用程序的运行。
  • Educate up front - 当权限的需求不那么明显,但权限是运行程序所必需的。

[1] 在上下文中请求

应用程序应该在它知道为何需要权限的情况下请求权限。用户更可能在它们理解它们想使用的特性时授予权限。

举例,应用程序可能需要用户位置以显示附近所被感兴趣的地点。当用户点击搜索附近地点时,应用程序可以理解请求位置权限,因为搜索附近和位置权限有一个明显的关系。这种关系的明显性让应用程序展示额外的教程屏幕变得必要。

图4. 在上下文中请求

[2] 在上下文中提供教程

若有必要,在请求权限前可以选择提供额外的教程。如果应用程序为何要请求该权限来完成某动作的话,应用程序应该为一个特定的操作在上下文中展示该教程。


图5. 上下文教程
图5展示了一个上下文教程的例子。在启动时该应用程序不需要请求权限,但是教程线索出现在活动(位置检测)中的一部分中。当用户按该线索时,权限请求屏幕出现来允许让用户解锁屏幕检测。

可以使用shouldShowRequestPermissionRationale()方法来帮助应用程序决定是否提供更多的信息。更多的信息见Requesting Permissions at Run Time。

[3] Ask up front - 提前请求

若应用程序清楚的知道权限是为了应用程序的运行,可以尝试该权限当用户安装该应用程序时。如地图应用程序很显然需要访问设备的位置。所以没有对改权限做进一步的教程说明。

图6. Asking up front

[4] Educate up front - 提前用教程说明

在某些情况下,应用程序会为一些基本功能请求权限,但是权限请求的原因却不那么明显。在这些情况下,当用户第一次安装程序或设置表面时,应用程序或表面可能需要选择程序按教程说明给用户以请求该权限。

图7. 提前教程说明

[5] 处理权限请求被拒绝的情况

若用户否认了一个对于启动某个活动的关键权限的授予,不要阻碍启动其它的应用程序。如果确定某活动是被权限的否认而被关闭,提供一个视觉上的、可操作的回馈。图8展示了使用上锁图标因为用户不授予权限来使用它。

图8. 上锁图标,展示了某个功能因为被否认了相应的权限而被锁住了

当之前否认的可穿戴权限的对话框出现了一两秒后,它包括否认,不再显示的选项。如果用户选择了该选项,唯一允许该权限来开启该功能的方法进入可穿戴设备的设置中。

图9. 提供不再显示权限的屏幕

(3) 服务权限

正如以上所提到的,只有活动能够调用requestPermissions()方法,所以如果用户通过服务和应用程序交互,如表面,服务必须在请求权限前打开背景活动。该活动能够提供额外的教程说明,或者能够简单地称为不可见的活动并出现一个系统对话框。

如果可穿戴应用程序运行一个不是表面的服务,且用户的确没有启动应用程序且对需要权限也不那么敏感,可以张贴一个教程说明在可穿戴屏幕上。通知可以提供动作打开活动然后出发权限对话框。

注:这只权限请求的通知流的通知的使用。

图10. 服务请求权限

(4) 设置

因为有手持设备,用户可以在设置里面在任何时候改变Wear应用程序的权限。因此,当用户尝试请求权限来做某些事情时,应用程序应该总能够调用checkSelPermission()方法来看应用程序当前是否已经有了执行该操作的权限。应用程序应该执行该检查即使知道用户之前已经授予了该权限,因为用户可能随后会重新调用该权限。

图11. 通过设置应用程序改变(权限)设置

01.13

8. 在可穿戴设备上使用扬声器

如何在Android可穿戴设备上使用扬声器。

一些Android Wear设备包含了扬声器,使用它们嵌入声音到应用程序中并为用户提供额外的使用理由。Wear设备上的扬声器可能会触发时钟或闹铃,通过音频通知完成。通过提供不仅视觉还有音效的Wear上的游戏也会变得更加的好玩。

此小节描述运行在Wear设备(Android 6.0)上的应用程序可以使用类似于Android APIs通过设备扬声器来播放音效。

(1) 检测扬声器

Wear应用程序首先要检测可穿戴设备上是否有扬声器。在以下的例子中,应用程序结合FEATURE_AUDIO_OUTPUT值调用getDevices()方法来确认设备是否自带扬声器:

PackageManager packageManager = context.getPackageManager();
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);// Check whether the device has a speaker.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// Check FEATURE_AUDIO_OUTPUT to guard against false positives.if (!packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) {return false;}AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);for (AudioDeviceInfo device : devices) {if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {return true;}}
}
return false;

(2) 播放音效

一旦检测到扬声器,在Android Wear上播放音效的过程跟手持设备上的一样。更多信息见Media Playback。

若也想在可穿戴设备上从麦克风处记录音效,应用程序必须获取使用麦克风的权限。欲学习更多则见Permission on Android Wear。

[2015.12.23 - 22:01]

pAdTy_5 构建可穿戴设备的应用程序相关推荐

  1. 借助智能手机应用程序和可穿戴设备在夏季塑造身材

    With the advent of the summer sun just starting to peek out, it's time to whip out those running sho ...

  2. adafruit1306_Adafruit的2015年最佳开源可穿戴设备

    adafruit1306 过去一年中,可穿戴电子产品爆炸式增长. 现在市场上有无数的小型设备,不仅用于健身追踪,而且还用于改善姿势,防晒霜提醒,肌肉感应手势控制等等. 随着人体技术的普及比以往任何时候 ...

  3. 可穿戴设备:越来越清晰的苹果iWatch

    可穿戴设备:越来越清晰的苹果iWatch 发表于2013-02-10 11:09| 7473次阅读| 来源CSDN| 20 条评论| 作者张勇 iWatchApple可穿戴设备智能手表 摘要:目前智能 ...

  4. 穿戴设备 之主芯片市场

    小编语:在这些厂商名单中能够看到中国厂商君正和锐迪科的名字小编颇感欣慰,不管芯片真实研发水平是否能和国际大厂抗衡,小编觉得国内业者都要感谢国产芯片公司的努力和付出,因为它们的出现在很大程度上制衡了进口 ...

  5. imx6 休眠 功耗 电流_无线物联网和可穿戴设备的低功耗电源测量挑战

    无线物联网节点和可穿戴设备的功耗和电池的测试挑战在哪里?EEWorldonline此次邀请了测试测量行业的巨头,共同探讨这一问题,其中包括:Keysight Technologies物联网行业解决方案 ...

  6. 构建meteor应用程序_我构建了一个渐进式Web应用程序并将其发布在3个应用程序商店中。 这是我学到的。...

    构建meteor应用程序 by JudahGabriel Himango 犹大(Gabriel Himango) 我构建了一个渐进式Web应用程序并将其发布在3个应用程序商店中. 这是我学到的. (I ...

  7. 如何构建一个简单的语音识别应用程序

    "In this 10-year time frame, I believe that we'll not only be using the keyboard and the mouse ...

  8. 弱性能穿戴设备App化之Lua For STM32

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/hellogv/article/details/26618611 本文来自http://blog.cs ...

  9. 脱离微信,在硬件设备运行小程序?小程序硬件框架大揭秘!

    受访者 | 微信小程序硬件框架团队 采访者 | 伍杏玲 出品 | CSDN(ID:CSDNnews) 在 2017 年的微信公开课 PRO 上,张小龙谈到微信小程序的设计初衷:"我认为所有的 ...

最新文章

  1. 自动回复_小程序消息自动回复
  2. “非深度网络”12层打败50层,普林斯顿+英特尔:更深不一定更好
  3. bzoj 2007 海拔 —— 最短路
  4. 面试题总结~~(google level)
  5. asp.net faq: 在html文件中,用js获取session
  6. Linux程序设计实验项目六,《linux程序设计》实验教学大纲
  7. GIS基础知识汇总篇(五)-无人机真正射影像的概念和制作原理
  8. 单片机的引脚,你都清楚吗?
  9. 用字典给Model赋值
  10. 苹果公司支付1.13亿美元和解“降速门”指控;三大运营商或于年底联合宣布5G消息商用;DBeaver 7.2.5 发布|极客头条...
  11. 网络编程之 socket编程
  12. linux配置java环境变量(详细)(转)
  13. 【三十二】thinkphp之连接数据库、实例化模型
  14. oracle设置memory_target,oracle初始化参数之memory_target
  15. linux通过80端口系统入侵,【转】21和80端口的入侵
  16. 如何在word中对在论文标题添加脚注,并且去掉脚注的编号
  17. 计算机考试记事本创建文件,你可能永远不知道的记事本功能
  18. layui分页和模板引擎
  19. 计算机网盘变成红色是怎么回事,电脑小知识:硬盘变红了会带来哪些危害?
  20. Android SDK 国内镜像

热门文章

  1. 为了更好地了解植物,这些识别植物的软件值得一试
  2. 贾玲版花木兰,文化行业恰到好处的反思
  3. 教程 | 10分钟掌握手帐入门技能
  4. 又出大Bug了!iPhone 13屏幕触控失灵:重启才能解决
  5. 大数乘法(带小数点)
  6. 360浏览器扩展下载网页视频
  7. 特斯拉又有新动作了,真正的全自动驾驶!
  8. vector转换成数组
  9. 【Blender】使用Blender渲染一段360度旋转的动画
  10. Windows Server 2012 如何实现多个用户远程桌面登陆