1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.wearable.watchface; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.graphics.Bitmap; 24 import android.graphics.BitmapFactory; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.ColorMatrix; 28 import android.graphics.ColorMatrixColorFilter; 29 import android.graphics.Paint; 30 import android.graphics.Rect; 31 import android.os.Bundle; 32 import android.os.Handler; 33 import android.os.Message; 34 import android.support.v7.graphics.Palette; 35 import android.support.wearable.watchface.CanvasWatchFaceService; 36 import android.support.wearable.watchface.WatchFaceService; 37 import android.support.wearable.watchface.WatchFaceStyle; 38 import android.util.Log; 39 import android.view.SurfaceHolder; 40 41 import java.util.Calendar; 42 import java.util.TimeZone; 43 import java.util.concurrent.TimeUnit; 44 45 /** 46 * Sample analog watch face with a ticking second hand. In ambient mode, the second hand isn't 47 * shown. On devices with low-bit ambient mode, the hands are drawn without anti-aliasing in ambient 48 * mode. The watch face is drawn with less contrast in mute mode. 49 * 50 * {@link SweepWatchFaceService} is similar but has a sweep second hand. 51 */ 52 public class AnalogWatchFaceService extends CanvasWatchFaceService { 53 private static final String TAG = "AnalogWatchFaceService"; 54 55 /* 56 * Update rate in milliseconds for interactive mode. We update once a second to advance the 57 * second hand. 58 */ 59 private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1); 60 61 @Override onCreateEngine()62 public Engine onCreateEngine() { 63 return new Engine(); 64 } 65 66 private class Engine extends CanvasWatchFaceService.Engine { 67 private static final int MSG_UPDATE_TIME = 0; 68 69 private static final float HOUR_STROKE_WIDTH = 5f; 70 private static final float MINUTE_STROKE_WIDTH = 3f; 71 private static final float SECOND_TICK_STROKE_WIDTH = 2f; 72 73 private static final float CENTER_GAP_AND_CIRCLE_RADIUS = 4f; 74 75 private static final int SHADOW_RADIUS = 6; 76 77 private Calendar mCalendar; 78 private boolean mRegisteredTimeZoneReceiver = false; 79 private boolean mMuteMode; 80 81 private float mCenterX; 82 private float mCenterY; 83 84 private float mSecondHandLength; 85 private float sMinuteHandLength; 86 private float sHourHandLength; 87 88 /* Colors for all hands (hour, minute, seconds, ticks) based on photo loaded. */ 89 private int mWatchHandColor; 90 private int mWatchHandHighlightColor; 91 private int mWatchHandShadowColor; 92 93 private Paint mHourPaint; 94 private Paint mMinutePaint; 95 private Paint mSecondPaint; 96 private Paint mTickAndCirclePaint; 97 98 private Paint mBackgroundPaint; 99 private Bitmap mBackgroundBitmap; 100 private Bitmap mGrayBackgroundBitmap; 101 102 private boolean mAmbient; 103 private boolean mLowBitAmbient; 104 private boolean mBurnInProtection; 105 106 private Rect mPeekCardBounds = new Rect(); 107 108 private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() { 109 @Override 110 public void onReceive(Context context, Intent intent) { 111 mCalendar.setTimeZone(TimeZone.getDefault()); 112 invalidate(); 113 } 114 }; 115 116 /* Handler to update the time once a second in interactive mode. */ 117 private final Handler mUpdateTimeHandler = new Handler() { 118 @Override 119 public void handleMessage(Message message) { 120 121 if (Log.isLoggable(TAG, Log.DEBUG)) { 122 Log.d(TAG, "updating time"); 123 } 124 invalidate(); 125 if (shouldTimerBeRunning()) { 126 long timeMs = System.currentTimeMillis(); 127 long delayMs = INTERACTIVE_UPDATE_RATE_MS 128 - (timeMs % INTERACTIVE_UPDATE_RATE_MS); 129 mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs); 130 } 131 132 } 133 }; 134 135 @Override onCreate(SurfaceHolder holder)136 public void onCreate(SurfaceHolder holder) { 137 if (Log.isLoggable(TAG, Log.DEBUG)) { 138 Log.d(TAG, "onCreate"); 139 } 140 super.onCreate(holder); 141 142 setWatchFaceStyle(new WatchFaceStyle.Builder(AnalogWatchFaceService.this) 143 .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT) 144 .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) 145 .setShowSystemUiTime(false) 146 .build()); 147 148 mBackgroundPaint = new Paint(); 149 mBackgroundPaint.setColor(Color.BLACK); 150 mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg); 151 152 /* Set defaults for colors */ 153 mWatchHandColor = Color.WHITE; 154 mWatchHandHighlightColor = Color.RED; 155 mWatchHandShadowColor = Color.BLACK; 156 157 mHourPaint = new Paint(); 158 mHourPaint.setColor(mWatchHandColor); 159 mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH); 160 mHourPaint.setAntiAlias(true); 161 mHourPaint.setStrokeCap(Paint.Cap.ROUND); 162 mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 163 164 mMinutePaint = new Paint(); 165 mMinutePaint.setColor(mWatchHandColor); 166 mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH); 167 mMinutePaint.setAntiAlias(true); 168 mMinutePaint.setStrokeCap(Paint.Cap.ROUND); 169 mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 170 171 mSecondPaint = new Paint(); 172 mSecondPaint.setColor(mWatchHandHighlightColor); 173 mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH); 174 mSecondPaint.setAntiAlias(true); 175 mSecondPaint.setStrokeCap(Paint.Cap.ROUND); 176 mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 177 178 mTickAndCirclePaint = new Paint(); 179 mTickAndCirclePaint.setColor(mWatchHandColor); 180 mTickAndCirclePaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH); 181 mTickAndCirclePaint.setAntiAlias(true); 182 mTickAndCirclePaint.setStyle(Paint.Style.STROKE); 183 mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 184 185 /* Extract colors from background image to improve watchface style. */ 186 Palette.generateAsync( 187 mBackgroundBitmap, 188 new Palette.PaletteAsyncListener() { 189 @Override 190 public void onGenerated(Palette palette) { 191 if (palette != null) { 192 if (Log.isLoggable(TAG, Log.DEBUG)) { 193 Log.d(TAG, "Palette: " + palette); 194 } 195 196 mWatchHandHighlightColor = palette.getVibrantColor(Color.RED); 197 mWatchHandColor = palette.getLightVibrantColor(Color.WHITE); 198 mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK); 199 updateWatchHandStyle(); 200 } 201 } 202 }); 203 204 mCalendar = Calendar.getInstance(); 205 } 206 207 @Override onDestroy()208 public void onDestroy() { 209 mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); 210 super.onDestroy(); 211 } 212 213 @Override onPropertiesChanged(Bundle properties)214 public void onPropertiesChanged(Bundle properties) { 215 super.onPropertiesChanged(properties); 216 if (Log.isLoggable(TAG, Log.DEBUG)) { 217 Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient); 218 } 219 220 mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false); 221 mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false); 222 } 223 224 @Override onTimeTick()225 public void onTimeTick() { 226 super.onTimeTick(); 227 invalidate(); 228 } 229 230 @Override onAmbientModeChanged(boolean inAmbientMode)231 public void onAmbientModeChanged(boolean inAmbientMode) { 232 super.onAmbientModeChanged(inAmbientMode); 233 if (Log.isLoggable(TAG, Log.DEBUG)) { 234 Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode); 235 } 236 mAmbient = inAmbientMode; 237 238 updateWatchHandStyle(); 239 240 /* Check and trigger whether or not timer should be running (only in active mode). */ 241 updateTimer(); 242 } 243 updateWatchHandStyle()244 private void updateWatchHandStyle(){ 245 if (mAmbient){ 246 mHourPaint.setColor(Color.WHITE); 247 mMinutePaint.setColor(Color.WHITE); 248 mSecondPaint.setColor(Color.WHITE); 249 mTickAndCirclePaint.setColor(Color.WHITE); 250 251 mHourPaint.setAntiAlias(false); 252 mMinutePaint.setAntiAlias(false); 253 mSecondPaint.setAntiAlias(false); 254 mTickAndCirclePaint.setAntiAlias(false); 255 256 mHourPaint.clearShadowLayer(); 257 mMinutePaint.clearShadowLayer(); 258 mSecondPaint.clearShadowLayer(); 259 mTickAndCirclePaint.clearShadowLayer(); 260 261 } else { 262 mHourPaint.setColor(mWatchHandColor); 263 mMinutePaint.setColor(mWatchHandColor); 264 mSecondPaint.setColor(mWatchHandHighlightColor); 265 mTickAndCirclePaint.setColor(mWatchHandColor); 266 267 mHourPaint.setAntiAlias(true); 268 mMinutePaint.setAntiAlias(true); 269 mSecondPaint.setAntiAlias(true); 270 mTickAndCirclePaint.setAntiAlias(true); 271 272 mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 273 mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 274 mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 275 mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 276 } 277 } 278 279 @Override onInterruptionFilterChanged(int interruptionFilter)280 public void onInterruptionFilterChanged(int interruptionFilter) { 281 super.onInterruptionFilterChanged(interruptionFilter); 282 boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE); 283 284 /* Dim display in mute mode. */ 285 if (mMuteMode != inMuteMode) { 286 mMuteMode = inMuteMode; 287 mHourPaint.setAlpha(inMuteMode ? 100 : 255); 288 mMinutePaint.setAlpha(inMuteMode ? 100 : 255); 289 mSecondPaint.setAlpha(inMuteMode ? 80 : 255); 290 invalidate(); 291 } 292 } 293 294 @Override onSurfaceChanged(SurfaceHolder holder, int format, int width, int height)295 public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { 296 super.onSurfaceChanged(holder, format, width, height); 297 298 /* 299 * Find the coordinates of the center point on the screen, and ignore the window 300 * insets, so that, on round watches with a "chin", the watch face is centered on the 301 * entire screen, not just the usable portion. 302 */ 303 mCenterX = width / 2f; 304 mCenterY = height / 2f; 305 306 /* 307 * Calculate lengths of different hands based on watch screen size. 308 */ 309 mSecondHandLength = (float) (mCenterX * 0.875); 310 sMinuteHandLength = (float) (mCenterX * 0.75); 311 sHourHandLength = (float) (mCenterX * 0.5); 312 313 314 /* Scale loaded background image (more efficient) if surface dimensions change. */ 315 float scale = ((float) width) / (float) mBackgroundBitmap.getWidth(); 316 317 mBackgroundBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap, 318 (int) (mBackgroundBitmap.getWidth() * scale), 319 (int) (mBackgroundBitmap.getHeight() * scale), true); 320 321 /* 322 * Create a gray version of the image only if it will look nice on the device in 323 * ambient mode. That means we don't want devices that support burn-in 324 * protection (slight movements in pixels, not great for images going all the way to 325 * edges) and low ambient mode (degrades image quality). 326 * 327 * Also, if your watch face will know about all images ahead of time (users aren't 328 * selecting their own photos for the watch face), it will be more 329 * efficient to create a black/white version (png, etc.) and load that when you need it. 330 */ 331 if (!mBurnInProtection && !mLowBitAmbient) { 332 initGrayBackgroundBitmap(); 333 } 334 } 335 initGrayBackgroundBitmap()336 private void initGrayBackgroundBitmap() { 337 mGrayBackgroundBitmap = Bitmap.createBitmap( 338 mBackgroundBitmap.getWidth(), 339 mBackgroundBitmap.getHeight(), 340 Bitmap.Config.ARGB_8888); 341 Canvas canvas = new Canvas(mGrayBackgroundBitmap); 342 Paint grayPaint = new Paint(); 343 ColorMatrix colorMatrix = new ColorMatrix(); 344 colorMatrix.setSaturation(0); 345 ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix); 346 grayPaint.setColorFilter(filter); 347 canvas.drawBitmap(mBackgroundBitmap, 0, 0, grayPaint); 348 } 349 350 @Override onDraw(Canvas canvas, Rect bounds)351 public void onDraw(Canvas canvas, Rect bounds) { 352 if (Log.isLoggable(TAG, Log.VERBOSE)) { 353 Log.v(TAG, "onDraw"); 354 } 355 long now = System.currentTimeMillis(); 356 mCalendar.setTimeInMillis(now); 357 358 if (mAmbient && (mLowBitAmbient || mBurnInProtection)) { 359 canvas.drawColor(Color.BLACK); 360 } else if (mAmbient) { 361 canvas.drawBitmap(mGrayBackgroundBitmap, 0, 0, mBackgroundPaint); 362 } else { 363 canvas.drawBitmap(mBackgroundBitmap, 0, 0, mBackgroundPaint); 364 } 365 366 /* 367 * Draw ticks. Usually you will want to bake this directly into the photo, but in 368 * cases where you want to allow users to select their own photos, this dynamically 369 * creates them on top of the photo. 370 */ 371 float innerTickRadius = mCenterX - 10; 372 float outerTickRadius = mCenterX; 373 for (int tickIndex = 0; tickIndex < 12; tickIndex++) { 374 float tickRot = (float) (tickIndex * Math.PI * 2 / 12); 375 float innerX = (float) Math.sin(tickRot) * innerTickRadius; 376 float innerY = (float) -Math.cos(tickRot) * innerTickRadius; 377 float outerX = (float) Math.sin(tickRot) * outerTickRadius; 378 float outerY = (float) -Math.cos(tickRot) * outerTickRadius; 379 canvas.drawLine(mCenterX + innerX, mCenterY + innerY, 380 mCenterX + outerX, mCenterY + outerY, mTickAndCirclePaint); 381 } 382 383 /* 384 * These calculations reflect the rotation in degrees per unit of time, e.g., 385 * 360 / 60 = 6 and 360 / 12 = 30. 386 */ 387 final float seconds = 388 (mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f); 389 final float secondsRotation = seconds * 6f; 390 391 final float minutesRotation = mCalendar.get(Calendar.MINUTE) * 6f; 392 393 final float hourHandOffset = mCalendar.get(Calendar.MINUTE) / 2f; 394 final float hoursRotation = (mCalendar.get(Calendar.HOUR) * 30) + hourHandOffset; 395 396 /* 397 * Save the canvas state before we can begin to rotate it. 398 */ 399 canvas.save(); 400 401 canvas.rotate(hoursRotation, mCenterX, mCenterY); 402 canvas.drawLine( 403 mCenterX, 404 mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS, 405 mCenterX, 406 mCenterY - sHourHandLength, 407 mHourPaint); 408 409 canvas.rotate(minutesRotation - hoursRotation, mCenterX, mCenterY); 410 canvas.drawLine( 411 mCenterX, 412 mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS, 413 mCenterX, 414 mCenterY - sMinuteHandLength, 415 mMinutePaint); 416 417 /* 418 * Ensure the "seconds" hand is drawn only when we are in interactive mode. 419 * Otherwise, we only update the watch face once a minute. 420 */ 421 if (!mAmbient) { 422 canvas.rotate(secondsRotation - minutesRotation, mCenterX, mCenterY); 423 canvas.drawLine( 424 mCenterX, 425 mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS, 426 mCenterX, 427 mCenterY - mSecondHandLength, 428 mSecondPaint); 429 430 } 431 canvas.drawCircle( 432 mCenterX, 433 mCenterY, 434 CENTER_GAP_AND_CIRCLE_RADIUS, 435 mTickAndCirclePaint); 436 437 /* Restore the canvas' original orientation. */ 438 canvas.restore(); 439 440 /* Draw rectangle behind peek card in ambient mode to improve readability. */ 441 if (mAmbient) { 442 canvas.drawRect(mPeekCardBounds, mBackgroundPaint); 443 } 444 } 445 446 @Override onVisibilityChanged(boolean visible)447 public void onVisibilityChanged(boolean visible) { 448 super.onVisibilityChanged(visible); 449 450 if (visible) { 451 registerReceiver(); 452 /* Update time zone in case it changed while we weren't visible. */ 453 mCalendar.setTimeZone(TimeZone.getDefault()); 454 invalidate(); 455 } else { 456 unregisterReceiver(); 457 } 458 459 /* Check and trigger whether or not timer should be running (only in active mode). */ 460 updateTimer(); 461 } 462 463 @Override onPeekCardPositionUpdate(Rect rect)464 public void onPeekCardPositionUpdate(Rect rect) { 465 super.onPeekCardPositionUpdate(rect); 466 mPeekCardBounds.set(rect); 467 } 468 registerReceiver()469 private void registerReceiver() { 470 if (mRegisteredTimeZoneReceiver) { 471 return; 472 } 473 mRegisteredTimeZoneReceiver = true; 474 IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); 475 AnalogWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter); 476 } 477 unregisterReceiver()478 private void unregisterReceiver() { 479 if (!mRegisteredTimeZoneReceiver) { 480 return; 481 } 482 mRegisteredTimeZoneReceiver = false; 483 AnalogWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver); 484 } 485 486 /** 487 * Starts/stops the {@link #mUpdateTimeHandler} timer based on the state of the watch face. 488 */ updateTimer()489 private void updateTimer() { 490 if (Log.isLoggable(TAG, Log.DEBUG)) { 491 Log.d(TAG, "updateTimer"); 492 } 493 mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); 494 if (shouldTimerBeRunning()) { 495 mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME); 496 } 497 } 498 499 /** 500 * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer 501 * should only run in active mode. 502 */ shouldTimerBeRunning()503 private boolean shouldTimerBeRunning() { 504 return isVisible() && !mAmbient; 505 } 506 } 507 } 508