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