安卓穿戴项目(五)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:让我们以智能的方式进行聊天 - 通知及其他

前一章帮助我们构建了一个对话式消息应用,但穿戴设备的应用界面非常普通,没有任何通知功能。通知是消息应用非常重要的一个方面,但实现它需要复杂的基础架构来处理通知。当发送者向接收者发送消息时,接收者应该收到一个通知,传达一些信息,比如发送者的名字以及一条快速消息预览。

通知是 Android 中我们可以用来显示信息的组件。在消息应用的情况下,接收者应该收到推送通知来实例化通知组件。所以,每当 Firebase 中的实时数据库更新时,手持设备和穿戴设备都应该收到通知。幸运的是,我们不需要服务器来处理通知;Firebase 会为你的消息应用处理推送通知。当实时数据库有更新时,我们需要触发推送通知。

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

  • Firebase 函数

  • 通知

  • 材料设计穿戴应用

Firebase 函数

Firebase 函数是监控数据库触发器以及许多服务器相关执行的最智能解决方案。我们不需要托管一个服务器来监听 Firebase 数据库的变化,然后再触发推送通知,我们可以利用 Firebase 的一项技术来完成这项任务。Firebase 函数对所有 Firebase 技术都有有效的控制,比如 Firebase 认证、存储、分析等。Firebase 函数可以在各种方面使用;例如,当你的分析群组达到某个里程碑时,你可以发送有针对性的通知、邀请等。任何你可能在 Firebase 系统中实现的服务器级别的业务逻辑都可以用 Firebase 函数来完成。我们将使用 Firebase 函数在数据库触发时发送推送通知。完成这项任务我们需要具备入门级的 JavaScript 知识。

要开始使用 Firebase 函数,我们需要 Node.js 环境,你可以从 nodejs.org/ 安装它。安装 Node.js 时,它还会安装 node 包管理器 (npm)。它有助于安装 JavaScript 框架、插件等。安装 Node.js 后,打开终端或命令行。

输入 $node --version 检查是否安装了 node。如果 CLI 返回最新的版本号,那么你就可以开始了:

//Install Firebase CLI 
$ npm install -g firebase-tools

如果你遇到任何错误,你应该以超级用户模式执行命令:

sudo su
npm install -g firebase-tools

导航到你想保存 Firebase 函数程序的目录,并使用以下命令进行身份验证:

//CLI authentication
$ firebase login

成功认证后,你可以初始化 Firebase 函数:

//Initialise Firebase functions
$ firebase init functions

CLI 工具将在您初始化的目录中生成 Firebase 函数和必要的代码。用您喜欢的编辑器打开 index.js。我们使用 functions.database 创建一个用于处理实时数据库触发器的 Realtime 数据库事件的 Firebase 函数。我们将调用 ref(path),以从函数中达到特定的数据库 pathonwrite() 方法,该方法将在数据库更新时发送通知。

现在,为了构建我们的通知负载,让我们了解我们的实时数据库结构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以看到,消息有一个名为 ashok_geetha 的子节点,这意味着这两个用户的对话存储在其中,具有唯一的 Firebase ID。对于此实现,我们将选择 ashok_geetha 以推送通知。

现在,在 index.js 文件中,添加以下代码:

//import firebase functions modules
const functions = require('firebase-functions');
//import admin module
        const admin = require('firebase-admin');
        admin.initializeApp(functions.config().firebase);

// Listens for new messages added to messages/:pushId
        exports.pushNotification = functions.database
        .ref('/messages/ashok_geetha/{pushId}').onWrite( event => {

        console.log('Push notification event triggered');

        //  Grab the current value of what was written to the Realtime 
        Database.
        var valueObject = event.data.val();

        if(valueObject.photoUrl != null) {
        valueObject.photoUrl= "Sent you a lot of love!";
        }

        // Create a notification
        const payload = {
        notification: {
        title:valueObject.message,
        body: valueObject.user || valueObject.photoUrl,
        sound: "default"
        },
        };

        //Create an options object that contains the time to live for 
        the notification and the priority
        const options = {
        priority: "high",
        timeToLive: 60 * 60 * 24
        };

        return admin.messaging().sendToTopic("pushNotifications", 
        payload, options);
        });

当我们拥有不同的 Firebase 结构时,通过使用 url-id 配置,我们可以触发 Firebase 函数,以使推送通知能够与所有用户一起工作。我们只需在 url 中进行以下更改即可。 /messages/{ChatID}/{pushId}

现在,在终端中,使用 $ firebase deploy 命令完成 Firebase 函数的部署:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

之前的 Node.js 设置使用 Firebase 函数触发推送通知。现在,我们需要一种机制,以便在我们的本地移动和穿戴应用程序中接收来自 Firebase 的消息。

切换到 Android Studio,并将以下依赖项添加到您的移动项目 gradle 模块文件中:

//Add the dependency
dependencies {
     compile 'com.google.firebase:firebase-messaging:10.2.4'
}

添加依赖项后,创建一个名为 MessagingService 的类,该类扩展自 FirebaseMessagingService 类。FirebaseMessaging 类扩展自 com.google.firebase.iid.zzb,而 zzb 类扩展自 Android Service 类。这个类将帮助 Firebase 消息传递和 Android 应用之间的通信过程。它还可以自动显示通知。让我们创建一个类并将其扩展到 FirebaseMessagingService 类,如下所示:

public class MessagingService extends FirebaseMessagingService {

    //Override methods
}

是时候添加 onMessageReceived 重写方法了。当应用在前台或后台时,该方法接收通知,我们可以使用 getnotification() 方法检索所有通知参数:

@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
    super.onMessageReceived(remoteMessage);
}

RemoteMessage 对象将包含我们从 Firebase 函数中的通知负载中发送的所有数据。在方法内部,添加以下代码以获取标题和消息内容。我们在 Firebase 函数上发送标题参数中的消息;您可以根据自己的用例进行自定义:

String notificationTitle = null, notificationBody = null;
// Check if message contains a notification payload.
if (remoteMessage.getNotification() != null) {
    Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
    notificationTitle = remoteMessage.getNotification().getTitle();
    notificationBody = remoteMessage.getNotification().getBody();
}

为了构建通知,我们将使用 NotificationCompat.Builder,当用户点击通知时,我们会将他带到 MainActivity

private void sendNotification(String notificationTitle, String notificationBody) {
    Intent intent = new Intent(this, MainActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, 
    intent,
            PendingIntent.FLAG_ONE_SHOT);

    Uri defaultSoundUri= RingtoneManager.getDefaultUri
    (RingtoneManager.TYPE_NOTIFICATION);
    NotificationCompat.Builder notificationBuilder = 
    (NotificationCompat.Builder) new NotificationCompat.Builder(this)
            .setAutoCancel(true)   //Automatically delete the 
                                   notification
            .setSmallIcon(R.mipmap.ic_launcher) //Notification icon
            .setContentIntent(pendingIntent)
            .setContentTitle(notificationTitle)
            .setContentText(notificationBody)
            .setSound(defaultSoundUri);

    NotificationManager notificationManager = (NotificationManager) 
    getSystemService(Context.NOTIFICATION_SERVICE);

    notificationManager.notify(0, notificationBuilder.build());
}

onMessageReceived 内部调用该方法,并将内容传递给 sendNotification 方法:

sendNotification(notificationTitle, notificationBody);

完整的类代码如下所示:

public class MessagingService extends FirebaseMessagingService {

    private static final String TAG = "MessagingService";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        String notificationTitle = null, notificationBody = null;
        // Check if message contains a notification payload.
        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification Body: " + 
            remoteMessage.getNotification().getBody());
            notificationTitle = 
            remoteMessage.getNotification().getTitle();
            notificationBody = 
            remoteMessage.getNotification().getBody();

            sendNotification(notificationTitle, notificationBody);

        }
    }

    private void sendNotification(String notificationTitle, String 
    notificationBody) {
        Intent intent = new Intent(this, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 
       0, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri= RingtoneManager.getDefaultUri
        (RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder =     
        (NotificationCompat.Builder) 
        new NotificationCompat.Builder(this)
                .setAutoCancel(true)   //Automatically delete the 
                                  notification
                .setSmallIcon(R.mipmap.ic_launcher) //Notification icon
                .setContentIntent(pendingIntent)
                .setContentTitle(notificationTitle)
                .setContentText(notificationBody)
                .setSound(defaultSoundUri);

        NotificationManager notificationManager = (NotificationManager) 
       getSystemService(Context.NOTIFICATION_SERVICE);

        notificationManager.notify(0, notificationBuilder.build());
    }
}

我们现在可以在清单中注册之前的服务:

<service
    android:name=".MessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
</service>

毕竟,我们现在有一个用于监听 pushNotification 的服务,但我们需要监听我们发送的特定字符串。我们可以将字符串添加到一些常量中或 XML 文件中,但是当我们要求 Firebase 发送特定频道的通知时,我们需要订阅一个称为主题的频道。在 ChatActivity 中添加以下代码,并在方法内部:

FirebaseMessaging.getInstance().subscribeToTopic("pushNotifications");

为了使之前的操作全局化,创建一个扩展 Application 类的类。在 onCreate 方法内部,我们可以按照以下方式订阅主题:

public class PacktApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Firebase.setAndroidContext(this);
        FirebaseMessaging.getInstance()
        .subscribeToTopic("pushNotifications");
    }
}

