On 1st April 2016 I published Something O’Clock, a watch face app for Android Wear, to Play Store. The app is lighthearted in nature (because of the date of publication), it allows the user to set the time to “beer o’clock”, or “sleep o’clock”, or even “burger o’clock”. Although the app itself is quite lighthearted the code behind it is worthy of study and, in this series we’ll take a look at various aspects of developing custom watch faces for Android Wear.
The final app will consist of three separate modules: The app which runs on the Wear device; the companion app which runs on the phone; and a common library which will contain some shared code & resources. We’ll begin by looking at the Wear app itself. This is the component which will actually display the watch face on the phone.
In the wear module we’ll need the Wearable Support Library, and Play Services Wearable Client libraries as dependencies. The first contains the class we’ll need to subclass in order to create a watch face; and the second is to enable data transfer between the phone and wear apps – we’ll need this later on.
The heart of any watch face is actually rather different than would seem normal for Android – it’s not an Activity it’s a Service, albeit one with a UI component. The reason for this is actually quite sensible when you consider how a watch face differs from a more traditional app. It doesn’t follow the usual Activity lifecycle because it’s always on – something that simply isn’t appropriate for a well behaved Activity, but is perfectly acceptable for a well-behaved foreground Service. This makes more sense if we consider that a watch face is somewhat similar to a Live Wallpaper – running constantly. So we need to subclass CanvasWatchFaceService which, unsurprisingly, itself subclasses WallpaperService:
public class SomethingOClockFace extends CanvasWatchFaceService { @Override public Engine onCreateEngine() { return new Engine(); } . . . }
Believe it or not that is actually the Service in its entirety. Sort of, at least. We can do stuff that’s appropriate to a Service if we need to, but we don’t need to here. We do have to override the onCreateEngine()
method, though. This handles the UI and needs to be an inner class of CanvasWatchFaceService.
public class SomethingOClockFace extends CanvasWatchFaceService { @Override public Engine onCreateEngine() { return new Engine(); } private class Engine extends CanvasWatchFaceService.Engine { private boolean ambient; private boolean isRound; private InsetCalculator insetCalculator; private TextLayout textLayout; private boolean lowBitAmbient; private int activeBackgroundColour; private int ambientBackgroundColour; @Override public void onCreate(SurfaceHolder holder) { super.onCreate(holder); Context context = SomethingOClockFace.this; setWatchFaceStyle(new WatchFaceStyle.Builder(SomethingOClockFace.this) .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT) .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) .setShowSystemUiTime(false) .setAcceptsTapEvents(false) .build()); activeBackgroundColour = ContextCompat.getColor(context, R.color.background); ambientBackgroundColour = Color.BLACK; int textColour = ContextCompat.getColor(context, R.color.digital_text); textLayout = TextLayout.newInstance(textColour); } @Override public void onApplyWindowInsets(WindowInsets insets) { super.onApplyWindowInsets(insets); isRound = insets.isRound(); insetCalculator = InsetCalculator.newInstance(SomethingOClockFace.this, isRound); } @Override public void onPropertiesChanged(Bundle properties) { super.onPropertiesChanged(properties); lowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false); } @Override public void onAmbientModeChanged(boolean inAmbientMode) { super.onAmbientModeChanged(inAmbientMode); if (ambient != inAmbientMode) { ambient = inAmbientMode; if (lowBitAmbient) { textLayout.setAntiAlias(!inAmbientMode); } } invalidate(); } @Override public void onDraw(Canvas canvas, Rect bounds) { if (ambient) { canvas.drawColor(ambientBackgroundColour); } else { canvas.drawColor(activeBackgroundColour); } boolean hasChanged = insetCalculator.hasBoundsChanged(bounds); Rect insetBounds = insetCalculator.getInsetBounds(bounds); if (hasChanged) { textLayout.invalidateLayout(); } textLayout.draw(canvas, insetBounds); } } }
There’s quite a bit going on here, but it’s not too daunting if we break it down a little.
In onCreate()
we build the WatchFaceStyle which specifies some basic behaviour of our watch face – such as whether it should receive touch events, the size of notification card ‘peeks’ etc. In our case we want small ‘peeks’ because the watch face is text-based and having text-based peek cards overlaying the watch face in ambient mode really doesn’t work well. We’re not supporting touch events, and we don’t want to clutter the display with system UI time. Next we initialise three colour values for the background colour (a deep purple), the ambient background colour (black), and the text colour (white). Finally we create a new TextLayout object – more on this in the next article.
The next method, onApplyWindowInsets()
, gets called with the dimension of the overall surface which will generally be visible to the user. What we need here is to determine whether the device has a round or square face as our positioning calculations are different depending on the face shape. We create a new InsetCalculator which will be used to aid positioning of the text.
onPropertiesChanged()
gets called with the hardware properties of the device in a Bundle. In our case we’re interested in PROPERTY_LOW_BIT_AMBIENT
which indicates whether the screen switches to fewer bits for each colour value when it is in ambient mode. We’ll use this to disable anti-aliasing when switching to ambient mode for devices supporting low bit ambient mode.
Next we have onAmbientModeChanged()
which gets called when the device enters or exits ambient mode. For those unfamiliar with ambient mode it is a low-power mode which provides an ‘always on’ display, but implements a much less detailed display which does not update as frequently as the main display. For the benefit of AMOLED displays, which consume much less power when pixels are not lit, this generally means large areas of black, unlit pixels. In our case we store the ambient mode status in a boolean, set the anti alias mode accordingly, and then call invalidate()
to force a redraw of the display.
Finally we have onDraw()
which should be a bit more familiar – it is responsible for drawing the watch face. We first draw the appropriate background depending on whether or not we’re in ambient mode. Next we determine whether the device bounds have changed – if so we need to recalculate our layout. Finally we draw the text. Much of the logic for all of this is contained within TextLayout and InsetCalculator and we’ll take a look at these in due course. I have deliberately mimicked the naming and methods of Android Layouts here because essentially that’s what we’re doing. Here we have to draw directly to the Canvas and that’s exactly what Views and Layouts do for us. However the requirements here are to dynamically size the text elements so we need to do some stuff ourselves.
One thing worth mentioning is that Something O’Clock is a static watch face. The only time it changes is if new text is selected (more on that later in the series), and when we switch in and out of ambient mode. For most dynamic watch faces you’ll also need to override onTimeTick()
or even set your own timers which will enable you to update the display in real time.
The only thing remaining is to actually declare this in our Manifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.stylingandroid.something.oclock"> <uses-feature android:name="android.hardware.type.watch" /> <!-- Required to act as a custom watch face. --> <uses-permission android:name="android.permission.WAKE_LOCK" /> <application android:allowBackup="false" android:fullBackupOnly="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@android:style/Theme.DeviceDefault" tools:ignore="GoogleAppIndexingWarning"> <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> <service android:name="com.stylingandroid.something.oclock.SomethingOClockFace" android:allowEmbedded="true" android:label="@string/face_name" android:permission="android.permission.BIND_WALLPAPER" android:taskAffinity=""> <meta-data android:name="android.service.wallpaper" android:resource="@xml/watch_face" /> <meta-data android:name="com.google.android.wearable.watchface.preview" android:resource="@drawable/preview_digital" /> <meta-data android:name="com.google.android.wearable.watchface.preview_circular" android:resource="@drawable/preview_digital_circular" /> <meta-data android:name="com.google.android.wearable.watchface.companionConfigurationAction" android:value="com.stylingandroid.customoclock.CONFIGURATION" /> <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>
It is vital to declare the WAKE_LOCK
permission otherwise your watch face will crash shortly after install. All watch faces require this permission because they need to periodically prevent the device from sleeping when updating the time. We can also provide preview images which will be shown on the device itself and on the Wear app on the paired phone to provide a visual representation of the watch face. Also there is a wallpaper meta-data implementation which is simplicity itself:
<?xml version="1.0" encoding="UTF-8"?> <wallpaper />
So that’s the main skeleton of the watch face, but we haven’t yet looked at how we’ll actually position and render the text – which we’ll look at in the next article.
There is no code being published along with this article because we don’t yet have something working. By the end of the next article we’ll have a fully working watch face (albeit one with limited functionality) and the code will be published then – I promise!.
Many thanks to Daniele Bonaldo, Sebastiano Poggi, Erik Hellman, Hasan Hosgel, Said Tahsin Dane & Murat Yener – my beta testers.
© 2016, Mark Allison. All rights reserved.
Copyright © 2016 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.