/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.example.watchface; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v7.graphics.Palette; import android.support.wearable.watchface.CanvasWatchFaceService; import android.support.wearable.watchface.WatchFaceStyle; import android.util.Log; import android.view.SurfaceHolder; import java.util.Calendar; import java.util.TimeZone; import java.util.concurrent.TimeUnit; /** * Analog watch face with a ticking second hand. In ambient mode, the second hand isn't shown. On * devices with low-bit ambient mode, the hands are drawn without anti-aliasing in ambient mode. */ public class MyWatchFaceService extends CanvasWatchFaceService { /** * Update rate in milliseconds for interactive mode. We update once a second to advance the * second hand. */ private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1); @Override public Engine onCreateEngine() { return new Engine(); } private class Engine extends CanvasWatchFaceService.Engine { /* Handler to update the time once a second in interactive mode. */ private final Handler mUpdateTimeHandler = new Handler() { @Override public void handleMessage(Message message) { if (R.id.message_update == message.what) { invalidate(); if (shouldTimerBeRunning()) { long timeMs = System.currentTimeMillis(); long delayMs = INTERACTIVE_UPDATE_RATE_MS - (timeMs % INTERACTIVE_UPDATE_RATE_MS); mUpdateTimeHandler.sendEmptyMessageDelayed(R.id.message_update, delayMs); } } } }; private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mCalendar.setTimeZone(TimeZone.getDefault()); invalidate(); } }; private boolean mRegisteredTimeZoneReceiver = false; // Feel free to change these values and see what happens to the watch face. private static final float HAND_END_CAP_RADIUS = 4f; private static final float STROKE_WIDTH = 4f; private static final int SHADOW_RADIUS = 6; private Calendar mCalendar; private Paint mBackgroundPaint; private Paint mHandPaint; private boolean mAmbient; private Bitmap mBackgroundBitmap; private Bitmap mGrayBackgroundBitmap; private int mWatchHandColor; private int mWatchHandShadowColor; private float mHourHandLength; private float mMinuteHandLength; private float mSecondHandLength; /** * Whether the display supports fewer bits for each color in ambient mode. * When true, we disable anti-aliasing in ambient mode. */ private boolean mLowBitAmbient; /** * Whether the display supports burn in protection in ambient mode. * When true, remove the background in ambient mode. */ private boolean mBurnInProtection; private int mWidth; private int mHeight; private float mCenterX; private float mCenterY; private float mScale = 1; @Override public void onCreate(SurfaceHolder holder) { super.onCreate(holder); setWatchFaceStyle(new WatchFaceStyle.Builder(MyWatchFaceService.this).build()); mBackgroundPaint = new Paint(); mBackgroundPaint.setColor(Color.BLACK); /* * Toggle the backgroundResIds to see * the change of colors due to palette doing its magic. */ final int backgroundResId = R.drawable.custom_background; //final int backgroundResId = R.drawable.custom_background2; mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), backgroundResId); mHandPaint = new Paint(); mHandPaint.setColor(Color.WHITE); mHandPaint.setStrokeWidth(STROKE_WIDTH); mHandPaint.setAntiAlias(true); mHandPaint.setStrokeCap(Paint.Cap.ROUND); mHandPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, Color.BLACK); mHandPaint.setStyle(Paint.Style.STROKE); // Asynchronous call to generate Palette Palette.from(mBackgroundBitmap).generate( new Palette.PaletteAsyncListener() { public void onGenerated(Palette palette) { /* * Sometimes, palette is unable to generate a color palette * so we need to check that we have one. */ if (palette != null) { Log.d("onGenerated", palette.toString()); mWatchHandColor = palette.getVibrantColor(Color.WHITE); mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK); setWatchHandColor(); } } }); mCalendar = Calendar.getInstance(); } private void setWatchHandColor() { if (mAmbient) { mHandPaint.setColor(Color.WHITE); mHandPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, Color.BLACK); } else { mHandPaint.setColor(mWatchHandColor); mHandPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); } } @Override public void onDestroy() { mUpdateTimeHandler.removeMessages(R.id.message_update); super.onDestroy(); } @Override public void onPropertiesChanged(Bundle properties) { super.onPropertiesChanged(properties); mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false); mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false); } @Override public void onTimeTick() { super.onTimeTick(); invalidate(); } @Override public void onAmbientModeChanged(boolean inAmbientMode) { super.onAmbientModeChanged(inAmbientMode); if (mAmbient != inAmbientMode) { mAmbient = inAmbientMode; if (mLowBitAmbient || mBurnInProtection) { mHandPaint.setAntiAlias(!inAmbientMode); } setWatchHandColor(); invalidate(); } /* * Whether the timer should be running depends on whether we're visible (as well as * whether we're in ambient mode), so we may need to start or stop the timer. */ updateTimer(); } @Override public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { super.onSurfaceChanged(holder, format, width, height); mWidth = width; mHeight = height; /* * Find the coordinates of the center point on the screen. * Ignore the window insets so that, on round watches * with a "chin", the watch face is centered on the entire screen, * not just the usable portion. */ mCenterX = mWidth / 2f; mCenterY = mHeight / 2f; mScale = ((float) width) / (float) mBackgroundBitmap.getWidth(); /* * Calculate the lengths of the watch hands and store them in member variables. */ mHourHandLength = mCenterX * 0.5f; mMinuteHandLength = mCenterX * 0.7f; mSecondHandLength = mCenterX * 0.9f; mBackgroundBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap, (int) (mBackgroundBitmap.getWidth() * mScale), (int) (mBackgroundBitmap.getHeight() * mScale), true); if (!mBurnInProtection || !mLowBitAmbient) { initGrayBackgroundBitmap(); } } private void initGrayBackgroundBitmap() { mGrayBackgroundBitmap = Bitmap.createBitmap(mBackgroundBitmap.getWidth(), mBackgroundBitmap.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mGrayBackgroundBitmap); Paint grayPaint = new Paint(); ColorMatrix colorMatrix = new ColorMatrix(); colorMatrix.setSaturation(0); ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix); grayPaint.setColorFilter(filter); canvas.drawBitmap(mBackgroundBitmap, 0, 0, grayPaint); } @Override public void onDraw(Canvas canvas, Rect bounds) { long now = System.currentTimeMillis(); mCalendar.setTimeInMillis(now); if (mAmbient && (mLowBitAmbient || mBurnInProtection)) { canvas.drawColor(Color.BLACK); } else if (mAmbient) { canvas.drawBitmap(mGrayBackgroundBitmap, 0, 0, mBackgroundPaint); } else { canvas.drawBitmap(mBackgroundBitmap, 0, 0, mBackgroundPaint); } /* * These calculations reflect the rotation in degrees per unit of time, e.g., * 360 / 60 = 6 and 360 / 12 = 30. */ final float seconds = (mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f); final float secondsRotation = seconds * 6f; final float minutesRotation = mCalendar.get(Calendar.MINUTE) * 6f; final float hourHandOffset = mCalendar.get(Calendar.MINUTE) / 2f; final float hoursRotation = (mCalendar.get(Calendar.HOUR) * 30) + hourHandOffset; // save the canvas state before we begin to rotate it canvas.save(); canvas.rotate(hoursRotation, mCenterX, mCenterY); drawHand(canvas, mHourHandLength); canvas.rotate(minutesRotation - hoursRotation, mCenterX, mCenterY); drawHand(canvas, mMinuteHandLength); /* * Make sure the "seconds" hand is drawn only when we are in interactive mode. * Otherwise we only update the watch face once a minute. */ if (!mAmbient) { canvas.rotate(secondsRotation - minutesRotation, mCenterX, mCenterY); canvas.drawLine(mCenterX, mCenterY - HAND_END_CAP_RADIUS, mCenterX, mCenterY - mSecondHandLength, mHandPaint); } canvas.drawCircle(mCenterX, mCenterY, HAND_END_CAP_RADIUS, mHandPaint); // restore the canvas' original orientation. canvas.restore(); } private void drawHand(Canvas canvas, float handLength) { canvas.drawRoundRect(mCenterX - HAND_END_CAP_RADIUS, mCenterY - handLength, mCenterX + HAND_END_CAP_RADIUS, mCenterY + HAND_END_CAP_RADIUS, HAND_END_CAP_RADIUS, HAND_END_CAP_RADIUS, mHandPaint); } @Override public void onVisibilityChanged(boolean visible) { super.onVisibilityChanged(visible); if (visible) { registerReceiver(); // Update time zone in case it changed while we weren't visible. mCalendar.setTimeZone(TimeZone.getDefault()); invalidate(); } else { unregisterReceiver(); } /* * Whether the timer should be running depends on whether we're visible * (as well as whether we're in ambient mode), * so we may need to start or stop the timer. */ updateTimer(); } private void registerReceiver() { if (mRegisteredTimeZoneReceiver) { return; } mRegisteredTimeZoneReceiver = true; IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); MyWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter); } private void unregisterReceiver() { if (!mRegisteredTimeZoneReceiver) { return; } mRegisteredTimeZoneReceiver = false; MyWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver); } private void updateTimer() { mUpdateTimeHandler.removeMessages(R.id.message_update); if (shouldTimerBeRunning()) { mUpdateTimeHandler.sendEmptyMessage(R.id.message_update); } } /** * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer * should only run when we're visible and in interactive mode. */ private boolean shouldTimerBeRunning() { return isVisible() && !isInAmbientMode(); } } }