现在,我们需要在清单中注册应用程序类。应用程序类控制着应用程序生命周期的 onCreate 方法,这将有助于维护应用程序的生命状态:

<application
    android:name=".PacktApp"
    ...>

</application>

恭喜!我们已经成功配置了推送通知,并在我们的手机上收到了通知:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在已连接的 Wear 设备中,当我们收到通知时,我们将能够看到以下通知。默认情况下,NotificationCompat.Builder 类可以帮助 Wear 设备接收通知,如果我们想要自定义它,可以通过以下部分进行操作。

从 Wear 通知组件,我们可以直接从 Wear 设备接收回复到移动应用程序。为了实现这一点,我们将使用 NotificationCompat 类中的 WearExtender 组件。使用此设置,用户将能够使用语音输入、输入法框架IMF)和表情符号回复通知:

 Notification notification = new NotificationCompat.Builder(mContext)
         .setContentTitle("New mail from " + sender.toString())
         .setContentText(subject)
         .setSmallIcon(R.drawable.new_mail)
         .extend(new NotificationCompat.WearableExtender()
                 .setContentIcon(R.drawable.new_mail))
         .build();
 NotificationManagerCompat.from(mContext).notify(0, notification);

将会有很多用例,我们需要发送带有已经存储的回复的快速响应,或者快速输入功能。在这种情况下,我们可以使用 Action.WearableExtender

/Android Wear requires a hint to display the reply action inline.
Action.WearableExtender actionExtender =
    new Action.WearableExtender()
        .setHintLaunchesActivity(true)
        .setHintDisplayActionInline(true);
wearableExtender.addAction(actionBuilder.extend(actionExtender).build());

现在,在项目中,让我们更新我们的消息服务类,当后台服务接收到推送通知时,我们会触发通知组件。

将以下实例添加到类的全局作用域中:

private static final String TAG = "MessagingService";
public static final String EXTRA_VOICE_REPLY = "extra_voice_reply";
public static final String REPLY_ACTION =
        "com.packt.smartcha.ACTION_MESSAGE_REPLY";

当我们从 Wear 设备收到回复时,我们将保留该引用并将其传递给通知处理程序:

// Creates an Intent that will be triggered when a reply is received.
private Intent getMessageReplyIntent(int conversationId) {
    return new Intent().setAction(REPLY_ACTION).putExtra("1223", 
    conversationId);
}

Wear 通知随着每个新的 Android 版本的发布而不断发展,在 NotificationCompat.Builder 中,我们有所有可以使您的移动应用程序与 Wear 设备交互的功能。当您有一个移动应用程序,并且它有来自 Wear 设备的交互,如通知等,即使没有 Wear 伴侣应用程序,您也可以获得文本回复。

消息服务类

MessagingService 类中,我们有一个名为 sendNotification 的方法,该方法向 Wear 和移动设备发送通知。让我们用以下代码更改更新该方法:

