绘制表盘

配置项目并添加用来实现表盘服务的类后,您便可以开始编写代码以初始化并绘制自定义表盘。

本课包含 WatchFace 示例中的例子,用来展示系统如何使用表盘服务。本课所述的服务实现的许多方面(如初始化和设备功能检测)都适用于所有表盘,所以您可以在自己的表盘中重复利用部分代码。

图 1. WatchFace 示例中的模拟和数字表盘。

初始化表盘

当系统加载您的服务时,您应分配并初始化表盘所需的大多数资源,其中包括加载位图资源、创建定时器对象以运行自定义动画、配置绘图样式和执行其他计算。您通常可以仅执行这些操作一次并重复利用它们的结果。这种做法可提升表盘的性能,并使代码维护起来更容易。

要初始化表盘,请按以下步骤操作:

  1. 声明自定义定时器、图形对象和其他元素的变量。
  2. Engine.onCreate() 方法中初始化表盘元素。
  3. Engine.onVisibilityChanged() 方法中初始化自定义定时器。

下面几部分将详细介绍这些步骤。

声明变量

您在系统加载服务时初始化的资源需要能够在整个实现过程中的不同时间点进行访问,以便您可以重复利用它们。为此,您可以在 WatchFaceService.Engine 实现中声明这些资源的成员变量。

声明以下元素的变量:

图形对象
大多数表盘都至少包含一张用作表盘背景的位图,如创建实现策略中所述。您可以使用其他位图来表示时钟指针或表盘的其他设计元素。
周期性定时器
当时间发生变化时,系统会每分钟通知表盘一次,但有些表盘会以自定义时间间隔运行动画。在这些情况下,您需要提供一个按更新表盘所需频率触发的自定义定时器。
时区变化接收器
用户在旅行时可以调整时区,系统会广播此事件。您的服务实现必须注册一个广播接收器,该接收器在时区发生变化时会收到通知并相应地更新时间。

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

Kotlin

    private const val MSG_UPDATE_TIME = 0

    class Service : CanvasWatchFaceService() {
        ...
        inner class Engine : CanvasWatchFaceService.Engine() {

            private lateinit var calendar: Calendar

            // device features
            private var lowBitAmbient: Boolean = false

            // graphic objects
            private lateinit var backgroundBitmap: Bitmap
            private var backgroundScaledBitmap: Bitmap? = null
            private lateinit var hourPaint: Paint
            private lateinit var minutePaint: Paint

            // handler to update the time once a second in interactive mode
            private val updateTimeHandler: Handler = UpdateTimeHandler(WeakReference(this))

            // receiver to update the time zone
            private val timeZoneReceiver: BroadcastReceiver = object : BroadcastReceiver() {
                override fun onReceive(context: Context, intent: Intent) {
                    calendar.timeZone = TimeZone.getDefault()
                    invalidate()
                }
            }

            // service methods (see other sections)
            ...
        }
        ...
        private class UpdateTimeHandler(val engineReference: WeakReference<Engine>) : Handler() {
            override fun handleMessage(message: Message) {
                engineReference.get()?.apply {
                    when (message.what) {
                        MSG_UPDATE_TIME -> {
                            invalidate()
                            if (shouldTimerBeRunning()) {
                                val timeMs: Long = System.currentTimeMillis()
                                val delayMs: Long =
                                        INTERACTIVE_UPDATE_RATE_MS - timeMs % INTERACTIVE_UPDATE_RATE_MS
                                sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs)
                            }
                        }
                    }
                }
            }
        }
        ...
    }
    

