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.app.PendingIntent; 20 import android.content.BroadcastReceiver; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.graphics.Bitmap; 26 import android.graphics.BitmapFactory; 27 import android.graphics.Canvas; 28 import android.graphics.Color; 29 import android.graphics.ColorMatrix; 30 import android.graphics.ColorMatrixColorFilter; 31 import android.graphics.Paint; 32 import android.graphics.Rect; 33 import android.graphics.Typeface; 34 import android.os.Bundle; 35 import android.os.Handler; 36 import android.os.Message; 37 import android.support.v7.graphics.Palette; 38 import android.support.wearable.complications.ComplicationData; 39 import android.support.wearable.complications.ComplicationHelperActivity; 40 import android.support.wearable.complications.ComplicationText; 41 import android.support.wearable.watchface.CanvasWatchFaceService; 42 import android.support.wearable.watchface.WatchFaceService; 43 import android.support.wearable.watchface.WatchFaceStyle; 44 import android.text.TextUtils; 45 import android.util.Log; 46 import android.util.SparseArray; 47 import android.view.SurfaceHolder; 48 49 import java.util.Calendar; 50 import java.util.TimeZone; 51 import java.util.concurrent.TimeUnit; 52 53 /** 54 * Demonstrates two simple complications in a watch face. 55 */ 56 public class ComplicationSimpleWatchFaceService extends CanvasWatchFaceService { 57 private static final String TAG = "SimpleComplicationWF"; 58 59 // Unique IDs for each complication. 60 private static final int LEFT_DIAL_COMPLICATION = 0; 61 private static final int RIGHT_DIAL_COMPLICATION = 1; 62 63 // Left and right complication IDs as array for Complication API. 64 public static final int[] COMPLICATION_IDS = {LEFT_DIAL_COMPLICATION, RIGHT_DIAL_COMPLICATION}; 65 66 // Left and right dial supported types. 67 public static final int[][] COMPLICATION_SUPPORTED_TYPES = { 68 {ComplicationData.TYPE_SHORT_TEXT}, 69 {ComplicationData.TYPE_SHORT_TEXT} 70 }; 71 72 /* 73 * Update rate in milliseconds for interactive mode. We update once a second to advance the 74 * second hand. 75 */ 76 private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1); 77 78 @Override 79 public Engine onCreateEngine() { 80 return new Engine(); 81 } 82 83 private class Engine extends CanvasWatchFaceService.Engine { 84 private static final int MSG_UPDATE_TIME = 0; 85 86 private static final float COMPLICATION_TEXT_SIZE = 38f; 87 private static final int COMPLICATION_TAP_BUFFER = 40; 88 89 private static final float HOUR_STROKE_WIDTH = 5f; 90 private static final float MINUTE_STROKE_WIDTH = 3f; 91 private static final float SECOND_TICK_STROKE_WIDTH = 2f; 92 93 private static final float CENTER_GAP_AND_CIRCLE_RADIUS = 4f; 94 95 private static final int SHADOW_RADIUS = 6; 96 97 private Calendar mCalendar; 98 private boolean mRegisteredTimeZoneReceiver = false; 99 private boolean mMuteMode; 100 101 private int mWidth; 102 private int mHeight; 103 private float mCenterX; 104 private float mCenterY; 105 106 private float mSecondHandLength; 107 private float mMinuteHandLength; 108 private float mHourHandLength; 109 110 // Colors for all hands (hour, minute, seconds, ticks) based on photo loaded. 111 private int mWatchHandColor; 112 private int mWatchHandHighlightColor; 113 private int mWatchHandShadowColor; 114 115 private Paint mHourPaint; 116 private Paint mMinutePaint; 117 private Paint mSecondPaint; 118 private Paint mTickAndCirclePaint; 119 120 private Paint mBackgroundPaint; 121 private Bitmap mBackgroundBitmap; 122 private Bitmap mGrayBackgroundBitmap; 123 124 // Variables for painting Complications 125 private Paint mComplicationPaint; 126 127 /* To properly place each complication, we need their x and y coordinates. While the width 128 * may change from moment to moment based on the time, the height will not change, so we 129 * store it as a local variable and only calculate it only when the surface changes 130 * (onSurfaceChanged()). 131 */ 132 private int mComplicationsY; 133 134 /* Maps active complication ids to the data for that complication. Note: Data will only be 135 * present if the user has chosen a provider via the settings activity for the watch face. 136 */ 137 private SparseArray<ComplicationData> mActiveComplicationDataSparseArray; 138 139 private boolean mAmbient; 140 private boolean mLowBitAmbient; 141 private boolean mBurnInProtection; 142 143 private Rect mPeekCardBounds = new Rect(); 144 145 private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() { 146 @Override 147 public void onReceive(Context context, Intent intent) { 148 mCalendar.setTimeZone(TimeZone.getDefault()); 149 invalidate(); 150 } 151 }; 152 153 // Handler to update the time once a second in interactive mode. 154 private final Handler mUpdateTimeHandler = new Handler() { 155 @Override 156 public void handleMessage(Message message) { 157 158 if (Log.isLoggable(TAG, Log.DEBUG)) { 159 Log.d(TAG, "updating time"); 160 } 161 invalidate(); 162 if (shouldTimerBeRunning()) { 163 long timeMs = System.currentTimeMillis(); 164 long delayMs = INTERACTIVE_UPDATE_RATE_MS 165 - (timeMs % INTERACTIVE_UPDATE_RATE_MS); 166 mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs); 167 } 168 169 } 170 }; 171 172 @Override 173 public void onCreate(SurfaceHolder holder) { 174 if (Log.isLoggable(TAG, Log.DEBUG)) { 175 Log.d(TAG, "onCreate"); 176 } 177 super.onCreate(holder); 178 179 mCalendar = Calendar.getInstance(); 180 181 setWatchFaceStyle(new WatchFaceStyle.Builder(ComplicationSimpleWatchFaceService.this) 182 .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT) 183 .setAcceptsTapEvents(true) 184 .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) 185 .setShowSystemUiTime(false) 186 .build()); 187 188 initializeBackground(); 189 initializeComplication(); 190 initializeWatchFace(); 191 } 192 193 private void initializeBackground() { 194 mBackgroundPaint = new Paint(); 195 mBackgroundPaint.setColor(Color.BLACK); 196 mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg); 197 } 198 199 private void initializeComplication() { 200 if (Log.isLoggable(TAG, Log.DEBUG)) { 201 Log.d(TAG, "initializeComplications()"); 202 } 203 mActiveComplicationDataSparseArray = new SparseArray<>(COMPLICATION_IDS.length); 204 205 mComplicationPaint = new Paint(); 206 mComplicationPaint.setColor(Color.WHITE); 207 mComplicationPaint.setTextSize(COMPLICATION_TEXT_SIZE); 208 mComplicationPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); 209 mComplicationPaint.setAntiAlias(true); 210 211 setActiveComplications(COMPLICATION_IDS); 212 } 213 214 private void initializeWatchFace() { 215 /* Set defaults for colors */ 216 mWatchHandColor = Color.WHITE; 217 mWatchHandHighlightColor = Color.RED; 218 mWatchHandShadowColor = Color.BLACK; 219 220 mHourPaint = new Paint(); 221 mHourPaint.setColor(mWatchHandColor); 222 mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH); 223 mHourPaint.setAntiAlias(true); 224 mHourPaint.setStrokeCap(Paint.Cap.ROUND); 225 mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 226 227 mMinutePaint = new Paint(); 228 mMinutePaint.setColor(mWatchHandColor); 229 mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH); 230 mMinutePaint.setAntiAlias(true); 231 mMinutePaint.setStrokeCap(Paint.Cap.ROUND); 232 mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 233 234 mSecondPaint = new Paint(); 235 mSecondPaint.setColor(mWatchHandHighlightColor); 236 mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH); 237 mSecondPaint.setAntiAlias(true); 238 mSecondPaint.setStrokeCap(Paint.Cap.ROUND); 239 mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 240 241 mTickAndCirclePaint = new Paint(); 242 mTickAndCirclePaint.setColor(mWatchHandColor); 243 mTickAndCirclePaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH); 244 mTickAndCirclePaint.setAntiAlias(true); 245 mTickAndCirclePaint.setStyle(Paint.Style.STROKE); 246 mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 247 248 // Asynchronous call extract colors from background image to improve watch face style. 249 Palette.from(mBackgroundBitmap).generate( 250 new Palette.PaletteAsyncListener() { 251 public void onGenerated(Palette palette) { 252 /* 253 * Sometimes, palette is unable to generate a color palette 254 * so we need to check that we have one. 255 */ 256 if (palette != null) { 257 Log.d("onGenerated", palette.toString()); 258 mWatchHandColor = palette.getVibrantColor(Color.WHITE); 259 mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK); 260 updateWatchHandStyle(); 261 } 262 } 263 }); 264 } 265 266 @Override 267 public void onDestroy() { 268 mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); 269 super.onDestroy(); 270 } 271 272 @Override 273 public void onPropertiesChanged(Bundle properties) { 274 super.onPropertiesChanged(properties); 275 if (Log.isLoggable(TAG, Log.DEBUG)) { 276 Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient); 277 } 278 279 mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false); 280 mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false); 281 } 282 283 /* 284 * Called when there is updated data for a complication id. 285 */ 286 @Override 287 public void onComplicationDataUpdate( 288 int complicationId, ComplicationData complicationData) { 289 Log.d(TAG, "onComplicationDataUpdate() id: " + complicationId); 290 291 // Adds/updates active complication data in the array. 292 mActiveComplicationDataSparseArray.put(complicationId, complicationData); 293 invalidate(); 294 } 295 296 @Override 297 public void onTapCommand(int tapType, int x, int y, long eventTime) { 298 Log.d(TAG, "OnTapCommand()"); 299 switch (tapType) { 300 case TAP_TYPE_TAP: 301 int tappedComplicationId = getTappedComplicationId(x, y); 302 if (tappedComplicationId != -1) { 303 onComplicationTap(tappedComplicationId); 304 } 305 break; 306 } 307 } 308 309 /* 310 * Determines if tap inside a complication area or returns -1. 311 */ 312 private int getTappedComplicationId(int x, int y) { 313 ComplicationData complicationData; 314 long currentTimeMillis = System.currentTimeMillis(); 315 316 for (int i = 0; i < COMPLICATION_IDS.length; i++) { 317 complicationData = mActiveComplicationDataSparseArray.get(COMPLICATION_IDS[i]); 318 319 if ((complicationData != null) 320 && (complicationData.isActive(currentTimeMillis)) 321 && (complicationData.getType() != ComplicationData.TYPE_NOT_CONFIGURED) 322 && (complicationData.getType() != ComplicationData.TYPE_EMPTY)) { 323 324 Rect complicationBoundingRect = new Rect(0, 0, 0, 0); 325 326 switch (COMPLICATION_IDS[i]) { 327 case LEFT_DIAL_COMPLICATION: 328 complicationBoundingRect.set( 329 0, // left 330 mComplicationsY - COMPLICATION_TAP_BUFFER, // top 331 (mWidth / 2), // right 332 ((int) COMPLICATION_TEXT_SIZE // bottom 333 + mComplicationsY 334 + COMPLICATION_TAP_BUFFER)); 335 break; 336 337 case RIGHT_DIAL_COMPLICATION: 338 complicationBoundingRect.set( 339 (mWidth / 2), // left 340 mComplicationsY - COMPLICATION_TAP_BUFFER, // top 341 mWidth, // right 342 ((int) COMPLICATION_TEXT_SIZE // bottom 343 + mComplicationsY 344 + COMPLICATION_TAP_BUFFER)); 345 break; 346 } 347 348 if (complicationBoundingRect.width() > 0) { 349 if (complicationBoundingRect.contains(x, y)) { 350 return COMPLICATION_IDS[i]; 351 } 352 } else { 353 Log.e(TAG, "Not a recognized complication id."); 354 } 355 } 356 } 357 return -1; 358 } 359 360 /* 361 * Fires PendingIntent associated with complication (if it has one). 362 */ 363 private void onComplicationTap(int complicationId) { 364 if (Log.isLoggable(TAG, Log.DEBUG)) { 365 Log.d(TAG, "onComplicationTap()"); 366 } 367 368 ComplicationData complicationData = 369 mActiveComplicationDataSparseArray.get(complicationId); 370 371 if (complicationData != null) { 372 373 if (complicationData.getTapAction() != null) { 374 try { 375 complicationData.getTapAction().send(); 376 } catch (PendingIntent.CanceledException e) { 377 Log.e(TAG, "On complication tap action error " + e); 378 } 379 380 } else if (complicationData.getType() == ComplicationData.TYPE_NO_PERMISSION) { 381 382 // Watch face does not have permission to receive complication data, so launch 383 // permission request. 384 ComponentName componentName = new ComponentName( 385 getApplicationContext(), 386 ComplicationSimpleWatchFaceService.class); 387 388 Intent permissionRequestIntent = 389 ComplicationHelperActivity.createPermissionRequestHelperIntent( 390 getApplicationContext(), componentName); 391 392 startActivity(permissionRequestIntent); 393 } 394 395 } else { 396 if (Log.isLoggable(TAG, Log.DEBUG)) { 397 Log.d(TAG, "No PendingIntent for complication " + complicationId + "."); 398 } 399 } 400 } 401 402 @Override 403 public void onTimeTick() { 404 super.onTimeTick(); 405 invalidate(); 406 } 407 408 @Override 409 public void onAmbientModeChanged(boolean inAmbientMode) { 410 super.onAmbientModeChanged(inAmbientMode); 411 if (Log.isLoggable(TAG, Log.DEBUG)) { 412 Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode); 413 } 414 mAmbient = inAmbientMode; 415 416 updateWatchHandStyle(); 417 418 // Updates complication style 419 mComplicationPaint.setAntiAlias(!inAmbientMode); 420 421 // Check and trigger whether or not timer should be running (only in active mode). 422 updateTimer(); 423 } 424 425 private void updateWatchHandStyle(){ 426 if (mAmbient){ 427 mHourPaint.setColor(Color.WHITE); 428 mMinutePaint.setColor(Color.WHITE); 429 mSecondPaint.setColor(Color.WHITE); 430 mTickAndCirclePaint.setColor(Color.WHITE); 431 432 mHourPaint.setAntiAlias(false); 433 mMinutePaint.setAntiAlias(false); 434 mSecondPaint.setAntiAlias(false); 435 mTickAndCirclePaint.setAntiAlias(false); 436 437 mHourPaint.clearShadowLayer(); 438 mMinutePaint.clearShadowLayer(); 439 mSecondPaint.clearShadowLayer(); 440 mTickAndCirclePaint.clearShadowLayer(); 441 442 } else { 443 mHourPaint.setColor(mWatchHandColor); 444 mMinutePaint.setColor(mWatchHandColor); 445 mSecondPaint.setColor(mWatchHandHighlightColor); 446 mTickAndCirclePaint.setColor(mWatchHandColor); 447 448 mHourPaint.setAntiAlias(true); 449 mMinutePaint.setAntiAlias(true); 450 mSecondPaint.setAntiAlias(true); 451 mTickAndCirclePaint.setAntiAlias(true); 452 453 mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 454 mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 455 mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 456 mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); 457 } 458 } 459 460 @Override 461 public void onInterruptionFilterChanged(int interruptionFilter) { 462 super.onInterruptionFilterChanged(interruptionFilter); 463 boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE); 464 465 /* Dim display in mute mode. */ 466 if (mMuteMode != inMuteMode) { 467 mMuteMode = inMuteMode; 468 mHourPaint.setAlpha(inMuteMode ? 100 : 255); 469 mMinutePaint.setAlpha(inMuteMode ? 100 : 255); 470 mSecondPaint.setAlpha(inMuteMode ? 80 : 255); 471 invalidate(); 472 } 473 } 474 475 @Override 476 public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { 477 super.onSurfaceChanged(holder, format, width, height); 478 479 // Used for complications 480 mWidth = width; 481 mHeight = height; 482 483 /* 484 * Find the coordinates of the center point on the screen, and ignore the window 485 * insets, so that, on round watches with a "chin", the watch face is centered on the 486 * entire screen, not just the usable portion. 487 */ 488 mCenterX = mWidth / 2f; 489 mCenterY = mHeight / 2f; 490 491 /* 492 * Since the height of the complications text does not change, we only have to 493 * recalculate when the surface changes. 494 */ 495 mComplicationsY = (int) ((mHeight / 2) + (mComplicationPaint.getTextSize() / 2)); 496 497 /* 498 * Calculate lengths of different hands based on watch screen size. 499 */ 500 mSecondHandLength = (float) (mCenterX * 0.875); 501 mMinuteHandLength = (float) (mCenterX * 0.75); 502 mHourHandLength = (float) (mCenterX * 0.5); 503 504 505 /* Scale loaded background image (more efficient) if surface dimensions change. */ 506 float scale = ((float) width) / (float) mBackgroundBitmap.getWidth(); 507 508 mBackgroundBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap, 509 (int) (mBackgroundBitmap.getWidth() * scale), 510 (int) (mBackgroundBitmap.getHeight() * scale), true); 511 512 /* 513 * Create a gray version of the image only if it will look nice on the device in 514 * ambient mode. That means we don't want devices that support burn-in 515 * protection (slight movements in pixels, not great for images going all the way to 516 * edges) and low ambient mode (degrades image quality). 517 * 518 * Also, if your watch face will know about all images ahead of time (users aren't 519 * selecting their own photos for the watch face), it will be more 520 * efficient to create a black/white version (png, etc.) and load that when you need it. 521 */ 522 if (!mBurnInProtection && !mLowBitAmbient) { 523 initGrayBackgroundBitmap(); 524 } 525 } 526 527 private void initGrayBackgroundBitmap() { 528 mGrayBackgroundBitmap = Bitmap.createBitmap( 529 mBackgroundBitmap.getWidth(), 530 mBackgroundBitmap.getHeight(), 531 Bitmap.Config.ARGB_8888); 532 Canvas canvas = new Canvas(mGrayBackgroundBitmap); 533 Paint grayPaint = new Paint(); 534 ColorMatrix colorMatrix = new ColorMatrix(); 535 colorMatrix.setSaturation(0); 536 ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix); 537 grayPaint.setColorFilter(filter); 538 canvas.drawBitmap(mBackgroundBitmap, 0, 0, grayPaint); 539 } 540 541 @Override 542 public void onDraw(Canvas canvas, Rect bounds) { 543 if (Log.isLoggable(TAG, Log.DEBUG)) { 544 Log.d(TAG, "onDraw"); 545 } 546 long now = System.currentTimeMillis(); 547 mCalendar.setTimeInMillis(now); 548 549 drawBackground(canvas); 550 drawComplications(canvas, now); 551 drawWatchFace(canvas); 552 553 554 } 555 556 private void drawBackground(Canvas canvas) { 557 if (mAmbient && (mLowBitAmbient || mBurnInProtection)) { 558 canvas.drawColor(Color.BLACK); 559 } else if (mAmbient) { 560 canvas.drawBitmap(mGrayBackgroundBitmap, 0, 0, mBackgroundPaint); 561 } else { 562 canvas.drawBitmap(mBackgroundBitmap, 0, 0, mBackgroundPaint); 563 } 564 } 565 566 private void drawComplications(Canvas canvas, long currentTimeMillis) { 567 ComplicationData complicationData; 568 569 for (int i = 0; i < COMPLICATION_IDS.length; i++) { 570 571 complicationData = mActiveComplicationDataSparseArray.get(COMPLICATION_IDS[i]); 572 573 if ((complicationData != null) 574 && (complicationData.isActive(currentTimeMillis))) { 575 576 // Both Short Text and No Permission Types can be rendered with the same code. 577 // No Permission will display "--" with an Intent to launch a permission prompt. 578 // If you want to support more types, just add a "else if" below with your 579 // rendering code inside. 580 if (complicationData.getType() == ComplicationData.TYPE_SHORT_TEXT 581 || complicationData.getType() == ComplicationData.TYPE_NO_PERMISSION) { 582 583 ComplicationText mainText = complicationData.getShortText(); 584 ComplicationText subText = complicationData.getShortTitle(); 585 586 CharSequence complicationMessage = 587 mainText.getText(getApplicationContext(), currentTimeMillis); 588 589 /* In most cases you would want the subText (Title) under the 590 * mainText (Text), but to keep it simple for the code lab, we are 591 * concatenating them all on one line. 592 */ 593 if (subText != null) { 594 complicationMessage = TextUtils.concat( 595 complicationMessage, 596 " ", 597 subText.getText(getApplicationContext(), currentTimeMillis)); 598 } 599 600 //Log.d(TAG, "Com id: " + COMPLICATION_IDS[i] + "\t" + complicationMessage); 601 double textWidth = 602 mComplicationPaint.measureText( 603 complicationMessage, 604 0, 605 complicationMessage.length()); 606 607 int complicationsX; 608 609 if (COMPLICATION_IDS[i] == LEFT_DIAL_COMPLICATION) { 610 complicationsX = (int) ((mWidth / 2) - textWidth) / 2; 611 } else { 612 // RIGHT_DIAL_COMPLICATION calculations 613 int offset = (int) ((mWidth / 2) - textWidth) / 2; 614 complicationsX = (mWidth / 2) + offset; 615 } 616 617 canvas.drawText( 618 complicationMessage, 619 0, 620 complicationMessage.length(), 621 complicationsX, 622 mComplicationsY, 623 mComplicationPaint); 624 } 625 } 626 } 627 } 628 629 private void drawWatchFace(Canvas canvas) { 630 /* 631 * Draw ticks. Usually you will want to bake this directly into the photo, but in 632 * cases where you want to allow users to select their own photos, this dynamically 633 * creates them on top of the photo. 634 */ 635 float innerTickRadius = mCenterX - 10; 636 float outerTickRadius = mCenterX; 637 for (int tickIndex = 0; tickIndex < 12; tickIndex++) { 638 float tickRot = (float) (tickIndex * Math.PI * 2 / 12); 639 float innerX = (float) Math.sin(tickRot) * innerTickRadius; 640 float innerY = (float) -Math.cos(tickRot) * innerTickRadius; 641 float outerX = (float) Math.sin(tickRot) * outerTickRadius; 642 float outerY = (float) -Math.cos(tickRot) * outerTickRadius; 643 canvas.drawLine(mCenterX + innerX, mCenterY + innerY, 644 mCenterX + outerX, mCenterY + outerY, mTickAndCirclePaint); 645 } 646 647 /* 648 * These calculations reflect the rotation in degrees per unit of time, e.g., 649 * 360 / 60 = 6 and 360 / 12 = 30. 650 */ 651 final float seconds = 652 (mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f); 653 final float secondsRotation = seconds * 6f; 654 655 final float minutesRotation = mCalendar.get(Calendar.MINUTE) * 6f; 656 657 final float hourHandOffset = mCalendar.get(Calendar.MINUTE) / 2f; 658 final float hoursRotation = (mCalendar.get(Calendar.HOUR) * 30) + hourHandOffset; 659 660 /* 661 * Save the canvas state before we can begin to rotate it. 662 */ 663 canvas.save(); 664 665 canvas.rotate(hoursRotation, mCenterX, mCenterY); 666 canvas.drawLine( 667 mCenterX, 668 mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS, 669 mCenterX, 670 mCenterY - mHourHandLength, 671 mHourPaint); 672 673 canvas.rotate(minutesRotation - hoursRotation, mCenterX, mCenterY); 674 canvas.drawLine( 675 mCenterX, 676 mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS, 677 mCenterX, 678 mCenterY - mMinuteHandLength, 679 mMinutePaint); 680 681 /* 682 * Ensure the "seconds" hand is drawn only when we are in interactive mode. 683 * Otherwise, we only update the watch face once a minute. 684 */ 685 if (!mAmbient) { 686 canvas.rotate(secondsRotation - minutesRotation, mCenterX, mCenterY); 687 canvas.drawLine( 688 mCenterX, 689 mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS, 690 mCenterX, 691 mCenterY - mSecondHandLength, 692 mSecondPaint); 693 694 } 695 canvas.drawCircle( 696 mCenterX, 697 mCenterY, 698 CENTER_GAP_AND_CIRCLE_RADIUS, 699 mTickAndCirclePaint); 700 701 /* Restore the canvas' original orientation. */ 702 canvas.restore(); 703 704 /* Draw rectangle behind peek card in ambient mode to improve readability. */ 705 if (mAmbient) { 706 canvas.drawRect(mPeekCardBounds, mBackgroundPaint); 707 } 708 } 709 710 @Override 711 public void onVisibilityChanged(boolean visible) { 712 super.onVisibilityChanged(visible); 713 714 if (visible) { 715 registerReceiver(); 716 // Update time zone in case it changed while we weren't visible. 717 mCalendar.setTimeZone(TimeZone.getDefault()); 718 invalidate(); 719 } else { 720 unregisterReceiver(); 721 } 722 723 /* Check and trigger whether or not timer should be running (only in active mode). */ 724 updateTimer(); 725 } 726 727 @Override 728 public void onPeekCardPositionUpdate(Rect rect) { 729 super.onPeekCardPositionUpdate(rect); 730 mPeekCardBounds.set(rect); 731 } 732 733 private void registerReceiver() { 734 if (mRegisteredTimeZoneReceiver) { 735 return; 736 } 737 mRegisteredTimeZoneReceiver = true; 738 IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); 739 ComplicationSimpleWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter); 740 } 741 742 private void unregisterReceiver() { 743 if (!mRegisteredTimeZoneReceiver) { 744 return; 745 } 746 mRegisteredTimeZoneReceiver = false; 747 ComplicationSimpleWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver); 748 } 749 750 /** 751 * Starts/stops the {@link #mUpdateTimeHandler} timer based on the state of the watch face. 752 */ 753 private void updateTimer() { 754 if (Log.isLoggable(TAG, Log.DEBUG)) { 755 Log.d(TAG, "updateTimer"); 756 } 757 mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); 758 if (shouldTimerBeRunning()) { 759 mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME); 760 } 761 } 762 763 /** 764 * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer 765 * should only run in active mode. 766 */ 767 private boolean shouldTimerBeRunning() { 768 return isVisible() && !mAmbient; 769 } 770 } 771 }