added watchface module

This commit is contained in:
danijoo
2015-12-03 12:41:39 +01:00
parent 91b747d7cd
commit 059200095c
17 changed files with 439 additions and 20 deletions

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
<!--
Copyright (C) 2015 The Android Open Source Project Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,9 +12,9 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.sunshine.app" > package="com.example.android.sunshine.app">
<!-- This permission is necessary in order for Sunshine to perform network access. --> <!-- This permission is necessary in order for Sunshine to perform network access. -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@@ -30,17 +29,19 @@
<!-- Permissions required for Google Cloud Messaging --> <!-- Permissions required for Google Cloud Messaging -->
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" /> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission <permission
android:name="com.example.android.sunshine.app.permission.C2D_MESSAGE" android:name="com.example.android.sunshine.app.permission.C2D_MESSAGE"
android:protectionLevel="signature" /> android:protectionLevel="signature" />
<uses-permission android:name="com.example.android.sunshine.app.permission.C2D_MESSAGE" /> <uses-permission android:name="com.example.android.sunshine.app.permission.C2D_MESSAGE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/AppTheme" android:supportsRtl="true"
android:supportsRtl="true"> android:theme="@style/AppTheme">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
@@ -64,7 +65,7 @@
android:name=".SettingsActivity" android:name=".SettingsActivity"
android:label="@string/title_activity_settings" android:label="@string/title_activity_settings"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:theme="@style/SettingsTheme" > android:theme="@style/SettingsTheme">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="com.example.android.sunshine.app.MainActivity" /> android:value="com.example.android.sunshine.app.MainActivity" />
@@ -77,7 +78,7 @@
android:syncable="true" /> android:syncable="true" />
<!-- SyncAdapter's dummy authentication service --> <!-- SyncAdapter's dummy authentication service -->
<service android:name=".sync.SunshineAuthenticatorService" > <service android:name=".sync.SunshineAuthenticatorService">
<intent-filter> <intent-filter>
<action android:name="android.accounts.AccountAuthenticator" /> <action android:name="android.accounts.AccountAuthenticator" />
</intent-filter> </intent-filter>
@@ -90,7 +91,7 @@
<!-- The SyncAdapter service --> <!-- The SyncAdapter service -->
<service <service
android:name=".sync.SunshineSyncService" android:name=".sync.SunshineSyncService"
android:exported="true" > android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.content.SyncAdapter" /> <action android:name="android.content.SyncAdapter" />
</intent-filter> </intent-filter>
@@ -103,49 +104,60 @@
<!-- GCM receiver --> <!-- GCM receiver -->
<receiver <receiver
android:name=".GcmBroadcastReceiver" android:name=".GcmBroadcastReceiver"
android:permission="com.google.android.c2dm.permission.SEND" > android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter> <intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" /> <action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="com.example.android.sunshine.app" /> <category android:name="com.example.android.sunshine.app" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- Muzei Extension --> <!-- Muzei Extension -->
<service android:name=".muzei.WeatherMuzeiSource" <service
android:name=".muzei.WeatherMuzeiSource"
android:description="@string/muzei_description"
android:icon="@drawable/ic_muzei" android:icon="@drawable/ic_muzei"
android:label="@string/app_name" android:label="@string/app_name">
android:description="@string/muzei_description" >
<intent-filter> <intent-filter>
<action android:name="com.google.android.apps.muzei.api.MuzeiArtSource" /> <action android:name="com.google.android.apps.muzei.api.MuzeiArtSource" />
</intent-filter> </intent-filter>
<meta-data android:name="color" android:value="@color/primary" />
<meta-data
android:name="color"
android:value="@color/primary" />
</service> </service>
<!-- Today Widget --> <!-- Today Widget -->
<receiver <receiver
android:name=".widget.TodayWidgetProvider" android:name=".widget.TodayWidgetProvider"
android:label="@string/title_widget_today" > android:label="@string/title_widget_today">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="com.example.android.sunshine.app.ACTION_DATA_UPDATED" /> <action android:name="com.example.android.sunshine.app.ACTION_DATA_UPDATED" />
</intent-filter> </intent-filter>
<meta-data android:name="android.appwidget.provider"
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info_today" /> android:resource="@xml/widget_info_today" />
</receiver> </receiver>
<service android:name=".widget.TodayWidgetIntentService" /> <service android:name=".widget.TodayWidgetIntentService" />
<!-- Detail Widget --> <!-- Detail Widget -->
<receiver <receiver
android:name=".widget.DetailWidgetProvider" android:name=".widget.DetailWidgetProvider"
android:label="@string/title_widget_detail" android:enabled="@bool/widget_detail_enabled"
android:enabled="@bool/widget_detail_enabled" > android:label="@string/title_widget_detail">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="com.example.android.sunshine.app.ACTION_DATA_UPDATED" /> <action android:name="com.example.android.sunshine.app.ACTION_DATA_UPDATED" />
</intent-filter> </intent-filter>
<meta-data android:name="android.appwidget.provider"
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info_detail" /> android:resource="@xml/widget_info_detail" />
</receiver> </receiver>
<service <service
android:name=".widget.DetailWidgetRemoteViewsService" android:name=".widget.DetailWidgetRemoteViewsService"
android:enabled="@bool/widget_detail_enabled" android:enabled="@bool/widget_detail_enabled"

