65.9K
CodeProject 正在变化。 阅读更多。
Home

Android Wear 表盘开发

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (3投票s)

2015年10月16日

Apache

10分钟阅读

viewsIcon

21703

downloadIcon

321

本文全部内容都关于 Android Wear 表盘开发。您将学习如何开发自定义表盘服务。

引言

在本文中,我将引导您完成 Android Wear 表盘的设计和开发。以下是本文将涵盖的主要主题,通过阅读本文,您将了解如何构建自定义表盘服务。

背景

请查看以下链接以了解 Android Wear。

https://codeproject.org.cn/Articles/1038337/Introduction-to-Android-Wear
 

Android Wear 表盘

如果您是表盘新手或从未听说过它,那么您可能会好奇它到底是什么?Android Wear 智能手表支持自定义表盘或旨在向用户显示时间和其他有用信息的应用程序。使用 Android Wear,在更改手表 UI 或表盘外观方面非常灵活。您可以直接选择可用的表盘或下载一个并使用它。

话虽如此,Android Wear 为开发者提供了完全的自定义功能来构建自定义表盘,这非常重要,因为智能手表用户将可以选择他们喜欢的表盘。2014 年 12 月,谷歌发布了表盘开发的官方 API。今天,您会在 Play 商店中看到无数表盘;这是相关链接。 

https://play.google.com/store/apps/collection/promotion_3001507_wear_watch_faces_all?hl=en

谈到设计挑战,如果您是对开发自定义表盘感兴趣的开发人员,您确实需要查看 Android 可穿戴设备团队的以下设计指南。

https://developer.android.com.cn/training/wearables/watch-faces/designing.html

https://developer.android.com.cn/design/wear/watchfaces.html

这是一个典型表盘的示例。

Odyssey-WatchFace

