lightbulb_outline Please take our October 2018 developer survey. Start survey

绘制表盘

在配置完项目并添加实现表盘 service 的类之后,您可以开始编写代码以初始化和绘制您的自定义表盘。

本课程包含 WatchFace 示例中的例子,将展示系统如何使用表盘 service。此处所介绍 service 实现(如初始化和设备功能检测)的许多方面都适用于任何表盘,所以您可以在自己的表盘中重用一些代码。

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

初始化表盘

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

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

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

下文将详细介绍这些步骤。

声明变量

在实现过程中,需要从不同点访问系统加载 service 时您初始化的资源,以便您可以重用它们。您可以通过为 WatchFaceService.Engine 实现中的这些资源声明成员变量来实现这一目标。

为下列元素声明变量:

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

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

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

    Calendar mCalendar;

    // device features
    boolean mLowBitAmbient;

    // graphic objects
    Bitmap mBackgroundBitmap;
    Bitmap mBackgroundScaledBitmap;
    Paint mHourPaint;
    Paint mMinutePaint;
    ...

    // handler to update the time once a second in interactive mode
    final Handler mUpdateTimeHandler = new Handler() {
        @Override
        public 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 zone
    final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            mCalendar.setTimeZone(TimeZone.getDefault());
            invalidate();
        }
    };

    // service methods (see other sections)
    ...
}

在上面的示例中,自定义计时器作为 Handler 实例实现,该示例会使用线程的消息队列发送和处理延迟的消息。对于这个特定的表盘,自定义计时器会每秒计时一次。在计时器计时时,处理程序会调用 invalidate() 函数,然后系统会调用 <a href="/reference/android/support/wearable/watchface/CanvasWatchFaceService.Engine.html#onDraw(android.graphics.Canvas, android.graphics.Rect)"onDraw() 函数来重新绘制表盘。

初始化表盘元素

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

Engine.onCreate() 函数中,初始化下列元素:

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

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

@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);
    mBackgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();

    // create graphic styles
    mHourPaint = 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 zone
    mCalendar = Calendar.getInstance();
}

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

初始化自定义计时器

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

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

声明变量中显示了一个来自于 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();
}

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

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

在表盘可见时,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);
}

在微光模式下更新表盘

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

在微光模式下,大多数表盘实现会在 <a href="/reference/android/support/wearable/watchface/WatchFaceService.Engine.html#onTimeTick()"Engine.onTimeTick() 函数中废弃画布以重新绘制表盘:

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

    invalidate();
}

配置系统界面

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

使用 Android Wear,您可以配置系统界面在您的表盘处于活动状态时的以下方面:

  • 指定系统是否在您的表盘上绘制时间。
  • 在它们周围使用纯色背景保护系统指示器。
  • 指定系统指示器的位置。

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

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

@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 参考文档

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

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

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

@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);
}

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

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

如需了解与低位微光模式和防烙印有关的详细信息,请参阅针对特殊屏幕优化。如需了解与如何停用位图过滤有关的详细信息,请参阅位图过滤

响应各模式之间的变化

当设备在微光模式与交互模式之间切换时,系统会调用 Engine.onAmbientModeChanged() 函数。您的 service 实现应进行必要的调整以在各模式之间切换,然后为系统调用 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 实例和您应在其内部绘制表盘的边界来调用 Engine.onDraw() 函数。边界会考虑任何插边区域,如一些圆形设备底部的“下巴”。您可以使用此画布按照如下所示方式直接绘制您的表盘:

  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);
    }
    
  2. 检查设备处于微光模式还是交互模式。
  3. 执行任何所需的图形计算。
  4. 在画布上绘制您的背景位图。
  5. 使用 Canvas 类中的函数绘制您的表盘。

以下代码片段显示了如何实现 onDraw() 函数:

@Override
public void onDraw(Canvas canvas, Rect bounds) {
    // Update the time
    mCalendar.setTimeInMillis(System.currentTimeMillis());

    // Constant to help calculate clock hand rotations
    final 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() 函数中初始化的图形样式在背景位图上方绘制它们。秒针仅在交互模式下绘制,不在微光模式下绘制。

如需了解与在画布实例上绘图有关的详细信息,请参阅画布和可绘制对象

WatchFace 示例包含其他表盘,您可以将这些表盘作为如何实现 onDraw() 函数的示例参考。