Java

    public class CanvasWatchFaceJava extends CanvasWatchFaceService {
        static final int MSG_UPDATE_TIME = 0;
        ...
        class Engine extends CanvasWatchFaceService.Engine {

            Calendar calendar;

            // device features
            boolean lowBitAmbient;

            // graphic objects
            Bitmap backgroundBitmap;
            Bitmap backgroundScaledBitmap;
            Paint hourPaint;
            Paint minutePaint;
            ...

            // handler to update the time once a second in interactive mode
            final Handler updateTimeHandler = new UpdateTimeHandler(new WeakReference<>(this));

            // receiver to update the time zone
            final BroadcastReceiver timeZoneReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    calendar.setTimeZone(TimeZone.getDefault());
                    invalidate();
                }
            };

            // service methods (see other sections)
            ...
        }
        ...
        private static class UpdateTimeHandler extends Handler {
            private WeakReference<Engine> engineReference;

            UpdateTimeHandler(WeakReference<Engine> engine) {
                this.engineReference = engine;
            }

            @Override
            public void handleMessage(Message message) {
                Engine engine = engineReference.get();
                if (engine != null) {
                    switch (message.what) {
                        case MSG_UPDATE_TIME:
                            engine.invalidate();
                            if (engine.shouldTimerBeRunning()) {
                                long timeMs = System.currentTimeMillis();
                                long delayMs = INTERACTIVE_UPDATE_RATE_MS
                                        - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
                                sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
                            }
                            break;
                    }
                }
            }
        }
        ...
    }
    

在上面的示例中,自定义定时器会实现为 Handler 实例,该实例使用线程的消息队列发送消息和处理延迟的消息。对于这一特定的表盘,自定义定时器每秒触发一次。当定时器触发的时候,处理程序会调用 invalidate() 方法,系统随后会调用 onDraw() 方法来重新绘制表盘。

初始化表盘元素

声明位图资源、绘图样式以及每次重新绘制表盘时都会重复利用的其他元素的成员变量后,您可以在系统加载您的服务时初始化这些元素。仅初始化这些元素一次并重复利用它们的话,可以提升性能并延长电池续航时间。

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

  • 加载背景图片。
  • 创建样式和颜色以绘制图形对象。
  • 分配对象以计算时间。
  • 配置系统界面。

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

Kotlin

    override fun onCreate(holder: SurfaceHolder?) {
        super.onCreate(holder)

        // configure the system UI (see next section)
        ...

        // load the background image
        backgroundBitmap = (resources.getDrawable(R.drawable.bg, null) as BitmapDrawable).bitmap

        // create graphic styles
        hourPaint = Paint().apply {
            setARGB(255, 200, 200, 200)
            strokeWidth = 5.0f
            isAntiAlias = true
            strokeCap = Paint.Cap.ROUND
        }
        ...

        // allocate a Calendar to calculate local time using the UTC time and time zone
        calendar = Calendar.getInstance()
    }
    

Java

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

        // configure the system UI (see next section)
        ...

        // load the background image
        Resources resources = AnalogWatchFaceService.this.getResources();
        Drawable backgroundDrawable = resources.getDrawable(R.drawable.bg, null);
        backgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();

        // create graphic styles
        hourPaint = new Paint();
        hourPaint.setARGB(255, 200, 200, 200);
        hourPaint.setStrokeWidth(5.0f);
        hourPaint.setAntiAlias(true);
        hourPaint.setStrokeCap(Paint.Cap.ROUND);
        ...

        // allocate a Calendar to calculate local time using the UTC time and time zone
        calendar = Calendar.getInstance();
    }
    

背景位图仅在系统初始化表盘时加载一次。图形样式是 Paint 类的实例。您可以使用这些样式在 Engine.onDraw() 方法中绘制表盘的元素,如绘制表盘中所述。

初始化自定义定时器

作为表盘开发者,您可以通过提供一个在设备处于交互模式时按所需频率触发的自定义定时器,决定您想要更新表盘的频率。这样,您就可以创建自定义动画和其他视觉效果。

注意:在微光模式下,系统无法可靠地调用自定义定时器。要在微光模式下更新表盘,请参阅在微光模式下更新表盘

声明变量部分展示了一个来自 AnalogWatchFaceService 类的定时器定义示例,该定时器每秒触发一次。在 Engine.onVisibilityChanged() 方法中,如果满足以下两个条件,会启动自定义定时器:

  • 表盘可见。
  • 设备处于交互模式。

AnalogWatchFaceService 类会根据需要安排定时器的下次触发,如下所示:

