Layout-based Watch Faces for Android Wear

Watch faces are basically live wallpapers for your smartwatch – this makes a certain sort of sense, and it is indeed how Google has implemented them on Android Wear. And like live wallpapers, they can be either Canvas or OpenGL-based. But while that works okay for most traditional live wallpapers, there are a lot of watch face designs for which neither of those models works overly well. Examples might include predominantly-digital faces, legacy faces (which were developed as Activities), faces that pack in a lot of data, and anything else with a fairly sophisticated layout.

What you’d like, in fact, is exactly that – to use a standard Android layout for your watch face. It turns out that you can, and in this post, I’m going to show you how. Which of course means that it’s long post, heavy with code. You have been warned.

We’re going to build our layout-based watch face on top of a CanvasWatchFaceService, and the key to it is that you can inflate an arbitrary layout into a View, and then call View.draw(canvas) to render a Canvas from it. The rest of the code is just window dressing, really, but some if it isn’t entirely obvious.

Before we get started, I just want to say that this guide is meant for programmers with some Android experience, and ideally a bit of experience on Android Wear. I’m not going to walk you through every last step of creating a project in the IDE, setting up and running an emulator, what ambient mode means, and so forth. There are lots of great tutorials online for those sorts of things, so I’m going to assume that you have those skills comfortably in hand.

With that said, it’s high time that we…

Get Started

Begin by creating a new Wear watch face project in Android Studio. The current version (1.2 as of this writing) includes watch face templates; we’ll use the Digital template for the example I’ll be explaining here.

[If you’re not using Android Studio yet, now’s a great time to start – and one of these days I’ll write up my hard-won guidelines for converting a mature Eclipse project over to AS. But today, we’ll be starting with a new project, so that’s not an issue.]

Here’s a walk-through of the project creation wizard. The details of these things tend to change from version to version of the IDE, so your screens may not look exactly like this, but hopefully they’ll be close enough.image10

Obviously, the app and package names are up to you.image06

Android Wear isn’t supported below API level 18, so it makes sense to choose that as the minSdkVersion on the phone/tablet side.

For this example, you don’t need an activity on the handheld (mobile) app, because it only serves as a container for installing the watch face. If the previous sentence doesn’t make sense to you, I suggest you go look at Packaging Wearable Apps for clarification.

On the Wear side, however, we do want an activity – or more precisely, a watch face:image04

And on the next screen, choose the Digital watch face template, and give it a name of LayoutFaceService:image00

When Android Studio has created the new project, go ahead and run it, just to make sure that you have a working starting point. It’s probably easiest to do so in an emulator, and when you do, it should look something like this..image03

Now that we have it working, let’s break it! This is an ordinary canvas-based watchface; it’s a reasonable foundation for what we’re going to build, but has some unnecessary bits. So, the next step is to clear those out. Delete the following from your LayoutFaceService source file:

  • The entire createTextPaint method
  • The contents of the onDraw method
  • The two Paint fields (mBackgroundPaint and mTextPaint) and all references to them in the code

And in the interest of brevity, I’m not going to walk you through cleaning up various other loose ends in the template (such as its use of the deprecated Time class). If it works at the end, we’ll call that good. Turning it into production-ready code is left as an exercise for the reader.

But before we move on, let’s create a few new fields that we’ll be needing soon. In the Engine class, insert the following:

private int specW, specH;
private View myLayout;
private TextView day, date, month, hour, minute, second;
private final Point displaySize = new Point(); 

Creating the layout

If all we wanted to render was a simple digital face (like the template), we wouldn’t need a layout – it’s easy enough to render directly. But the whole idea here is to render something more complicated, something that would be a pain to draw out directly onto the canvas.

On the other hand, I still want to keep it simple-ish for this example. image05Here’s a mockup of the watch face I had in mind when I started this post:

It’s just about complicated enough to make this technique worthwhile.

