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
| @Override public 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 variables private 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
| @Override public 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
| @Override public 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
| @Override public 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
| @Override public 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
| @Override public 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
| @Override public 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
| @Override public 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
| @Override public 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