View File

@@ -1 +1 @@
include ':app' include ':app', ':wear'

1
wear/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

27
wear/build.gradle Normal file
View File

@@ -0,0 +1,27 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
applicationId "com.example.android.sunshine.wear"
minSdkVersion 21
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.android.support:wearable:1.3.0'
compile 'com.google.android.gms:play-services-wearable:8.3.0'
}

17
wear/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /opt/android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.sunshine.wear">
<uses-feature android:name="android.hardware.type.watch" />
<!-- Required to act as a custom watch face. -->
<uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<service
android:name=".SunshineWatchFaceService"
android:label="@string/watchface_name"
android:permission="android.permission.BIND_WALLPAPER">
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_digital" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_digital_circular" />
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
</application>
</manifest>

View File

@@ -0,0 +1,299 @@
/*
* 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.sunshine.wear;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
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.wearable.watchface.CanvasWatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle;
import android.text.format.Time;
import android.view.SurfaceHolder;
import android.view.WindowInsets;
import java.lang.ref.WeakReference;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
/**
* Digital watch face with seconds. In ambient mode, the seconds aren't displayed. On devices with
* low-bit ambient mode, the text is drawn without anti-aliasing in ambient mode.
*/
public class SunshineWatchFaceService extends CanvasWatchFaceService {
private static final Typeface NORMAL_TYPEFACE =
Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
/**
* Update rate in milliseconds for interactive mode. We update once a second since seconds are
* displayed in interactive mode.
*/
private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
/**
* Handler message id for updating the time periodically in interactive mode.
*/
private static final int MSG_UPDATE_TIME = 0;
@Override
public SunshineWatchFaceEngine onCreateEngine() {
return new SunshineWatchFaceEngine();
}
private static class EngineHandler extends Handler {
private final WeakReference<SunshineWatchFaceEngine> mWeakReference;
public EngineHandler(SunshineWatchFaceEngine reference) {
mWeakReference = new WeakReference<>(reference);
}
@Override
public void handleMessage(Message msg) {
SunshineWatchFaceEngine engine = mWeakReference.get();
if (engine != null) {
switch (msg.what) {
case MSG_UPDATE_TIME:
engine.handleUpdateTimeMessage();
break;
}
}
}
}
private class SunshineWatchFaceEngine extends CanvasWatchFaceService.Engine {
final Handler mUpdateTimeHandler = new EngineHandler(this);
boolean mRegisteredTimeZoneReceiver = false;
Paint mBackgroundPaint;
Paint mTextPaint;
boolean mAmbient;
Time mTime;
final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mTime.clear(intent.getStringExtra("time-zone"));
mTime.setToNow();
}
};
int mTapCount;
float mXOffset;
float mYOffset;
/**
* Whether the display supports fewer bits for each color in ambient mode. When true, we
* disable anti-aliasing in ambient mode.
*/
boolean mLowBitAmbient;
@Override
public void onCreate(SurfaceHolder holder) {
super.onCreate(holder);
setWatchFaceStyle(new WatchFaceStyle.Builder(SunshineWatchFaceService.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
.setAcceptsTapEvents(true)
.build());
Resources resources = SunshineWatchFaceService.this.getResources();
mYOffset = resources.getDimension(R.dimen.digital_y_offset);
mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(resources.getColor(R.color.background));
mTextPaint = new Paint();
mTextPaint = createTextPaint(resources.getColor(R.color.digital_text));
mTime = new Time();
}
@Override
public void onDestroy() {
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
super.onDestroy();
}
private Paint createTextPaint(int textColor) {
Paint paint = new Paint();
paint.setColor(textColor);
paint.setTypeface(NORMAL_TYPEFACE);
paint.setAntiAlias(true);
return paint;
}
@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);
if (visible) {
registerReceiver();
// Update time zone in case it changed while we weren't visible.
mTime.clear(TimeZone.getDefault().getID());
mTime.setToNow();
} 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);
SunshineWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
}
private void unregisterReceiver() {
if (!mRegisteredTimeZoneReceiver) {
return;
}
mRegisteredTimeZoneReceiver = false;
SunshineWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
}
@Override
public void onApplyWindowInsets(WindowInsets insets) {
super.onApplyWindowInsets(insets);
// Load resources that have alternate values for round watches.
Resources resources = SunshineWatchFaceService.this.getResources();
boolean isRound = insets.isRound();
mXOffset = resources.getDimension(isRound
? R.dimen.digital_x_offset_round : R.dimen.digital_x_offset);
float textSize = resources.getDimension(isRound
? R.dimen.digital_text_size_round : R.dimen.digital_text_size);
mTextPaint.setTextSize(textSize);
}
@Override
public void onPropertiesChanged(Bundle properties) {
super.onPropertiesChanged(properties);
mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
}
@Override
public void onTimeTick() {
super.onTimeTick();
invalidate();
}
@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
if (mAmbient != inAmbientMode) {
mAmbient = inAmbientMode;
if (mLowBitAmbient) {
mTextPaint.setAntiAlias(!inAmbientMode);
}
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();
}
/**
* Captures tap event (and tap type) and toggles the background color if the user finishes
* a tap.
*/
@Override
public void onTapCommand(int tapType, int x, int y, long eventTime) {
Resources resources = SunshineWatchFaceService.this.getResources();
switch (tapType) {
case TAP_TYPE_TOUCH:
// The user has started touching the screen.
break;
case TAP_TYPE_TOUCH_CANCEL:
// The user has started a different gesture or otherwise cancelled the tap.
break;
case TAP_TYPE_TAP:
// The user has completed the tap gesture.
mTapCount++;
mBackgroundPaint.setColor(resources.getColor(mTapCount % 2 == 0 ?
R.color.background : R.color.background2));
break;
}
invalidate();
}
@Override
public void onDraw(Canvas canvas, Rect bounds) {
// Draw the background.
if (isInAmbientMode()) {
canvas.drawColor(Color.BLACK);
} else {
canvas.drawRect(0, 0, bounds.width(), bounds.height(), mBackgroundPaint);
}
// Draw H:MM in ambient mode or H:MM:SS in interactive mode.
mTime.setToNow();
String text = mAmbient
? String.format("%d:%02d", mTime.hour, mTime.minute)
: String.format("%d:%02d:%02d", mTime.hour, mTime.minute, mTime.second);
canvas.drawText(text, mXOffset, mYOffset, mTextPaint);
}
/**
* Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
* or stops it if it shouldn't be running but currently is.
*/
private void 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 when we're visible and in interactive mode.
*/
private boolean shouldTimerBeRunning() {
return isVisible() && !isInAmbientMode();
}
/**
* Handle updating the time periodically in interactive mode.
*/
private void handleUpdateTimeMessage() {
invalidate();
if (shouldTimerBeRunning()) {
long timeMs = System.currentTimeMillis();
long delayMs = INTERACTIVE_UPDATE_RATE_MS
- (timeMs % INTERACTIVE_UPDATE_RATE_MS);
mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background">#000000</color>
<color name="background2">#000088</color>
<color name="digital_text">#ffffff</color>
</resources>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="digital_text_size">40dp</dimen>
<dimen name="digital_text_size_round">45dp</dimen>
<dimen name="digital_x_offset">15dp</dimen>
<dimen name="digital_x_offset_round">25dp</dimen>
<dimen name="digital_y_offset">90dp</dimen>
</resources>

View File

@@ -0,0 +1,4 @@
<resources>
<string name="app_name">Sunshine</string>
<string name="watchface_name">Weather</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android" />