Drawing a GCB Watch Face with Android Wear

For the past couple of weeks, we have been using Grand Central Board, an open source dashboard for the Apple TV with a range of capabilities, including a reminder function that informs team members about upcoming meetings. Not everyone, however, has a TV permanently in sight, a fact that may be used as a convenient excuse for being late to company events and the like. Inspired by that fact, I decided to take up the challenge of transposing the event clock widget onto an Android Wear Watch Face. It should be noted that the primary function of the Watch Face I’ll be developing is displaying upcoming events from a selected calendar. In Part 1 of this series, we’ll be focusing on drawing the watch face using Android Wear 1.0. watch-face

The source code for the watch face is open and available on GitHub. This post also assumes that you know the basics of drawing with Canvas.

How to Start Working with Android Wear

To start your adventure with Android Wear, you don’t actually have to own a device. You can use the emulator to test your application. You need to remember, however, that the emulator can get glitchy. Additionally, some of the drawn elements will look differently on a real device – it’s hard to properly pick the resolution and density of an actual device. I’d recommend using the emulator in earlier stages of the project and for quick change previews, because it tremendously speeds up development. There is also the possibility of setting up a connection between a watch emulator and a physical device – a phone.

Personally, however, I prefer using actual, real devices although they’re hindered by certain lags related to Bluetooth-mediated communications. Nevertheless, this approach is the most stable and allows us to better test the communication between devices which is especially important in light of the version of Android Wear we’re using. Network requests, account handling using Google API Services and other actions are handled on the phone, while the watch serves as an extension of the user interface, displaying relevant information on the watch face.

If you want to be ready to support the new Android Wear 2.0 and learn about its new capabilities, I’d suggest having a go at the pre-release version. Per the announcement at the latest Google I/O, the 2.0 will support standalone watch apps, finally breaking the chains binding the watches to the phones.

Development and Debugging

Uploading applications and debugging via Bluetooth is possible thanks to adb. You can debug a Moto360 watch like mine via adb using the following instructions:

adb forward tcp:4444 localabstract:/adb-hub
adb connect 127.0.0.1:4444

Connecting to the watch via 127.0.0.1:4444 instead of localhost as described in the documentation is not without significance – the change is intentional and required to connect with the Moto360.

Developing Android Wear: What You Should Know Before Starting

Before you start building apps or watch faces, you should probably read the posts on developing for Android Wear on the Android Developers website. The sample apps section also features a couple of examples of watch faces, including the calendar-integrated Agenda Data and CalendarWatchFaceService. You don’t need any special software to start working, as Android Studio handles generating a new Watch Face project without issue.

Architecture of an Event-Tracking Watch Face

Android Wear support library provides us with two services for drawing purposes: CanvasWatchFaceService and Gles2WatchFaceService. Both are extensions of WatchFaceService, which itself is an extension of WallpaperService. If this is not your first encounter with WallpapersService, the logic of drawing a watch face should be very familiar to you. If you haven’t, fret not, because drawing watch faces using CanvasWatchFaceService is no different from drawing a custom View. Regardless of the service you’ll end up using, drawing implements the onDraw() method of the Engine object. The GCBWatchFace service from our example overrides the CanvasWatchFaceService.Engine#onCreateEngine() function.

@Override
public GCBWatchFaceEngine onCreateEngine() {
   return new GCBWatchFaceEngine();
}

GCBWatchFaceEngine is my implementation of CanvasWatchFaceService. For easier code maintenance, I decided to separate drawing individual GCB Watch Face elements into individual objects. Each of them implements the Drawer interface.

public interface Drawer {
   void setAmbientMode(boolean ambientModeOn);
}

Each implementation has its own draw() method and some have their own measure() method. The appearance of the aforementioned methods is not coincidental and stems from the fact that Engine in the onDraw() method calls draw() on them, and in onSurfaceChanged() it calls the measure() method. The methods assume parameters depending on the needs of a given Drawer. The objects are created and managed inside Engine. Therefore, drawing inside GCBWatchFaceEngine using Drawer objects can look something like this.

