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