private void sendNotification(String notificationTitle, String notificationBody) {
    // Wear 2.0 allows for in-line actions, which will be used for 
    "reply".
    NotificationCompat.Action.WearableExtender inlineActionForWear2 =
            new NotificationCompat.Action.WearableExtender()
                    .setHintDisplayActionInline(true)
                    .setHintLaunchesActivity(false);

    RemoteInput remoteInput = new 
    RemoteInput.Builder("extra_voice_reply").build();

    // Building a Pending Intent for the reply action to trigger.
    PendingIntent replyIntent = PendingIntent.getBroadcast(
            getApplicationContext(),
            0,
            getMessageReplyIntent(1),
            PendingIntent.FLAG_UPDATE_CURRENT);

    // Add an action to allow replies.
    NotificationCompat.Action replyAction =
            new NotificationCompat.Action.Builder(
                    R.drawable.googleg_standard_color_18,
                    "Notification",
                    replyIntent)

                    /// TODO: Add better wear support.
                    .addRemoteInput(remoteInput)
                    .extend(inlineActionForWear2)
                    .build();

    Intent intent = new Intent(this, ChatActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, 
    intent,
            PendingIntent.FLAG_ONE_SHOT);

    Uri defaultSoundUri = RingtoneManager.getDefaultUri
    (RingtoneManager.TYPE_NOTIFICATION);
    NotificationCompat.Builder notificationBuilder =      
    (NotificationCompat.Builder) 
    new NotificationCompat.Builder(this)
            .setAutoCancel(true)   //Automatically delete the     
            notification
            .setSmallIcon(R.mipmap.ic_launcher) //Notification icon
            .setContentIntent(pendingIntent)
            .addAction(replyAction)
            .setContentTitle(notificationTitle)
            .setContentText(notificationBody)
            .setSound(defaultSoundUri);

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

前一个方法在 Wear 设备上具有 IMF 输入、语音回复和绘制表情符号的功能。修改代码后完整的类如下所示:

package com.packt.smartchat;

/**
 * Created by ashok.kumar on 02/06/17.
 */

public class MessagingService extends FirebaseMessagingService {

    private static final String TAG = "MessagingService";
    public static final String EXTRA_VOICE_REPLY = "extra_voice_reply";
    public static final String REPLY_ACTION =
            "com.packt.smartcha.ACTION_MESSAGE_REPLY";
            public static final String SEND_MESSAGE_ACTION =
            "com.packt.smartchat.ACTION_SEND_MESSAGE";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        String notificationTitle = null, notificationBody = null;
        // Check if message contains a notification payload.
        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification Body: " + 
            remoteMessage.getNotification().getBody());
            notificationTitle = 
            remoteMessage.getNotification().getTitle();
            notificationBody = 
            remoteMessage.getNotification().getBody();

            sendNotification(notificationTitle, notificationBody);

        }
    }

    // Creates an intent that will be triggered when a message is read.
    private Intent getMessageReadIntent(int id) {
        return new Intent().setAction("1").putExtra("1482", id);
    }

    // Creates an Intent that will be triggered when a reply is 
    received.
    private Intent getMessageReplyIntent(int conversationId) {
        return new Intent().setAction(REPLY_ACTION).putExtra("1223", 
        conversationId);
    }

    private void sendNotification(String notificationTitle, String 
    notificationBody) {
        // Wear 2.0 allows for in-line actions, which will be used for 
        "reply".
        NotificationCompat.Action.WearableExtender 
        inlineActionForWear2 =
                new NotificationCompat.Action.WearableExtender()
                        .setHintDisplayActionInline(true)
                        .setHintLaunchesActivity(false);

        RemoteInput remoteInput = new 
        RemoteInput.Builder("extra_voice_reply").build();

        // Building a Pending Intent for the reply action to trigger.
        PendingIntent replyIntent = PendingIntent.getBroadcast(
                getApplicationContext(),
                0,
                getMessageReplyIntent(1),
                PendingIntent.FLAG_UPDATE_CURRENT);

        // Add an action to allow replies.
        NotificationCompat.Action replyAction =
                new NotificationCompat.Action.Builder(
                        R.drawable.googleg_standard_color_18,
                        "Notification",
                        replyIntent)

                        /// TODO: Add better wear support.
                        .addRemoteInput(remoteInput)
                        .extend(inlineActionForWear2)
                        .build();

        Intent intent = new Intent(this, ChatActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 
        0, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri = RingtoneManager.getDefaultUri
         (RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder = 
        (NotificationCompat.Builder) new 
         NotificationCompat.Builder(this)
                .setAutoCancel(true)   //Automatically delete the 
                notification
                .setSmallIcon(R.mipmap.ic_launcher) //Notification icon
                .setContentIntent(pendingIntent)
                .addAction(replyAction)
                .setContentTitle(notificationTitle)
                .setContentText(notificationBody)
                .setSound(defaultSoundUri);

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

收到的通知将如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当用户点击通知时,他将或她将得到以下三个选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 Wear 设备上收到通知后,用户可以通过文本或语音输入,或者借助表情符号来回复他们的想法。为了处理这种情况,我们需要编写一个广播接收器。让我们创建一个名为MessageReplyReceiver的类,继承自BroadcastReceiver类,并重写onReceive方法。当你收到回复时,只需按照以下方式使用conversationId更新意图:

@Override
public void onReceive(Context context, Intent intent) {
    if (MessagingService.REPLY_ACTION.equals(intent.getAction())) {
        int conversationId = intent.getIntExtra("reply", -1);
        CharSequence reply = getMessageText(intent);
        if (conversationId != -1) {
            Log.d(TAG, "Got reply (" + reply + ") for ConversationId " 
            + conversationId);
        }
        // Tell the Service to send another message.
        Intent serviceIntent = new Intent(context, 
        MessagingService.class);
        serviceIntent.setAction(MessagingService.SEND_MESSAGE_ACTION);
        context.startService(serviceIntent);
    }
}

remoteIntent对象中,为了接收数据并将意图数据转换为文本,使用以下方法:

private CharSequence getMessageText(Intent intent) {
    Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
    if (remoteInput != null) {
        return 
        remoteInput.getCharSequence
        (MessagingService.EXTRA_VOICE_REPLY);
    }
    return null;
}

MessageReplyReceiver类的完整代码如下:

public class MessageReplyReceiver extends BroadcastReceiver {

    private static final String TAG = 
    MessageReplyReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {
        if (MessagingService.REPLY_ACTION.equals(intent.getAction())) {
            int conversationId = intent.getIntExtra("reply", -1);
            CharSequence reply = getMessageText(intent);
            if (conversationId != -1) {
                Log.d(TAG, "Got reply (" + reply + ") for 
                ConversationId " + conversationId);
            }
            // Tell the Service to send another message.
            Intent serviceIntent = new Intent(context,    
            MessagingService.class);
            serviceIntent.setAction
           (MessagingService.SEND_MESSAGE_ACTION);
            context.startService(serviceIntent);
        }
    }

    private CharSequence getMessageText(Intent intent) {
        Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
        if (remoteInput != null) {
            return remoteInput.getCharSequence
            (MessagingService.EXTRA_VOICE_REPLY);
        }
        return null;
    }
}

之后,在清单文件中如下注册broadcastreceiver

<receiver android:name=".MessageReplyReceiver">
    <intent-filter>
        <action 
        android:name="com.packt.smartchat.ACTION_MESSAGE_REPLY"/>
    </intent-filter>
</receiver>

现在,我们已经完全准备好接收来自 Wear 通知组件到本地应用程序的数据。

从 Wear 通知组件的语音输入屏幕将如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个屏幕上使用绘图表情,Android 会预测你绘制了什么:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

IMF 可以用来通过键入输入进行回复:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总结

在本章中,你已经学习了如何使用 Firebase 函数发送推送通知,以及如何使用来自 Wear 支持库的通知组件。通知是智能设备中的核心组件;它们通过提醒用户发挥着至关重要的作用。我们已经理解了NotificationCompat.Builder类和WearableExtender类。我们还探索了输入方法框架以及使用多种回复机制(如表情符号、语音支持等)进行回复的最简单方式。

第十章:只为时间而生 - 手表表盘与服务

表盘,也称为表盘,是显示固定数字和移动指针的时间的部分。表盘的外观可以通过各种艺术手法和创新来设计。设计传统手表表盘是一门优美的艺术;手表表盘设计师会知道雕刻和设计传统可穿戴手表表盘需要什么。在安卓穿戴中,这个过程非常相似,除了作为表盘制作者,你手中没有任何工具,但需要知道你需要扩展哪个服务以及哪段代码可以帮助你自定义表盘的外观和感觉。表盘将显示时间和日期。在安卓穿戴中,表盘可以是模拟的,也可以是数字的。

安卓穿戴的表盘是包装在可穿戴应用内的服务。当用户选择可用的某个表盘时,可穿戴设备会显示表盘并调用其服务回调方法。自定义表盘使用动态的数字画布,可以融入色彩、活动和相关数据。在安卓穿戴中安装可穿戴表盘应用后,我们可以通过表盘选择器在不同的表盘之间切换。用户可以通过手机上的谷歌应用商店中的配套应用程序,在他们的手表上安装各种表盘。在本章中,你将学习以下主题:

  • CanvasWatchFaceService 类和注册你的手表表盘

  • CanvasWatchFaceService.Engine 和回调方法

  • 编写表盘和处理手势和点击事件

  • 理解表盘元素并初始化它们

  • 常见问题

CanvasWatchFaceService 类和注册你的手表表盘

表盘是具有绘图和视觉渲染能力的服务;所有表盘都将扩展CanvasWatchFaceService类。CanvasWatchFaceService类从WallpaperSeviceWallpaperService.Engine类中提取其功能。Engine类及其回调方法帮助表盘处理其生命周期。如果你需要为 Android Wear 制作一个表盘,你应该使用CanvasWatchfaceService类,而不是普通的旧WallpaperService。表盘服务与壁纸服务一样,只需实现onCreateEngine()方法。表盘引擎需要实现onTimeTick()方法以刷新时间并更新视图,以及onAmbientModeChanged(boolean)方法以在不同版本的表盘之间切换,例如灰色模式和彩色表盘。表盘引擎同样实现onInterruptionFilterChanged(int)以根据用户查询的信息量更新视图。对于在环境模式下发生的更新,将持有wake_lock,这样设备在表盘完成绘图过程之前不会进入休眠状态。在应用程序中注册表盘与注册壁纸非常相似,但需要几个额外的步骤。然而,表盘需要wake_lock权限,如下所示:

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

之后,你的表盘服务声明需要预览元数据:

 <meta-data
     android:name="com.google.android.wearable.watchface.preview"
     android:resource="@drawable/preview_face" />
 <meta-data
     android:name=
     "com.google.android.wearable.watchface.preview_circular"
     android:resource="@drawable/preview_face_circular" />

最后,我们需要添加一个特殊的意图过滤器,以便手表能够识别。

 <intent-filter>
     <action android:name="android.service.wallpaper.WallpaperService" 
     />
     <category
         android:name=
         "com.google.android.wearable.watchface.category.WATCH_FACE" />
 </intent-filter>

CanvasWatchFaceService.Engine

CanvasWatchFaceService.Engine类扩展了WatchFaceService.Engine类。在这里,可以在画布上绘制表盘的实际实现可以完成。我们应该实现onCreateEngine()以恢复你的具体引擎实现。CanvasWatchFaceService.Engine有一个公共构造函数和几个过程,使我们能够实现表盘。让我们探讨一下我们将在本章后半部分实现的一些方法:

  • void invalidate ():计划调用onDraw(Canvas, Rect)以绘制下一帧。这必须在主线程上处理。

  • void onDestroy ():在这个回调中,我们可以释放用于完成表盘的硬件和其他资源。

  • void onDraw(Canvas canvas, Rect bounds):绘制表盘、所有视觉组件以及时钟更新逻辑,此方法中完成其他时钟排列。

  • void onSurfaceChanged():这个方法包含四个参数,void onSurfaceChanged (SurfaceHolder holder, int organise, int width, int stature)SurfaceHolder参数允许你控制表面大小和不同的排列方式。

  • void postInvalidate():发布一个消息以安排调用onDraw(Canvas, Rect)以绘制下一帧。此外,这个方法是线程安全的。我们可以从任何线程调用这个方法。

这些方法在规划您的表盘时起着重要的作用。让我们开始制作表盘。在以下练习中,我们将了解如何制作数字表盘。

编写自己的表盘

Android Studio 是我们应该使用的主要工具,用于编写 Wear 应用,原因有很多;由于我们已经为 Wear 2.0 开发配置了开发环境,因此这不应成为挑战。让我们启动 Android Studio 并创建一个 Wear 项目。

在活动选择器中,选择添加无活动。由于表盘是一个服务,我们不需要活动:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们在前一节中读到,我们将扩展类到CanvasWatchFaceService,在画布上绘制表盘,另一个类是CanvasWatchFaceService.Engine**,**在那里我们将处理表盘的实际实现以及我们讨论的更重要方法。这将帮助我们实现表盘的必要设置。

现在,在包中创建一个名为PacktWatchFace的类文件。

PacktWatchFace类将扩展到CanvasWatchFaceService类:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建类文件后,将其扩展到**CanvasWatchFaceService**类;这是我们将在清单中注册的服务类。在这个类内部,我们需要为 Wear 设计实现创建一个子类。之后,在同一个类中,我们需要覆盖onCreateEngine()方法。

以下代码是 Wear 表盘设计的入口点设置:

public class PacktWatchFace extends CanvasWatchFaceService{

@Override
public Engine onCreateEngine() {
    return new Engine();
}

private class Engine extends CanvasWatchFaceService.Engine{

  }
}

PacktWatchFace类仅实现一个方法onCreateEngine(),该方法返回CanvasWatchFaceService.Engine的实际实现。现在,是时候在清单文件中注册watchFace服务类了。

在清单注册的应用范围内,添加PacktWatchFace类:

<service
    android:name=".PacktWatchFace"
    android:label="PacktWatchface Wear"
    android:permission="android.permission.BIND_WALLPAPER" >
    <meta-data
        android:name="android.service.wallpaper"
        android:resource="@xml/watch_face" />
    <meta-data
        android:name="com.google.android.wearable.watchface.preview"
        android:resource="@mipmap/ic_launcher" />
    <meta-data
        android:name=
        "com.google.android.wearable.watchface.preview_circular"
        android:resource="@mipmap/ic_launcher" />

    <intent-filter>
        <action android:name=
        "android.service.wallpaper.WallpaperService" />

        <category android:name=
        "com.google.android.wearable.watchface.category.WATCH_FACE" />
    </intent-filter>
</service>

res目录下的xml文件夹中创建一个文件,并将其命名为watch_face.xml。在内部,添加如下wallpaper XML 标签:

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

表盘服务声明需要预览元数据,如前所示。在可穿戴设备的表盘选择器预览中也会使用相同的元数据。这些元素将指定表盘服务的元数据。表盘将使用我们在元数据标签中提供的预览图片和其他信息。

在您的清单中添加以下权限:

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

让我们设置图形元素和年代实例的全局基本实例:

//Essential instances
private static Paint textPaint, boldTextPaint, backGround, whiteBackground, darkText;
private static Calendar calendar;
private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
private static final int MSG_UPDATE_TIME = 0;

onDraw方法中,我们可以绘制需要在表盘上显示的内容。绘制的视觉将保持静态;仅通过在画布上绘制,我们不能使其动态。时间显示的实现起着重要的作用。

表盘需要显示的信息和其他外观由表盘设计师决定。现在,让我们在onCreate方法中初始化所有实例:

backGround = new Paint() {{ setARGB(255, 120, 190, 0); }};
textPaint = createPaint(false, 40);
boldTextPaint = createPaint(true, 40);
whiteBackground = createPaint(false, 0);
darkText = new Paint() {{ setARGB(255, 50, 50, 50); setTextSize(18); }};

接下来,我们将编写一个单独的方法createPaint(),用于返回所有调用的值:

private Paint createPaint(final boolean bold, final int fontSize) {
    final Typeface typeface = (bold) ? Typeface.DEFAULT_BOLD : 
    Typeface.DEFAULT;

    return new Paint()
    {{
        setARGB(255, 255, 255, 255);
        setTextSize(fontSize);
        setTypeface(typeface);
        setAntiAlias(true);
    }};
}

处理点击事件和手势

在表盘上,用户可以进行交互,但CanvasWatchService.Engine类仅提供了一个交互方法,即单次点击。如果我们想要有其他的交互方式,需要重写onTapCommand方法。我们还需要在onCreate方法中通过更改 Wear 应用的风格来请求tapevents

setWatchFaceStyle(new WatchFaceStyle.Builder(PacktWatchFace.this)
    .setAcceptsTapEvents(true)
    .build());

之后,我们可以重写onTapCommand()方法来处理点击事件,并且可以重写函数,以便在用户点击应用时提供功能和服务。

以下代码展示了当用户点击表盘时显示的提示信息:

@Override
public void onTapCommand(
        @TapType int tapType, int x, int y, long eventTime) {
    switch (tapType) {
        case WatchFaceService.TAP_TYPE_TAP:
            // Handle the tap
            Toast.makeText(PacktWatchFace.this, "Tapped", 
            Toast.LENGTH_SHORT).show();
            break;

        // There are other cases, not mentioned here. <a 
        href="https://developer.android.com/training/wearables/watch-
        faces/interacting.html">Read Android guide</a>
        default:
            super.onTapCommand(tapType, x, y, eventTime);
            break;
    }
}

这样,我们可以自定义点击功能。默认的函数签名提供了两个坐标,xy;通过使用这些坐标,我们可以确定用户点击的位置,这有助于表盘设计师相应地自定义手势和点击事件。

支持不同的形态因素

Android Wear 设备有方形和矩形设计。让表盘在这两种形态上看起来一样是表盘开发者的责任。大多数为矩形显示屏设计的 UI 布局在圆形显示屏上会失败,反之亦然。为了解决这个问题,WallpaperService Engine 有一个名为onApplyWindowInsets的函数。onApplyWindowInsets方法帮助检查设备是否为圆形;通过确定这一点,我们可以绘制圆形或方形表盘:

@Override
public void onApplyWindowInsets(WindowInsets insets) {
    super.onApplyWindowInsets(insets);
    boolean isRound = insets.isRound();

    if(isRound) {
        // Render the Face in round mode.
    } else {
        // Render the Face in square (or rectangular) mode.
    }
}

现在,让我们编写一个能够定时更新表盘的完整方法:

  @Override
        public void onDraw(Canvas canvas, Rect bounds) {
            calendar = Calendar.getInstance();

            canvas.drawRect(0, 0, bounds.width(), bounds.height(), 
            whiteBackground); // Entire background Canvas
            canvas.drawRect(0, 60, bounds.width(), 240, backGround);

            canvas.drawText(new SimpleDateFormat("cccc")
            .format(calendar.getTime()), 130, 120, textPaint);

            // String time = String.format
            ("%02d:%02d", mTime.hour, mTime.minute);
            String time = new SimpleDateFormat
            ("hh:mm a").format(calendar.getTime());
            canvas.drawText(time, 130, 170, boldTextPaint);

            String date = new SimpleDateFormat
            ("MMMM dd, yyyy").format(calendar.getTime());
            canvas.drawText(date, 150, 200, darkText);
        }

onVisibilityChanged方法帮助注册和解注册告知表盘时间的接收器:

@Override
public void onVisibilityChanged(boolean visible) {
    super.onVisibilityChanged(visible);

    if (visible) {
        registerReceiver();
        // Update time zone in case it changed while we weren't 
        visible.
        calendar = Calendar.getInstance();
    } else {
        unregisterReceiver();
    }

    // Whether the timer should be running depends on whether we're 
    visible (as well as
    // whether we're in ambient mode), so we may need to start or stop 
    the timer.
    updateTimer();
}

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

private void unregisterReceiver() {
    if (!mRegisteredTimeZoneReceiver) {
        return;
    }
    mRegisteredTimeZoneReceiver = false;
    PacktWatchFace.this.unregisterReceiver(mTimeZoneReceiver);
}

/**
 * Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
 * or stops it if it shouldn't be running but currently is.
 */
private void updateTimer() {
    mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
    if (shouldTimerBeRunning()) {
        mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
    }
}

为了确保计时器只在表盘可见时运行,我们将设置以下配置:

private boolean shouldTimerBeRunning() {
    return isVisible() && !isInAmbientMode();
}

要在表盘中定期更新时间,请执行以下操作:

 private void handleUpdateTimeMessage() {
        invalidate();
        if (shouldTimerBeRunning()) {
            long timeMs = System.currentTimeMillis();
            long delayMs = INTERACTIVE_UPDATE_RATE_MS
                    - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
            mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, 
            delayMs);
        }
    }
}

现在,让我们用WeakReference类的实现来完善代码。弱引用对象将允许引用对象被最终确定,并且可以在之后访问。弱引用将使所有之前弱可达的对象被最终确定。最后,它将把那些最近清除的、注册在引用队列中的弱引用入队:

private static class EngineHandler extends Handler {
    private final WeakReference<Engine> mWeakReference;

    public EngineHandler(PacktWatchFace.Engine reference) {
        mWeakReference = new WeakReference<>(reference);
    }

    @Override
    public void handleMessage(Message msg) {
        PacktWatchFace.Engine engine = mWeakReference.get();
        if (engine != null) {
            switch (msg.what) {
                case MSG_UPDATE_TIME:
                    engine.handleUpdateTimeMessage();
                    break;
            }
        }
    }
}

要添加一个可绘制对象,我们可以使用BitmapFactory类:

bitmapObj = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);

// And set the bitmap object in onDraw methods canvas            canvas.drawBitmap(bob, 0, 40, null);

既然完整的逻辑定义已经完成,让我们看看最终定稿的表盘类:

/**
 * Created by ashok.kumar on 27/04/17.
 */

public class PacktWatchFace extends CanvasWatchFaceService{

    //Essential instances
    private static Paint textPaint, boldTextPaint, backGround, 
    whiteBackground, darkText;
    private static Calendar calendar;
    private static final long INTERACTIVE_UPDATE_RATE_MS = 
    TimeUnit.SECONDS.toMillis(1);
    private static final int MSG_UPDATE_TIME = 0;

    @Override
    public Engine onCreateEngine() {
        return new Engine();
    }

    private class Engine extends CanvasWatchFaceService.Engine {

        final Handler mUpdateTimeHandler = new EngineHandler(this);

        final BroadcastReceiver mTimeZoneReceiver = new 
        BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                calendar = Calendar.getInstance();
            }
        };
        boolean mRegisteredTimeZoneReceiver = false;

        boolean mLowBitAmbient;

        @Override
        public void onCreate(SurfaceHolder holder) {
            super.onCreate(holder);

            setWatchFaceStyle(new WatchFaceStyle.Builder
            (PacktWatchFace.this)
                    .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
                    .setBackgroundVisibility
                    (WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
                    .setShowSystemUiTime(false)
                    .build());

            backGround = new Paint() {{ setARGB(255, 120, 190, 0); }};
            textPaint = createPaint(false, 40);
            boldTextPaint = createPaint(true, 40);
            whiteBackground = createPaint(false, 0);
            darkText = new Paint() 
            {{ setARGB(255, 50, 50, 50); setTextSize(18); }};

            setWatchFaceStyle(new WatchFaceStyle.Builder
            (PacktWatchFace.this)
                    .setAcceptsTapEvents(true)
                    .build());

            calendar = Calendar.getInstance();
        }

        private Paint createPaint
        (final boolean bold, final int fontSize) {
            final Typeface typeface = 
            (bold) ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;

            return new Paint()
            {{
                setARGB(255, 255, 255, 255);
                setTextSize(fontSize);
                setTypeface(typeface);
                setAntiAlias(true);
            }};
        }

        @Override
        public void onTapCommand(
                @TapType int tapType, int x, int y, long eventTime) {
            switch (tapType) {
                case WatchFaceService.TAP_TYPE_TAP:
                    // Handle the tap
                    Toast.makeText(PacktWatchFace.this, 
                    "Tapped", Toast.LENGTH_SHORT).show();
                    break;

                default:
                    super.onTapCommand(tapType, x, y, eventTime);
                    break;
            }
        }

        @Override
        public void onDestroy() {
            mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
            super.onDestroy();
        }

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

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

        @Override
        public void onAmbientModeChanged(boolean inAmbientMode) {
            super.onAmbientModeChanged(inAmbientMode);
            if (inAmbientMode) {
                if (mLowBitAmbient) {
                }
                invalidate();
            }

            updateTimer();
        }

        @Override
        public void onDraw(Canvas canvas, Rect bounds) {
            calendar = Calendar.getInstance();

            canvas.drawRect(0, 0, bounds.width(), bounds.height(), 
            whiteBackground); // Entire background Canvas
            canvas.drawRect(0, 60, bounds.width(), 240, backGround);

            canvas.drawText(new SimpleDateFormat("cccc")
            .format(calendar.getTime()), 130, 120, textPaint);

            // String time = String.format("%02d:%02d", mTime.hour, 
            mTime.minute);
            String time = new SimpleDateFormat
            ("hh:mm a").format(calendar.getTime());
            canvas.drawText(time, 130, 170, boldTextPaint);

            String date = new SimpleDateFormat
            ("MMMM dd, yyyy").format(calendar.getTime());
            canvas.drawText(date, 150, 200, darkText);
        }

        @Override
        public void onVisibilityChanged(boolean visible) {
            super.onVisibilityChanged(visible);

            if (visible) {
                registerReceiver();
                calendar = Calendar.getInstance();
            } else {
                unregisterReceiver();
            }

            updateTimer();
        }

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

        private void unregisterReceiver() {
            if (!mRegisteredTimeZoneReceiver) {
                return;
            }
            mRegisteredTimeZoneReceiver = false;
            PacktWatchFace.this.unregisterReceiver(mTimeZoneReceiver);
        }

        private void updateTimer() {
            mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
            if (shouldTimerBeRunning()) {
                mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
            }
        }

        private boolean shouldTimerBeRunning() {
            return isVisible() && !isInAmbientMode();
        }

        private void handleUpdateTimeMessage() {
            invalidate();
            if (shouldTimerBeRunning()) {
                long timeMs = System.currentTimeMillis();
                long delayMs = INTERACTIVE_UPDATE_RATE_MS
                        - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
                mUpdateTimeHandler
                .sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
            }
        }
    }

    private static class EngineHandler extends Handler {
        private final WeakReference<Engine> mWeakReference;

        public EngineHandler(PacktWatchFace.Engine reference) {
            mWeakReference = new WeakReference<>(reference);
        }

        @Override
        public void handleMessage(Message msg) {
            PacktWatchFace.Engine engine = mWeakReference.get();
            if (engine != null) {
                switch (msg.what) {
                    case MSG_UPDATE_TIME:
                        engine.handleUpdateTimeMessage();
                        break;
                }
            }
        }
    }
}

