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 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 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 157 public void onDestroy() { 158 mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); 159 super.onDestroy(); 160 } 161 162 @Override 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 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 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 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 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 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 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 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 */ 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 */ 329 private boolean shouldTimerBeRunning() { 330 return isVisible() && !isInAmbientMode(); 331 } 332 333 } 334 } 335