Making an analog watch face

I've played around with the Garmin Connect IQ SDK a little in the previous tutorial by getting an app running in the simulator and changing some of the text on the screen. Now I really want to make something that could be used on a watch. So why not continue the learning by creating a watch face. Digital watches are too easy to make since they're simply writing numbers to the screen. I want a little more of a challenge, so I'm going to make an analog watch.

Getting started

In order to make an analog watch face there needs to be some way of representing the hours and minutes. For this tutorial, I'll stick with a standard summarized watch face that only has the 12, 3, 6, and 9 hours on them. There also needs to be a way to keep track of the current hour and minute. Sticking with the standard hour and minute hands will probably be easiest and most recognizable by a user.

Start by creating a new eclipse project. This one I'll call my_analog. It will have a Project Type of Watch Face and it will target the Round Watch platform. I like to check and make sure everything is working before adding new code, so create a new configuration for this project (I named mine Round Analog) and make sure that it's targeting the Round Watch platform. Start the simulator then click the play button and make sure that you can see the time.

Sweet. The default code still works.

Creating the background

You could set a background color, calculate the positions, and then draw the numbers, but that wouldn't be using the resource compiler at all. In order to create a good-looking background, you need to know the target device's available color palette and resolution. This information is found in a file called devices.xml in the Connect IQ SDK bin directory.

In that file there is section called <devices> that contains a list of <device> tags. Each <device> has an id attribute. Some devices have an optional name attribute. For example, the device with the id of square_watch has a name of Square Watch. This is the name that is used when designating which platform to build.

For each watch, there exists a _sim variant. So, for id="round_watch" there is also a id="round_watch_sim". Inside of the id="round_watch_sim" device, there is a <resolution> tag that describes the width and the height of the device. In this case, the round watch's width and height is 218px.

In the id="round_watch" device is a tag named <palette> that contains a list of <colors>. These are the available colors for the device. If you use one of the colors listed in the palette, it will show up as solid. If you use a color not listed in the palette, the device will try to create it using a combination of the available palette colors. Basically this means that a non-palette color won't look as good as a palette color.

Palette Color
An example of using a color found within the color palette: #AAAAAA.

Non-Palette Color
An example of using a color found outside of the color palette: #616161.

Keeping that in mind, I've created a round watch face background image that has a width and height of 218 pixels and only uses colors found in the palette. I've embedded the font into the image for reasons I'll explain later.

Round Background
The beginning of the watch face.

Adding the background image

Now that we have a suitable background image all that's left to do is write the code to load the image and display it. First, start by copying the background image into the resources/images directory. Next, open up the resources/resources.xml file and add the following <bitmap> tag:

<resources>
    <bitmap id="background" filename="images/round.png" />
</resources>

The id="background" is the id that we can use to refer to the resource and the filename is the path to the image relative to resources.xml.

Note: even though the tag is named bitmap, PNG images are still allowed. The resource compiler will convert it to the proper format as used by the device.

When the compiler is run, it finds all of the resource files passed to it (via the -z flag) and scans the resource file for specific tags. <bitmap> is one such tag. Any <bitmap> will be added to the Rez.Drawables module as Rez.Drawables.<id>. Using the XML we've just updated as an example, we can get our bitmap image resource id by Rez.Drawables.background.

In order to use a resource, it first needs to be loaded. All resources should be loaded within the onLayout() method. Inside of my_analogView.mc:

using Toybox.WatchUi as Ui;

class View extends Ui.View {
    // the BitmapResource for the background image
    var background;

    function onLayout(dc){
        background = Ui.loadResource(Rez.Drawables.background);
        // background is now a BitmapResource instance
    }
}

Notice that the background variable has been declared outside of the onLayout() function. It will need to be used later on within the onUpdate() method, so the reference is stored within the scope that contains both methods. Ui.loadResource takes in a resource id and converts it to an appropriate resource instance; in this case a BitmapResource instance.

Now, we just need to update the onUpdate() method to draw the image.

using Toybox.WatchUi as Ui;

class View extends Ui.View {
    // the BitmapResource for the background image
    var background;

    function onLayout(dc){
        background = Ui.loadResource(Rez.Drawables.background);
        // background is now a BitmapResource instance
    }

    function onUpdate(dc){
        // Set background image
        dc.drawBitmap(0, 0, background);
    }
}

The first two parameters for drawBitmap() are the x and y coordinates for where the top-left of the image should be placed. The third parameter is the BitmapResource instance to draw. Even though the actual screen is round, it still draws like a square where (0,0) lies outside of the circle.

drawable area explained
The drawable area starts outside of the viewable area.

The background image I've created accounts for this by not having anything important outside of the viewable area. So, we just draw the image starting at (0,0). Running the above code should produce the following result:

So far so good.
So far so good.

Drawing the hour and minute hands