最终编译的表盘将出现在你的 Wear 设备的表盘选择器中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

恭喜你构建了你的第一个表盘。

理解表盘元素并初始化它们

根据我们正在开发的表盘类型,我们需要规划表盘的某些元素。我们已经了解了构建数字表盘需要什么,但要构建模拟表盘,我们需要了解一些有助于构建表盘的表盘元素。

通常,模拟表盘是由以下三个基本组件组成的:

  • HOUR_STROKE

  • MINUTE_STROKE

  • SECOND_TICK_STROKE

现在,要构建一个模拟表盘,我们需要这三个组件,其余的事情与构建数字表盘几乎相同。在这里,我们需要在动画化笔画上投入更多的努力。

首先,我们需要设计Strokes,如下代码所示:

 mHourPaint = new Paint();
 mHourPaint.setColor(mWatchHandColor);
 mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH);
 mHourPaint.setAntiAlias(true);
 mHourPaint.setStrokeCap(Paint.Cap.ROUND);
 mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);

 mMinutePaint = new Paint();
 mMinutePaint.setColor(mWatchHandColor);
 mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH);
 mMinutePaint.setAntiAlias(true);
 mMinutePaint.setStrokeCap(Paint.Cap.ROUND);
 mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, 
 mWatchHandShadowColor);

 mSecondPaint = new Paint();
 mSecondPaint.setColor(mWatchHandHighlightColor);
 mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
 mSecondPaint.setAntiAlias(true);
 mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
 mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, 
 mWatchHandShadowColor);

 mTickAndCirclePaint = new Paint();
 mTickAndCirclePaint.setColor(mWatchHandColor);
 mTickAndCirclePaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
 mTickAndCirclePaint.setAntiAlias(true);
 mTickAndCirclePaint.setStyle(Paint.Style.STROKE);
 mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, 
 mWatchHandShadowColor);