If you have any Android dev experience, you can probably create a layout like this in your sleep. I’m using a simple RelativeLayout, and you can find it in the source code for this example on GitHub. But really, the important thing to take away is that it’s in res/layout/watchface.xml, in my project’s wear module.image01

Initialization

Now we need to load that layout into our watch face service. The place to do so is in the Engine.onCreate method, and the code is as follows:

LayoutInflater inflater = 
    (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
myLayout = inflater.inflate(R.layout.watchface, null);

If you’ve ever worked with layouts in Android Java code, this will look familiar. It’s pretty standard stuff.

In order to use this layout, we’ll also need some data about the display, so put the following code in onCreate also:

Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE))
        .getDefaultDisplay();
display.getSize(displaySize);

specW = View.MeasureSpec.makeMeasureSpec(displaySize.x, 
    View.MeasureSpec.EXACTLY);
specH = View.MeasureSpec.makeMeasureSpec(displaySize.y,
    View.MeasureSpec.EXACTLY);

It looks complicated, but this is just boilerplate; you can copy this stuff verbatim into any layout-based watch face you build.

Finally, we’ll initialize the TextView fields to avoid having to find them every time we need to update the date and time. Again, this is quite basic stuff.

day = (TextView) myLayout.findViewById(R.id.day);
date = (TextView) myLayout.findViewById(R.id.date);
month = (TextView) myLayout.findViewById(R.id.month);
hour = (TextView) myLayout.findViewById(R.id.hour);
minute = (TextView) myLayout.findViewById(R.id.minute);
second = (TextView) myLayout.findViewById(R.id.second);

Making it work

In all Canvas-based watch faces, the onDraw method is where the pixels meet the code. Ours is no different, but one of the advantages to a layout-based face is that onDraw tends to be simpler than if we were drawing everything manually. In essence, we’ve front-loaded the drawing when we set up the layout, so we can offload the actual drawing code to the layout itself.

All of the following code goes in the onDraw handler. The first line is unchanged from the template that Android Studio generated for us:

mTime.setToNow();

That sets up the Time variable to the current instant. Next we apply it to all the on-screen date and time TextViews:

day.setText(String.format("%ta", mTime.toMillis(false)));
date.setText(String.format("%02d", mTime.monthDay));
month.setText(String.format("%ta", mTime.toMillis(false)));

hour.setText(String.format("%02d", mTime.hour));
minute.setText(String.format("%02d", mTime.minute));
if (!mAmbient) {
    second.setText(String.format("%02d", mTime.second));
}

These all follow a similar pattern, extracting a field from mTime and formatting it for display. It’s fiddly, but not actually hard.

So now, all our text fields have been updated, and we need to output it to the screen. But we’re not quite ready to draw it to the Canvas yet; first, we need to finalize the layout.

myLayout.measure(specW, specH);
myLayout.layout(0, 0, myLayout.getMeasuredWidth(), 
myLayout.getMeasuredHeight());

This is the crucial step to making a layout-based watch face work. If you’ve been doing ordinary Android development, it’s likely you haven’t seen code like this before; it usually happens behind the scenes in the View classes, and you don’t need to worry about it. But in our case, we’re using a “naked” layout – one not attached to a view hierarchy – so we need to do this ourselves. If we didn’t the layout simply wouldn’t render, and this technique wouldn’t work.

Whatever you do, don’t skip this step.

Now, we’re ready to output it to the Canvas:

canvas.drawColor(Color.BLACK);
myLayout.draw(canvas);

The first line clears the Canvas to black, and the second one draws the current contents of our layout.

And that’s it for interactions with the Canvas: usually the biggest part of onDraw, but reduced by the layout approach to a trivial line of code.

Making it work in the real world

At this point, we have a working watch face. It updates the display fields to the current time, and draws them to the screen.

Before moving on, I’d just like to note that much of the job has been done for us by the template itself. It contains quite a few pre-written methods for triggering updates every minute (when in ambient mode) or every second (when not), as well as more esoteric details like tracking time zone changes. Google has put all of this code into the template, and we don’t need to touch any of it.image02