public void onDraw(Canvas canvas, Rect bounds) {
   long currentTime = System.currentTimeMillis();
   time.setTimeInMillis(currentTime);
   canvas.drawColor(colorPalette.backgroundColor);

   float centerX = bounds.centerX();
   float centerY = bounds.centerY();
   int minutes = time.get(Calendar.MINUTE);
   boolean eventToDisplay = eventFormatter.hasEvent();

   if (drawInEventMode) {
       if (eventToDisplay) {
           eventDrawer.draw(eventFormatter, canvas, innerOvalRadius, centerX, centerY, time.getTimeInMillis());
       } else {
           placeholderDrawer.draw(canvas, bounds.width(), centerX, centerY);
       }
   } else {
       hourDrawer.draw(canvas, time, centerX, centerY);
   }

   faceDrawer.draw(minutes);

   if (eventToDisplay) {
       indicatorDrawer.draw(eventFormatter.getHourMinutes());
   } else {
       indicatorDrawer.clearIndication();
   }
   canvas.drawBitmap(faceBitmap, bitmapOffset, bitmapOffset, bitmapPaint);
}

Drawing Individual Elements of the Watch Face

I created five separate Drawers, some of which draw directly on the Canvas, while others draw to a Bitmap object which is then drawn on the same main Canvas. The watch works in two modes: one displays the time, the other displays the upcoming events. To switch between the two, you have to tap the watch face. Below is a description of a couple of more interesting Drawers that were created during development:

EventDrawer

EventDrawer

Responsible for drawing the text inside the watch face. Given the limited space, the name of the event has to be cut down to fit two lines of text and the available area. You can use the Paint#getTextBounds(String text, int start, int end, Rect bounds) function to measure the text. However, clipping the text to fit the available space and adding an ellipsis at the end is not an easy task. If you’ve ever happened upon a similar problem, you may just have some idea as to what solving it can do to an Android developer. At first glance, you start thinking about going with StaticLayout, but a voice inside your head tries to warn you that you’ve been through this before and whether you’re sure you really want to go that way again. The main problem with StaticLayout is the fact that the constructor which assumes the maxLines parameter has been hidden (and for a good reason, probably). Ever since the release of API level 23, however, you can simply use Builder and the problem of the maxLines setting disappears. But what happens with applications developed for APIs below level 23? Well, there are tricks that will allow you to reach the hidden constructor using reflections but these solutions are far from elegant. You can try to create your own component but the time you’ll end up spending on complicated logic and all possible edge cases would significantly prolong the duration of the project. What is the best way out of this complicated situation? Well, I’d suggest using TextView. I happened upon an explanation of drawing TextView bitmaps in a reply to a StackOverflow thread. Setting a flag for TextView#setDrawingCacheEnabled(true) gives you a Bitmap that you can draw on a Canvas. Below is an example of using this method to draw the event title:

private void drawEventName(Canvas canvas, Paint bitmapPaint, CharSequence eventName, float centerX, float centerY) {
   eventNameTextView.setText(eventName);
   eventNameTextView.setDrawingCacheEnabled(true);
   if (eventNameTextView.getDrawingCache() != null) {
       canvas.drawBitmap(eventNameTextView.getDrawingCache(), centerX - desiredEventNameWidth / 2,
               centerY - eventNameHeight, bitmapPaint);
   }
   eventNameTextView.setDrawingCacheEnabled(false);
}

EventIndicationDrawer

Event-Indicator-Drawer

Responsible for drawing the Event marker inside the inner circle of the watch face. The white marker is supposed to indicate in which slice of the watch face is the event supposed to commence. Its logic resembles that of FaceDrawer. After the draw() method is called, Paint is used to draw a circle using the Paint.Style.STROKE style and setPathEffect set to DashPathEffect. Using that effects allows us to separate the circle into 12 identical sections. To create the “dashed circles” look, I introduced an additional DashedCirclePaintWrapper which automatically imparts the said look on the Paint object it wraps. Initializing Paint to draw these dashed circles looks something like this:

