After you have configured your project and added a class that implements the watch face service, you can start writing code to initialize and draw your custom watch face.
This lesson includes examples from the WatchFace sample on GitHub to show how the system uses the watch face service. Many aspects of the service implementations described here, such as initialization and device features detection, apply to any watch face, so that you can reuse some of the code in your own watch faces.


Figure 1. The analog and digital watch faces from the WatchFace sample.
Initialize your watch face
When the system loads your service, you should allocate and initialize most of the resources that your watch face needs, including loading bitmap resources, creating timer objects to run custom animations, configuring paint styles, and performing other computations. You can usually perform these operations only once and reuse their results. This practice improves the performance of your watch face and makes it easier to maintain your code.
To initialize your watch face, follow these steps:
- Declare variables for a custom timer, graphic objects, and other elements.
- Initialize the watch face elements in the
Engine.onCreate()
method. - Initialize the custom timer in the
Engine.onVisibilityChanged()
method.
The following sections describe these steps in detail.
Declare variables
The resources that you initialize when the system loads your service need to be accessible
at different points throughout your implementation, so you can reuse them. You achieve this
by declaring member variables for these resources in your
WatchFaceService.Engine
implementation.
Declare variables for the following elements:
- Graphic objects
- Most watch faces contain at least one bitmap image used as the background of the watch face, as described in Plan the implementation of the watch face. You can use additional bitmap images that represent clock hands or other design elements of your watch face.
- Periodic timer
- The system notifies the watch face once per minute when the time changes, but some watch faces run animations at custom time intervals. In these cases, you need to provide a custom timer that ticks with the frequency required to update your watch face.
- Time zone change receiver
- Users can adjust their time zone when they travel, and the system broadcasts this event. Your service implementation must register a broadcast receiver that is notified when the time zone changes and update the time accordingly.
The following sample shows how to define these variables:
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; } } } } ... }
In the previous example, the custom timer is implemented as a
Handler
instance that sends and processes delayed messages using the thread's
message queue. For this particular watch face, the custom timer ticks once every second. When the
timer ticks, the handler calls the
invalidate()
method and the system then calls the
onDraw()
method to redraw the watch face.
Initialize watch face elements
After declaring member variables for bitmap resources, paint styles, and other elements that you reuse every time you redraw your watch face, initialize them when the system loads your service. Initializing these elements only once and reusing them improves performance and battery life.
In the
Engine.onCreate()
method, do the following:
- Load the background image.
- Create styles and colors to draw graphic objects.
- Allocate an object to calculate the time.
- Configure the system UI.
The following sample shows how to initialize these elements:
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(); }
The background bitmap is loaded only once, when the system initializes the watch face. The
graphic styles are instances of the Paint
class. Use these
styles to draw the elements of your watch face inside the
Engine.onDraw()
method, as described in Draw your watch face.
Initialize the custom timer
As a watch face developer, you decide how often you want to update your watch face by providing a custom timer that ticks with the required frequency while the device is in interactive mode. This enables you to create custom animations and other visual effects.
Note: In ambient mode, the system doesn't reliably call the custom timer. To update the watch face in ambient mode, see Update the watch face in ambient mode.
An example timer definition from the AnalogWatchFaceService
class that ticks once
every second is shown in Declare variables. In the
Engine.onVisibilityChanged()
method, start the custom timer if these two conditions apply:
- The watch face is visible.
- The device is in interactive mode.
The AnalogWatchFaceService
class schedules the next timer tick, if required, as
follows:
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(); }
This custom timer ticks once every second, as described in Declare variables.
In the
onVisibilityChanged()
method, start the timer, if required, and register the
receiver for time zone changes, as shown in the following code sample:
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() } // The timer runs depending on whether the app is visible and // whether the app in ambient mode, so you 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 you weren't visible. calendar.setTimeZone(TimeZone.getDefault()); } else { unregisterReceiver(); } // The timer runs depending on whether the app is visible and // whether the app is in ambient mode, so you may need to start or stop the timer updateTimer(); }
When the watch face is visible, the
onVisibilityChanged()
method registers the receiver for time zone changes. If the device is in interactive mode, this
method also starts the custom timer. When the watch face isn't visible, this
method stops the custom timer and unregisters the receiver for time zone changes.
The
registerReceiver()
and
unregisterReceiver()
methods are implemented as follows:
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); }
Update the watch face in ambient mode
In ambient mode, the system calls the
Engine.onTimeTick()
method every minute. It is usually sufficient to update your watch face once per minute in this
mode. To update your watch face while in interactive mode, you must provide a custom timer as
described in Initialize the custom timer.
In ambient mode, most watch face implementations simply invalidate the canvas to redraw the watch
face in the
Engine.onTimeTick()
method, as shown in the following code sample:
Kotlin
override fun onTimeTick() { super.onTimeTick() invalidate() }
Java
@Override public void onTimeTick() { super.onTimeTick(); invalidate(); }
Configure the system UI
Watch faces shouldn't interfere with system UI elements. If your watch face has a light background or shows information near the bottom of the screen, you may have to configure the size of notification cards or enable background protection.
Wear OS by Google enables you to do the following with your system UI if your watch face is active:
- Specify whether the system draws the time over your watch face.
- Protect the system indicators with a solid background around them.
- Specify the positioning of the system indicators.
To configure these aspects of the system UI, create a
WatchFaceStyle
instance and pass it to the
Engine.setWatchFaceStyle()
method.
The AnalogWatchFaceService
class configures the system UI as shown in the following
code sample:
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()); ... }
The previous code sample configures the system to not show the time (since this watch face draws its own time representation).
You can configure the style of the system UI at any point in your watch face implementation. For example, if the user selects a white background, you can add background protection for the system indicators.
For more details about configuring the system UI, see the Wear API reference documentation.
Manage the unread notification indicator
Users often want to have a clear indication that there are unread notifications. Therefore an unread notification indicator is provided. This indicator appears as an encircled dot at the bottom of the screen. It is displayed if there is more than one unread notification in the stream.