现在,使用之前设计的Strokes,我们可以按照自己的想法设计和定制表盘,还可以在画布上添加不同的背景以及其他装饰元素,使你的模拟表盘变得特别。

对于数字表盘,你需要为文本以及表盘中使用的其他图形元素提供一个参考。

常见问题

Wear 表盘应用与 Wear 应用不同。表盘应用遇到的最常见问题是不同的形态因素,如方形和圆形表盘下巴。为解决这个问题,程序员需要在表盘执行前检测 Wear 的形态因素。正如我们已经讨论的,CanvasWatchFaceService.EngineonApplyWindowInsets()方法可以帮助找到 Wear 的形状。

表盘应用始终在运行;实际上,表盘服务扩展了壁纸服务。当我们有大量从网络 API 调用获取数据的服务时,电池可能会很快耗尽。这些服务可能包括:

  • 不同的形态因素

  • 电池效率

  • 用户界面适应

  • 过多的动画

  • 我们用来构建 Wear 表盘的资源

  • 依赖硬件的表盘

用户界面适应是表盘制作者面临的另一个挑战;当我们启用表盘样式中的setHotwordIndicator()时,Android 系统应该能够在我们构建的表盘上方发布和叠加通知卡片。我们构建的模拟表盘必须考虑这种情况,因为模拟表盘在调整大小方面有些不情愿,并且在连续的笔画动画中,它不会与系统通知协调。表盘中过多的动画不是一个好主意。有许多动画会导致 CPU 和 GPU 性能问题。当我们在表盘中使用动画时需要考虑以下事项:

  • 减少动画的帧率

  • 让 CPU 在动画之间休眠

  • 减小所使用的位图资源的大小

  • 绘制缩放位图时禁用抗锯齿

  • 将昂贵的操作移出绘图方法

当你的表盘依赖于硬件来显示数据时,你应该确保周期性地访问硬件并释放它。例如,当我们使用 GPS 来显示当前位置,并且表盘不断监听 GPS 时,不仅会耗尽电池电量,垃圾回收器还可能会抛出错误。

交互式表盘

每当 Wear 2.0 更新到来时,趋势都会发生变化,带来新的互动表盘,这些表盘也可以具有独特的互动和风格表达。所有为 Wear 开发表盘的开发者可能需要开始考虑互动表盘。

什么是一个真正的互动表盘?

这个想法是让用户通过及时提供令人愉悦和有用的信息来喜欢和爱上表盘,这改变了用户对表盘的体验。

谷歌提出以下方法来实现互动表盘:

  • 创意视觉

  • 不同的形态因素

  • 显示模式

  • 系统 UI 元素

  • 集成数据的表盘

Android Wear 以非常高效的方式表达时间提供了一个数字画布。Android Wear 还提供了在表盘上集成数据,以实现更高层次的个性化。表盘和设计需要一目了然,并且应向表盘用户传达优先信息。

我们知道,Android Wear 的不同形态因素是表盘开发者的实现挑战。表盘在设计语言上应保持一致,这包括一套通用的颜色、线宽、阴影和其他设计元素。

在 Wear 2.0 中,有两种显示模式:

  • 活跃模式

  • 常亮模式

活跃模式是当用户扭动手腕或触摸显示屏来查看时间时。Wear 将点亮显示屏并激活表盘。在这个模式下,表盘可以使用丰富多彩的动画和流畅的设计语言来表达时间和其他信息。

常亮模式有助于节省电池电量,当 Wear 设备进入常亮模式时,显示能力仅限于黑、白和灰色。我们需要仔细设计在常亮模式下显示的内容,使其与表盘设计相似,但颜色和动画较少。

系统 UI 元素指示 Wear 设备的状态;例如,电池水平和其他系统 UI 元素。表盘应允许这些指示器显示在 Wear 设备的特定位置。

集成数据的表盘可以帮助表盘用户一目了然地查看所选信息,例如,步数、天气预报等可以显示在表盘上。

总结

在本章中,我们探讨了设计表盘的基本理解,并构建了一个数字表盘。我们了解了CanvasWatchFaceService类如何在构建表盘时提供帮助,并且也了解了以下与表盘相关的概念:

  • CanvasWatchFaceService

  • canvasWatchFaceService.Engine方法

  • 在 Wear 模块清单中注册表盘

  • 处理轻触手势

  • 不同的形态因素

  • 向表盘添加位图图像

  • 表盘元素

  • 常见问题

  • 互动表盘

制作手表表盘是一项卓越的艺术工程技术,包括我们应该在表盘上表达哪些数据,以及时间和日期数据是如何被展示的。ComplicationsAPI是 Wear 2.0 中的新特性。我们将在下一章讨论这个内容,同时也会涉及一些高级概念。

第十一章:关于 Wear 2.0 的更多信息