图片来源 - (https://play.google.com/store/apps/details?id=co.smartwatchface.odyssey.watch.face&hl=en)

设计表盘应用时要考虑的关键事项

引用:关键点基于 Android 设计指南 [1]

电池寿命 – 在为表盘开发应用程序时,考虑电池使用情况至关重要且高度重要。表盘有两种运行模式。在环境模式下,您可以显示一些非常简单的东西,甚至无需担心背景图像等,因为此时没有用户交互。另一种模式是交互模式。当用户与手表交互时,您可以显示带有图形和令人愉悦颜色的表盘。

显示 – 作为表盘设计师或开发人员,应该考虑在环境模式和交互模式下显示表盘,这样可以利用电池使用。此外,表盘的设计应该能够在矩形和圆形手表中透明地工作。这意味着内容不应该被裁剪或显示时应该没有任何偏移。

数据获取 – 有时您必须获取数据并将其显示在表盘上,例如您希望显示天气温度信息,不要每分钟都获取数据。相反,您可以在表盘服务创建时获取一次,然后使用相同的数据。这样做的目的是为了避免手表电池耗尽。

可配置 – 这不是一个强制选项,但最好有。您应该允许用户配置表盘,例如选择背景颜色或设置一些文本等。 
 

演示表盘服务

让我们看一个关于如何构建表盘服务的演示,并尝试理解其内部工作原理。这是我们自定义表盘服务的快照。第一张图片是在按下表盘时显示的,它允许用户选择一个可用的表盘。第二张图片是选择表盘后您将看到的。

  

引用:信息

请注意 – 我们将重用 [2] 中的示例代码。此代码是开源的,并具有“Apache 版本 2.0”许可证。 

在我们查看示例代码之前。首先,我们需要了解构建表盘服务的必要要求。创建一个新的 Java 类并从 CanvasWatchFaceService 扩展它,以创建我们自己的表盘服务。此外,我们必须覆盖以下方法以返回一个 Engine 实例。 

您可以在下面看到,我们有一个名为“WatchFaceEngine”的私有内部类,它继承自 Engine 并覆盖了构建表盘服务所需的某些方法。 

public class CustomWatchFaceService extends CanvasWatchFaceService {

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

    private class WatchFaceEngine extends Engine {...}
}

表盘服务引擎

CanvasWatchFaceService 有一个“Engine”类,其中包含可以覆盖并在画布上绘制的方法,这就是我们将在表盘服务上显示内容的方式。下面是“Engine”类的代码片段。 

public class Engine extends android.support.wearable.watchface.WatchFaceService.Engine {
    public Engine() {
        super(CanvasWatchFaceService.this);
    }

    public void onDestroy() {
    }

    public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    public void onSurfaceRedrawNeeded(SurfaceHolder holder) {
    }

    public void onSurfaceCreated(SurfaceHolder holder) {
    }

    public void invalidate() {
    }

    public void postInvalidate() {
    }

    public void onDraw(Canvas canvas, Rect bounds) {
    }
}

一个“Engine”是一个抽象类,它继承自 android.service.wallpaper.WallpaperService.Engine,并包含我们可以覆盖并作为表盘服务的一部分提供实现的方法。

下面是“Engine”类的骨架。我们将覆盖并实现 Engine 类的一些方法。 

public class Engine extends android.support.wearable.watchface.WatchFaceService.Engine {
    public void onAmbientModeChanged(boolean inAmbientMode) {
    }

    public void onInterruptionFilterChanged(int interruptionFilter) {
    }

    public void onPeekCardPositionUpdate(Rect rect) {
    }

    public void onUnreadCountChanged(int count) {
    }

    public void onPropertiesChanged(Bundle properties) {
    }

    public void onTimeTick() {
    }

    public void onCreate(SurfaceHolder holder) {
    }

    public void onVisibilityChanged(boolean visible) {
    }

    public final boolean isInAmbientMode() {
    }

    public final int getInterruptionFilter() {
    }

    public final int getUnreadCount() {
    }

    public final Rect getPeekCardPosition() {
    }
}

项目结构

让我们看一下项目结构,看看构建表盘服务所需的必要内容。下面是项目结构的快照,我们所需要的是一个自定义表盘服务类,以及我们将在 AndroidManifest.xml 文件中用于显示表盘服务图标的启动器图标。 

WatchFace_ProjectStructure

AndroidManifest.xml

下面是 AndroidManifest.xml 文件内容的代码片段,您可以在其中看到我们应用程序所需的权限。“WAKE_LOCK”权限是使智能手表 CPU 保持开启状态的权限,以便表盘服务可以执行一些操作,例如重绘画布。这是必需的,因为可穿戴设备闲置一段时间后会进入睡眠状态。 

<application> 元素下,您将看到定义了我们自定义手表服务的 <service> 元素。此外,wallpaper、preview 和 preview_circular 的元数据也是必需的。最后,我们将使用 WallpaperService 作为 action 和 WATCH_FACE 作为 category 来设置 <intent-filter>

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tutsplus.androidwearwatchface" >

    <uses-feature android:name="android.hardware.type.watch" />

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.DeviceDefault" >
        <service
            android:name=".service.CustomWatchFaceService"
            android:label="Tuts+ Wear Watch Face"
            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>
    </application>

</manifest>

表盘服务 - onCreate 覆盖

让我们深入研究自定义表盘服务类,以了解其内部工作原理。这是 onCreate 覆盖的代码片段。 我们将通过调用 setWatchFaceStyle 方法并传入 WatchFaceStyle 实例来设置表盘样式。请注意,Builder 方法是使用自定义表盘服务调用的。还调用了用于设置背景可见性、卡片模式等各种其他方法,以便设置适当的 WatchFaceStyle 属性。 

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

    setWatchFaceStyle( new WatchFaceStyle.Builder( CustomWatchFaceService.this )
                    .setBackgroundVisibility( WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE )
                    .setCardPeekMode( WatchFaceStyle.PEEK_MODE_VARIABLE )
                    .setShowSystemUiTime( false )
                    .build()
    );

    initBackground();
    initDisplayText();

    mDisplayTime = new Time();
}