To draw the hands on the watch we'll want to draw lines that start at the center of the watch, extend out some length, and rotate by some angle relative to the current time. We'll start by first finding the center of the circle. Since the center of the watch should not change between draw calls, I'm going to find the center in the onLayout() method:

// ...
class View extends Ui.View {
    // the x coordinate for the center
    var center_x;
    // the y coordinate for the center
    var center_y;

    function onLayout(dc){
        // ...
        center_x = dc.getWidth() / 2;
        center_y = dc.getHeight() / 2;
    }
}

Next, I want to define the length of the hour and minute hands relative to the radius of the circle.

// ...
class View extends Ui.View {
    // the length of the minute hand
    var minute_radius;
    // the length of the hour hand
    var hour_radius;

    function onLayout(dc){
        // ...
        // i've arbitrarily decided that i want
        // the minute hand to be 7/8 the length of the radius
        minute_radius = 7/8.0 * center_x;
        // i've also arbitrarily decided that i want
        // the hour hand to be 2/3 the length of the minute hand
        hour_radius = 2/3.0 * minute_radius;
    }
}

Note: to prevent integer division one of the numbers must be a float. Also note that both the center coordinates and the hand length variables are declared outside of the onLayout() method. They will both be used in the onUpdate() method.

Now that we have the starting point and the radius of the hands we just have to figure out the angle based on the current time. All of this needs to occur within the onUpdate() method. We'll start by first getting the current time:

// ...
using Toybox.System as Sys;

class View extends Ui.View {
    // ...
    function onUpdate(dc){
        // ...
        // easier shown than explained
        var now = sys.getClockTime();
        var hour = now.hour;
        var minute = now.minute;
    }
}

The angle (in radians) for the current minute is going to be what fraction of the hour the minute represents multiplied by 2π. The hour returned from getClockTime().hour is some number between 0 up to, but not including, 24. Since the analog face only shows 12 hours, we'll have to compensate by using the modulus 12. Then we need to find the fraction of the 12 hour period and again multiplying that fraction by 2π to get the angle.

// ...
using Toybox.Math as Math;

class View extends Ui.View {
    // ...
    var TWO_PI = Math.PI * 2;
    function onUpdate(dc){
        // ...
        var hour_fraction = minute / 60.0;
        var minute_angle = hour_fraction * TWO_PI;
        var hour_angle = ((hour % 12) / 12.0) * TWO_PI;
    }
}

Now that we have the proper angles, the length of the hands, and the center of the circle, we can find the point on the circle where the hands will end. We can use the following equation to draw a line from the center to the correct position:

start = (x0, y0)
end = (x0 + length * cos(θ), y0 + length * sin(θ))

All we have to do now is a use a drawLine() call to draw a line starting at the start coordinates and ending at the end coordinates:

// ...

class View extends Ui.View {
    // ...
    function onUpdate(dc){
        // ...
        // draw the minute hand
        dc.drawLine(center_x, center_y,
            (center_x + minute_radius * Math.cos(minute_angle)),
            (center_y + minute_radius * Math.sin(minute_angle)));
        // draw the hour hand
        dc.drawLine(center_x, center_y,
            (center_x + hour_radius * Math.cos(hour_angle)),
            (center_y + hour_radius * Math.sin(hour_angle)));
    }
}

Running the above should produce the following:

Uh oh. Wrong time.
Uh oh. It's actually 3 o'clock, but 6:15 is showing. Hmm...

Oh no. It's actually 3 o'clock, but the watch shows 6:15. Oh yeah. That's right. Noon is relative to the radian angle 0 and everyone knows that when θ=0 the starting coordinate is (1,0). So, we just need to compensate by rotating the angle backwards by 90 degrees, I mean, π/2 radians. Adding the following should fix the problem:

// ...

class View extends Ui.View {
    // ...
    var ANGLE_ADJUST = Math.PI / 2.0;
    function onUpdate(dc){
        // ...
        // compensate the starting position
        minute_angle -= ANGLE_ADJUST;
        hour_angle -= ANGLE_ADJUST;

        // draw the minute hand
        dc.drawLine(center_x, center_y,
            (center_x + minute_radius * Math.cos(minute_angle)),
            (center_y + minute_radius * Math.sin(minute_angle)));
        // draw the hour hand
        dc.drawLine(center_x, center_y,
            (center_x + hour_radius * Math.cos(hour_angle)),
            (center_y + hour_radius * Math.sin(hour_angle)));
    }
}

Which should produce the following:

Yay!
Yay! It's actually 3 o'clock

There's only one more minor thing to change. For the most part, this code works. However, unlike a true analog watch, the hour hand will remain pointing at the current hour until the next hour. A true analog watch will move the hour hand as the minute hand progresses. Not a problem. All we have to do is use the fraction of an hour and see what fraction of a twelve hour period that is and add that to the original hour fraction before multiplying by 2π. The entire onUpdate() method including the hour fraction fix:

// ...

