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