存在两个私有方法用于初始化背景和显示文本。这是相同的代码片段。我们所做的只是设置 Paint 颜色和 Paint 文本颜色实例,以及颜色、文本大小等。这些颜料颜色稍后会在画布上用于显示文本和矩形。 

private void initBackground() {
    mBackgroundColorPaint = new Paint();
    mBackgroundColorPaint.setColor( mBackgroundColor );
}

private void initDisplayText() {
    mTextColorPaint = new Paint();
    mTextColorPaint.setColor( mTextColor );
    mTextColorPaint.setTypeface( WATCH_TEXT_TYPEFACE );
    mTextColorPaint.setAntiAlias( true );
    mTextColorPaint.setTextSize( getResources().getDimension( R.dimen.text_size ) );
}

表盘服务 – onDraw 覆盖

现在让我们尝试理解 onDraw 覆盖方法的代码块。我们需要设置显示时间并调用方法来绘制背景和显示当前时间的文本。

@Override
public void onDraw(Canvas canvas, Rect bounds) {
    super.onDraw(canvas, bounds);

    mDisplayTime.setToNow();

    drawBackground( canvas, bounds );
    drawTimeText( canvas );
}

下面是 drawBackgrounddrawTimeText 方法的代码片段,我们基本上是在 Canvas 实例上绘图。设置背景颜色,并显示格式化的文本,以“AM”或“PM”显示当前时间。

private void drawBackground( Canvas canvas, Rect bounds ) {
    canvas.drawRect( 0, 0, bounds.width(), bounds.height(), mBackgroundColorPaint );
}

private void drawTimeText( Canvas canvas ) {
    String timeText = getHourString() + ":" + String.format( "%02d", mDisplayTime.minute );
    if( isInAmbientMode() || mIsInMuteMode ) {
        timeText += ( mDisplayTime.hour < 12 ) ? "AM" : "PM";
    } else {
        timeText += String.format( ":%02d", mDisplayTime.second);
    }
    canvas.drawText( timeText, mXOffset, mYOffset, mTextColorPaint );
}

private String getHourString() {
    if( mDisplayTime.hour % 12 == 0 )
        return "12";
    else if( mDisplayTime.hour <= 12 )
        return String.valueOf( mDisplayTime.hour );
    else
        return String.valueOf( mDisplayTime.hour - 12 );
}

处理环境模式

让我们看看表盘服务中处理环境模式的代码块。如前所述的表盘服务设计,环境模式是我们需要明确处理的事情,以确保我们设置适当的背景或颜色等。在这里,您可以做一些事情来最大限度地减少电池使用。

我们正在做的是:

1)    仅在环境模式下将文本颜色设置为白色。

2)    调用 setAntiAlias 方法,在环境模式下将其设置为 false。抗锯齿基本上使边缘平滑,视觉上看起来很吸引人,但我们不需要在环境模式下这样做。

3)    调用 invalidate 方法以重绘画布。

4)    更新计时器,这样我们就不会每秒重绘画布。这对于环境模式来说并不是必需的。    

@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
    super.onAmbientModeChanged(inAmbientMode);

    if( inAmbientMode ) {
        mTextColorPaint.setColor( Color.parseColor( "white" ) );
    } else {
        mTextColorPaint.setColor( Color.parseColor( "red" ) );
    }

    if( mIsLowBitAmbient ) {
        mTextColorPaint.setAntiAlias( !inAmbientMode );
    }

    invalidate();
    updateTimer();
}

使用更新的时间重绘表盘

当表盘服务运行时,您需要确保时间每秒更新。下面的代码负责以 1000 毫秒的更新速率重绘画布。  