Our watch face works! But, it’s not quite ready for release.

Ambient Mode

One key behavior of Android Wear watches is the two modes they operate in, ambient and normal. The latter is when the user’s actively engaging with their watch, and we’ve got that covered.

But a watch spends most of its time in ambient mode, and a watch face needs to behave differently when it does. The Engine class we’re working from includes a method, onAmbientModeChanged, which is where we’ll put the code to handle the changes.

First, the time-keeping infrastructure built into the template drops its refresh rate from once-per-second to once-per-minute, and as a result, we don’t want to be showing the “seconds” fields when in ambient mode. Because we’re using a standard Android layout, we hide and show the seconds the same way you would in any Activity:

if (inAmbientMode) {
    second.setVisibility(View.GONE);
    myLayout.findViewById(R.id.second_label)
            .setVisibility(View.GONE);
} else {
    second.setVisibility(View.VISIBLE);
    myLayout.findViewById(R.id.second_label)
            .setVisibility(View.VISIBLE);
}

The second change is a bit less obvious. The visual design I’m working from is based around chunky fonts, and I’ve used boldface type to achieve that effect. But that’s not a good idea when a watch is in ambient mode, for a couple of reasons.

  • AMOLED screens use less power when fewer pixels are lit, so boldface text probably uses twice as much power as normal text.
  • Some screen technologies have an issue with burn-in, and Android Wear addresses this by shifting pixels slightly on-screen. This works much better with thin lines, however, so watch face developers are encouraged to avoid thick strokes in ambient mode.

The upshot is, I only want to use boldface for my text fields when the watch is not in ambient mode. Here’s the code I use to do it:

Typeface font = Typeface.create("sans-serif-condensed",
        inAmbientMode ? Typeface.NORMAL : Typeface.BOLD);
ViewGroup group = (ViewGroup) myLayout;
for (int i = group.getChildCount() - 1; i >= 0; i--) {
    ((TextView) group.getChildAt(i)).setTypeface(font);
} 

You’ll notice that this is kind of a cheat, and only works because every view in my layout is a TextView. image07But it does work – here’s my watch face in ambient mode:

In most cases, more complicated layouts will require more complicated transitions between ambient and normal modes, but the details will need to be determined case-by-case.

Screen shape

In this example, I’m primarily designing for a square watch face. But in the real world, you’ll want to support round watches too – they make up the majority of Android Wear devices in the wild..