private void initInnerOvalPaint(ColorPalette colorPalette, float innerStrokeSize) {
   innerOvalPaint = new Paint();
   innerOvalPaint.setAntiAlias(true);
   innerOvalPaint.setColor(colorPalette.colorWhite);
   innerOvalPaint.setStrokeWidth(innerStrokeSize);
   innerOvalPaint.setStyle(Paint.Style.STROKE);
   dashedCirclePaintWrapper = new DashedCirclePaintWrapper(innerOvalPaint);
}

Because DashedCirclePaintWrapper is dependent on the changes in diameter of the circle in the EventIndicationDrawer#measure() method, we need to call DashedCirclePaintWrapper#onDiameterChange(innerOval.width()) After drawing the circle I had to hide its unnecessary elements. This is where blending properties came in, with a little help from PorterDuffXfermode. Paint, with an appropriate Xfrmode, is able to blend with other layers drawn on the Canvas. After setting a proper PorterDuff.Mode and a color we will have our “eraser” at the ready and will be able to hide unwanted elements of the screen. The “eraser” Paint can be defined like this:

private void initInactiveInnerPiecesPaint() {
   innerArcPaint = new Paint();
   innerArcPaint.setAntiAlias(true);
   innerArcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
   innerArcPaint.setColor(Color.TRANSPARENT);
}

While drawing an arc using the Paint outlined above in an area that already contains a previously drawn circle, I masked its entire framing except for one section. This section is supposed indicate the time at which the upcoming meeting is supposed to commence. Drawing a full arc will hide the entire circle. More information on using porter duff masks can be found in a post written by Søren Sandmann Pedersen. We need to remember that using Paint with a set PorterDuffXfermode will blend with all elements drawn on a given Canvas. Because I did not want to blend all the layers, some of them will be drawn to a single Bitmap object. Drawing the event indicator when we’re already familiar with how PorterDuffXfermode works becomes extremely simple.

public void draw(int minutes) {
   indicatorCanvas.save();
   indicatorCanvas.rotate(ovalRotation, innerOval.centerX(), innerOval.centerY());
   indicatorCanvas.drawOval(innerOval, innerOvalPaint);
   indicatorCanvas.restore();
   indicatorCanvas.drawArc(arcRect, getStartAngle(minutes), ARC_MASK_SWAP_ANGLE, true, innerArcPaint);
}

FaceDrawer

FaceDrawer

Responsible for drawing the external circle. Its logic resembles that responsible for drawing the event indicator. The Paint used for masking in this particular instance has a color selected from the color palette instead of having it set to TRANSPARENT. The masked circle has a specific radius gradient. It’s not easy to line up the gaps between the dashes symbolizing hours with the gradient. In this particular case I had to adjust the gradient parameters to fit the gaps created by the “dashed circle” style. This is where these strange values come from, at least strange at first sight:

private final float[] OVAL_GRADIENT_POSITION = new float[]{0f, 0.495f, 0.495f, 0.995f, 0.995f};

The constant is passed to the SweepGradient constructor and the object passes it to Paint#setShader(). In theory, using the declared Paint will result in our gradient painting itself beautifully on the face of our watch 🙂

Anti-Aliasing

I have encountered a couple of problems during my watch face drawing adventures, one of them being anti-aliasing for all drawn circles. Even with appropriate flags for Paint#setAntiAlias() and Paint#setFilterBitmap(), the final effect was less than satisfactory. The tried and tested trick with disabling hardware acceleration has failed in this case, too. I’m planning on replacing all FaceDrawer and EventIndicationDrawer implementations in the future, in order to have them use ready-made bitmaps with the sections and PorterDuffXfermode blending.

Supporting Ambient Mode

The Drawer interface contains a onAmbientModeChanged() method which gives us control over Paints and other implementation elements of individual Drawers during changes in the watch’s ambient mode. We advise you to read the optimalization recommendations for watch face services, as it will help you create a proper architecture that will allow the disabling of unnecessary calculations/functions in power saving mode.

Displaying Events from the Calendar

In the second part of this series, I’ll take a closer look at issues and limitation related to displaying calendar events on the watch face.

Swap Your Old-Fashioned Keychain for a Location-Aware App
Use Kotlin Anko DSL and Say No to Android Layouts Written in XML