プロジェクトを設定し、ウォッチフェイス サービスを実装するクラスを追加すると、カスタムのウォッチフェイスを初期化および描画するコードを作成できるようになります。
以下の関連リソースもご覧ください。
このレッスンでは、ウォッチフェイスのサンプルを用い、システムがウォッチフェイス サービスをどのように使用するかを説明します。初期化やデバイスの機能の検出など、ここで説明するサービスの実装手続きの多くはどのウォッチフェイスにも共通するため、一部のコードはウォッチフェイスを設計する際に再利用できます。


図 1. アナログとデジタルのウォッチフェイスのサンプル
ウォッチフェイスを初期化する
システムがサービスを読み込むときに、ウォッチフェイスで必要なリソースの大部分の割り当てと初期化(ビットマップ リソースの読み込み、カスタムのアニメーションを実行するためのタイマー オブジェクトの作成、ペイントのスタイルの設定、その他の計算の実行など)を行う必要があります。通常、こうした処理は 1 回だけ行われ、その結果は再利用できます。これにより、ウォッチフェイスのパフォーマンスが向上し、コードのメンテナンスを簡単に行えるようになります。
ウォッチフェイスを初期化する手順は次のとおりです。
- カスタム タイマー、グラフィック オブジェクトなどの要素の変数を宣言します。
Engine.onCreate()
メソッドでウォッチフェイスの要素を初期化します。Engine.onVisibilityChanged()
メソッドでカスタム タイマーを初期化します。
以下のセクションで、この手順について詳しく説明します。
変数を宣言する
システムがサービスを読み込むときに初期化するリソースは、実装の至るところからいつでもアクセスして再利用できるようにする必要があります。そのためには、WatchFaceService.Engine
の実装でそれらのリソース用のメンバー変数を宣言します。
以下の要素の変数を宣言します。
- グラフィック オブジェクト
- 実装戦略の策定で説明されているように、ほとんどのウォッチフェイスには、ウォッチフェイスの背景として使用されるビットマップ画像が少なくとも 1 つ含まれています。追加のビットマップ画像を使用すると、時計の針などのデザイン要素をウォッチフェイスに描画できます。
- 定期タイマー
- 時刻(分)が変わったときにシステムからウォッチフェイスに通知が送信されますが、中には独自の間隔でアニメーションを実行するウォッチフェイスもあります。そのような場合、ウォッチフェイスを更新するために必要な頻度で作動するカスタム タイマーを提供する必要があります。
- タイムゾーン変更レシーバ
- ユーザーは旅行などの際にタイムゾーンを調整でき、システムはそのイベントをブロードキャストします。 サービスの実装では、タイムゾーンが変わり、それに応じて時刻が変更されたときに通知を受け取るブロードキャスト レシーバを登録する必要があります。
次のスニペットは、これらの変数を定義する方法を示しています。
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
インスタンスとしてカスタム タイマーが実装されています。このウォッチフェイスのカスタム タイマーは 1 秒に 1 回作動します。タイマーが作動すると、ハンドラが invalidate()
メソッドを呼び出し、その後でシステムが onDraw()
メソッドを呼び出してウォッチフェイスを再描画します。
ウォッチフェイスの要素を初期化する
ウォッチフェイスを再描画するたびに再利用する、ビットマップ リソース、ペイントのスタイルなどの要素のメンバー変数を宣言した後、システムがサービスを読み込むときにそれらを初期化します。これらの要素を 1 回だけ初期化して、それらを再利用することで、パフォーマンスが向上し、電池寿命が改善します。
Engine.onCreate()
メソッドで以下の要素を初期化します。
- 背景画像を読み込みます。
- グラフィック オブジェクトの描画に使用するスタイルと色を作成します。
- 時間の計算に使用するオブジェクトを割り当てます。
- システム UI を設定します。
次のスニペットは、これらの要素を初期化する方法を示しています。
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(); }
背景ビットマップは、システムがウォッチフェイスを初期化するときに 1 回だけ読み込まれます。グラフィックのスタイルは、Paint
クラスのインスタンスです。後述のウォッチフェイスを描画するで説明しますが、これらのスタイルは、Engine.onDraw()
メソッド内でウォッチフェイスの要素を描画する際に使用されます。
カスタム タイマーを初期化する
ウォッチフェイスの開発では、デバイスがインタラクティブ モードのときに必要な頻度で作動するカスタム タイマーを提供することによってウォッチフェイスの更新頻度を決定します。これにより、カスタムのアニメーションやその他の視覚効果を作成できるようになります。
注: 「常に画面表示」モードでは、システムはカスタム タイマーを確実に呼び出すことができません。常に画面表示モードでのウォッチフェイスの更新については、常に画面表示モードでウォッチフェイスを更新するをご覧ください。
AnalogWatchFaceService
クラスでのタイマーの定義の例(1 秒に 1 回作動するタイマー)は、変数を宣言するに示されています。Engine.onVisibilityChanged()
メソッドでは、以下の 2 つの条件を満たした場合にカスタム タイマーを開始します。
- ウォッチフェイスが表示されている。
- デバイスがインタラクティブ モードになっている。
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(); }
変数を宣言するで説明したように、このカスタム タイマーは 1 秒に 1 回作動します。
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()
メソッドを 1 分おきに呼び出します。このモードでは通常、ウォッチフェイスの更新は 1 分に 1 回で十分です。インタラクティブ モードのときにウォッチフェイスを更新するには、カスタム タイマーを初期化するで説明したように、カスタム タイマーを提供する必要があります。
常に画面表示モードの場合、ほとんどのウォッチフェイスの実装では、Engine.onTimeTick()
メソッドでウォッチフェイスを再描画するためにキャンバスを無効にします。
Kotlin
override fun onTimeTick() { super.onTimeTick() invalidate() }
Java
@Override public void onTimeTick() { super.onTimeTick(); invalidate(); }
システム UI を設定する
システム UI の要素への対応に関する説明のとおり、ウォッチフェイスがシステム UI の要素の妨げにならないようにする必要があります。ウォッチフェイスの背景が明るい場合や、画面下部に情報を表示する場合は、通知カードのサイズを設定するか、背景保護を有効にすることが必要になる場合があります。
Wear OS by Google では、ウォッチフェイスがアクティブな場合、システム UI の以下の要素を設定できます。
- システムがウォッチフェイスの上に時刻を描画するかどうかを指定する。
- 周囲に単一色の背景を使用してシステム インジケーターを保護する。
- システム インジケーターの位置を指定する。
システム UI のこれらの要素を設定するには、WatchFaceStyle
インスタンスを作成して Engine.setWatchFaceStyle()
メソッドに渡します。
AnalogWatchFaceService
クラスでは、システム UI を次のように設定します。
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()); ... }
上のコード スニペットでは、システム時刻が表示されないように設定しています(このウォッチフェイスでは時刻の描画に独自の表現を使用するため)。
システム UI のスタイルは、ウォッチフェイスの実装内の任意の場所で設定できます。たとえば、ユーザーが白の背景を選択した場合、システム インジケーターの背景保護を追加できます。
システム UI の設定について詳しくは、Wear API リファレンス ドキュメントをご覧ください。
未読通知インジケーターを管理する
ユーザーは多くの場合、未読の通知があるかどうかを明確に示してほしいと考えています。そうした要望に応えるために、未読通知インジケーターが提供されています。このインジケーターは、画面下部に丸のついたドットとして、ストリーム内に未読通知がある場合に表示されます。

