| /* |
| * Copyright (C) 2014 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.example.android.wearable.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.support.v7.graphics.Palette; |
| import android.support.wearable.watchface.CanvasWatchFaceService; |
| import android.support.wearable.watchface.WatchFaceService; |
| import android.support.wearable.watchface.WatchFaceStyle; |
| import android.util.Log; |
| import android.view.SurfaceHolder; |
| |
| import java.util.Calendar; |
| import java.util.TimeZone; |
| |
| /** |
| * Sample analog watch face with a sweep 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. |
| * The watch face is drawn with less contrast in mute mode. |
| * |
| * {@link AnalogWatchFaceService} is similar but has a ticking second hand. |
| */ |
| public class SweepWatchFaceService extends CanvasWatchFaceService { |
| |
| private static final String TAG = "SweepWatchFaceService"; |
| |
| @Override |
| public Engine onCreateEngine() { |
| return new Engine(); |
| } |
| |
| private class Engine extends CanvasWatchFaceService.Engine { |
| |
| private static final float HOUR_STROKE_WIDTH = 5f; |
| private static final float MINUTE_STROKE_WIDTH = 3f; |
| private static final float SECOND_TICK_STROKE_WIDTH = 2f; |
| |
| private static final float CENTER_GAP_AND_CIRCLE_RADIUS = 4f; |
| |
| private static final int SHADOW_RADIUS = 6; |
| |
| private Calendar mCalendar; |
| private boolean mRegisteredTimeZoneReceiver = false; |
| private boolean mMuteMode; |
| |
| private float mCenterX; |
| private float mCenterY; |
| |
| private float mSecondHandLength; |
| private float mMinuteHandLength; |
| private float mHourHandLength; |
| |
| /* Colors for all hands (hour, minute, seconds, ticks) based on photo loaded. */ |
| private int mWatchHandColor; |
| private int mWatchHandHightlightColor; |
| private int mWatchHandShadowColor; |
| |
| private Paint mHourPaint; |
| private Paint mMinutePaint; |
| private Paint mSecondPaint; |
| private Paint mTickAndCirclePaint; |
| |
| private Paint mBackgroundPaint; |
| private Bitmap mBackgroundBitmap; |
| private Bitmap mGrayBackgroundBitmap; |
| |
| private boolean mAmbient; |
| private boolean mLowBitAmbient; |
| private boolean mBurnInProtection; |
| |
| private Rect mPeekCardBounds = new Rect(); |
| |
| private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| mCalendar.setTimeZone(TimeZone.getDefault()); |
| invalidate(); |
| } |
| }; |
| |
| @Override |
| public void onCreate(SurfaceHolder holder) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "onCreate"); |
| } |
| super.onCreate(holder); |
| |
| setWatchFaceStyle(new WatchFaceStyle.Builder(SweepWatchFaceService.this) |
| .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT) |
| .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) |
| .setShowSystemUiTime(false) |
| .build()); |
| |
| mBackgroundPaint = new Paint(); |
| mBackgroundPaint.setColor(Color.BLACK); |
| mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg); |
| |
| /* Set defaults for colors */ |
| mWatchHandColor = Color.WHITE; |
| mWatchHandHightlightColor = Color.RED; |
| mWatchHandShadowColor = Color.BLACK; |
| |
| mHourPaint = new Paint(); |
| mHourPaint.setColor(mWatchHandColor); |
| mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH); |
| mHourPaint.setAntiAlias(true); |
| mHourPaint.setStrokeCap(Paint.Cap.ROUND); |
| mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); |
| |
| mMinutePaint = new Paint(); |
| mMinutePaint.setColor(mWatchHandColor); |
| mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH); |
| mMinutePaint.setAntiAlias(true); |
| mMinutePaint.setStrokeCap(Paint.Cap.ROUND); |
| mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); |
| |
| mSecondPaint = new Paint(); |
| mSecondPaint.setColor(mWatchHandHightlightColor); |
| mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH); |
| mSecondPaint.setAntiAlias(true); |
| mSecondPaint.setStrokeCap(Paint.Cap.ROUND); |
| mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); |
| |
| mTickAndCirclePaint = new Paint(); |
| mTickAndCirclePaint.setColor(mWatchHandColor); |
| mTickAndCirclePaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH); |
| mTickAndCirclePaint.setAntiAlias(true); |
| mTickAndCirclePaint.setStyle(Paint.Style.STROKE); |
| mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); |
| |
| /* Extract colors from background image to improve watchface style. */ |
| Palette.generateAsync( |
| mBackgroundBitmap, |
| new Palette.PaletteAsyncListener() { |
| @Override |
| public void onGenerated(Palette palette) { |
| if (palette != null) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Palette: " + palette); |
| } |
| |
| mWatchHandHightlightColor = palette.getVibrantColor(Color.RED); |
| mWatchHandColor = palette.getLightVibrantColor(Color.WHITE); |
| mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK); |
| updateWatchHandStyle(); |
| } |
| } |
| }); |
| |
| mCalendar = Calendar.getInstance(); |
| } |
| |
| @Override |
| public void onPropertiesChanged(Bundle properties) { |
| super.onPropertiesChanged(properties); |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient); |
| } |
| |
| 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 (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode); |
| } |
| mAmbient = inAmbientMode; |
| |
| updateWatchHandStyle(); |
| |
| invalidate(); |
| } |
| |
| private void updateWatchHandStyle(){ |
| if (mAmbient){ |
| mHourPaint.setColor(Color.WHITE); |
| mMinutePaint.setColor(Color.WHITE); |
| mSecondPaint.setColor(Color.WHITE); |
| mTickAndCirclePaint.setColor(Color.WHITE); |
| |
| mHourPaint.setAntiAlias(false); |
| mMinutePaint.setAntiAlias(false); |
| mSecondPaint.setAntiAlias(false); |
| mTickAndCirclePaint.setAntiAlias(false); |
| |
| mHourPaint.clearShadowLayer(); |
| mMinutePaint.clearShadowLayer(); |
| mSecondPaint.clearShadowLayer(); |
| mTickAndCirclePaint.clearShadowLayer(); |
| |
| } else { |
| mHourPaint.setColor(mWatchHandColor); |
| mMinutePaint.setColor(mWatchHandColor); |
| mSecondPaint.setColor(mWatchHandHightlightColor); |
| mTickAndCirclePaint.setColor(mWatchHandColor); |
| |
| mHourPaint.setAntiAlias(true); |
| mMinutePaint.setAntiAlias(true); |
| mSecondPaint.setAntiAlias(true); |
| mTickAndCirclePaint.setAntiAlias(true); |
| |
| mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); |
| mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); |
| mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); |
| mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor); |
| } |
| } |
| |
| @Override |
| public void onInterruptionFilterChanged(int interruptionFilter) { |
| super.onInterruptionFilterChanged(interruptionFilter); |
| boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE); |
| |
| /* Dim display in mute mode. */ |
| if (mMuteMode != inMuteMode) { |
| mMuteMode = inMuteMode; |
| mHourPaint.setAlpha(inMuteMode ? 100 : 255); |
| mMinutePaint.setAlpha(inMuteMode ? 100 : 255); |
| mSecondPaint.setAlpha(inMuteMode ? 80 : 255); |
| invalidate(); |
| } |
| } |
| |
| @Override |
| public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { |
| super.onSurfaceChanged(holder, format, width, height); |
| |
| /* |
| * Find the coordinates of the center point on the screen, and 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 = width / 2f; |
| mCenterY = height / 2f; |
| |
| /* |
| * Calculate lengths of different hands based on watch screen size. |
| */ |
| mSecondHandLength = (float) (mCenterX * 0.875); |
| mMinuteHandLength = (float) (mCenterX * 0.75); |
| mHourHandLength = (float) (mCenterX * 0.5); |
| |
| |
| /* Scale loaded background image (more efficient) if surface dimensions change. */ |
| float scale = ((float) width) / (float) mBackgroundBitmap.getWidth(); |
| |
| mBackgroundBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap, |
| (int) (mBackgroundBitmap.getWidth() * scale), |
| (int) (mBackgroundBitmap.getHeight() * scale), true); |
| |
| /* |
| * Create a gray version of the image only if it will look nice on the device in |
| * ambient mode. That means we don't want devices that support burn-in |
| * protection (slight movements in pixels, not great for images going all the way to |
| * edges) and low ambient mode (degrades image quality). |
| * |
| * Also, if your watch face will know about all images ahead of time (users aren't |
| * selecting their own photos for the watch face), it will be more |
| * efficient to create a black/white version (png, etc.) and load that when you need it. |
| */ |
| 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) { |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| Log.v(TAG, "onDraw"); |
| } |
| 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); |
| } |
| |
| /* |
| * Draw ticks. Usually you will want to bake this directly into the photo, but in |
| * cases where you want to allow users to select their own photos, this dynamically |
| * creates them on top of the photo. |
| */ |
| float innerTickRadius = mCenterX - 10; |
| float outerTickRadius = mCenterX; |
| for (int tickIndex = 0; tickIndex < 12; tickIndex++) { |
| float tickRot = (float) (tickIndex * Math.PI * 2 / 12); |
| float innerX = (float) Math.sin(tickRot) * innerTickRadius; |
| float innerY = (float) -Math.cos(tickRot) * innerTickRadius; |
| float outerX = (float) Math.sin(tickRot) * outerTickRadius; |
| float outerY = (float) -Math.cos(tickRot) * outerTickRadius; |
| canvas.drawLine(mCenterX + innerX, mCenterY + innerY, |
| mCenterX + outerX, mCenterY + outerY, mTickAndCirclePaint); |
| } |
| |
| /* |
| * 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 can begin to rotate it. |
| */ |
| canvas.save(); |
| |
| canvas.rotate(hoursRotation, mCenterX, mCenterY); |
| canvas.drawLine( |
| mCenterX, |
| mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS, |
| mCenterX, |
| mCenterY - mHourHandLength, |
| mHourPaint); |
| |
| canvas.rotate(minutesRotation - hoursRotation, mCenterX, mCenterY); |
| canvas.drawLine( |
| mCenterX, |
| mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS, |
| mCenterX, |
| mCenterY - mMinuteHandLength, |
| mMinutePaint); |
| |
| /* |
| * Ensure 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 - CENTER_GAP_AND_CIRCLE_RADIUS, |
| mCenterX, |
| mCenterY - mSecondHandLength, |
| mSecondPaint); |
| |
| } |
| canvas.drawCircle( |
| mCenterX, |
| mCenterY, |
| CENTER_GAP_AND_CIRCLE_RADIUS, |
| mTickAndCirclePaint); |
| |
| /* Restore the canvas' original orientation. */ |
| canvas.restore(); |
| |
| /* Draw rectangle behind peek card in ambient mode to improve readability. */ |
| if (mAmbient) { |
| canvas.drawRect(mPeekCardBounds, mBackgroundPaint); |
| } |
| |
| /* Draw every frame as long as we're visible and in interactive mode. */ |
| if ((isVisible()) && (!mAmbient)) { |
| invalidate(); |
| } |
| } |
| |
| @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(); |
| } |
| } |
| |
| @Override |
| public void onPeekCardPositionUpdate(Rect rect) { |
| super.onPeekCardPositionUpdate(rect); |
| mPeekCardBounds.set(rect); |
| } |
| |
| private void registerReceiver() { |
| if (mRegisteredTimeZoneReceiver) { |
| return; |
| } |
| mRegisteredTimeZoneReceiver = true; |
| IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); |
| SweepWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter); |
| } |
| |
| private void unregisterReceiver() { |
| if (!mRegisteredTimeZoneReceiver) { |
| return; |
| } |
| mRegisteredTimeZoneReceiver = false; |
| SweepWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver); |
| } |
| } |
| } |