Android Wear 2.0 是一个重要的更新,包含了许多新功能,包括 Google 助手、独立应用程序、新手表表盘以及支持第三方复杂功能。在前面的章节中,我们探讨了如何编写不同类型的 Wear 应用。Wear 2.0 根据当前市场研究提供了更多功能,谷歌正在与合作伙伴公司合作,为 Wear 构建强大的生态系统。

在本章中,让我们了解如何通过以下概念将我们的现有技能向前推进:

  • 独立应用程序

  • 曲面布局和更多的 UI 组件

  • 复杂功能 API

  • 不同的导航和动作

  • 手腕手势

  • 输入法框架

  • 将 Wear 应用分发到 Play 商店

独立应用程序

在 Wear 2.0 中,独立应用程序是穿戴生态系统的强大功能。在没有手机的情况下使用穿戴应用是多么酷啊!有许多场景,Wear 设备曾经需要依赖手机,例如,接收新电子邮件通知,Wear 需要连接到手机以使用互联网服务。现在,穿戴设备可以独立连接 Wi-Fi,并且可以同步所有应用进行新更新。用户现在可以在没有配对手机的情况下,使用穿戴应用完成更多任务。

将应用标识为独立应用

独立应用程序的理念是穿戴平台的一个伟大特性。Wear 2.0 通过 Android 清单文件中的元数据元素区分独立应用。

<application>应用标签内部,</application>元数据元素与com.google.android.wearable.standalone一起放置,值为 true 或 false。新的元数据元素指示穿戴应用是否为独立应用,不需要配对手机即可操作。当元数据元素设置为 true 时,应用也可以适用于与 iPhone 合作的穿戴设备。通常,手表应用可以分为以下几类:

  • 完全独立于手机应用

  • 半独立(没有手机配对,穿戴应用的功能将有限)

  • 依赖于手机应用

要使 Wear 应用完全独立或半独立,请将元数据的值设置为 true,如下所示:

<application>
...
  <meta-data
    android:name="com.google.android.wearable.standalone"
    android:value="true" />
...
</application>

现在,任何平台,比如没有 Play 商店的 iPhone 或 Android 手机,也可以使用穿戴应用,直接从穿戴设备中的 Play 商店下载。通过将元数据值设置为 false,我们告诉 Android Wear 这个穿戴应用依赖于带有 Play 商店应用的手机。

**注意:**不管值可能是错误的,手表应用可以在相应的手机应用安装之前安装。这样,如果手表应用识别到配套手机没有必要的手机应用,手表应用应该提示用户安装手机应用。

独立应用存储

你可以使用标准的 Android 存储 API 在本地存储数据。例如,你可以使用 SharedPreference API、SQLite 或内部存储。到目前为止,我们已经探讨了如何将 ORM 库(如 Realm)集成到穿戴应用中,不仅仅是为了存储数据,同时也为了在穿戴应用和手机应用之间共享代码。另一方面,特定于形状组件和外形尺寸的代码可以放在不同的模块中。

在另一台设备上检测穿戴应用。

Android 穿戴应用和 Android 手机应用可以使用 Capability API 识别支持的应用。手机和穿戴应用可以静态和动态地向配对的设备广播。当应用在用户的穿戴网络中的节点上时,Capability API使另一个应用能够识别相应安装的应用。

广告功能。

要从穿戴设备在手持设备上启动活动,请使用MessageAPI类发送请求。多个穿戴设备可以与手持 Android 设备关联;穿戴应用需要确定关联的节点是否适合从手持设备应用启动活动。要广告手持应用的功能,请执行以下步骤:

  1. 在项目的res/values/目录中创建一个 XML 配置文件,并将其命名为wear.xml

  2. wear.xml中添加一个名为android_wear_capabilities的资源。

  3. 定义设备提供的功能。

注意:功能是自定义字符串,由你定义,并且在你的应用中应该是唯一的。

下面的示例展示了如何向wear.xml添加一个名为voice_transcription的功能:

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

获取具有所需功能的节点。

最初,我们可以通过调用CapabilityAPI.getCapability()方法来检测有能力的节点。以下示例展示了如何手动检索具有voice_transcription功能的可达节点的结果:

private static final String
        VOICE_TRANSCRIPTION_CAPABILITY_NAME = "voice_transcription";

private GoogleApiClient mGoogleApiClient;

...

private void setupVoiceTranscription() {
    CapabilityApi.GetCapabilityResult result =
            Wearable.CapabilityApi.getCapability(
                    mGoogleApiClient, 
                    VOICE_TRANSCRIPTION_CAPABILITY_NAME,
                    CapabilityApi.FILTER_REACHABLE).await();

    updateTranscriptionCapability(result.getCapability());
}

要在穿戴设备连接时检测到有能力的节点,请将CapabilityAPI.capabilityListner()实例注册到googleAPIclient。以下示例展示了如何注册监听器并检索具有voice_transcription功能的可达节点的结果:

private void setupVoiceTranscription() {
    ...

    CapabilityApi.CapabilityListener capabilityListener =
            new CapabilityApi.CapabilityListener() {
                @Override
                public void onCapabilityChanged(CapabilityInfo 
                capabilityInfo) {
                    updateTranscriptionCapability(capabilityInfo);
                }
            };

    Wearable.CapabilityApi.addCapabilityListener(
            mGoogleApiClient,
            capabilityListener,
            VOICE_TRANSCRIPTION_CAPABILITY_NAME);
}

注意:如果你创建了一个扩展WearableListenerService的服务来识别功能变化,你可能需要重写onConnectedNodes()方法,以监听更细粒度的连接细节,例如,当穿戴设备从 Wi-Fi 更改为与手机通过蓝牙连接时。有关如何监听重要事件的信息,请阅读数据层事件

在识别出有能力的节点后,确定消息发送的位置。你应该选择一个与你的可穿戴设备接近的节点,以减少通过多个节点路由消息。一个附近的节点被定义为直接与设备连接的节点。要确定一个节点是否在附近,请调用Node.isNearby()方法。

检测并指导用户安装手机应用

现在,我们知道如何使用 Capability API 检测穿戴和手机应用。现在是引导用户从 Play 商店安装相应应用的时候了。

使用CapabilityApi检查你的手机应用是否已安装在配对的手机上。有关更多信息,请参见 Google 示例。如果你的手机应用没有安装在手机上,使用PlayStoreAvailability.getPlayStoreAvailabilityOnPhone()检查它是什么类型的手机。

如果返回PlayStoreAvailability.PLAY_STORE_ON_PHONE_AVAILABLE有效,这意味着手机是一部安装了 Play 商店的 Android 手机。在穿戴设备上调用RemoteIntent.startRemoteActivity(),在手机上打开 Play 商店。使用你的电话应用的市场 URI(可能与你的手机 URI 不同)。例如,使用市场 URI:market://details?id=com.example.android.wearable.wear.finddevices

如果返回PlayStoreAvailability.PLAY_STORE_ON_PHONE_UNAVAILABLE,这意味着手机很可能是 iOS 手机(无法访问 Play 商店)。通过在穿戴设备上调用RemoteIntent.startRemoteActivity(),在 iPhone 上打开 App Store。你可以指定你的应用在 iTunes 的 URL,例如,itunes.apple.com/us/application/yourappname.同样,注意从手表打开 URL。在 iPhone 上,从 Android Wear,你不能编程确定你的手机应用是否已安装。作为最佳实践,为用户提供一种机制(例如,一个按钮)手动触发打开 App Store。

使用之前描述的remoteIntent API,你可以确定任何 URL 都可以从穿戴设备在手机上打开,而不需要手机应用。

只获取重要的信息

在大多数用例中,当我们从互联网获取数据时,我们只获取必要的信息。超出这个范围,我们可能会遇到不必要的延迟、内存使用和电池消耗。

当穿戴设备与蓝牙 LE 连接关联时,穿戴应用可能只能访问每秒 4 千字节的数据传输能力。根据穿戴设备的不同,建议采取以下步骤:

  1. 审查你的网络请求和响应,以获取更多信息,这是针对手机应用的。

  2. 在通过网络发送到手表之前缩小大图片

云消息传递

对于通知任务,应用可以直接在手表应用中使用 Firebase 云消息传递FCM);在 2.0 版本的手表中不支持 Google 云消息传递。

没有特定的 FCM API 用于手表应用;它遵循与移动应用通知相似的配置:FCM 与手表以及休眠模式下的工作良好。推荐使用 FCM 来发送和接收手表设备的通知。

从服务器接收通知的过程是,应用需要将设备的唯一 Firebase registration_id 发送到服务器。然后服务器可以识别 FCM_REST 端点并发送通知。FCM 消息采用 JSON 格式,并可以包含以下任一负载:

  • 通知负载:通用通知数据;当通知到达手表时,应用可以检查通知,用户可以启动接收通知的应用。

  • 数据负载:负载将包含自定义的键值对。该负载将被作为数据传输给手表应用。

在开发特定于手表设备的应用时,手表应用包含了许多关注点,比如获取高带宽网络和根据手表标准降低图片质量等。此外,还有 UI 设计和保持后台服务等。在开发应用时牢记这些,将使它们在众多应用中脱颖而出。

复杂功能 API

