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.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
     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
    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
    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
    191         public void onTimeTick() {
    192             super.onTimeTick();
    193             invalidate();
    194         }
    195 
    196         @Override
    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 
    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
    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
    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 
    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
    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
    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
    431         public void onPeekCardPositionUpdate(Rect rect) {
    432             super.onPeekCardPositionUpdate(rect);
    433             mPeekCardBounds.set(rect);
    434         }
    435 
    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 
    445         private void unregisterReceiver() {
    446             if (!mRegisteredTimeZoneReceiver) {
    447                 return;
    448             }
    449             mRegisteredTimeZoneReceiver = false;
    450             SweepWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
    451         }
    452     }
    453 }
    454