on
Italy
- Get link
- X
- Other Apps


WatchFaceService. In this article, you're going create an extension of the CanvasWatchFaceService class, which is an implementation of WatchFaceService that also provides a Canvas for drawing out your watch face. Start by creating a new Java class under the wear module in Android Studio that extends CanvasWatchFaceService.
1
| public class WatchFaceService extends CanvasWatchFaceService |
WatchFaceEngine in the source files of this article, that extends Engine. This is the watch face engine that handles system events, such as the screen turning off or going into ambient mode.
1
| private class WatchFaceEngine extends Engine |
WatchFaceEngine is in, go back to the outer class and override the onCreateEngine method to return your new inner class. This will associate your watch face service with the code that will drive the display.
1
2
3
4
| @Overridepublic Engine onCreateEngine() { return new WatchFaceEngine();} |
1
| <uses-feature android:name="android.hardware.type.watch" /> |
1
2
| |
application node with permission to BIND_WALLPAPER, a few sets of meta-data containing
reference images of your watch face for the selection screen (in this
example we're just using the launcher icon), and an intent-filter to let the system know that your service is meant for displaying a watch face.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
| <service android:name=".service.CustomWatchFaceService" android:label="Tuts+ Wear Watch Face" 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="@mipmap/ic_launcher" /> <meta-data android:name="com.google.android.wearable.watchface.preview_circular" android:resource="@mipmap/ic_launcher" /> <intent-filter> <action android:name="android.service.wallpaper.WallpaperService" /> <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /> </intent-filter></service> |
PROVIDE_BACKGROUND and WAKE_LOCK, because Android Wear requires that both the wear and mobile modules
request the same permissions for the wear APK to be installed on a
user's watch. Once both manifest files are filled in, you can return to CustomWatchFaceService.java to start implementing the engine.Engine object
associated with your service is what drives your watch face. It handles
timers, displaying your user interface, moving in and out of ambient
mode, and getting information about the physical watch display. In
short, this is where the magic happens.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
| //Member variablesprivate Typeface WATCH_TEXT_TYPEFACE = Typeface.create( Typeface.SERIF, Typeface.NORMAL );private static final int MSG_UPDATE_TIME_ID = 42;private long mUpdateRateMs = 1000;private Time mDisplayTime;private Paint mBackgroundColorPaint;private Paint mTextColorPaint;private boolean mHasTimeZoneReceiverBeenRegistered = false;private boolean mIsInMuteMode;private boolean mIsLowBitAmbient;private float mXOffset;private float mYOffset;private int mBackgroundColor = Color.parseColor( "black" );private int mTextColor = Color.parseColor( "red" ); |
TypeFace that we will use for our digital watch text as well as the watch face background color and text color. The Time object is used for, you guessed it, keeping track of the current device time. mUpdateRateMs is
used to control a timer that we will need to implement to update our
watch face every second (hence the 1000 milliseconds value for mUpdateRateMs), because the standard WatchFaceService only keeps track of time in one minute increments. mXOffset and mYOffset are
defined once the engine knows the physical shape of the watch so that
our watch face can be drawn without being too close to the top or left
of the screen, or being cut off by a rounded corner. The
three boolean values are used to keep track of different device and
application states.
1
2
3
4
5
6
7
| final BroadcastReceiver mTimeZoneBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mDisplayTime.clear( intent.getStringExtra( "time-zone" ) ); mDisplayTime.setToNow(); }}; |
Handler to takes care of updating your watch face every second. This is necessary because of the limitations of WatchFaceService discussed above. If your own watch face only needs to be updated every minute, then you can safely ignore this section.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
| private final Handler mTimeHandler = new Handler() { @Override public void handleMessage(Message msg) { switch( msg.what ) { case MSG_UPDATE_TIME_ID: { invalidate(); if( isVisible() && !isInAmbientMode() ) { long currentTimeMillis = System.currentTimeMillis(); long delay = mUpdateRateMs - ( currentTimeMillis % mUpdateRateMs ); mTimeHandler.sendEmptyMessageDelayed( MSG_UPDATE_TIME_ID, delay ); } break; } } }}; |
Handler is pretty straightforward. It first checks the message ID. If matches MSG_UPDATE_TIME_ID, it continues to invalidate the current view for redrawing. After the view has been invalidated, the Handler checks
to see if the screen is visible and not in ambient mode. If it is
visible, it sends a repeat request a second later. The reason we're only
repeating the action in the Handler when
the watch face is visible and not in ambient mode is that it can be a
little battery intensive to keep updating every second. If the user
isn't looking at the screen, we simply fall back on the WatchFaceService implementation that updates every minute.Engine has an onCreate method
that should be used for creating objects and other tasks that can take a
significant amount of time and battery. You will also want to set a few
flags for the WatchFaceStyle here to control how the system interacts with the user when your watch face is active.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
| @Overridepublic void onCreate(SurfaceHolder holder) { super.onCreate(holder); setWatchFaceStyle( new WatchFaceStyle.Builder( CustomWatchFaceService.this ) .setBackgroundVisibility( WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE ) .setCardPeekMode( WatchFaceStyle.PEEK_MODE_VARIABLE ) .setShowSystemUiTime( false ) .build() ); mDisplayTime = new Time(); initBackground(); initDisplayText();} |
setWatchFaceStyle to
set the background of your notification cards to briefly show if the
card type is set as interruptive. You'll also set the peek mode so that
notification cards only take up as much room as necessary.WatchFaceStyle.Builder object.WatchFaceStyle has been set, you can initialize mDisplayTime as a new Time object.initBackground and initDisplayText allocate the two Paint objects
that you defined at the top of the engine. The background and text then
have their color set and the text has its typeface and font size set,
while also turning on anti-aliasing.
01
02
03
04
05
06
07
08
09
10
11
12
| private void initBackground() { mBackgroundColorPaint = new Paint(); mBackgroundColorPaint.setColor( mBackgroundColor );}private void initDisplayText() { mTextColorPaint = new Paint(); mTextColorPaint.setColor( mTextColor ); mTextColorPaint.setTypeface( WATCH_TEXT_TYPEFACE ); mTextColorPaint.setAntiAlias( true ); mTextColorPaint.setTextSize( getResources().getDimension( R.dimen.text_size ) );} |
Engine class that are triggered by changes to the device state. We'll start by going over the onVisibilityChanged method, which is called when the user hides or shows the watch face.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| @Overridepublic void onVisibilityChanged( boolean visible ) { super.onVisibilityChanged(visible); if( visible ) { if( !mHasTimeZoneReceiverBeenRegistered ) { IntentFilter filter = new IntentFilter( Intent.ACTION_TIMEZONE_CHANGED ); CustomWatchFaceService.this.registerReceiver( mTimeZoneBroadcastReceiver, filter ); mHasTimeZoneReceiverBeenRegistered = true; } mDisplayTime.clear( TimeZone.getDefault().getID() ); mDisplayTime.setToNow(); } else { if( mHasTimeZoneReceiverBeenRegistered ) { CustomWatchFaceService.this.unregisterReceiver( mTimeZoneBroadcastReceiver ); mHasTimeZoneReceiverBeenRegistered = false; } } updateTimer();} |
BroadcastReceiver that you defined at the top of the Engine is registered. If it isn't, the method creates an IntentFilter for the ACTION_TIMEZONE_CHANGED action and registers the BroadcastReceiver to listen for it.BroadcastReceiver can be unregistered. Once the BroadcastReceiver has been handled, updateTimer is called to trigger invalidating the watch face and redraw the watch face. updateTimer stops any Handler actions that are pending and checks to see if another should be sent.
1
2
3
4
5
6
| private void updateTimer() { mTimeHandler.removeMessages( MSG_UPDATE_TIME_ID ); if( isVisible() && !isInAmbientMode() ) { mTimeHandler.sendEmptyMessage( MSG_UPDATE_TIME_ID ); }} |
onApplyWindowInsets is
called. This is used to determine if the device your watch face is
running on is rounded or squared. This lets you change your watch face
to match up with the hardware.
01
02
03
04
05
06
07
08
09
10
11
12
| @Overridepublic void onApplyWindowInsets(WindowInsets insets) { super.onApplyWindowInsets(insets); mYOffset = getResources().getDimension( R.dimen.y_offset ); if( insets.isRound() ) { mXOffset = getResources().getDimension( R.dimen.x_offset_round ); } else { mXOffset = getResources().getDimension( R.dimen.x_offset_square ); }} |
onPropertiesChanged.
This method is called when the hardware properties for the Wear device
are determined, for example, if the device supports burn-in protection
or low bit ambient mode.Engine.
1
2
3
4
5
6
7
8
| @Overridepublic void onPropertiesChanged( Bundle properties ) { super.onPropertiesChanged( properties ); if( properties.getBoolean( PROPERTY_BURN_IN_PROTECTION, false ) ) { mIsLowBitAmbient = properties.getBoolean( PROPERTY_LOW_BIT_AMBIENT, false ); }} |
onAmbientModeChanged and onInterruptionFilterChanged. As the name implies, onAmbientModeChanged is called when the device moves in or out of ambient mode.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
| @Overridepublic void onAmbientModeChanged(boolean inAmbientMode) { super.onAmbientModeChanged(inAmbientMode); if( inAmbientMode ) { mTextColorPaint.setColor( Color.parseColor( "white" ) ); } else { mTextColorPaint.setColor( Color.parseColor( "red" ) ); } if( mIsLowBitAmbient ) { mTextColorPaint.setAntiAlias( !inAmbientMode ); } invalidate(); updateTimer();} |

