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.
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.
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:
[assembly: UsesPermission(Manifest.Permission.WakeLock)] [assembly: UsesPermission( "com.google.android.permission.PROVIDE_BACKGROUND")]
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); } }
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); } });
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;
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);
OnTimeTick()
method, so we can simply request for the view to be redrawn:Invalidate();
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); }
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;
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:
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); } }
[Service]
attribute:[Service( Label = "XamarinCookbook", Permission = Android.Manifest.Permission.BindWallpaper)]
[IntentFilter]
attribute:[IntentFilter(new [] { Android.Service.Wallpaper.WallpaperService.ServiceInterface }, Categories = new [] { "com.google.android.wearable.watchface.category.WATCH_FACE" })]
watch_face.xml
:<?xml version="1.0" encoding="UTF-8"?> <wallpaper xmlns:android="http://schemas.android.com/apk/res/android"/>
[MetaData]
attribute:[MetaData( Android.Service.Wallpaper.WallpaperService.ServiceMetaData, Resource = "@xml/watch_face")]
preview.png
and preview_round.png
in the drawable
resource folder.[MetaData]
attributes:[MetaData( "com.google.android.wearable.watchface.preview", Resource = "@drawable/preview")] [MetaData( "com.google.android.wearable.watchface.preview_circular", Resource = "@drawable/preview_round")]
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.
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.
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.
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.
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.
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.
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
.
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
.
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.
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.