Kotlin

    private fun updateTimer() {
        updateTimeHandler.removeMessages(MSG_UPDATE_TIME)
        if (shouldTimerBeRunning()) {
            updateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME)
        }
    }

    fun shouldTimerBeRunning(): Boolean = isVisible && !isInAmbientMode
    

Java

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

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

此自定义定时器每秒触发一次,如声明变量中所述。

onVisibilityChanged() 方法中,根据需要启动定时器并注册时区变化接收器,如下所示:

Kotlin

    override fun onVisibilityChanged(visible: Boolean) {
        super.onVisibilityChanged(visible)

        if (visible) {
            registerReceiver()

            // Update time zone in case it changed while we weren't visible.
            calendar.timeZone = 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 timer
        updateTimer()
    }
    

Java

    @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.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 timer
        updateTimer();
    }
    

当表盘可见时,onVisibilityChanged() 方法会注册时区变化接收器。如果设备处于交互模式,此方法也会启动自定义定时器。当表盘不可见时,此方法会停止自定义定时器并取消注册时区变化接收器。registerReceiver()unregisterReceiver() 方法的实现方式如下:

Kotlin

    private fun registerReceiver() {
        if (registeredTimeZoneReceiver) return
        registeredTimeZoneReceiver = true
        IntentFilter(Intent.ACTION_TIMEZONE_CHANGED).also { filter ->
            this@AnalogWatchFaceService.registerReceiver(timeZoneReceiver, filter)
        }
    }

    private fun unregisterReceiver() {
        if (!registeredTimeZoneReceiver) return
        registeredTimeZoneReceiver = false
        this@AnalogWatchFaceService.unregisterReceiver(timeZoneReceiver)
    }
    

Java

    private void registerReceiver() {
        if (registeredTimeZoneReceiver) {
            return;
        }
        registeredTimeZoneReceiver = true;
        IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
        AnalogWatchFaceService.this.registerReceiver(timeZoneReceiver, filter);
    }

    private void unregisterReceiver() {
        if (!registeredTimeZoneReceiver) {
            return;
        }
        registeredTimeZoneReceiver = false;
        AnalogWatchFaceService.this.unregisterReceiver(timeZoneReceiver);
    }
    

在微光模式下更新表盘

在微光模式下,系统每分钟调用一次 Engine.onTimeTick() 方法。在此模式下,每分钟更新一次表盘通常已经足够。要在交互模式下更新表盘,您必须提供一个自定义定时器,如初始化自定义定时器中所述。

在微光模式下,大多数表盘实现会直接使画布无效,以在 Engine.onTimeTick() 方法中重新绘制表盘:

Kotlin

    override fun onTimeTick() {
        super.onTimeTick()

        invalidate()
    }
    

Java

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

        invalidate();
    }
    

配置系统界面

表盘不应干扰系统界面元素,如适应系统界面元素中所述。如果您的表盘具有浅色背景或在屏幕底部附近显示信息,则您可能必须配置通知卡片的大小或启用背景保护。

使用“Wear OS by Google 谷歌”时,您可以配置系统界面在表盘处于活动状态时的以下几个方面:

  • 指定系统是否在表盘上绘制时间。
  • 在系统指示器周围使用纯色背景对这些指示器加以保护。
  • 指定系统指示器的位置。

要配置系统界面的这些方面,请创建一个 WatchFaceStyle 实例并将其传递给 Engine.setWatchFaceStyle() 方法。

AnalogWatchFaceService 类按如下方式配置系统界面:

Kotlin

    override fun onCreate(holder: SurfaceHolder?) {
        super.onCreate(holder)

        // configure the system UI
        setWatchFaceStyle(WatchFaceStyle.Builder(this@AnalogWatchFaceService)
                .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
                .setShowSystemUiTime(false)
                .build())
        ...
    }
    

Java

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

        // configure the system UI
        setWatchFaceStyle(new WatchFaceStyle.Builder(AnalogWatchFaceService.this)
                .setBackgroundVisibility(WatchFaceStyle
                                        .BACKGROUND_VISIBILITY_INTERRUPTIVE)
                .setShowSystemUiTime(false)
                .build());
        ...
    }
    

