From 351c8be73ed3a39cc8c8e3e4470ca2ec3b8586c6 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Thu, 12 May 2016 20:24:07 -0700 Subject: Adds simple complication sample from I/O code lab (w/o use of Oli's library) for public release after code lab. Change-Id: I19ec97231800d362701b2f45bd0a0d7f39770875 --- .../Wearable/src/main/AndroidManifest.xml | 65 ++ .../ComplicationSimpleConfigActivity.java | 191 ++++++ .../ComplicationSimpleWatchFaceService.java | 744 +++++++++++++++++++++ .../provider/RandomNumberProviderService.java | 96 +++ .../drawable-hdpi/preview_complication_simple.png | Bin 0 -> 471002 bytes .../res/drawable-xhdpi/complications_left_dial.png | Bin 0 -> 6692 bytes .../drawable-xhdpi/complications_right_dial.png | Bin 0 -> 6704 bytes .../layout/activity_complication_simple_config.xml | 38 ++ .../activity_complication_simple_list_item.xml | 34 + .../Wearable/src/main/res/values/arrays.xml | 26 + .../Wearable/src/main/res/values/strings.xml | 4 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../screenshots/complication_simple_face.png | Bin 0 -> 472204 bytes wearable/wear/WatchFace/template-params.xml | 6 +- 14 files changed, 1204 insertions(+), 2 deletions(-) create mode 100644 wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/ComplicationSimpleConfigActivity.java create mode 100644 wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/ComplicationSimpleWatchFaceService.java create mode 100644 wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/provider/RandomNumberProviderService.java create mode 100644 wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_complication_simple.png create mode 100644 wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/complications_left_dial.png create mode 100644 wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/complications_right_dial.png create mode 100644 wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_complication_simple_config.xml create mode 100644 wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_complication_simple_list_item.xml create mode 100644 wearable/wear/WatchFace/Wearable/src/main/res/values/arrays.xml create mode 100644 wearable/wear/WatchFace/screenshots/complication_simple_face.png diff --git a/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml b/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml index 88bb0336..a9222971 100644 --- a/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml @@ -132,6 +132,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/ComplicationSimpleConfigActivity.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/ComplicationSimpleConfigActivity.java new file mode 100644 index 00000000..66e208c2 --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/ComplicationSimpleConfigActivity.java @@ -0,0 +1,191 @@ +/* + * 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.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.wearable.complications.ProviderChooserIntent; +import android.support.wearable.view.WearableListView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +/** + * The watch-side config activity for {@link ComplicationSimpleWatchFaceService}, which + * allows for setting complications on the left and right of watch face. + */ +public class ComplicationSimpleConfigActivity extends Activity implements + WearableListView.ClickListener { + + private static final String TAG = "CompSimpleConfig"; + + private static final int PROVIDER_CHOOSER_REQUEST_CODE = 1; + + private WearableListView mWearableConfigListView; + private ConfigurationAdapter mAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_complication_simple_config); + + mAdapter = new ConfigurationAdapter(getApplicationContext(), getComplicationItems()); + + mWearableConfigListView = (WearableListView) findViewById(R.id.wearable_list); + mWearableConfigListView.setAdapter(mAdapter); + mWearableConfigListView.setClickListener(this); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == PROVIDER_CHOOSER_REQUEST_CODE + && resultCode == RESULT_OK) { + finish(); + } + } + + @Override + public void onClick(WearableListView.ViewHolder viewHolder) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onClick()"); + } + + Integer tag = (Integer) viewHolder.itemView.getTag(); + ComplicationItem complicationItem = mAdapter.getItem(tag); + + startActivityForResult(ProviderChooserIntent.createProviderChooserIntent( + complicationItem.watchFace, + complicationItem.complicationId, + complicationItem.supportedTypes), PROVIDER_CHOOSER_REQUEST_CODE); + } + + private List getComplicationItems() { + ComponentName watchFace = new ComponentName( + getApplicationContext(), ComplicationSimpleWatchFaceService.class); + + String[] complicationNames = + getResources().getStringArray(R.array.complication_simple_names); + + int[] complicationIds = ComplicationSimpleWatchFaceService.COMPLICATION_IDS; + + TypedArray icons = getResources().obtainTypedArray(R.array.complication_simple_icons); + + List items = new ArrayList<>(); + for (int i = 0; i < complicationIds.length; i++) { + items.add(new ComplicationItem(watchFace, + complicationIds[i], + ComplicationSimpleWatchFaceService.COMPLICATION_SUPPORTED_TYPES[i], + icons.getDrawable(i), + complicationNames[i])); + } + return items; + } + + @Override + public void onTopEmptyRegionClick() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onTopEmptyRegionClick()"); + } + } + + /* + * Inner class representing items of the ConfigurationAdapter (WearableListView.Adapter) class. + */ + private final class ComplicationItem { + ComponentName watchFace; + int complicationId; + int[] supportedTypes; + Drawable icon; + String title; + + public ComplicationItem(ComponentName watchFace, int complicationId, int[] supportedTypes, + Drawable icon, String title) { + this.watchFace = watchFace; + this.complicationId = complicationId; + this.supportedTypes = supportedTypes; + this.icon = icon; + this.title = title; + } + } + + private static class ConfigurationAdapter extends WearableListView.Adapter { + + private Context mContext; + private final LayoutInflater mInflater; + private List mItems; + + + public ConfigurationAdapter (Context context, List items) { + mContext = context; + mInflater = LayoutInflater.from(mContext); + mItems = items; + } + + // Provides a reference to the type of views you're using + public static class ItemViewHolder extends WearableListView.ViewHolder { + private ImageView iconImageView; + private TextView textView; + public ItemViewHolder(View itemView) { + super(itemView); + iconImageView = (ImageView) itemView.findViewById(R.id.icon); + textView = (TextView) itemView.findViewById(R.id.name); + } + } + + @Override + public WearableListView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + + // Inflate custom layout for list items. + return new ItemViewHolder( + mInflater.inflate(R.layout.activity_complication_simple_list_item, null)); + } + + @Override + public void onBindViewHolder(WearableListView.ViewHolder holder, int position) { + + ItemViewHolder itemHolder = (ItemViewHolder) holder; + + ImageView imageView = itemHolder.iconImageView; + imageView.setImageDrawable(mItems.get(position).icon); + + TextView textView = itemHolder.textView; + textView.setText(mItems.get(position).title); + + holder.itemView.setTag(position); + } + + @Override + public int getItemCount() { + return mItems.size(); + } + + public ComplicationItem getItem(int position) { + return mItems.get(position); + } + } + +} \ No newline at end of file diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/ComplicationSimpleWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/ComplicationSimpleWatchFaceService.java new file mode 100644 index 00000000..9eca2c33 --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/ComplicationSimpleWatchFaceService.java @@ -0,0 +1,744 @@ +/* + * 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.app.PendingIntent; +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.graphics.Typeface; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.v7.graphics.Palette; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.support.wearable.watchface.CanvasWatchFaceService; +import android.support.wearable.watchface.WatchFaceService; +import android.support.wearable.watchface.WatchFaceStyle; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.SurfaceHolder; + +import java.util.Calendar; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +/** + * Demonstrates two simple complications in a watch face. + */ +public class ComplicationSimpleWatchFaceService extends CanvasWatchFaceService { + private static final String TAG = "SimpleComplicationWF"; + + // Unique IDs for each complication. + private static final int LEFT_DIAL_COMPLICATION = 0; + private static final int RIGHT_DIAL_COMPLICATION = 1; + + // Left and right complication IDs as array for Complication API. + public static final int[] COMPLICATION_IDS = {LEFT_DIAL_COMPLICATION, RIGHT_DIAL_COMPLICATION}; + + // Left and right dial supported types. + public static final int[][] COMPLICATION_SUPPORTED_TYPES = { + {ComplicationData.TYPE_SHORT_TEXT}, + {ComplicationData.TYPE_SHORT_TEXT} + }; + + /* + * 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 { + private static final int MSG_UPDATE_TIME = 0; + + private static final float COMPLICATION_TEXT_SIZE = 38f; + private static final int COMPLICATION_TAP_BUFFER = 40; + + 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 int mWidth; + private int mHeight; + 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 mWatchHandHighlightColor; + private int mWatchHandShadowColor; + + private Paint mHourPaint; + private Paint mMinutePaint; + private Paint mSecondPaint; + private Paint mTickAndCirclePaint; + + private Paint mBackgroundPaint; + private Bitmap mBackgroundBitmap; + private Bitmap mGrayBackgroundBitmap; + + // Variables for painting Complications + private Paint mComplicationPaint; + + /* To properly place each complication, we need their x and y coordinates. While the width + * may change from moment to moment based on the time, the height will not change, so we + * store it as a local variable and only calculate it only when the surface changes + * (onSurfaceChanged()). + */ + private int mComplicationsY; + + /* Maps active complication ids to the data for that complication. Note: Data will only be + * present if the user has chosen a provider via the settings activity for the watch face. + */ + private SparseArray mActiveComplicationDataSparseArray; + + 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(); + } + }; + + // Handler to update the time once a second in interactive mode. + private final Handler mUpdateTimeHandler = new Handler() { + @Override + public void handleMessage(Message message) { + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "updating time"); + } + invalidate(); + if (shouldTimerBeRunning()) { + long timeMs = System.currentTimeMillis(); + long delayMs = INTERACTIVE_UPDATE_RATE_MS + - (timeMs % INTERACTIVE_UPDATE_RATE_MS); + mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs); + } + + } + }; + + @Override + public void onCreate(SurfaceHolder holder) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onCreate"); + } + super.onCreate(holder); + + mCalendar = Calendar.getInstance(); + + setWatchFaceStyle(new WatchFaceStyle.Builder(ComplicationSimpleWatchFaceService.this) + .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT) + .setAcceptsTapEvents(true) + .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) + .setShowSystemUiTime(false) + .build()); + + initializeBackground(); + initializeComplication(); + initializeWatchFace(); + } + + private void initializeBackground() { + mBackgroundPaint = new Paint(); + mBackgroundPaint.setColor(Color.BLACK); + mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg); + } + + private void initializeComplication() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "initializeComplications()"); + } + mActiveComplicationDataSparseArray = new SparseArray<>(COMPLICATION_IDS.length); + + mComplicationPaint = new Paint(); + mComplicationPaint.setColor(Color.WHITE); + mComplicationPaint.setTextSize(COMPLICATION_TEXT_SIZE); + mComplicationPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + mComplicationPaint.setAntiAlias(true); + + setActiveComplications(COMPLICATION_IDS); + } + + private void initializeWatchFace() { + /* Set defaults for colors */ + mWatchHandColor = Color.WHITE; + mWatchHandHighlightColor = 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(mWatchHandHighlightColor); + 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); + + // Asynchronous call extract colors from background image to improve watch face style. + 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); + updateWatchHandStyle(); + } + } + }); + } + + @Override + public void onDestroy() { + mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); + super.onDestroy(); + } + + @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); + } + + /* + * Called when there is updated data for a complication id. + */ + @Override + public void onComplicationDataUpdate( + int complicationId, ComplicationData complicationData) { + Log.d(TAG, "onComplicationDataUpdate() id: " + complicationId); + + // Adds/updates active complication data in the array. + mActiveComplicationDataSparseArray.put(complicationId, complicationData); + invalidate(); + } + + @Override + public void onTapCommand(int tapType, int x, int y, long eventTime) { + Log.d(TAG, "OnTapCommand()"); + switch (tapType) { + case TAP_TYPE_TAP: + int tappedComplicationId = getTappedComplicationId(x, y); + if (tappedComplicationId != -1) { + onComplicationTap(tappedComplicationId); + } + break; + } + } + + /* + * Determines if tap inside a complication area or returns -1. + */ + private int getTappedComplicationId(int x, int y) { + ComplicationData complicationData; + long currentTimeMillis = System.currentTimeMillis(); + + for (int i = 0; i < COMPLICATION_IDS.length; i++) { + complicationData = mActiveComplicationDataSparseArray.get(COMPLICATION_IDS[i]); + + if ((complicationData != null) + && (complicationData.isActive(currentTimeMillis)) + && (complicationData.getType() != ComplicationData.TYPE_NOT_CONFIGURED) + && (complicationData.getType() != ComplicationData.TYPE_EMPTY)) { + + Rect complicationBoundingRect = new Rect(0, 0, 0, 0); + + switch (COMPLICATION_IDS[i]) { + case LEFT_DIAL_COMPLICATION: + complicationBoundingRect.set( + 0, // left + mComplicationsY - COMPLICATION_TAP_BUFFER, // top + (mWidth / 2), // right + ((int) COMPLICATION_TEXT_SIZE // bottom + + mComplicationsY + + COMPLICATION_TAP_BUFFER)); + break; + + case RIGHT_DIAL_COMPLICATION: + complicationBoundingRect.set( + (mWidth / 2), // left + mComplicationsY - COMPLICATION_TAP_BUFFER, // top + mWidth, // right + ((int) COMPLICATION_TEXT_SIZE // bottom + + mComplicationsY + + COMPLICATION_TAP_BUFFER)); + break; + } + + if (complicationBoundingRect.width() > 0) { + if (complicationBoundingRect.contains(x, y)) { + return COMPLICATION_IDS[i]; + } + } else { + Log.e(TAG, "Not a recognized complication id."); + } + } + } + return -1; + } + + /* + * Fires PendingIntent associated with complication (if it has one). + */ + private void onComplicationTap(int complicationId) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onComplicationTap()"); + } + ComplicationData complicationData = + mActiveComplicationDataSparseArray.get(complicationId); + + if ((complicationData != null) && (complicationData.getTapAction() != null)) { + try { + complicationData.getTapAction().send(); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "On complication tap action error " + e); + } + invalidate(); + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "No PendingIntent for complication " + complicationId + "."); + } + } + } + + @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(); + + // Updates complication style + mComplicationPaint.setAntiAlias(!inAmbientMode); + + // Check and trigger whether or not timer should be running (only in active mode). + updateTimer(); + } + + 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(mWatchHandHighlightColor); + 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); + + // Used for complications + mWidth = width; + mHeight = 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 = mWidth / 2f; + mCenterY = mHeight / 2f; + + /* + * Since the height of the complications text does not change, we only have to + * recalculate when the surface changes. + */ + mComplicationsY = (int) ((mHeight / 2) + (mComplicationPaint.getTextSize() / 2)); + + /* + * 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.DEBUG)) { + Log.d(TAG, "onDraw"); + } + long now = System.currentTimeMillis(); + mCalendar.setTimeInMillis(now); + + drawBackground(canvas); + drawComplications(canvas, now); + drawWatchFace(canvas); + + + } + + private void drawBackground(Canvas canvas) { + if (mAmbient && (mLowBitAmbient || mBurnInProtection)) { + canvas.drawColor(Color.BLACK); + } else if (mAmbient) { + canvas.drawBitmap(mGrayBackgroundBitmap, 0, 0, mBackgroundPaint); + } else { + canvas.drawBitmap(mBackgroundBitmap, 0, 0, mBackgroundPaint); + } + } + + private void drawComplications(Canvas canvas, long currentTimeMillis) { + ComplicationData complicationData; + + for (int i = 0; i < COMPLICATION_IDS.length; i++) { + + complicationData = mActiveComplicationDataSparseArray.get(COMPLICATION_IDS[i]); + + if ((complicationData != null) + && (complicationData.isActive(currentTimeMillis)) + && (complicationData.getType() == ComplicationData.TYPE_SHORT_TEXT)) { + + ComplicationText mainText = complicationData.getShortText(); + ComplicationText subText = complicationData.getShortTitle(); + + CharSequence complicationMessage = + mainText.getText(getApplicationContext(), currentTimeMillis); + + /* In most cases you would want the subText (Title) under the mainText (Text), + * but to keep it simple for the code lab, we are concatenating them all on one + * line. + */ + if (subText != null) { + complicationMessage = TextUtils.concat( + complicationMessage, + " ", + subText.getText(getApplicationContext(), currentTimeMillis)); + } + + //Log.d(TAG, "Comp id: " + COMPLICATION_IDS[i] + "\t" + complicationMessage); + double textWidth = + mComplicationPaint.measureText( + complicationMessage, + 0, + complicationMessage.length()); + + int complicationsX; + + if (COMPLICATION_IDS[i] == LEFT_DIAL_COMPLICATION) { + complicationsX = (int) ((mWidth / 2) - textWidth) / 2; + } else { + // RIGHT_DIAL_COMPLICATION calculations + int offset = (int) ((mWidth / 2) - textWidth) / 2; + complicationsX = (mWidth / 2) + offset; + } + + canvas.drawText( + complicationMessage, + 0, + complicationMessage.length(), + complicationsX, + mComplicationsY, + mComplicationPaint); + } + } + } + + private void drawWatchFace(Canvas canvas) { + /* + * 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); + } + } + + @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(); + } + + /* Check and trigger whether or not timer should be running (only in active mode). */ + updateTimer(); + } + + @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); + ComplicationSimpleWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter); + } + + private void unregisterReceiver() { + if (!mRegisteredTimeZoneReceiver) { + return; + } + mRegisteredTimeZoneReceiver = false; + ComplicationSimpleWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver); + } + + /** + * Starts/stops the {@link #mUpdateTimeHandler} timer based on the state of the watch face. + */ + private void updateTimer() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "updateTimer"); + } + mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); + if (shouldTimerBeRunning()) { + mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME); + } + } + + /** + * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer + * should only run in active mode. + */ + private boolean shouldTimerBeRunning() { + return isVisible() && !mAmbient; + } + } +} \ No newline at end of file diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/provider/RandomNumberProviderService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/provider/RandomNumberProviderService.java new file mode 100644 index 00000000..916f90fd --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/provider/RandomNumberProviderService.java @@ -0,0 +1,96 @@ +package com.example.android.wearable.watchface.provider; + +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationManager; +import android.support.wearable.complications.ComplicationProviderService; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; + +import java.util.Locale; + +/** + * Example Watch Face Complication data provider provides a random number on every update. + */ +public class RandomNumberProviderService extends ComplicationProviderService { + + private static final String TAG = "RandomNumberProvider"; + + /* + * Called when a complication has been activated. The method is for any one-time + * (per complication) set-up. + * + * You can continue sending data for the active complicationId until onComplicationDeactivated() + * is called. + */ + @Override + public void onComplicationActivated( + int complicationId, int dataType, ComplicationManager complicationManager) { + Log.d(TAG, "onComplicationActivated(): " + complicationId); + super.onComplicationActivated(complicationId, dataType, complicationManager); + } + + /* + * Called when the complication needs updated data from your provider. There are four scenarios + * when this will happen: + * + * 1. An active watch face complication is changed to use this provider + * 2. A complication using this provider becomes active + * 3. The period of time you specified in the manifest has elapsed (UPDATE_PERIOD_SECONDS) + * 4. You triggered an update from your own class via the + * ProviderUpdateRequester.requestUpdate() method. + */ + @Override + public void onComplicationUpdate( + int complicationId, int dataType, ComplicationManager complicationManager) { + Log.d(TAG, "onComplicationUpdate()"); + + + // Retrieve your data, in this case, we simply create a random number to display. + int randomNumber = (int) Math.floor(Math.random() * 10); + + String randomNumberText = + String.format(Locale.getDefault(), "%d!", randomNumber); + + ComplicationData complicationData = null; + + switch (dataType) { + case ComplicationData.TYPE_RANGED_VALUE: + complicationData = new ComplicationData.Builder(ComplicationData.TYPE_RANGED_VALUE) + .setValue(randomNumber) + .setMinValue(0) + .setMaxValue(10) + .setShortText(ComplicationText.plainText(randomNumberText)) + .build(); + break; + case ComplicationData.TYPE_SHORT_TEXT: + complicationData = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) + .setShortText(ComplicationText.plainText(randomNumberText)) + .build(); + break; + case ComplicationData.TYPE_LONG_TEXT: + complicationData = new ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT) + .setLongText( + ComplicationText.plainText("Random Number: " + randomNumberText)) + .build(); + break; + default: + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + + if (complicationData != null) { + complicationManager.updateComplicationData(complicationId, complicationData); + } + } + + /* + * Called when the complication has been deactivated. If you are updating the complication + * manager outside of this class with updates, you will want to update your class to stop. + */ + @Override + public void onComplicationDeactivated(int complicationId) { + Log.d(TAG, "onComplicationDeactivated(): " + complicationId); + super.onComplicationDeactivated(complicationId); + } +} \ No newline at end of file diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_complication_simple.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_complication_simple.png new file mode 100644 index 00000000..48d25ef8 Binary files /dev/null and b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_complication_simple.png differ diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/complications_left_dial.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/complications_left_dial.png new file mode 100644 index 00000000..93bb31ca Binary files /dev/null and b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/complications_left_dial.png differ diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/complications_right_dial.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/complications_right_dial.png new file mode 100644 index 00000000..5db3c982 Binary files /dev/null and b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/complications_right_dial.png differ diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_complication_simple_config.xml b/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_complication_simple_config.xml new file mode 100644 index 00000000..01f5cac7 --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_complication_simple_config.xml @@ -0,0 +1,38 @@ + + + + + + + + + \ No newline at end of file diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_complication_simple_list_item.xml b/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_complication_simple_list_item.xml new file mode 100644 index 00000000..e9952718 --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_complication_simple_list_item.xml @@ -0,0 +1,34 @@ + + + + + + + \ No newline at end of file diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/values/arrays.xml b/wearable/wear/WatchFace/Wearable/src/main/res/values/arrays.xml new file mode 100644 index 00000000..a2e37acb --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/values/arrays.xml @@ -0,0 +1,26 @@ + + + + + Left dial + Right dial + + + @drawable/complications_left_dial + @drawable/complications_right_dial + + \ No newline at end of file diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml b/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml index 4090995d..e4b27173 100644 --- a/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml +++ b/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ Sample OpenGL Sample Interactive Sample Analog + Sample Complication Simple Sample Sweep Sample Card Bounds Sample Digital @@ -42,6 +43,9 @@ Calendar Permission Activity WatchFace requires Calendar access. + Configuration + Random Number + Black Blue diff --git a/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.properties index 1f89809f..128405e9 100644 --- a/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.properties +++ b/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Fri Apr 15 15:26:25 PDT 2016 +#Thu May 12 20:18:40 PDT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/wearable/wear/WatchFace/screenshots/complication_simple_face.png b/wearable/wear/WatchFace/screenshots/complication_simple_face.png new file mode 100644 index 00000000..f756b144 Binary files /dev/null and b/wearable/wear/WatchFace/screenshots/complication_simple_face.png differ diff --git a/wearable/wear/WatchFace/template-params.xml b/wearable/wear/WatchFace/template-params.xml index e339fe26..f4aa1059 100644 --- a/wearable/wear/WatchFace/template-params.xml +++ b/wearable/wear/WatchFace/template-params.xml @@ -26,8 +26,12 @@ 23 23 + com.google.android.gms:play-services-wearable com.android.support:palette-v7:23.1.1 - com.google.android.support:wearable:1.3.0 + com.google.android.support:wearable:1.4.0 + + com.google.android.support:wearable:2.0.0-alpha1 + com.google.android.gms:play-services-fitness com.google.android.gms:play-services-fitness -- cgit v1.2.3