To avoid making this long post even longer, all I’m going to do to adjust this watchface for round screens is to shrink it a bit, so that the square text fits in the round hole. I’ll do that in two places; the first is the template’s existing onApplyWindowInsets method. Replace the code in that method (after the call to super.onApplyWindowInsets with the following:

if (insets.isRound()) {
    mXOffset = mYOffset = displaySize.x * 0.1f;
    displaySize.x -= 2 * mXOffset;
    displaySize.y -= 2 * mYOffset;

    specW = View.MeasureSpec.makeMeasureSpec(displaySize.x, 
         View.MeasureSpec.EXACTLY);
    specH = View.MeasureSpec.makeMeasureSpec(displaySize.y,
         View.MeasureSpec.EXACTLY);
} else {
    mXOffset = mYOffset = 0;
}

For round screens, we basically compute a margin – 10% of the square layout width – and apply it to the displaySize we got in onCreate. From there, we just recompute the MeasureSpec fields accordingly – this is determines the actual size of the layout.

We also need to use this margin when drawing, and that happens in onDraw, not surprisingly. All that’s needed is one new line of code, highlighted below, in the canvas-drawing section:

canvas.drawColor(Color.BLACK);
canvas.translate(mXOffset, mYOffset);
myLayout.draw(canvas);

As its name implies, translate just moves the Canvas before drawing to it, applying the offsets that we calculated above.image09

Here’s what it looks like on a round screen:

Nothing fancy, but it works. However, this is another case where the exact implementation details will depend upon your design. Besides just shrinking the design like this, other solutions might include moving elements of the layout around to fit, or using a different layout completely.

Our layout-based watch face is now complete, and just about ready to deploy. All that’s left are metadata elements like previews and icons, but these are no different than any other watch face, and don’t really merit coverage here.

Caveats

So far, I’ve mostly talked about the advantages of layout-based watch faces: primarily that you can use all of Android’s UI tools to build your watch face, rather than laboriously drawing it by hand (as it were).

But there are a few downsides as well.

Layout isn’t automatic

This is the biggest pitfall of this approach, for Android devs of all experience levels. Ordinarily, you create your layout, stick it in an activity, and it displays on screen. With a watch face, it’s not quite that simple.

I touched on this back in the onDraw method, but it’s important enough that it bears repeating. Remember these two lines of code?

myLayout.measure(specW, specH);
myLayout.layout(0, 0, myLayout.getMeasuredWidth(), 
         myLayout.getMeasuredHeight());

These two methods, measure and layout, need to be called after any change to your content that affects the size of any layout element. In my example, I ensure that it will happen by putting them in onDraw, right before drawing to the Canvas. But if you have more complicated logic – perhaps with different parts of your layout being updated in different places – you’ll need to take some care to ensure that measure and layout are always called when they need to be.

If you ever find that your layout is not rendering correctly, this is the first thing to look for.

Battery impact is not out of the question

I’ve developed (and deployed) several watch faces using layouts, and none of them seem to use an excessive amount of battery. However, battery life is always a concern on Android Wear, and it at least seems plausible that using a layout for your watch face will incur some extra overhead – and thus use more power.

As I said, I haven’t seen an issue with it yet, but given this is a new technique, I feel I should mention it.

And while we’re on the subject, you should take care to optimize your code for battery life generally. In the interests of brevity and clarity, I’ve taken a few shortcuts with this example (like updating the date fields every second) that you probably shouldn’t do in the real world. Google has some specific guidelines in this area; read them, follow them, and use your own good sense.

WatchViewStub doesn’t seem to work

A few paragraphs ago, I mentioned the possibility of using different layouts for round versus square screens. If you’ve done much Android Wear development, you’re probably familiar with a standard pattern for addressing this: WatchViewStub, which automagically selects the correct layout based on screen shape.

Unfortunately, WatchViewStub doesn’t work with the technique I’ve outlined here. It’s not open-source, so I can’t delve into the details of why not, but in my testing, inflating a WatchViewStub-based layout just doesn’t work. The magic smoke escapes somewhere, and you always end up with the same layout, regardless of screen shape.

Of course, you can still use different layouts for different shapes; this simply means that you need to select the proper one manually. And the good news is that onApplyWindowInsets does work, so you can easily get the screen shape there – and inflate the proper layout accordingly. Take a look at the Android docs (or the onApplyWindowInsets method in the template) for guidance on how to use this handy method.

Are layout-based watch faces for you?

It’s a fair question – they’re not right for every case.

If you’re creating a face that’s mostly built from graphical assets, it’s probably easier to render them directly to the screen, rather than shunting them through a layout first. Likewise, if your design involves moving many elements around the screen (such as analog clock hands, for example), using a layout may actually be more work.image08

On the other hand, if your watch face design is text-heavy, or has complicated interrelationships between the elements, or perhaps is based off an existing Android app – then using a layout probably is a good choice. And some operations that are a real PITA with direct rendering – like wrapping text – are just effortless with a layout.

It’s also worth noting that layouts aren’t an all-or-nothing proposition. It’s perfectly reasonable to draw some graphical elements directly to the Canvas, then draw your layout. I’ve used this hybrid technique in production watchfaces too.

In any case, I hope you’ve found this guide useful, and it helps you build better watch faces. The sample watch face I’ve built here can be found on both GitHub and Google Play.


Update 11 May 2015:

Fixed a bug with the onApplyWindowOffsets code for round watch face support.

Advertisements