• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.res.Resources;
24 import android.graphics.Bitmap;
25 import android.graphics.Canvas;
26 import android.graphics.Paint;
27 import android.graphics.Rect;
28 import android.graphics.drawable.BitmapDrawable;
29 import android.graphics.drawable.Drawable;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Message;
33 import android.support.wearable.watchface.CanvasWatchFaceService;
34 import android.support.wearable.watchface.WatchFaceService;
35 import android.support.wearable.watchface.WatchFaceStyle;
36 import android.text.format.Time;
37 import android.util.Log;
38 import android.view.SurfaceHolder;
39 
40 import java.util.TimeZone;
41 import java.util.concurrent.TimeUnit;
42 
43 /**
44  * Sample analog watch face with a ticking second hand. In ambient mode, the second hand isn't
45  * shown. On devices with low-bit ambient mode, the hands are drawn without anti-aliasing in ambient
46  * mode. The watch face is drawn with less contrast in mute mode.
47  *
48  * {@link SweepWatchFaceService} is similar but has a sweep second hand.
49  */
50 public class AnalogWatchFaceService extends CanvasWatchFaceService {
51     private static final String TAG = "AnalogWatchFaceService";
52 
53     /**
54      * Update rate in milliseconds for interactive mode. We update once a second to advance the
55      * second hand.
56      */
57     private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
58 
59     @Override
onCreateEngine()60     public Engine onCreateEngine() {
61         return new Engine();
62     }
63 
64     private class Engine extends CanvasWatchFaceService.Engine {
65         static final int MSG_UPDATE_TIME = 0;
66 
67         Paint mHourPaint;
68         Paint mMinutePaint;
69         Paint mSecondPaint;
70         Paint mTickPaint;
71         boolean mMute;
72         Time mTime;
73 
74         /** Handler to update the time once a second in interactive mode. */
75         final Handler mUpdateTimeHandler = new Handler() {
76             @Override
77             public void handleMessage(Message message) {
78                 switch (message.what) {
79                     case MSG_UPDATE_TIME:
80                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
81                             Log.v(TAG, "updating time");
82                         }
83                         invalidate();
84                         if (shouldTimerBeRunning()) {
85                             long timeMs = System.currentTimeMillis();
86                             long delayMs = INTERACTIVE_UPDATE_RATE_MS
87                                     - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
88                             mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
89                         }
90                         break;
91                 }
92             }
93         };
94 
95         final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
96             @Override
97             public void onReceive(Context context, Intent intent) {
98                 mTime.clear(intent.getStringExtra("time-zone"));
99                 mTime.setToNow();
100             }
101         };
102         boolean mRegisteredTimeZoneReceiver = false;
103 
104         /**
105          * Whether the display supports fewer bits for each color in ambient mode. When true, we
106          * disable anti-aliasing in ambient mode.
107          */
108         boolean mLowBitAmbient;
109 
110         Bitmap mBackgroundBitmap;
111         Bitmap mBackgroundScaledBitmap;
112 
113         @Override
onCreate(SurfaceHolder holder)114         public void onCreate(SurfaceHolder holder) {
115             if (Log.isLoggable(TAG, Log.DEBUG)) {
116                 Log.d(TAG, "onCreate");
117             }
118             super.onCreate(holder);
119 
120             setWatchFaceStyle(new WatchFaceStyle.Builder(AnalogWatchFaceService.this)
121                     .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
122                     .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
123                     .setShowSystemUiTime(false)
124                     .build());
125 
126             Resources resources = AnalogWatchFaceService.this.getResources();
127             Drawable backgroundDrawable = resources.getDrawable(R.drawable.bg);
128             mBackgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();
129 
130             mHourPaint = new Paint();
131             mHourPaint.setARGB(255, 200, 200, 200);
132             mHourPaint.setStrokeWidth(5.f);
133             mHourPaint.setAntiAlias(true);
134             mHourPaint.setStrokeCap(Paint.Cap.ROUND);
135 
136             mMinutePaint = new Paint();
137             mMinutePaint.setARGB(255, 200, 200, 200);
138             mMinutePaint.setStrokeWidth(3.f);
139             mMinutePaint.setAntiAlias(true);
140             mMinutePaint.setStrokeCap(Paint.Cap.ROUND);
141 
142             mSecondPaint = new Paint();
143             mSecondPaint.setARGB(255, 255, 0, 0);
144             mSecondPaint.setStrokeWidth(2.f);
145             mSecondPaint.setAntiAlias(true);
146             mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
147 
148             mTickPaint = new Paint();
149             mTickPaint.setARGB(100, 255, 255, 255);
150             mTickPaint.setStrokeWidth(2.f);
151             mTickPaint.setAntiAlias(true);
152 
153             mTime = new Time();
154         }
155 
156         @Override
onDestroy()157         public void onDestroy() {
158             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
159             super.onDestroy();
160         }
161 
162         @Override
onPropertiesChanged(Bundle properties)163         public void onPropertiesChanged(Bundle properties) {
164             super.onPropertiesChanged(properties);
165             mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
166             if (Log.isLoggable(TAG, Log.DEBUG)) {
167                 Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient);
168             }
169         }
170 
171         @Override
onTimeTick()172         public void onTimeTick() {
173             super.onTimeTick();
174             if (Log.isLoggable(TAG, Log.DEBUG)) {
175                 Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
176             }
177             invalidate();
178         }
179 
180         @Override
onAmbientModeChanged(boolean inAmbientMode)181         public void onAmbientModeChanged(boolean inAmbientMode) {
182             super.onAmbientModeChanged(inAmbientMode);
183             if (Log.isLoggable(TAG, Log.DEBUG)) {
184                 Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
185             }
186             if (mLowBitAmbient) {
187                 boolean antiAlias = !inAmbientMode;
188                 mHourPaint.setAntiAlias(antiAlias);
189                 mMinutePaint.setAntiAlias(antiAlias);
190                 mSecondPaint.setAntiAlias(antiAlias);
191                 mTickPaint.setAntiAlias(antiAlias);
192             }
193             invalidate();
194 
195             // Whether the timer should be running depends on whether we're in ambient mode (as well
196             // as whether we're visible), so we may need to start or stop the timer.
197             updateTimer();
198         }
199 
200         @Override
onInterruptionFilterChanged(int interruptionFilter)201         public void onInterruptionFilterChanged(int interruptionFilter) {
202             super.onInterruptionFilterChanged(interruptionFilter);
203             boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
204             if (mMute != inMuteMode) {
205                 mMute = inMuteMode;
206                 mHourPaint.setAlpha(inMuteMode ? 100 : 255);
207                 mMinutePaint.setAlpha(inMuteMode ? 100 : 255);
208                 mSecondPaint.setAlpha(inMuteMode ? 80 : 255);
209                 invalidate();
210             }
211         }
212 
213         @Override
onDraw(Canvas canvas, Rect bounds)214         public void onDraw(Canvas canvas, Rect bounds) {
215             mTime.setToNow();
216 
217             int width = bounds.width();
218             int height = bounds.height();
219 
220             // Draw the background, scaled to fit.
221             if (mBackgroundScaledBitmap == null
222                     || mBackgroundScaledBitmap.getWidth() != width
223                     || mBackgroundScaledBitmap.getHeight() != height) {
224                 mBackgroundScaledBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
225                         width, height, true /* filter */);
226             }
227             canvas.drawBitmap(mBackgroundScaledBitmap, 0, 0, null);
228 
229             // Find the center. Ignore the window insets so that, on round watches with a
230             // "chin", the watch face is centered on the entire screen, not just the usable
231             // portion.
232             float centerX = width / 2f;
233             float centerY = height / 2f;
234 
235             // Draw the ticks.
236             float innerTickRadius = centerX - 10;
237             float outerTickRadius = centerX;
238             for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
239                 float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
240                 float innerX = (float) Math.sin(tickRot) * innerTickRadius;
241                 float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
242                 float outerX = (float) Math.sin(tickRot) * outerTickRadius;
243                 float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
244                 canvas.drawLine(centerX + innerX, centerY + innerY,
245                         centerX + outerX, centerY + outerY, mTickPaint);
246             }
247 
248             float secRot = mTime.second / 30f * (float) Math.PI;
249             int minutes = mTime.minute;
250             float minRot = minutes / 30f * (float) Math.PI;
251             float hrRot = ((mTime.hour + (minutes / 60f)) / 6f ) * (float) Math.PI;
252 
253             float secLength = centerX - 20;
254             float minLength = centerX - 40;
255             float hrLength = centerX - 80;
256 
257             if (!isInAmbientMode()) {
258                 float secX = (float) Math.sin(secRot) * secLength;
259                 float secY = (float) -Math.cos(secRot) * secLength;
260                 canvas.drawLine(centerX, centerY, centerX + secX, centerY + secY, mSecondPaint);
261             }
262 
263             float minX = (float) Math.sin(minRot) * minLength;
264             float minY = (float) -Math.cos(minRot) * minLength;
265             canvas.drawLine(centerX, centerY, centerX + minX, centerY + minY, mMinutePaint);
266 
267             float hrX = (float) Math.sin(hrRot) * hrLength;
268             float hrY = (float) -Math.cos(hrRot) * hrLength;
269             canvas.drawLine(centerX, centerY, centerX + hrX, centerY + hrY, mHourPaint);
270         }
271 
272         @Override
onVisibilityChanged(boolean visible)273         public void onVisibilityChanged(boolean visible) {
274             super.onVisibilityChanged(visible);
275             if (Log.isLoggable(TAG, Log.DEBUG)) {
276                 Log.d(TAG, "onVisibilityChanged: " + visible);
277             }
278 
279             if (visible) {
280                 registerReceiver();
281 
282                 // Update time zone in case it changed while we weren't visible.
283                 mTime.clear(TimeZone.getDefault().getID());
284                 mTime.setToNow();
285             } else {
286                 unregisterReceiver();
287             }
288 
289             // Whether the timer should be running depends on whether we're visible (as well as
290             // whether we're in ambient mode), so we may need to start or stop the timer.
291             updateTimer();
292         }
293 
registerReceiver()294         private void registerReceiver() {
295             if (mRegisteredTimeZoneReceiver) {
296                 return;
297             }
298             mRegisteredTimeZoneReceiver = true;
299             IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
300             AnalogWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
301         }
302 
unregisterReceiver()303         private void unregisterReceiver() {
304             if (!mRegisteredTimeZoneReceiver) {
305                 return;
306             }
307             mRegisteredTimeZoneReceiver = false;
308             AnalogWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
309         }
310 
311         /**
312          * Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
313          * or stops it if it shouldn't be running but currently is.
314          */
updateTimer()315         private void updateTimer() {
316             if (Log.isLoggable(TAG, Log.DEBUG)) {
317                 Log.d(TAG, "updateTimer");
318             }
319             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
320             if (shouldTimerBeRunning()) {
321                 mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
322             }
323         }
324 
325         /**
326          * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
327          * only run when we're visible and in interactive mode.
328          */
shouldTimerBeRunning()329         private boolean shouldTimerBeRunning() {
330             return isVisible() && !isInAmbientMode();
331         }
332 
333     }
334 }
335