您将在下面看到我们正在覆盖 handleMessage 方法并编写逻辑,仅在交互模式下更新或重绘画布。如果用户正在积极使用手表且未处于环境模式,那么我们将获取当前时间的毫秒数,并根据当前时间和更新速率(即 1000 毫秒)计算延迟。 

如果您想知道时间处理器如何每秒调用一次,有一个 sendEmptyMessageDelayed 方法,我们将其与“int”值 - MSG_UPDATE_TIME_ID 和延迟一起调用,这会触发 handleMessage 调用。

private final Handler mTimeHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        switch( msg.what ) {
            case MSG_UPDATE_TIME_ID: {
                invalidate();
                if( isVisible() && !isInAmbientMode() ) {
                    long currentTimeMillis = System.currentTimeMillis();
                    long delay = mUpdateRateMs - ( currentTimeMillis % mUpdateRateMs );
                    mTimeHandler.sendEmptyMessageDelayed( MSG_UPDATE_TIME_ID, delay );
                }
                break;
            }
        }
    }
};

处理时区变化

让我们看看如何处理时区变化,以根据您所在的时区更新表盘时间。 

下面是代码片段,您可以看到通过调用“clear”方法清除时间实例,然后我们将时间设置为当前时间。

final BroadcastReceiver mTimeZoneBroadcastReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        mDisplayTime.clear( intent.getStringExtra( "time-zone" ) );
        mDisplayTime.setToNow();
    }
};

处理圆形和方形手表的文本偏移

在方形和圆形设备上在画布上显示文本时,我们应该设置适当的偏移量,以便正确显示文本。为此,我们需要覆盖 onApplyWindowInsets 方法并根据设备是否为圆形来设置偏移量。

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

    mYOffset = getResources().getDimension( R.dimen.y_offset );

    if( insets.isRound() ) {
        mXOffset = getResources().getDimension( R.dimen.x_offset_round );
    } else {
        mXOffset = getResources().getDimension( R.dimen.x_offset_square );
    }
}

注册和注销广播接收器 

之前,我们已经在表盘服务中看到了广播接收器的使用。为了使广播接收器正常工作,我们必须使用适当的 IntentFilter 进行注册。我们只在表盘可见且尚未注册时注册广播接收器。您可以在下面看到,我们正在创建一个 IntentFilter 实例,其操作为 Intent.ACTION_TIMEZONE_CHANGED,然后调用 registerReceiver 方法,传入广播接收器和 Intent 过滤器实例。

我们需要做一件重要的事情是当手表不活动时注销接收器。可见性设置为 false,所以我们可以调用 unregisterReceiver 并传入广播接收器实例。  

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

    if( visible ) {
        if( !mHasTimeZoneReceiverBeenRegistered ) {

            IntentFilter filter = new IntentFilter( Intent.ACTION_TIMEZONE_CHANGED );
            CustomWatchFaceService.this.registerReceiver( mTimeZoneBroadcastReceiver, filter );

            mHasTimeZoneReceiverBeenRegistered = true;
        }

        mDisplayTime.clear( TimeZone.getDefault().getID() );
        mDisplayTime.setToNow();
    } else {
        if( mHasTimeZoneReceiverBeenRegistered ) {
            CustomWatchFaceService.this.unregisterReceiver( mTimeZoneBroadcastReceiver );
            mHasTimeZoneReceiverBeenRegistered = false;
        }
    }

    updateTimer();
}

参考

1. Android 设计指南 -  https://developer.android.com.cn/training/wearables/watch-faces/designing.html

2. Github 代码示例 -  https://github.com/tutsplus/AndroidWear-WatchFaces ,采用 Apache 2.0 开源许可证。

关注点

在学习如何自定义编码表盘服务方面,这是一次很棒的体验。现在我们有了 Google API 来构建表盘服务,我们可以轻松尝试构建一个。

历史

版本 1.0 - 发布文章的初始版本,包含演示代码 - 2015 年 10 月 16 日

© . All rights reserved.