Home | History | Annotate | Download | only in com.example.android.wearable.watchface
      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