上面的代码段将系统时间配置为不显示(因为此表盘会绘制自己的时间表示形式)。

您可以在表盘实现过程中随时配置系统界面的样式。例如,如果用户选择白色背景,您可以为系统指示器添加背景保护。

如需详细了解如何配置系统界面,请参阅 Wear API 参考文档

管理未读通知指示器

用户通常希望系统清楚地表明存在未读通知。因此,系统提供了未读通知指示器。此指示器显示为外套圆圈的圆点(位于屏幕底部)。如果信息流中存在多条未读通知,将显示此指示器。

未读通知指示器

图 2. 未读通知指示器。

注意:在 Wear 2.8.0 正式版中,未启用未读通知指示器。建议开发者使用最新的 Wear 模拟器测试他们的实现。从下一个用户版 Wear(版本 2.9.0)开始,默认情况下会显示此功能。

默认情况下,会将该指示器添加到您的表盘。我们强烈建议您将该指示器显示给用户。不过,如果您的表盘已提供未读通知的指示,或者新指示器的位置与表盘的元素发生冲突,那么您可以选择不显示系统指示器。构建手表样式时,请使用以下方法之一:

提供自定义未读通知计数 在状态栏中提供未读通知计数

图 3. 自定义通知计数或状态栏通知计数。

如果您选择允许未读通知指示器显示在表盘上,您可以自定义其外圈的颜色。请调用 WatchFaceStyle.Builder.setAccentColor 并指定所需的 color。默认情况下,外圈为白色。

获取与设备屏幕有关的信息

系统在确定设备屏幕的属性(如设备是否使用低色彩位数微光模式以及屏幕是否要求防烙印)时会调用 Engine.onPropertiesChanged() 方法。

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

Kotlin

    override fun onPropertiesChanged(properties: Bundle?) {
        super.onPropertiesChanged(properties)
        properties?.apply {
            lowBitAmbient = getBoolean(PROPERTY_LOW_BIT_AMBIENT, false)
            burnInProtection = getBoolean(PROPERTY_BURN_IN_PROTECTION, false)
        }
    }
    

Java

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

在绘制表盘时,您应考虑这些设备属性:

  • 对于使用低色彩位数微光模式的设备,屏幕支持在微光模式下为每种颜色使用更少的位数,所以当设备切换到微光模式时,您应停用抗锯齿和位图过滤。
  • 对于要求防烙印的设备,请避免在微光模式下使用大块的白色像素且不要在屏幕边缘 10 个像素内放置内容,因为系统会周期性地移动内容以避免像素烙印。

如需详细了解低色彩位数微光模式和防烙印,请参阅针对特殊屏幕优化。如需详细了解如何停用位图过滤,请参阅位图过滤

响应模式变化

当设备在微光模式与交互模式之间切换时,系统会调用 Engine.onAmbientModeChanged() 方法。您的服务实现应进行必要的调整以在模式之间切换,然后调用 invalidate() 方法,让系统重新绘制表盘。

以下代码段展示了如何实现此方法:

Kotlin

    override fun onAmbientModeChanged(inAmbientMode: Boolean) {

        super.onAmbientModeChanged(inAmbientMode)

        if (lowBitAmbient) {
            !inAmbientMode.also { antiAlias ->
                hourPaint.isAntiAlias = antiAlias
                minutePaint.isAntiAlias = antiAlias
                secondPaint.isAntiAlias = antiAlias
                tickPaint.isAntiAlias = antiAlias
            }
        }
        invalidate()
        updateTimer()
    }
    

Java

    @Override
    public void onAmbientModeChanged(boolean inAmbientMode) {

        super.onAmbientModeChanged(inAmbientMode);

        if (lowBitAmbient) {
            boolean antiAlias = !inAmbientMode;
            hourPaint.setAntiAlias(antiAlias);
            minutePaint.setAntiAlias(antiAlias);
            secondPaint.setAntiAlias(antiAlias);
            tickPaint.setAntiAlias(antiAlias);
        }
        invalidate();
        updateTimer();
    }
    

