Connect IQ SDK

User Interface

Making a user interface toolkit that can adapt to multiple products is hard, but it is even harder in our ecosystem. The user interfaces of our devices vary by product. Some have physical buttons, some have coordinate touch screens, and some provide up/down/action buttons. The mechanical designs of these products may be very different depending on the use case the product is designed for.

Rather than design a user interface toolkit that adapts to what device the UI is running on, the Connect IQ SDK makes it easier to port the user interface to different products. At the resource compiler level, we include a way to define a page in XML as well as specify page definitions per product. These tools make it easy to support multiple devices with a single app.

Drawables, Views and Layouts

Watch faces and apps have a page stack. A View is an object that represents a page. Views can be pushed onto and popped off of the page stack, or a View can replace another View on the page stack with a transition.

//! View is the base class for a drawable page. It handles
//! the lifecycle of each View in an app. For widgets and
//! watch-apps the lifecyle is onStart(), onLayout(), onShow(),
//! onUpdate() and onHide(). For watchfaces and datafields
//! the lifecycle is onLayout(), onShow() and onUpdate().
class View
{
  //! The entry point for the View is onLayout(). This is called before the
  //! View is shown to load resources and set up the layout of the View.
  //! @param [Graphics.Dc] dc The drawing context
  //! @return [Boolean] true if handled, false otherwise
  function onLayout(dc);

  //! When the View is brought into the foreground, onShow() is called.
  //! @return [Boolean] true if handled, false otherwise
  function onShow();

  //! When a View is active, onUpdate() is used to update
  //! dynamic content. This function is called when
  //! the View is brought to the foreground. For widgets and
  //! watch-apps it is also called when WatchUi.requestUpdate()
  //! is called. For watchfaces it is called once a minute and
  //! for datafields it is called once a second. If a class that
  //! extends View does not implement this function then any
  //! Drawables contained in the View will automatically be drawn.
  //! @param [Graphics.Dc] dc The drawing context
  //! @return [Boolean] true if handled, false otherwise
  function onUpdate(dc);

  //! Before the View is removed from the foreground, onHide() is called.
  function onHide();

  //! Use setLayout() to set the layout for the View. If the extending class does
  //! not override onUpdate(), then all Drawables contained in layout will
  //! automatically be drawn by the View.
  //! @param [Array] layout An array of Drawables
  function setLayout(layout);
}

Each View contains a Layout. A Layout is an array of Drawables, where each Drawable is positioned in the View. A Drawable is an abstract object that can draw itself to a device context through the draw(dc) method.

Any object that extends Drawable is a drawable object. Monkey C provides the basic drawables Text and Bitmap, allowing Text and Bitmap resources to be included in layouts. Drawables expose their properties publicly, which will become useful when we discuss the animation system.

Using a Layout

When defining a layout in XML, you will simply list the drawable objects you want included in your layout inside a set of layout tags. Each of the drawables you list will be turned into Monkey C drawable objects and drawn one after the other. Because of this, if two drawables in your layout overlap each other, the drawable that is defined second will draw on top of the drawable that is defined first.

<resources>
  <layout id="MainLayout" background="Gfx.COLOR_WHITE">
    <drawable id="MainBackground" />
    <label text="Page Heading" x="10" y="5" font="Gfx.FONT_LARGE" color="Gfx.COLOR_BLACK" />
    <label text="Your information goes here." x="10" y="25" font="Gfx.FONT_MEDIUM" color="Gfx.COLOR_DK_GREY" />
  </layout>
</resources>

To use this layout in your code simply call setLayout() inside your onLayout(dc) function. You will need to call View’s onUpdate(dc) if you plan on using the onUpdate(dc) function to update dynamic values on the screen.

class MainView {
    function onLayout(dc) {
        setLayout(Rez.Layouts.MainLayout(dc));
    }
 
    function onUpdate(dc) {
        // Call parent’s onUpdate(dc) to redraw the layout
        View.onUpdate(dc);
           
        // Update anything that needs update here.
    }
}

The following attributes are supported by the layout tag.

Attribute Definition Valid Values Default Value
id (required) The handle for the layout. This is used to reference the layout in the Rez module. Any value that starts with a letter. NA

Drawables (both bitmaps and drawable XML resources) can be included in a layout using the drawable tag. The following attributes are supported by the drawable tag.

Attribute Definition Valid Values Default Value
id (required) The handle for the drawable. You must define the drawable you want to use as a drawable XML resource. The ID you provide here simply references the defined drawable. Any value that starts with a letter. The drawable must also be defined in a resource file. NA

Text can also be include in layouts. To inlude text use the label tag. The label tag supports the following attribute.

Attribute Definition Valid Values Default Value
text The text to be displayed. NA An empty string
font The font to be used when drawing the text. Gfx Font constant or the ID of a user defined font. Gfx.FONT_MEDIUM
x The X coordinate of the point that the text will be justified against. Pixel value 0
y The Y coordinate of the point that the text will be justified against. Pixel value 0
justification How the text should be justified in relation to the X, Y location. Gfx text justify constant. Gfx.TEXT_JUSTIFY_LEFT
color The color of the text. Gfx color constant. The draw context’s foreground color.

Drawables

Drawable XML resources consist of a list of basic drawables: both bitmaps and shapes. To create an XML drawable you will defince a drawable-list in an XML resource file. Inside the drawable-list you can place both the bitmap and shape tags. An example drawable-list if given below.

<resources>
  <drawable-list id="Smiley" background="Gfx.COLOR_YELLOW">
    <shape type="circle" x="10" y="10" radius="5" color="Gfx.COLOR_BLACK" />
    <shape type="circle" x="30" y="10" radius="5" color="Gfx.COLOR_BLACK" />
    <bitmap id="mouth" x="15" y="25" filename="../bitmaps/mouth.png" />
  </drawable-list>
</resources>

To use this drawable in code use the following code.

function onUpdate(dc) {
    var mySmiley = new Rez.Drawables.Smiley();
    mySmiley.draw(dc);
}

The drawable-list tag supports the following attributes.

Attribute Definition Valid Values Default Value
id (required) The ID of the drawable. Any string that starts with a character. NA
x The X coordinate of the top left corner in relation to the parent element. Pixel value 0
y The Y coordinate of the top left corner in relation to the parent element. Pixel value 0
foreground The color of elements (shapes and text) drawn in this layout. Gfx Color constant Integer Current draw context’s foreground color.

The shape tag supports the following attributes.

Attribute Definition Valid Values Default Value
type (required) The type of the shape to be drawn. rectangle
ellipse
circle
polygon
NA
x The X coordinate of the top left corner in relation to the parent element. Pixel value 0
y The Y coordinate of the top left corner in relation to the parent element. Pixel value 0
points (required, only for type = polygon) A list of points which defines the polygon. [[x1, y1], [x2, y2], …, [xN, yN]] (must have at least 3 points) NA
width (only for type = ellipse and rectangle) The width of the shape to be drawn. Pixel value or “fill” Fill
height (only for type = ellipse and rectangle) The height of the shape to be drawn. Pixel value or “fill” Fill
color The color of the shape. Gfx Color constant Integer Current draw context’s foreground color.
corner_radius (only for type = rectangle) The radius of the rounded corners of a rectangle. Pixel value 0
radius (required, only for type = circle) The radius of the circle. Pixel value NA
border_width (only for type = rectangle, ellipse and circle) The width of the border around the shape. Pixel value 0
border_color (only for type = rectangle, ellipse and circle) The color of the border to be drawn around the shape. Gfx Color constant Integer Current draw context’s foreground color.

The bitmap tag supports the following attributes.

Attribute Definition Valid Values Default Value
id (required) The ID of the drawable. Any string that starts with a character. NA
x The X coordinate of the top left corner in relation to the parent element. Pixel value 0
y The Y coordinate of the top left corner in relation to the parent element. Pixel value 0
filename (required) The relative path to the image that should be shown. A valid, relative path. NA

Defining Layouts and Drawables in Resources

The resource compiler allows you to customize your page layouts to a particular device without changing any Monkey C code. In addition, it allows you to define DrawableList objects, which are Drawable objects that can draw a number of graphics primitives.

Input Handling

As if input handling wasn’t already one of the most important and complicated pieces of a UI toolkit, our devices take the complication up a level. Unlike those touchable glowing rectangles that are modern smart phones, our devices come in lots of shapes and sizes. Touch screens are not always ideal for all watch products, and we will have a mix of input styles and screen technologies. It's the job of the UI toolkit to make this coherent to the developer.

Input Delegates

The delegate object is an object that implements a certain interface specific to input handling. Monkey C provides a low level InputDelegate that allows handling of events at a basic level. This is good when the app needs to handle button presses and key holds in a particular way.

module WatchUi
{
  //! This class implements basic input handling.
  //! A developer needs to override
  //! the events they want to handle.
  class InputDelegate
  {
  //! Key event
    /! @param evt KEY_XXX enum value
    //! @return true if handled, false otherwise
    function onKey(evt);
    //! Click event
    //! @param evt Event object with click information
    //! @return true if handled, false otherwise
    function onTap(evt);
    //! Screen hold event. Sent if user is touching
    //! the screen
    //! @param evt Event object with hold information
    //! @return true if handled, false otherwise
    function onHold(evt);
    //! Screen release. Sent after an onHold
    //! @param evt Event object with hold information
    //! @return true if handled, false otherwise
    function onRelease(evt);
    //! Screen swipe event
    //! @param evt Event
    function onSwipe(evt);
  }
}

Behaviors