class View extends Ui.View {
    // ...
    function onUpdate(dc) {
        // Set background image
        dc.drawBitmap(0, 0, background);
        var now = Sys.getClockTime();
        var hour = now.hour;
        var minute = now.min;

        // what part of the hour is this?
        var hour_fraction = minute / 60.0;
        var minute_angle = hour_fraction * TWO_PI;
        var hour_angle = (((hour % 12) / 12.0) + (hour_fraction / 12.0)) * TWO_PI;

        // compensate the starting position
        minute_angle -= ANGLE_ADJUST;
        hour_angle -= ANGLE_ADJUST;

        // draw the minute hand
        dc.drawLine(center_x, center_y,
            (center_x + minute_radius * Math.cos(minute_angle)),
            (center_y + minute_radius * Math.sin(minute_angle)));
        // draw the hour hand
        dc.drawLine(center_x, center_y,
            (center_x + hour_radius * Math.cos(hour_angle)),
            (center_y + hour_radius * Math.sin(hour_angle)));
    }
}

Yay! We now have a fully-working watch face!

Making a square watch face

If you're like me, you're constantly looking for ways to re-use code. We've got all of this code written to make a watch face, but now someone has gotten a wild idea to make a square watch face. We could always create a new project and copy and paste the code only changing the parts that affect the squareness. But, we all know that isn't very DRY. Since I'm not the sort to suggest bad ideas and leave, there has to be a better way!™ And guess what? There is!

The power of the resource compiler

The resource compiler does more than just collect resources. It also allows for conditionally using resources based upon language and device. When you compile with the -z flag with a list of resource files, the directory of the resource file is used to determine whether the resource belongs to a specific language or device.

Let's say we want to use the round background image only for round devices. We tell the resource compiler that the resources contained within a directory is for round watch devices by naming the directory resources-round.

But wait. The name of the device is Round Watch and the id in devices.xml is round_watch. Why is the directory name resources-round? The resource compiler splits the directory names by the - and uses those as qualifiers (the first element is ignored). The resource compiler then iterates over each device in devices.xml, splitting each id by _ and using only the first element. So, round_watch becomes round. Thus, any resources found in resources-round will be used specifically for the round_* devices.

Note: anything in a device or language specific resource directory will override anything in a non-specific resource directory. For example, if resources/resources.xml specifies a bitmap with the id background and resources-round/resources.xml specifies a bitmap with the id background and the code is compiled for a device with an id that starts with round, then the round resource background is used.

Also note that both language specific and device specific resources can be combined: a directory named resources-round-eng would target round devices using the English language.

Using the resource compiler to keep our code DRY

Now that we have a better understanding of how the resource compiler works let's use that to create two watch faces using (almost) the same code. We'll start by creating a new background image. Within devices.xml you can see that a square watch has a width of 205px and a height of 148px. It also has the same color palette as the round watch. Using that information, I can create another background image:

Square Face
A new watch face.

I've embedded the font in the image due to the fact that I can position the numbers perfectly in the image without having to change any of the code or know anything specific about a device within the code itself. See? I told you I would explain later.

There is only one more minor change in our layout code that is needed to be made before we can run. Currently, the code assumes that the watch face is round and arbitrarily picks the center_x value to determine the radius of the minute hand. The problem with doing that for a square face like this one is that when the minute hand points around noon or 6, it will go outside of the drawable area. To remedy this, we just need to pick the smallest dimension and use that to create our hour and minute hands:

// ...
class View extends Ui.View {
    function onLayout(dc) {
        // ...
        var smallest;
        if(center_x < center_y){
            smallest = center_x;
        }
        else{
            smallest = center_y;
        }
        // use the smaller dimension to determine the hand sizes
        minute_radius = 7/8.0 * smallest;
        // I want the hour hand to be 2/3 of the minute hand
        hour_radius = 2/3.0 * minute_radius;
    }
}

Create a new directory named resources-square and inside of that directory create another directory named images. Add the background image square.png there. Back in the resources-square directory, create a new resources.xml (or whatever name you want to give it). Inside of that file have the following:

<resources>
    <bitmap id="background" filename="images/square.png" />
</resources>

Rename the resources directory (what is currently containing our round image and resource file) to resources-round.

One final change is needed in the manifest.xml. In the <iq:products> section, we need to add the square watch product:

<iq:products>
    <iq:product id="round_watch"/>
    <!-- Add the following to your manifest -->
    <iq:product id="square_watch"/>
</iq:products>

Finally, in eclipse, go to Run > Run Configurations and create a new Connect IQ run configuration. Name it Square Analog. Make sure the project is correct and set the Target Device to Square Watch. Click apply, then click run.

If all goes well, you should see your new watch face being used on a square device!

Square Face
Why is it always 3 o'clock?

Check out the source on GitHub

I've create a repo of the full source including all source files, images, resource files, and even the InkScape SVGs used to create the images: https://github.com/frenchtoast747/connectiq-analog-watchface


Comments