此示例对一些图形样式进行了调整并使画布无效,以便系统可以重新绘制表盘。

绘制表盘

要绘制自定义表盘,系统会使用 Canvas 实例和您应在其内部绘制表盘的边界来调用 Engine.onDraw() 方法。这些边界将所有边衬区(如一些圆形设备底部的“下巴”)考虑在内。您可以使用此画布直接绘制表盘,具体步骤如下:

  1. 替换 onSurfaceChanged() 方法以缩放背景,使其在视图发生变化时适应设备。

    Kotlin

        override fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
            if (backgroundScaledBitmap?.width != width || backgroundScaledBitmap?.height != height) {
                backgroundScaledBitmap = Bitmap.createScaledBitmap(backgroundBitmap,
                        width, height, true /* filter */)
            }
            super.onSurfaceChanged(holder, format, width, height)
        }
        

    Java

        @Override
        public void onSurfaceChanged(
                SurfaceHolder holder, int format, int width, int height) {
            if (backgroundScaledBitmap == null
                    || backgroundScaledBitmap.getWidth() != width
                    || backgroundScaledBitmap.getHeight() != height) {
                backgroundScaledBitmap = Bitmap.createScaledBitmap(backgroundBitmap,
                        width, height, true /* filter */);
            }
            super.onSurfaceChanged(holder, format, width, height);
        }
        
  2. 检查设备处于微光模式还是交互模式。
  3. 执行所需的任何图形计算。
  4. 在画布上绘制背景位图。
  5. 使用 Canvas 类中的方法绘制表盘。

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

Kotlin

    override fun onDraw(canvas: Canvas, bounds: Rect) {
        val frameStartTimeMs: Long = SystemClock.elapsedRealtime()

        // Drawing code here

        if (shouldTimerBeRunning()) {
            var delayMs: Long = SystemClock.elapsedRealtime() - frameStartTimeMs
            delayMs = if (delayMs > INTERACTIVE_UPDATE_RATE_MS) {
                // This scenario occurs when drawing all of the components takes longer than an actual
                // frame. It may be helpful to log how many times this happens, so you can
                // fix it when it occurs.
                // In general, you don't want to redraw immediately, but on the next
                // appropriate frame (else block below).
                0
            } else {
                // Sets the delay as close as possible to the intended framerate.
                // Note that the recommended interactive update rate is 1 frame per second.
                // However, if you want to include the sweeping hand gesture, set the
                // interactive update rate up to 30 frames per second.
                INTERACTIVE_UPDATE_RATE_MS - delayMs
            }
            updateTimeHandler.sendEmptyMessageDelayed(MSG_CODE_UPDATE_TIME, delayMs)
        }
    }
    

Java

    @Override
    public void onDraw(Canvas canvas, Rect bounds) {
        long frameStartTimeMs = SystemClock.elapsedRealtime();

        // Drawing code here

        if (shouldTimerBeRunning()) {
            long delayMs = SystemClock.elapsedRealtime() - frameStartTimeMs;
            if (delayMs > INTERACTIVE_UPDATE_RATE_MS) {
                // This scenario occurs when drawing all of the components takes longer than an actual
                // frame. It may be helpful to log how many times this happens, so you can
                // fix it when it occurs.
                // In general, you don't want to redraw immediately, but on the next
                // appropriate frame (else block below).
                delayMs = 0;
            } else {
                // Sets the delay as close as possible to the intended framerate.
                // Note that the recommended interactive update rate is 1 frame per second.
                // However, if you want to include the sweeping hand gesture, set the
                // interactive update rate up to 30 frames per second.
                delayMs = INTERACTIVE_UPDATE_RATE_MS - delayMs;
            }
            updateTimeHandler.sendEmptyMessageDelayed(MSG_CODE_UPDATE_TIME, delayMs);
        }
    }
    

如需详细了解如何在画布实例上绘图,请参阅画布和可绘制对象

WatchFace 示例包含其他表盘,您在实现 onDraw() 方法时可以参考这些示例。