図 2. 未読通知インジケーター
注: 未読通知インジケーターは、Wear 2.8.0 の製品版では利用できません。代わりに、最新の Wear エミュレータを使って実装をテストすることをおすすめします。未読通知インジケーターは、次回の Wear 正式リリース(バージョン 2.9.0)からデフォルトで表示されます。
未読通知インジケーターはデフォルトでウォッチフェイスに追加されます。そのままユーザーが未読通知インジケーターを利用できるようにしておくことを強くおすすめします。ただし、すでにウォッチフェイスに未読通知が示されるようになっている場合や、未読通知インジケーターとウォッチフェイスの要素の位置が重なっている場合は、システム インジケーターを表示しなくてもかまいません。スマートウォッチのスタイルを作成する場合は、次のいずれかの方法を使用します。
WatchFaceStyle.Builder.setHideNotificationIndicator
をtrue
に設定して、インジケーターを明示的に非表示にします。ウォッチフェイスに未読通知数を表示する場合は、WatchFaceStyle.getUnreadCount
を使用します。WatchFaceStyle.Builder.setShowUnreadCountIndicator
をtrue
に設定して、未読通知数がステータスバーに表示されるようリクエストします。


図 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()
メソッドが呼び出されます。描画範囲では、インセット領域(一部の丸形のデバイスの下部に表示される「あご」など)が考慮されます。このキャンバスを使用すると、次のようにウォッチフェイスを直接描画できます。
- ビューの変更時にデバイスに合わせて背景のサイズを調整できるよう、
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); }
- デバイスが常に画面表示モードかインタラクティブ モードかを確認します。
- 必要なグラフィック計算を実行します。
- キャンバスに背景ビットマップを描画します。
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); } }
Canvas インスタンスへの描画について詳しくは、キャンバスとドローアブルについての記事をご覧ください。
ウォッチフェイスのサンプルには、onDraw()
メソッドの実装方法の例として参照可能なその他のウォッチフェイスも含まれています。