Figure 2. Unread notification indicator.
Note: The unread notifications indicator isn't enabled in the production version of Wear 2.8.0. Test your implementation using the latest Wear emulator instead. This feature is displayed by default starting from version 2.9.0.
By default, the indicator is added to your watch face. We strongly recommend that you leave the indicator available to your users. However, if your watch face already provides an indication of unread notifications, or if the position of the new indicator clashes with an element of the watch face, you can opt out of having the system indicator displayed. Use one of the following methods when you build the watch style:
- Explicitly hide the indicator by setting
WatchFaceStyle.Builder.setHideNotificationIndicator()
totrue
. UseWatchFaceStyle.getUnreadCount()
to display the unread notification count in your watch face. - Request that the unread count is shown in the status bar by setting
WatchFaceStyle.Builder.setShowUnreadCountIndicator()
totrue
.


Figure 3. Custom notification count or status bar notification count.
If you choose to display the unread notification indicator on your watch face,
you can customize the color of its outer ring. Call
WatchFaceStyle.Builder.setAccentColor()
and specify the desired color.
By default, the outer ring is white.
Obtain information about the device screen
The system calls the
Engine.onPropertiesChanged()
method when it determines the properties of the device screen, such as whether the device uses
low-bit ambient mode and whether the screen requires burn-in protection.
The following code sample shows how to obtain these properties:
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); }
You should take these device properties into account when drawing your watch face:
- For devices that use low-bit ambient mode, the screen supports fewer bits for each color in ambient mode, so you should disable anti-aliasing and bitmap filtering when the device switches to ambient mode.
- For devices that require burn-in protection, avoid using large blocks of white pixels in ambient mode and don't place content within 10 pixels of the edge of the screen, since the system shifts the content periodically to avoid pixel burn-in.
For more information on how to disable bitmap filtering, read Use bitmap filtering.
Respond to changes between modes
When the device switches between ambient and interactive modes, the system calls the
Engine.onAmbientModeChanged()
method. Your service implementation should make any necessary adjustments to switch between modes
and then call the
invalidate()
method for the system to redraw the watch face.
The following sample shows how to implement this method:
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(); }
This example makes adjustments to some graphic styles and invalidates the canvas so the system can redraw the watch face.
Draw your watch face
To draw a custom watch face, the system calls the
Engine.onDraw()
method with a Canvas
instance and the bounds in which you should draw your
watch face. The bounds take into account any inset areas, such as the "chin" on the bottom of some
round devices. You can use this canvas to draw your watch face directly as shown in the following
code sample:
- Override the
onSurfaceChanged()
method to scale your background to fit the device any time the view changes.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); }
- Check whether the device is in ambient mode or interactive mode.
- Perform any required graphic computations.
- Draw your background bitmap on the canvas.
- Use the methods in the
Canvas
class to draw your watch face.
The following sample shows how to implement the
onDraw()
method:
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 follows). 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 follows). 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); } }
For more information about drawing on a Canvas instance, see Drawables overview.
The
WatchFace
sample on GitHub includes additional watch faces that you can refer to as examples of how to
implement the
onDraw()
method.
Related resources
Refer to the following related resources: