Building watch faces

Android wearables are far more than just watches, but they still need to perform the basic task of presenting the user with the current time.

Getting ready

Although, automatically installed when creating a new Android Wear project, we do need to have the Xamarin.Android.Wear NuGet installed in the wearable project.

How to do it...

Creating a watch face consists of creating two parts. The first is the rendering engine, which inherits from one of the WatchFaceService.Engine types, and the other is WatchFaceService, which manages it:

  1. We need to specify the permissions for both the handheld and wearable apps. The handheld does not require any activities, but just requires the permissions to be set:
    [assembly: UsesPermission(Manifest.Permission.WakeLock)]
    [assembly: UsesPermission(
      "com.google.android.permission.PROVIDE_BACKGROUND")]
  2. For our watch face, we use CanvasWatchFaceService.Engine as our base type. In the constructor, we keep a reference to CanvasWatchFaceService because we will need it for creating the watch face:
    class WatchFaceEngine : CanvasWatchFaceService.Engine {
      private readonly CanvasWatchFaceService service;
      public WatchFaceEngine(CanvasWatchFaceService service)
      : base (service) {
        this.service = service;
      }
      public override void OnCreate(ISurfaceHolder surface) {
        base.OnCreate(surface);
      }
      public override void OnTimeTick() {
        base.OnTimeTick();
      }
      public override void OnVisibilityChanged(bool visible) {
        base.OnVisibilityChanged(visible);
      }
      public override void OnAmbientModeChanged(bool isAmbient) {
        base.OnAmbientModeChanged(isAmbient);
      }
      public override void OnSurfaceChanged(
      ISurfaceHolder holder, Format format,
      int width, int height) {
        base.OnSurfaceChanged(holder, format, width, height);
      }
      public override void OnDraw(Canvas canvas, Rect bounds) {
        base.OnDraw(canvas, bounds);
      }
    }
  3. In the constructor, we set up a Handler instance to trigger at every second so that we can draw the second hand:
    private const int MessageId = 123;
    private const long Interval = 1000;
    
    tickHandler = new Handler(message => {
      var timerEnabled = IsVisible&&!IsInAmbientMode;
      if (message.What == MessageId&&timerEnabled) {
      Invalidate();
        long time = Java.Lang.JavaSystem.CurrentTimeMillis();
        long delay = Interval - (time % Interval);
        tickHandler.SendEmptyMessageDelayed(MessageId, delay);
      }
    });
  4. Because we are going to be painting every second, we initialize the Paint instances in the OnCreate() method to reduce the overhead:
    hourPaint = new Paint();
    hourPaint.SetARGB(255, 200, 200, 200);
    hourPaint.StrokeWidth = 5.0f;
    hourPaint.AntiAlias = true;
    hourPaint.StrokeCap = Paint.Cap.Round;
    minutePaint = new Paint();
    minutePaint.SetARGB(255, 200, 200, 200);
    minutePaint.StrokeWidth = 3.0f;
    minutePaint.AntiAlias = true;
    minutePaint.StrokeCap = Paint.Cap.Round;
    secondPaint = new Paint();
    secondPaint.SetARGB(255, 200, 200, 200);
    secondPaint.StrokeWidth = 4.0f;
    secondPaint.AntiAlias = true;
    secondPaint.StrokeCap = Paint.Cap.Round;
  5. Also in the OnCreate() method, we set the WatchFaceStyle instance for our watch face, using a reference to the watch face service:
    var style = new WatchFaceStyle.Builder(service)
      .SetCardPeekMode(WatchFaceStyle.PeekModeShort)
      .SetBackgroundVisibility(
        WatchFaceStyle.BackgroundVisibilityInterruptive)
      .SetShowSystemUiTime(false)
      .Build();
    SetWatchFaceStyle(style);
  6. Every minute, Android will automatically invoke the OnTimeTick() method, so we can simply request for the view to be redrawn:
    Invalidate();
  7. In the OnVisibilityChanged() and OnAmbientModeChanged() methods, we need to request for the face to be redrawn, as well as making sure the second-hand timer is enabled or disabled, depending on the visibility and ambient values:
    Invalidate();
    tickHandler.RemoveMessages(UpdateMessageId);
    if (IsVisible&&!IsInAmbientMode) {
      tickHandler.SendEmptyMessage(UpdateMessageId);
    }
  8. Before we draw, the OnSurfaceChanged view may be invoked indicating that the drawing surface has changed, so we can adjust our drawing parameters there:
    centerX = width / 2f;
    centerY = height / 2f;
    secondHandLength = centerX * 0.875f;
    minuteHandLength = centerX * 0.75f;
    hourHandLength = centerX * 0.5f;
  9. Finally, in the OnDraw() method, we draw the watch face onto the screen, drawing as little as possible in the ambient mode:
    // background color
    if (IsInAmbientMode) {
      canvas.DrawColor(Color.Black);
    }
    else {
      canvas.DrawColor(Color.DarkGreen);
    }
    // hand rotations
    var time = DateTime.Now.TimeOfDay;
    float seconds = time.Seconds;
    float secRot = seconds / 60f * (float)Math.PI * 2f;
    float minutes = time.Minutes + seconds / 60f;
    float minRot = minutes / 60f * (float)Math.PI * 2f;
    float hours = time.Hours + minutes / 60f;
    float hrRot = hours / 12f * (float)Math.PI * 2f;
    // hour hand
    float hrX = (float)Math.Sin(hrRot) * hourHandLength;
    float hrY = (float)-Math.Cos(hrRot) * hourHandLength;
    canvas.DrawLine(
      centerX, centerY,
      centerX + hrX, centerY + hrY,
      hourPaint);
    // minute hand
    float minX = (float)Math.Sin(minRot) * minuteHandLength;
    float minY = (float)-Math.Cos(minRot) * minuteHandLength;
    canvas.DrawLine(
      centerX, centerY,
      centerX + minX, centerY + minY,
      minutePaint);
    // second hand only in interactive mode.
    if (!IsInAmbientMode) {
      float secX = (float)Math.Sin(secRot) * secondHandLength;
      float secY = (float)-Math.Cos(secRot) * secondHandLength;
      canvas.DrawLine(
        centerX, centerY,
        centerX + secX, centerY + secY,
        secondPaint);
    }

Our watch face engine is complete. So now, we set up the wearable app to have a watch face service. We don't need any activities in either project, but just a service in the wearable project:

  1. Our watch face service will inherit from a WatchFaceService type, such as CanvasWatchFaceService. We override the OnCreateEngine() method to return a new instance of our watch face engine:
    public class WatchFaceService : CanvasWatchFaceService {
      public override WallpaperService.EngineOnCreateEngine() {
        return new WatchFaceEngine(this);
      }
    }
  2. The watch face service has a set of attributes that will be used to describe the service to the Android device. The first is declaring our type as a service by adding a [Service] attribute:
    [Service(
      Label = "XamarinCookbook",
      Permission = Android.Manifest.Permission.BindWallpaper)]
  3. Next, we specify that this service is started as a watch face using an [IntentFilter] attribute:
    [IntentFilter(new [] {
      Android.Service.Wallpaper.WallpaperService.ServiceInterface
    }, Categories = new [] {
      "com.google.android.wearable.watchface.category.WATCH_FACE"
    })]
  4. Then, in the wearable's resources, we add an XML resource named watch_face.xml:
    <?xml version="1.0" encoding="UTF-8"?>
    <wallpaper xmlns:android="http://schemas.android.com/apk/res/android"/>
  5. Then, we add this resource to the service using a [MetaData] attribute:
    [MetaData(
      Android.Service.Wallpaper.WallpaperService.ServiceMetaData,
      Resource = "@xml/watch_face")]
  6. Similarly, we provide a preview image for the Android Wear companion app using two images. One should be a round preview and the other should be a square preview of the watch face. In our case, we save the images as preview.png and preview_round.png in the drawable resource folder.
  7. Finally, we add these preview images to the watch face by also using the [MetaData] attributes:
    [MetaData(
      "com.google.android.wearable.watchface.preview",
      Resource = "@drawable/preview")]
    [MetaData(
      "com.google.android.wearable.watchface.preview_circular",
      Resource = "@drawable/preview_round")]

How it works...

Although we call them wearables, they were originally called smartwatches. The name changed to wearables after they grew to surpass the watch features. Wearables are much more than a smartwatch, but they still keep the basic idea of a watch, and one of the most important features of a watch, or rather, the only important feature of a watch, is to display the current time to the wearer.

Android wear is more than a watch operating system, but it is still a watch. And as a watch, it needs to display the current time. It does this by utilizing a special wallpaper that can handle displaying the current time when the device is awake, and when the device goes to sleep or enters the ambient mode.

Watch faces exist as a special type of service that are managed by the Android Wear companion app installed on the handheld.

Tip

An app that provides a watch face does not need to have any activities, but one might exist for configuration purposes.

When creating a watch face, we need to ensure that we design for both, square and round devices, as well as support the ambient mode properly. For the battery to last long, we need to ensure that when drawing in the ambient mode, we draw as little as possible.

Unlike traditional apps that support the ambient mode, watch faces cannot be closed by the user and are always displayed. As watch faces run continuously, we need to ensure that all the work is kept to a minimum, and network operations are done as little as possible.

Because a watch face supports the ambient mode and is also a wallpaper, we need to specify the wake lock and the com.google.android.permission.PROVIDE_BACKGROUND permissions.

Note

Creating a watch face consists of two parts: the engine that processes events and draws the face, and the service that instantiates and manages the watch face.

The watch face engine inherits from the WatchFaceService.Engine type. There are two types to inherit from: the 2D, canvas-based CanvasWatchFaceService.Engine and the 3D, OpenGL-based Gles2WatchFaceService.Engine.

Most watch faces can be created using the canvas-based drawing mechanism. In this case, we only need to override several watch face lifecycle methods to do so. Although, the watch face engine methods cover most of the events, we do need to create the mechanism that redraws the screen every second.

The simplest way to redraw the screen every second is to create a Handler instance and post delayed messages. As we want the screen to be redrawn every second, we use a delay of 1 second. We must make sure that we only use the handler when the device is not in the ambient mode and is actually visible on the screen. The base type provides two properties to test these cases: the IsInAmbientMode and IsVisible properties.

Tip

The watch face engine does not provide a mechanism for updating these on hand position, this must be created soon.

To reduce any kind of overhead when drawing on the screen, we should initialize as much as we can in the OnCreate() method. This includes any drawing mechanisms, such as Paint instances and bitmaps.

Because the operating system needs to draw all the UI elements, such as battery indicators at the top of the watch face, we need to specify where and how to draw them. We do this in the OnCreate() method using the SetWatchFaceStyle() method. We pass in an instance of a WatchFaceStyle instance created using the WatchFaceStyle.Builder type.

Once we have set up all the pieces required to draw our watch face, we can start overriding the lifecycle events. Watch faces have a default update interval of one minute and at that time, the OnTimeTick() method is invoked. As we don't need to do anything special, we can simply request that the screen be redrawn by invoking the Invalidate() method. We use the OnTimeTick() method instead of using the handler to redraw the screen when in the ambient mode.

In the OnVisibilityChanged() and OnAmbientModeChanged() methods, we can also just request a redraw. We can even invoke the Invalidate() method here. However, as the device may be going to sleep or an app launching over the watch, we need to stop the Handler instance from requesting redraws. The device may also be waking up, so we may also need to start the Handler instance again.

Note

The second hand should not be drawn when in the ambient mode, and thus, the mechanism to update them can be disabled.

In addition to responding to ambient changes, we might need to handle cases where the screen has a reduced color mode. We can override the OnPropertiesChanged() method and read the Bundle argument. The Boolean bundle value for the PropertyLowBitAmbient key indicates whether the screen supports fewer bits for each color. So, we should disable the anti-aliasing and bitmap filtering. Also, the Boolean value for the PropertyBurnInProtection key indicates whether we need to avoid large areas of white pixels or drawing near the edges of the screen when in the ambient mode.

Tip

Some devices have extra requirements when drawing in the ambient mode.

During initialization, and if the drawing surface changes for some reason, the OnSurfaceChanged() method is invoked. We can use this method to update any screen-related drawing parameters, such as the surface size. This method is useful for performing any kind of image scaling or size calculations. It is better to only perform these tasks once rather than to do them each time the face is drawn.

The last thing to do with the watch face is to draw it. We draw our watch face in the OnDraw() method using all the normal drawing tools. However, we do need to ensure that we check to see if the device is in the ambient mode using the IsInAmbientMode property. If so, then we should draw as little as possible and on a black background. We also need to keep in mind that in the ambient mode, drawing only occurs once every minute.

Note

In the ambient mode, the background should be black, but this is more of a recommendation rather than a requirement.

Once the watch face engine is complete, we can create the watch face service and its metadata. The service is run on the wearable device, and the metadata is used when listing the watch face in the Android Wear companion app.

The watch face service is very simple; all we do is override the OnCreateEngine() method. Here, we return a new instance of our watch face engine. The service should inherit from the appropriate watch face type, such as the CanvasWatchFaceService type or the Gles2WatchFaceService type, depending on what our drawing engine is used for.

As the watch face service is just a specialized service, we need to add the [Service] attribute to it. We also need to add set the Permission property of the attribute to android.permission.BIND_WALLPAPER. Then, we need to add an [IntentFilter] attribute, and set the Action property to android.service.wallpaper.WallpaperService and the Categories property to com.google.android.wearable.watchface.category.WATCH_FACE.

We also need to add a [MetaData] attribute pointing to an XML resource. The XML resource should contain a <Wallpaper> element and be stored in the xml resource folder. The attribute Name should be android.service.wallpaper and Resource should point to the XML resource. If our XML is in a file named watch_face.xml, then the Resource property would be @xml/watch_face.

Tip

The code for the watch face service consists of one method with most of the work done through attributes on the service.

The last thing needed to list the watch face in the companion app is a preview. A preview for both, the round and square faces, should be provided in the drawable resource folder. We specify the previews for the service using a[MetaData] attribute. The metadata Name for a square preview is com.google.android.wearable.watchface.preview, and for a round preview is com.google.android.wearable.watchface.preview_circular. The resource for each of these would be set using the metadata Resource property. In the case of a square face, we might have a value of @drawable/preview_square and for a round face, @drawable/preview_round.

Note

If no preview is specified, then the watch face will not appear in the picker.

When the app is installed on the wearable, both the companion app and the wearable will receive a new entry in the watch face picker. The user will then be able to select it as they would any other watch face. The preview images specified by the [MetaData] data attributes will be used as the picker item.

There's more...

In addition to displaying the current time, the watch face can also present information to the user. Such information could be of any upcoming events on the calendar, any unread emails, or fitness information. Similar to creating a Handler instance to redraw the screen every second, we can create a Handler instance to retrieve data from a calendar or inbox very so often.

Watch faces can also respond to tap events. To receive tap events, we need to specify that we handle these events when creating the WatchFaceStyle instance. The WatchFaceStyle.Builder type has a method, SetAcceptsTapEvents(),which is used to perform this. If we pass true into this method, when the user taps the watch face, the OnTapCommand() method of the watch face engine will be invoked. We can override this method to process the tap event.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.40.207