We make products with a purpose, and that purpose can alter the design of one product line over another. The decision if a product has a touch screen or has buttons can depend on the environment the user will take it; if the product is intended to be used in water (swimming, canoeing, on a boat), it may not have a touch screen. These decisions make for superior products, but also developer frustration over device fragmentation.

Often all products will support common behaviors (next page, back), but how they are executed by the user may differ based on the available input types. To help with this dilemma, Monkey C provides exposes events at a behavior level. Behaviors separate high-level intentions from the actual input type - next page versus screen press. The BehaviorDelegate is a super class of InputDelegate and maps its low level inputs to common operations across multiple products. Using the BehaviorDelegate can lead to much more portable code.

//! BehaviorDelegate handles behavior inputs. A BehaviorDelegate differs from
//! an InputDelegate in that it acts upon device independent behaviours such as
//! next page and previous page. On touch screen devices these behaviors might be
//! mapped to the basic swipe left and right inputs, while on non-touch screen
//! devices these behaviors might be mapped to actual keys.
//! BehaviorDelegate extends InputDelegate so it can also act on basic inputs as
//! well. If a BehaviorDelegate does return true for a function, indicating that
//! the input was used, then the InputDelegate function that corresponds to the
//! behavior will be called.
//! @see InputDelegate
class BehaviorDelegate extends InputDelegate
{
  //! When a next page behavior occurs, onNextPage() is called.
  //! @return [Boolean] true if handled, false otherwise
  function onNextPage();

  //! When a previous page behavior occurs, onPreviousPage() is called.
  //! @return [Boolean] true if handled, false otherwise
  function onPreviousPage();

  //! When a menu behavior occurs, onMenu() is called.
  //! @return [Boolean] true if handled, false otherwise
  function onMenu();
	
  //! When a back behavior occurs, onBack() is called.
  //! @return [Boolean] true if handled, false otherwise
  function onBack();
	
  //! When a next mode behavior occurs, onNextMode() is called.
  //! @return [Boolean] true if handled, false otherwise
  function onNextMode();
	
  //! When a previous mode behavior occurs, onPreviousMode() is called.
  //! @return [Boolean] true if handled, false otherwise
  function onPreviousMode();
}

Built-in Handlers

In general, complicated input from users should not be done on a wearable device, but should instead be done via a phone app or web page. However, sometimes the device needs to get some form of user input. WatchUi provides two main widgets to handle input: Menus and the Number Picker.

Menu

A menu is a list of options for the user. The options are displayed in a list that matches the device the app is running on.

A menu can be defined in XML in the resource file using the following format:

<menu id="MainMenu">
  <menu-item id="item_1">Item 1</menu-item>
  <menu-item id="item_2">Item 2</menu-item>
</menu>

The resource compiler will then take this XML and generate a Menu object in the Rez module. In order to use this menu, a developer simply needs to push the menu and a delegate for the menu using Ui.pushView().

class MyClass extends Ui.View {
  function openTheMenu() {
    menu = new MainMenu(self);
         Ui.pushView(new Rez.Menus.MainMenu(), new MyMenuDelegate(), Ui.SLIDE_UP);
  }
}   

class MyMenuDelegate extends Ui.MenuInputDelegate {
    function onMenuItem(item) {
        if (item == :item_1) {
            // Do something here
        } else if (item == :item_2) {
            // Do something else here
        }
    }
}

Number Picker

To allow the user to pick or adjust numerical data, the number picker widget is your best bet. The number picker widget allows for editing/adjusting of common numerical types.

enum
{
  //! A Float in meters
  NUMBER_PICKER_DISTANCE,
  //! A Duration
  NUMBER_PICKER_TIME,
  //! A Duration
  NUMBER_PICKER_TIME_MIN_SEC,
  //! A Duration representing the number of seconds since midnight
  NUMBER_PICKER_TIME_OF_DAY,
  //! A Float in kilograms
  NUMBER_PICKER_WEIGHT,
  //! A Float in meters
  NUMBER_PICKER_HEIGHT,
  //! A Number
  NUMBER_PICKER_CALORIES,
  //! A Number
  NUMBER_PICKER_BIRTH_YEAR
}

//! This is the on-screen representation of number picker.
//! The look-and-feel will be device specific. NumberPickers are pushed
//! using WatchUi.pushView(), providing a NumberPickerDelegate
//! for the input delegate. There are minimum and maximum
//! values enforced by the product for each mode. The initial
//! value will be modified to be within these bounds.
class NumberPicker
{
  //! Constructor
  //! @param mode An enum value of type NUMBER_PICKER_*
  //! @param initialValue The initial value for the Number Picker, type 
  //!depends on mode
  function initialize(mode, initialValue);
}

//! NumberPickerDelegate responds to a number being chosen.
//! This class should be extended to get the chosen number.
class NumberPickerDelegate
{
  //! When a number is chosen, onNumberPicked() is called, passing the //! chosen value.
  //! @param value The chosen number, type depends on the NumberPicker mode
  function onNumberPicked(value);
}