If you are completely new to Android Wear development, it is highly recommended you start from another short tutorial, “Building Your First Wearable Android App”. You will learn how to set up for mobile and wearable devices as well as configure for Android Studio.
This tutorial will focus on creating the most widely used feature on smart watches: a personalized watch face from a software developer’s perspective. It includes how to prepare the implementation of a watch face service and engine, how to render the watch faces, and what performance issues to pay attention to.
Overview
Before we start working on the code, there are a few things that are noteworthy:
First off, there are at least a couple of ways to select your favorite watch faces. On the mobile side, you can do it through the default Android Wear companion app, which provides the preview of all installed watch faces as well as detailed configuration options, if provided, as shown in Figure 1. On the wearable side, doing a long press of the current watch face will prompt you a list of scrollable watch faces with optional simple settings, as in Figure 2.
Secondly, make sure you have the minimum API versions installed for your development. When you create a new project in Android Studio, you should select both form factors in “Phone and Tablet” and “Wearable”. The former requires a minimum of API 18 while the latter requires API 21. If you don’t have these minimum requirements, you should update your APIs through Android Studio’s “Tools -> Android -> SDK Manager”.
Thirdly, make sure both form factors have the following permissions in the manifest file, AndroidManifest.xml.
<uses-permission android_name= "com.google.android.permission.PROVIDE_BACKGROUND" /> <uses-permission android_name= "android.permission.WAKE_LOCK" />
The credits for all the amazing background photos should go to my Australian friend, Chris Blunt. He is kind enough to give me permission to use them. If you’re interested, he has many more in the reference link, illustrating the magnificent Australian scenery.
Figure 1: Watch Faces on Mobile Companion App
Figure 2: Watch Faces on Wearable Device
Implementing the Watch Face Service
Watch faces are available through the Android Wear companion app on the mobile device and selector on your wearables. They are implemented as services and their methods are initiated when a watch face is active. If you have done Android Live Wallpaper or Widget programming, creating a service for watch faces should look very familiar to you at first glance.
The service needs to be registered; that can be done inside the AndroidManifest.xml “application” element. This is more like a template, so you can just modify the service name next time around. As an example, “CLiu Digital” is our watch face name with the service titled WatchFaceCLiuDigitalService.
<service android_name=".WatchFaceCLiuDigitalService" android_label="CLiu Digital" android_allowEmbedded="true" android_taskAffinity="" android_permission= "android.permission.BIND_WALLPAPER" > <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_cliudigital" /> <meta-data android_name= "com.google.android.wearable.watchface. preview_circular" android_resource= "@drawable/preview_cliudigital_circular" /> <intent-filter> <action android_name= "android.service.wallpaper.WallpaperService" /> <category android_name= "com.google.android.wearable.watchface. category.WATCH_FACE" /> </intent-filter> </service>
And the main service code extends the CanvasWatchFaceService and CanvasWatchFaceService.Engine classes. Again, the following code can be treated like a template and your real job is to override the callback methods in the CanvasWatchFaceService.Engine class and fill in your own modifications. Our example sets the update interval to one second (1000 milliseconds), as in UPDATE_INTERVAL, when the watch face is active.
public class WatchFaceCLiuDigitalService extends CanvasWatchFaceService { private static final long UPDATE_INTERVAL = TimeUnit.SECONDS.toMillis(1); @Override public Engine onCreateEngine() { return new Engine(); } private class Engine extends CanvasWatchFaceService.Engine { Paint mDigitalPaint; Paint mDigitalPaintOuter; boolean mMute; Time mTime; static final int MESSAGE_ID_UPDATE_TIME = 1000; final Handler mUpdateTimeHandler = new Handler() { @Override public void handleMessage(Message message) { switch (message.what) { case MESSAGE_ID_UPDATE_TIME: invalidate(); if (isVisible() && !isInAmbientMode()) { long delay = UPDATE_INTERVAL - (System.currentTimeMillis() % UPDATE_INTERVAL); mUpdateTimeHandler.sendEmptyMessageDelayed (MESSAGE_ID_UPDATE_TIME, delay); } break; } } }; final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mTime.clear(intent.getStringExtra("time-zone")); mTime.setToNow(); } }; boolean mRegisteredTimeZoneReceiver = false; boolean mLowBitAmbient = false; ... @Override public void onCreate(SurfaceHolder holder) { super.onCreate(holder); setWatchFaceStyle(new WatchFaceStyle.Builder (WatchFaceCLiuDigitalService.this) .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT) .setAmbientPeekMode(WatchFaceStyle.AMBIENT _PEEK_MODE_HIDDEN) .setBackgroundVisibility(WatchFaceStyle.BACKGROUND _VISIBILITY_INTERRUPTIVE) .setShowSystemUiTime(false) .build()); ... mTime = new Time(); } @Override public void onDestroy() { mUpdateTimeHandler.removeMessages (MESSAGE_ID_UPDATE_TIME); super.onDestroy(); } @Override public void onPropertiesChanged(Bundle properties) { super.onPropertiesChanged(properties); mLowBitAmbient = properties.getBoolean (PROPERTY_LOW_BIT_AMBIENT, false); } @Override public void onTimeTick() { super.onTimeTick(); invalidate(); } @Override public void onAmbientModeChanged(boolean inAmbientMode) { super.onAmbientModeChanged(inAmbientMode); if (mLowBitAmbient) { mDigitalPaint.setAntiAlias(!inAmbientMode); mDigitalPaintOuter.setAntiAlias(!inAmbientMode); } invalidate(); updateTimer(); } @Override public void onDraw(Canvas canvas, Rect bounds) { ... } @Override public void onVisibilityChanged(boolean visible) { super.onVisibilityChanged(visible); if (visible) { registerReceiver(); mTime.clear(TimeZone.getDefault().getID()); mTime.setToNow(); } else unregisterReceiver(); updateTimer(); } private void registerReceiver() { if (mRegisteredTimeZoneReceiver) return; mRegisteredTimeZoneReceiver = true; IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); WatchFaceCLiuDigitalService.this .registerReceiver(mTimeZoneReceiver, filter); } private void unregisterReceiver() { if (!mRegisteredTimeZoneReceiver) return; mRegisteredTimeZoneReceiver = false; WatchFaceCLiuDigitalService.this .unregisterReceiver(mTimeZoneReceiver); } private void updateTimer() { mUpdateTimeHandler.removeMessages (MESSAGE_ID_UPDATE_TIME); if (isVisible() && !isInAmbientMode()) mUpdateTimeHandler.sendEmptyMessage (MESSAGE_ID_UPDATE_TIME); } } }
Rendering a Simple Watch Face with Digits and a Background
Now, the watch face service is ready, so we can start drawing the face. As a first example, we are going to just display the digital time and calendar in outlined text on top of one of the three randomly selected photos by Chris. We basically store the photos as drawable resource IDs and use Bitmap.createScaledBitmap and BitmapFactory.decodeResource to obtain their original bitmaps. One key to note is inside the overridden onDraw method; Bitmap.createScaledBitmap only scales the image once and only when the image dimensions do not match with those of the drawing canvas. This is trying to avoid overdoing the expensive bitmap scaling operation that will have an impact on the performance and battery life. The results are illustrated in Figure 3, Figure 4, and Figure 5.
final int [] mBackgroundIDs = {R.drawable.cb1, R.drawable.cb2, R.drawable.cb3}; Bitmap mBG; @Override public void onCreate(SurfaceHolder holder) { ... // randomly pick one of the three photos by Chris Blunt mBG = Bitmap.createScaledBitmap(BitmapFactory .decodeResource(getResources(), mBackgroundIDs[(int)(mBackgroundIDs.length * Math.random())]), 320, 320, false); mDigitalPaint = new Paint(); mDigitalPaint.setARGB(255, 255, 255, 255); mDigitalPaint.setStrokeWidth(5.f); mDigitalPaint.setTextSize(24); mDigitalPaint.setStyle(Paint.Style.FILL); mDigitalPaint.setAntiAlias(true); mDigitalPaintOuter = new Paint(); mDigitalPaintOuter.setARGB(255, 0, 0, 0); mDigitalPaintOuter.setStrokeWidth(5.f); mDigitalPaintOuter.setTextSize(24); mDigitalPaintOuter.setStyle(Paint.Style.FILL); mDigitalPaintOuter.setAntiAlias(true); ... } @Override public void onDraw(Canvas canvas, Rect bounds) { mTime.setToNow(); // draw the background image if (mBG == null || mBG.getWidth() != bounds.width() || mBG.getHeight() != bounds.height()) mBG = Bitmap.createScaledBitmap(mBG, bounds.width(), bounds.height(), false); canvas.drawBitmap(mBG, 0, 0, null); // draw the time String ts1 = String.format("%02d:%02d:%02d %s", mTime.hour % 12, mTime.minute, mTime.second, (mTime.hour < 12) ? "am" : "pm"); float tw1 = mDigitalPaint.measureText(ts1); float tx1 = (bounds.width() - tw1) / 2 + 50; float ty1 = bounds.height() / 2 - 80; canvas.drawText(ts1, tx1 - 1, ty1 - 1, mDigitalPaintOuter); canvas.drawText(ts1, tx1 + 1, ty1 - 1, mDigitalPaintOuter); canvas.drawText(ts1, tx1 - 1, ty1 + 1, mDigitalPaintOuter); canvas.drawText(ts1, tx1 + 1, ty1 + 1, mDigitalPaintOuter); canvas.drawText(ts1, tx1, ty1, mDigitalPaint); // draw the date String ts2 = String.format("%02d/%02d/%04d", mTime.month + 1, mTime.monthDay, mTime.year); float tw2 = mDigitalPaint.measureText(ts2); float tx2 = (bounds.width() - tw2) / 2 + 50; float ty2 = bounds.height() / 2 - 50; canvas.drawText(ts2, tx2 - 1, ty2 - 1, mDigitalPaintOuter); canvas.drawText(ts2, tx2 + 1, ty2 - 1, mDigitalPaintOuter); canvas.drawText(ts2, tx2 - 1, ty2 + 1, mDigitalPaintOuter); canvas.drawText(ts2, tx2 + 1, ty2 + 1, mDigitalPaintOuter); canvas.drawText(ts2, tx2, ty2, mDigitalPaint); }
Figure 3: Watch Face – CLiu Digital 1
Figure 4: Watch Face – CLiu Digital 2
Figure 5: Watch Face – CLiu Digital 3
Getting Creative with Various Watch Faces
As mentioned in the previous sections, most of the service codes are template-like and will not change much except for the onDraw method in CanvasWatchFaceService.Engine. And, of course, this is the part that actually shows the unique differences of your own watch faces. It is where you can get more creative in your designs. Android provides several handy classes you can use to generate algorithmic patterns. For example, inside android.graphics.Shader, there are gradient generators in RadialGradient, LinearGradient, SweepGradient, and so forth. One example we demonstrate here is to use the RadialGradient class to render the watch face background instead of using the photos. This time, we draw the tick marks and all the hands for hours, minutes, and seconds. Note we do not draw the hand for seconds when the watch is in ambient mode, for energy-saving reasons. The result is shown in Figure 6.
@Override public void onDraw(Canvas canvas, Rect bounds) { mTime.setToNow(); int w = bounds.width(), h = bounds.height(); float cx = w / 2.0f, cy = h / 2.0f; // draw background pattern if (mGradient == null) { mGradient = new RadialGradient(cx, cy, cx, 0xff500000, 0xff000050, android.graphics.Shader.TileMode.CLAMP); mBackgroundPaint.setDither(true); mBackgroundPaint.setShader(mGradient); } canvas.drawRect(0, 0, w, h, mBackgroundPaint); double sinVal = 0, cosVal = 0, angle = 0; float length1 = 0, length2 = 0; float x1 = 0, y1 = 0, x2 = 0, y2 = 0; // draw ticks length1 = cx - 25; length2 = cx; for (int i = 0; i < 60; i++) { angle = (i * Math.PI * 2 / 60); sinVal = Math.sin(angle); cosVal = Math.cos(angle); float len = (i % 5 == 0) ? length1 : (length1 + 15); x1 = (float)(sinVal * len); y1 = (float)(-cosVal * len); x2 = (float)(sinVal * length2); y2 = (float)(-cosVal * length2); canvas.drawLine(cx + x1, cy + y1, cx + x2, cy + y2, mTickPaint); } // draw hours length1 = cx - 100; angle = ((mTime.hour + (mTime.minute / 60f)) / 6f ) * (float) Math.PI; x1 = (float)(Math.sin(angle) * length1); y1 = (float)(-Math.cos(angle) * length1); canvas.drawLine(cx, cy, cx + x1, cy + y1, mHourPaint); // draw minutes length1 = cx - 70; angle = mTime.minute / 30f * (float) Math.PI; x1 = (float)(Math.sin(angle) * length1); y1 = (float)(-Math.cos(angle) * length1); canvas.drawLine(cx, cy, cx + x1, cy + y1, mMinutePaint); // draw seconds if (!isInAmbientMode()) { length1 = cx - 40; angle = mTime.second / 30f * (float) Math.PI; x1 = (float) Math.sin(angle) * length1; y1 = (float) -Math.cos(angle) * length1; canvas.drawLine(cx, cy, cx + x1, cy + y1, mSecondPaint); } }
Figure 6: Watch Face – CLiu Analog
Considering Battery Life Saving and Performance
Almost all of the current smart watches have the short-battery-life issue. And, because of the extremely limited resources available on the watches, performance optimization and battery saving should always be on developer’s mind whenever possible. We handle these a little bit in the examples; for example, bitmap scaling, ambient mode, and so on.
Expensive operations dealing with bitmap scaling and loading, animations, anti-aliasing, and constant info updates must be paid very careful attention to. If they can be prepared in advance, try to perform just once or when in great need. When something is unnecessary when not in interactive mode, make sure it is stopped or put into sleep mode. Google’s Android Wear developer page has several recommendations you should study as guidelines.
Conclusion
Watch faces are the most popular and thus the important feature for users. Certainly, for smart watches, displaying the current time only uses the very basic functionality. A variety of info or data can also be shown on the watch face. In the next tutorial, we can learn to provide users some configuration options in an interactive manner so that they can set up their preferences as what they want to see on their watches. As developers, we also need to know how to retrieve data in a reasonably efficient manner so that overall performance and battery life are only affected to a minimum.
References
- Download example source code from the link at the end of this article
- Chris Blunt Photography at: https://www.facebook.com/Chris.Blunt.Photography
- Android Developers at: http://www.android.com/developers/
- Androidlet at http://www.androidlet.com
About the Author
Chunyen Liu has been a software professional in Taiwan and the United States. He is a published author of 35+ articles and 100+ tiny apps, software patentee, technical reviewer, and programming contest winner by ACM/IBM/SUN. He holds advanced Computer Science degrees and trained in 20+ graduate-level courses. On the non-technical side, he is a certified coach, certified umpire, and rated player of USA Table Tennis, having won categorized events at State Championships and the US Open.