onInterruptionFilterChanged is
called when the user manually changes the interruption settings on
their wearable. When this happens, you will need to check if the device
is muted and then alter the user interface accordingly. In this
situation, you will change the transparency of your watch face, set your
Handler to only update every minute if the device is muted, and then redraw your watch face.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
| @Overridepublic void onInterruptionFilterChanged(int interruptionFilter) { super.onInterruptionFilterChanged(interruptionFilter); boolean isDeviceMuted = ( interruptionFilter == android.support.wearable.watchface.WatchFaceService.INTERRUPTION_FILTER_NONE ); if( isDeviceMuted ) { mUpdateRateMs = TimeUnit.MINUTES.toMillis( 1 ); } else { mUpdateRateMs = DEFAULT_UPDATE_RATE_MS; } if( mIsInMuteMode != isDeviceMuted ) { mIsInMuteMode = isDeviceMuted; int alpha = ( isDeviceMuted ) ? 100 : 255; mTextColorPaint.setAlpha( alpha ); invalidate(); updateTimer(); }} |
Handler timer will be disabled. Your watch face can still update with the current time every minute through using the built-in onTimeTick method to invalidate the Canvas.
1
2
3
4
5
6
| @Overridepublic void onTimeTick() { super.onTimeTick(); invalidate();} |
CanvasWatchFaceService uses a standard Canvas object, so you will need to add onDraw to your Engine and manually draw out your watch face.onDraw to
easily support an analog watch face. In this method, you will want to
verify that you are displaying the correct time by updating your Time object and then you can start applying your watch face.
1
2
3
4
5
6
7
8
9
| @Overridepublic void onDraw(Canvas canvas, Rect bounds) { super.onDraw(canvas, bounds); mDisplayTime.setToNow(); drawBackground( canvas, bounds ); drawTimeText( canvas );} |
drawBackground applies a solid color to the background of the Wear device.
1
2
3
| private void drawBackground( Canvas canvas, Rect bounds ) { canvas.drawRect( 0, 0, bounds.width(), bounds.height(), mBackgroundColorPaint );} |
drawTimeText,
however, creates the time text that will be displayed with the help of a
couple helper methods and then applies it to the canvas at the x and y
offset points that you defined in onApplyWindowInsets.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
| private void drawTimeText( Canvas canvas ) { String timeText = getHourString() + ":" + String.format( "%02d", mDisplayTime.minute ); if( isInAmbientMode() || mIsInMuteMode ) { timeText += ( mDisplayTime.hour < 12 ) ? "AM" : "PM"; } else { timeText += String.format( ":%02d", mDisplayTime.second); } canvas.drawText( timeText, mXOffset, mYOffset, mTextColorPaint );}private String getHourString() { if( mDisplayTime.hour % 12 == 0 ) return "12"; else if( mDisplayTime.hour <= 12 ) return String.valueOf( mDisplayTime.hour ); else return String.valueOf( mDisplayTime.hour - 12 );} |

Canvas based watch face with an OpenGL implementation, or derive your own class from WatchFaceService to meet your needs.
Comments
Post a Comment