复杂功能对手表来说肯定不是什么新鲜事物。互联网上说,第一个带有复杂功能的怀表是在十六世纪被展示的。智能手表是我们考虑复杂功能组件的理想之地。在 Android Wear 中,手表表盘显示的不仅仅是时间和日期,还包括步数计数器、天气预报等。到目前为止,这些复杂功能的工作方式有一个主要限制。到目前为止,每个自定义手表表盘应用都需要执行自己的逻辑来获取显示信息。例如,如果两个表盘都有获取步数并显示相关信息的功能,那么这将是一种浪费。Android Wear 2.0 旨在通过新的复杂功能 API 解决这个问题。

在复杂功能的情况下,手表表盘通信数据提供者扮演着主要角色。它包含获取信息的逻辑。手表表盘不会直接访问数据提供者;当有选择复杂功能的其他数据时,它会得到回调。另一方面,数据提供者不会知道数据将如何被使用;这取决于手表表盘。

下列描述讨论了手表表盘如何从数据提供者获取复杂功能数据:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

复杂功能数据提供者

新的并发症 API 具有巨大的潜力;它可以访问电池电量、气候、步数等。并发症数据提供商是一个服务,扩展了 ComplicationProviderService。这个基类有一组回调,目的是为了知道提供商何时被选为当前表盘的数据源:

  • (onComplicationActivated): 当并发症被激活时,会调用此回调方法。

  • (onComplicationDeactivated): 当并发症被停用时,会调用此回调方法。

  • (onComplicationUpdate): 当特定并发症 ID 的并发症更新信息时,会调用此回调。

ComplicationProviderService 类是一个抽象类,扩展到了一个服务。提供商服务必须实现 onComplicationUpdate(整型,整型和 ComplicationManager)以响应并发症系统对更新的请求。此服务的清单声明必须包含对 ACTION_COMPLICATION_UPDATE_REQUEST 的意图过滤器。如果需要,还应该包含确定支持的类型、刷新周期和配置动作的元数据:(METADATA_KEY_SUPPORTED_TYPES, METADATA_KEY_UPDATE_PERIOD_SECONDS, 和 METADATA_KEY_PROVIDER_CONFIG_ACTION)。

服务的清单条目还应包含一个 android:Icon 属性。那里给出的图标应该是一个单色的白色图标,代表提供商。此图标将出现在提供商选择器界面中,并且可能还会被包含在 ComplicationProviderInfo 中,提供给表盘在它们的配置活动中显示。

下面的代码演示了使用 ComplicationsData 的构建器模式将 ComplicationData 填充为短文本类型,并带有下一个事件日期和可选图标:

ComplicationData.Builder(ComplicationData.*TYPE_SHORT_TEXT*)
    .setShortText(ComplicationText.*plainText*(formatShortDate(date)))
    .setIcon(Icon.*createWithResource*(context, R.drawable.*ic_event*))
    .setTapAction(createContactEventsActivityIntent())
    .build();

向手表表盘添加并发症

Android Wear 2.0 为您的智能手表带来了许多新组件。然而,更引人注目的是在表盘上增加了可适应的并发症。并发症是一个两部分的系统;手表表盘工程师可以设计他们的表盘以拥有并发症的开放插槽,应用程序开发人员可以将应用程序的一部分作为并发症整合进来。手表表盘应用可以接收并发症数据,并允许用户选择数据提供商。Android Wear 提供了一个数据源的用户界面。我们可以向某些表盘添加并发症,或来自应用程序的数据。您的 Wear 2.0 表盘上的并发症可以显示电池寿命和日期,这只是开始。您还可以包含一些第三方应用程序的并发症。

接收数据和渲染并发症

要开始接收并发症数据,表盘会在 WatchFaceService.Engine 类中调用 setActiveComplications,并传入表盘并发症 ID 列表。表盘创建这些 ID 以便显著标识出并发症可以出现的位置,并将它们传递给 createProviderChooserIntent 方法,使用户能够选择哪个并发症应出现在哪个位置。并发症数据通过 onComplicationDataUpdateWatchFaceService.Engine 的回调)来传达。

允许用户选择数据提供商

Android Wear 提供了一个 UI(通过一个活动),使用户能够为特定并发症选择提供商。表盘可以调用 createProviderChooserIntent 方法来获取一个可用于展示选择器界面的意图。这个意图必须与 startActivityForResult 一起使用。当表盘调用 createProviderChooserIntent 时,表盘提供一个表盘并发症 ID 和支持的类型列表。

用户与并发症的交互

提供商可以指定用户点击并发症时发生的动作,因此大多数并发症应该是可以被点击的。这个动作将被指定为 PendingIntent,包含在 ComplicationData 对象中。表盘负责检测并发症上的点击,并在点击发生时触发挂起意图。

并发数据权限

Wear 应用必须拥有以下权限才能接收并发症数据并打开提供商选择器:

com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA

打开提供商选择器

如果表盘没有获得前面的权限,将无法启动提供商选择器。为了更容易请求权限并启动选择器,Wearable Support Library 提供了 ComplicationHelperActivity 类。在几乎所有情况下,应使用此类别代替 ProviderChooserIntent 来启动选择器。要使用 ComplicationHelperActivity,请在清单文件中将它添加到表盘:

<activity android:name="android.support.wearable.complications.ComplicationHelperActivity"/>

要启动提供商选择器,请调用 ComplicationHelperActivity.createProviderChooserHelperIntent 方法来获取意图。新的意图可以与 startActivitystartActivityForResult 一起使用来启动选择器:

startActivityForResult(
  ComplicationHelperActivity.createProviderChooserHelperIntent(
     getActivity(),
     watchFace,
     complicationId,
     ComplicationData.TYPE_LARGE_IMAGE),
  PROVIDER_CHOOSER_REQUEST_CODE);

当帮助活动启动时,它会检查权限是否已授予。如果未授予权限,帮助活动会提出运行时权限请求。如果权限请求被接受(或不必要),则会显示提供商选择器。

对于表盘来说,有许多场景需要考虑。在您的表盘实现并发症之前,请检查所有这些场景。您是如何接收并发症数据的?是来自提供商、远程服务器还是 REST 服务?提供商和表盘是否来自同一个应用?您还应该检查是否缺少适当的权限等。

了解 Wear 的不同导航方式

安卓穿戴在各个方面都在演进。在穿戴 1.0 中,屏幕之间的切换曾经让用户感到繁琐和困惑。现在,谷歌引入了材料设计和交互式抽屉,例如:

  • 导航抽屉:导航抽屉与移动应用导航抽屉类似的组件。它将允许用户在视图之间切换。用户可以通过在内容区域的顶部向下轻扫来在穿戴设备上访问导航抽屉。我们可以通过将setShouldOnlyOpenWhenAtTop()方法设置为 false,允许在滚动父内容内的任何位置打开抽屉,并且可以通过设置为 true 来限制它。

  • 单页导航抽屉:穿戴应用可以在单页和多页上向用户展示视图。新的导航抽屉组件通过将app:navigation_style设置为single_page,允许内容保持在单页上。

  • 操作抽屉:有一些每个类别应用都会进行的通用操作。操作抽屉为穿戴应用提供了访问所有这些操作的途径。通常,操作抽屉位于穿戴应用的底部区域,它可以帮助提供与手机应用中的操作栏类似的上下文相关用户操作。开发者可以选择将操作抽屉定位在底部或顶部,并且当用户滚动内容时可以触发操作抽屉。

下图是穿戴 2.0 导航抽屉的快速预览:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下面的例子展示了在消息应用中使用操作执行的动作回复:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实现

要在您的应用中使用新引入的组件,请使用WearableDrawerLayout对象作为您布局的根视图来声明用户界面。在WearableDrawerLayout内,添加一个实现NestedScrollingChild的视图来包含主要内容,以及额外的视图来包含抽屉的内容。

下面的 XML 代码展示了我们如何为WearableDrawerLayout赋予生命:

<android.support.wearable.view.drawer.WearableDrawerLayout
    android:id="@+id/drawer_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:deviceIds="wear">

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

    <LinearLayout
            android:id="@+id/linear_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical" />

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

    <android.support.wearable.view.drawer.WearableNavigationDrawer
        android:id="@+id/top_navigation_drawer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

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

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

单页导航抽屉

单页导航抽屉在穿戴应用中更快、更流畅地切换不同视图。要创建单页导航抽屉,请在抽屉上应用navigation_style="single_page"属性。例如:

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

现在,下一个主要任务是填充抽屉布局上的数据。我们可以在 XML 布局中通过抽屉布局的app:using_menu属性以及从菜单目录加载 XML 文件来完成这个任务。

使用WearableDrawerView,我们可以设计自己的自定义抽屉布局。下面的代码展示了自定义抽屉布局:

<android.support.wearable.view.drawer.WearableDrawerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="top"
        android:background="@color/red"
        app:drawer_content="@+id/drawer_content"
        app:peek_view="@+id/peek_view">
        <FrameLayout
            android:id="@id/drawer_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <!-- Drawer content goes here.  -->
        </FrameLayout>
        <LinearLayout
            android:id="@id/peek_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:paddingTop="8dp"
            android:paddingBottom="8dp"
            android:orientation="horizontal">
            <!-- Peek view content goes here.  -->
        <LinearLayout>
    </android.support.wearable.view.drawer.WearableDrawerView>

