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