有主要的抽屉事件,如onDrawerOpened()onDrawerClosed()onDrawerStateChanged()。我们也可以创建自定义事件;默认情况下,我们可以使用早期的一组回调来监听抽屉活动。

穿戴 2.0 的通知

Wear 2.0 更新了通知的视觉风格和交互范式。Wear 2.0 引入了可扩展通知,提供了更多内容区域和动作,以提供最佳体验。视觉更新包括材料设计、通知的触摸目标、深色背景以及通知的水平滑动手势。

内联动作

内联动作允许用户在通知流卡片内执行特定于上下文的操作。如果通知配置了内联动作,它将显示在通知的底部区域。内联动作是可选的;谷歌推荐在不同使用场景中使用,例如用户在查看通知后需要执行某个操作,如短信回复和停止健身活动。通知只能有一个内联动作,要启用它,我们需要将setHintDisplayActionInline()设置为 true。

要向通知添加内联动作,请执行以下步骤:

  1. 按如下方式创建一个RemoteInput.Builder的实例:
String[] choices = context.getResources().getStringArray(R.array.notification_reply_choices);     choices = WearUtil.addEmojisToCannedResponse(choices);   
RemoteInput remoteInput = new RemoteInput.
Builder(Intent.EXTRA_TEXT)         
.setLabel(context.getString
      (R.string.notification_prompt_reply))      
     .setChoices(choices)    
     .build();

  1. 使用addRemoteInput()方法,我们可以附加RemoteInput对象:
NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action.Builder(
        R.drawable.ic_full_reply, R.string.notification_reply, 
        replyPendingIntent);
    actionBuilder.addRemoteInput(remoteInput);
    actionBuilder.setAllowGeneratedReplies(true);

  1. 最后,添加一个提示以显示内联动作,并使用添加动作方法将动作添加到通知中:
// Android Wear 2.0 requires a hint to display the reply action inline.
    Action.WearableExtender actionExtender =
        new Action.WearableExtender()
            .setHintLaunchesActivity(true)
            .setHintDisplayActionInline(true);
    wearableExtender.addAction
    (actionBuilder.extend(actionExtender).build());

扩展通知

Wear 2.0 引入了可扩展通知,能够为每个通知显示大量内容和动作。扩展通知遵循材料设计标准,当我们向通知附加额外内容页面时,它们将在扩展通知内可用,用户在检查通知中的动作和内容时将获得应用内体验。

扩展通知的最佳实践

何时使用扩展通知:

  1. 配对手机的通知应使用扩展通知。

  2. 当应用通知在本地运行且仅通过点击启动应用时,我们不应使用扩展通知。

通知的桥接模式

桥接模式指的是穿戴设备和伴随应用之间共享通知的系统。独立应用和伴随应用可以获得复制通知。Android 穿戴整合了处理复制通知问题的组件。

开发者可以如下更改通知的行为:

  • 在清单文件中指定桥接配置

  • 在运行时指定桥接配置

  • 设置一个消除 ID,以便通知消除在设备间同步

在清单文件中的桥接配置:

<application>
...
  <meta-data
    android:name="com.google.android.wearable.notificationBridgeMode"
    android:value="NO_BRIDGING" />
...
</application>

在运行时进行桥接配置(使用BridgingManager类):

BridgingManager.fromContext(context).setConfig(
  new BridgingConfig.Builder(context, false)
    .build());

使用消除 ID 同步通知消除:

NotificationCompat.WearableExtender wearableExtender =
  new NotificationCompat.WearableExtender().setDismissalId("abc123");
Notification notification = new NotificationCompat.Builder(context)
// ... set other fields ...
  .extend(wearableExtender)
  .build();

通知是吸引用户在穿戴设备上使用应用的重要组件。Android Wear 2.0 提供了更多智能回复、消息样式等,并将继续提供更多功能。

Wear 2.0 输入方法框架

我们在前面的章节中构建的应用程序中已经看到了穿戴设备的输入机制。Wear 2.0 通过将 Android 的输入法框架IMF)扩展到 Android Wear,支持除语音之外的其他输入方式。IMF 允许使用虚拟的、屏幕上的键盘和其他输入方法进行文本输入。尽管由于屏幕尺寸的限制,使用方式略有不同,但用于穿戴设备的 IMF API 与其他设备形态的 API 是相同的。Wear 2.0 带来了系统默认的输入法编辑器IME),并为第三方开发者开放了 IMF API,以便为 Wear 创建自定义输入方法。

调用穿戴设备的 IMF

要调用穿戴设备的 IMF,您的平台 API 级别应为 23 或更高。在包含 EditText 字段的 Android Wear 应用中:触摸文本字段会将光标置于该字段,并在获得焦点时自动显示 IMF。

手腕手势

Wear 2.0 支持手腕手势。当您无法使用穿戴设备的触摸屏时,可以利用手腕手势进行快速的单手操作,例如,当用户在慢跑时,他想要使用手腕手势执行特定上下文的操作。有一些手势不适用于应用,例如,按下手腕、抬起手腕和摇动手腕。每个手腕手势都映射到按键事件类中的一个整型常量:

手势KeyEvent描述
向外挥动手腕KEYCODE_NAVIGATE_NEXT此按键代码跳转到下一个项目。
向内挥动手腕KEYCODE_NAVIGATE_PREVIOUS此按键代码返回上一个项目。

使用应用中手势的最佳实践

以下是使用应用中手势的最佳实践:

  • 查阅 KeyEventKeyEvent.Callback 页面,了解将按键事件传递到您的视图的相关信息

  • 为手势提供触摸平行支持

  • 提供视觉反馈

  • 不要将重复的手势重新解释为您的自定义新手势。它可能与系统的摇动手腕手势发生冲突。

  • 小心使用 requestFocus()clearFocus()

认证协议

随着独立手表的出现,穿戴应用现在可以在不依赖伴随应用的情况下完全在手表上运行。这一新功能也意味着,当应用需要从云端访问数据时,Android Wear 独立应用需要自行管理认证。Android Wear 支持多种认证方法,以使独立穿戴应用能够获取用户认证凭据。现在,穿戴支持以下功能:

  • Google 登录

  • OAuth 2.0 支持

  • 通过数据层传递令牌

  • 自定义代码认证

所有这些协议遵循与移动应用编程相同的标准;在穿戴设备上集成 Google 登录或其他协议时没有大的变化,但这些协议有助于授权用户。

应用分发

我们现在知道如何为 Wear 2.0 开发应用,并且在过去的经验中,我们可能已经将一个 Android 应用发布到了 Play 商店。那么,通过谷歌开发者控制台将一个独立的可穿戴应用或一般的可穿戴应用发布到 Play 商店需要什么呢?

Wear 2.0 捆绑了 Play 商店应用;用户可以搜索特定的可穿戴应用,并在连接到互联网时直接在可穿戴设备上安装它们。通常,Play 商店中的 Wear 2.0 应用需要在清单文件中至少和目标 API 级别 25 或更高。

发布你的第一款可穿戴应用

要让你的应用在手表上的 Play 商店中显示,请生成一个已签名的可穿戴 APK。如果这是一个独立的可穿戴应用,发布应用的过程将类似于发布移动应用。如果不是独立应用,且需要上传多个 APK,请遵循developer.android.com/google/play/publishing/multiple-apks.html

让我们在 Play 商店中发布 Wear-Note 应用。这是谷歌为开发者提供的专用仪表板,让你可以管理 Play 商店中的应用。谷歌有一次性的 25 美元注册费用,你需要在上传应用之前支付。收取费用的原因是防止虚假、重复账户,从而避免不必要的低质量应用充斥 Play 商店。

以下步骤展示了我们如何将可穿戴应用发布到 Play 商店的过程:

  1. 访问play.google.com/apps/publish/

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 点击创建应用并为你的应用起一个标题。

  2. 之后,你将看到一个表格,需要填写描述和其他详细信息,包括应用的屏幕截图和图标以及促销图形。

  3. 在商店列表中,填写有关应用的所有正确信息。

  4. 现在,上传已签名的可穿戴 APK。

  5. 填写内容评级问卷,获得评级,并将评级应用到你的应用中。

  6. 在定价和分发方面,你需要拥有一个商家账户才能在定价模式下分发你的应用。现在,可穿戴笔记应用是一款免费的 Wear 应用,并允许你选择免费。

  7. 选择列表中的所有国家并选择可穿戴设备 APK:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 谷歌将审查可穿戴应用的二进制文件,并在准备好后批准其在 Wear Play 商店中分发。

  2. 恭喜你!现在,你的应用已经准备好发布了:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总结

在本章中,我们已经了解了独立应用程序和 Complications API。我们看到了如何使用 Capability API 检测伴随应用,并且对独立应用程序以及发布可穿戴应用有了清晰的认识。

本章节探讨了如何加强我们对 Wear 2.0 及其组件的理解,包括对独立应用、曲线布局以及更多用户界面组件的详尽了解,以及使用导航抽屉和操作抽屉构建可穿戴应用。同时,本章还提供了关于手腕手势及其在可穿戴应用中的使用、输入法框架的使用,以及将可穿戴应用分发到 Google Play 商店的简要了解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值