Skip to content

Instantly share code, notes, and snippets.

@Nia-TN1012
Last active December 20, 2016 12:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nia-TN1012/1578b74b9b3a40b6cf6785d4a4c001ab to your computer and use it in GitHub Desktop.
Save Nia-TN1012/1578b74b9b3a40b6cf6785d4a4c001ab to your computer and use it in GitHub Desktop.
C#とXamarin.Androidで作る、Android Wearのウォッチフェイスアプリのプログラムです。
<?xml version="1.0" encoding="utf-8"?>
<!-- manifest要素は、マニフェストの構成を定義します。
package属性は、アプリのパッケージ名( Google PlayでのURLで使用します )を表します。
versionCode属性は、Google Playで管理するためのバージョン番号(整数)を表します。
versionName属性は、Google Playで表示するアプリのバージョンを表します。 -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="WatchFaceTest.WatchFaceTest"
android:versionCode="1" android:versionName="1.0"
android:installLocation="auto">
<!-- uses-sdk要素は、Android SDKのバージョンを表します。
minSdkVersion属性は、ターゲットにするAndroid SDKの最小バージョンを表します。
※ Watch Face API は Android SDK 5.0(API Level 21)以上が必要です。
※ 単位はAPI Levelです。 -->
<uses-sdk android:minSdkVersion="21" />
<!-- user-feature要素は、アプリケーションで使用している機能や、使用するデバイスの種類を宣言します。
Watch Faceアプリの場合、name属性に「android.hardware.type.watch」を設定します。 -->
<uses-feature android:name="android.hardware.type.watch" />
<!-- uses-permission要素は、アプリケーションがユーザーに許可を要求する機能を宣言します。
Watch Faceアプリの場合、「WAKE_LOCK」と「PROVIDE_BACKGROUND」が必要です。 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
<!-- application要素は、アプリケーションの構成を定義します。
label属性は、アプリのタイトル名を表します。
allowEmbedded属性は、このActivity / Serviceを他のActivityの子として埋め込み、起動できるかどうかを表します。
taskAffinity属性は、タスクをグループ化するためのAffinityを表します。デフォルト(もしくは空欄)の場合、パッケージ名となります。
icon属性は、アプリケーションのアイコンを表します。
allowBackup属性は、バックアップサポートを利用するかどうかを表します。
supportsRtl属性は、右から左方向へのレイアウトをサポートするかどうかを表します(アラビア語など、右から左方向へ文字を書く言語をサポートするためです)。
theme属性は、アプリケーションで使用するテーマを表します。 -->
<application android:label="@string/app_name"
android:allowEmbedded="true"
android:taskAffinity=""
android:allowBackup="true" android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
</application>
</manifest>
using System;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Service.Wallpaper;
using Android.Support.V4.Content;
using Android.Support.Wearable.Watchface;
using Android.Text.Format;
using Android.Views;
namespace WatchFaceTest {
// ウォッチフェイスサービス要素の内容を構成します。
// Labelプロパティには、ウォッチフェイスの選択に表示する名前を設定します。
// 注:Nameプロパティについて :
// Xamarinでは、「.」から始まる AndroidのShortcut が利用できません。
//  なお、省略した場合、Nameには「md[GUID].クラス名」となります。
[Service( Label = "@string/watch_name", Permission = "android.permission.BIND_WALLPAPER" )]
// 壁紙にバインドするパーミッションを宣言します。
[MetaData( "android.service.wallpaper", Resource = "@xml/watch_face" )]
// プレビュー画面に表示する画像を指定します。
// previewが四角形用、preview_circularが丸形用です。
[MetaData( "com.google.android.wearable.watchface.preview", Resource = "@drawable/preview" )]
[MetaData( "com.google.android.wearable.watchface.preview_circular", Resource = "@drawable/preview_circular" )]
// WallpaperServiceクラスに追加のIntentフィルターを設定します。
[IntentFilter( new[] { "android.service.wallpaper.WallpaperService" }, Categories = new[] { "com.google.android.wearable.watchface.category.WATCH_FACE" } )]
/// <summary>
/// アナログ時計のウォッチフェイスサービスを提供します。
/// </summary>
public class AnalogWatchFaceService : CanvasWatchFaceService {
/// <summary>
/// インタラクティブモードにおける更新間隔(ミリ秒単位)を表します。
/// </summary>
/// <remarks>
/// アンビエントモードの時は、このフィールドの設定にかかわらず、1分間隔で更新されます。
/// </remarks>
private static readonly long InteractiveUpdateRateMilliseconds = Java.Util.Concurrent.TimeUnit.Seconds.ToMillis( 1 );
/// <summary>
/// インタラクティブモードにて、定期的に時刻を更新するための、ハンドラー用のメッセージのIDを表します。
/// </summary>
private const int MessageUpdateTime = 0;
/// <summary>
/// <see cref="CanvasWatchFaceService.Engine"/>オブジェクトが作成される時に実行します。
/// </summary>
/// <returns><see cref="CanvasWatchFaceService.Engine"/>クラスを継承したオブジェクト</returns>
public override WallpaperService.Engine OnCreateEngine() {
return new AnalogWatchFaceEngine( this );
}
/// <summary>
/// アナログ時計のウォッチフェイスの<see cref="CanvasWatchFaceService.Engine"/>機能を提供します。
/// </summary>
private class AnalogWatchFaceEngine : CanvasWatchFaceService.Engine {
/// <summary>
/// <see cref="CanvasWatchFaceService"/>オブジェクトの参照を格納します。
/// </summary>
private CanvasWatchFaceService owner;
/// <summary>
/// 時刻を更新する時の処理を行うハンドラーを表します。
/// </summary>
private readonly Handler updateTimeHandler;
/// <summary>
/// 現在時刻を表します。
/// </summary>
private Java.Util.Calendar nowTime;
/// <summary>
/// 背景用のペイントオブジェクトを表します。
/// </summary>
private Paint backgroundPaint;
/// <summary>
/// 時針用のオブジェクトを表します。
/// </summary>
private Paint hourHandPaint;
/// <summary>
/// 分針用のオブジェクトを表します。
/// </summary>
private Paint minuteHandPaint;
/// <summary>
/// 秒針用のオブジェクトを表します。
/// </summary>
private Paint secondHandPaint;
/// <summary>
/// アンビエントモードであるかどうかを表します。
/// </summary>
private bool isAmbient;
/// <summary>
/// アンビエントモード時、デバイスがLow-Bitの制限を必要としているかどうかを表します。
/// </summary>
private bool isRequiredLowBitAmbient;
/// <summary>
/// アンビエントモード時、デバイスが焼き付け防止を必要としているかどうかを表します。
/// </summary>
private bool isReqiredBurnInProtection;
/// <summary>
/// デバイスがミュート状態であるかどうかを表します。
/// </summary>
private bool isMute;
/// <summary>
/// タイムゾーンを変更した時に通知を受け取るレシーバーを表します。
/// </summary>
private TimeZoneReceiver timeZoneReceiver;
/// <summary>
/// timeZoneReceiverが<see cref="Application.Context"/>に登録されているかどうかを表します。
/// </summary>
private bool timeZoneReceiverRegistered = false;
/// <summary>
/// <see cref="AnalogWatchFaceEngine"/>クラスの新しいインスタンスを生成します。
/// </summary>
/// <param name="owner"><see cref="CanvasWatchFaceService"/>クラスを継承したオブジェクトの参照</param>
public AnalogWatchFaceEngine( CanvasWatchFaceService owner ) : base( owner ) {
// CanvasWatchFaceServiceクラスを継承したオブジェクトの参照をセットします。
this.owner = owner;
// 時刻を更新する時の処理を構成します。
updateTimeHandler = new Handler(
message => {
// Whatプロパティでメッセージを判別します。
switch( message.What ) {
case MessageUpdateTime:
// TODO : 時刻の更新のメッセージの時の処理を入れます。
// ウォッチフェイスを再描画します。
Invalidate();
// UpdateTimeHandlerを動作させるかどうかを判別します。
if( ShouldTimerBeRunning ) {
long timeMillseconds = DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond;
// delayMs = 更新間隔 - ( 現在時刻(ミリ秒) % 更新間隔) -> 更新間隔との差
long delayMilliseconds = InteractiveUpdateRateMilliseconds - ( timeMillseconds % InteractiveUpdateRateMilliseconds );
// UpdateTimeHandlerにメッセージをセットします。
// SendEmptyMessageDelayedメソッドは指定した時間後にメッセージを発行します。
updateTimeHandler.SendEmptyMessageDelayed( MessageUpdateTime, delayMilliseconds );
}
break;
}
}
);
// TimeZoneReceiverのインスタンスを生成します。
timeZoneReceiver = new TimeZoneReceiver(
intent => {
nowTime.TimeZone = Java.Util.TimeZone.Default;
}
);
}
/// <summary>
/// <see cref="AnalogWatchFaceEngine"/>のインスタンスが生成された時に実行します。
/// </summary>
/// <param name="holder">ディスプレイ表面を表すオブジェクト</param>
public override void OnCreate( ISurfaceHolder holder ) {
// TODO : ここでは主に、以下の処理を行います。
// ・リソースから画像の読み込み
// ・Paintなどのグラフィックスオブジェクトを生成
// ・時刻を格納するオブジェクトの作成
// ・ウォッチフェイスのスタイル(ステータスアイコンやOK Googleの表示など)を設定
// ウォッチフェイスのスタイルを設定します。
SetWatchFaceStyle(
new WatchFaceStyle.Builder( owner )
// ユーザーからのタップイベントを有効にするかどうか設定します。
// true : 有効
// false : 無効(デフォルト)
//.SetAcceptsTapEvents( true )
// 通知が来た時の通知カードの高さを設定します。
.SetCardPeekMode( WatchFaceStyle.PeekModeShort )
// 通知カード(small cardの表示時)の背景の表示方法を設定します。
// WatchFaceStyle.BackgroundVisibilityInterruptive : 一部の重要な通知に限り、表示します。(デフォルト)
// WatchFaceStyle.BackgroundVisibilityPersistent : 通知カードの種類にかかわらず、表示します。
.SetBackgroundVisibility( WatchFaceStyle.BackgroundVisibilityInterruptive )
// システムUIのデジタル時計を表示するするかどうかを設定します。(使用している例として、デフォルトで用意されている「シンプル」があります。)
// true : 表示
// false : 非表示(デフォルト)
.SetShowSystemUiTime( false )
// 設定したスタイル情報をビルドします。このメソッドは最後に呼び出します。
.Build()
);
base.OnCreate( holder );
var resources = owner.Resources;
// 背景用のグラフィックスオブジェクトを生成します。
backgroundPaint = new Paint();
// リソースから背景色を読み込みます。
backgroundPaint.Color = resources.GetColor( Resource.Color.background );
// 時針用のPaintグラフィックスオブジェクトを生成します。
hourHandPaint = new Paint();
hourHandPaint.Color = resources.GetColor( Resource.Color.analog_hands );
// 時針の幅を設定します。
hourHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.hour_hand_stroke );
// アンチエイリアスを有効にします。
hourHandPaint.AntiAlias = true;
// 線端の形は丸形を指定します。
hourHandPaint.StrokeCap = Paint.Cap.Round;
// 分針用のPaintグラフィックスオブジェクトを生成します。
minuteHandPaint = new Paint();
minuteHandPaint.Color = hourHandPaint.Color;
minuteHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.minute_hand_stroke );
minuteHandPaint.AntiAlias = true;
minuteHandPaint.StrokeCap = Paint.Cap.Round;
// 秒針用のPaintグラフィックスオブジェクトを生成します。
secondHandPaint = new Paint();
secondHandPaint.Color = resources.GetColor( Resource.Color.analog_second_hand );
secondHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.second_hand_stroke );
secondHandPaint.AntiAlias = true;
secondHandPaint.StrokeCap = Paint.Cap.Round;
// 時刻を格納するオブジェクトを生成します。
nowTime = Java.Util.Calendar.GetInstance( Java.Util.TimeZone.Default );
}
/// <summary>
/// <see cref="AnalogWatchFaceEngine"/>のインスタンスが破棄される時に実行します。
/// </summary>
public override void OnDestroy() {
// UpdateTimeHandlerにセットされているメッセージを削除します。
updateTimeHandler.RemoveMessages( MessageUpdateTime );
base.OnDestroy();
}
/// <summary>
/// 現在の<see cref="WindowInsets"/>を適用する時に実行します。
/// </summary>
/// <param name="insets">適用される<see cref="WindowInsets"/>オブジェクト</param>
/// <remarks>Android Wearが丸形がどうかを、このメソッド内で判別することができます。</remarks>
public override void OnApplyWindowInsets( WindowInsets insets ) {
base.OnApplyWindowInsets( insets );
// Android Wearが丸形かどうかを判別します。
//bool isRound = insets.IsRound;
}
/// <summary>
/// Android Wearデバイスのプロパティが定められた時に実行します。
/// </summary>
/// <param name="properties">プロパティ値を格納したバンドルオブジェクト</param>
public override void OnPropertiesChanged( Bundle properties ) {
base.OnPropertiesChanged( properties );
// アンビエントモード時、Low-Bit制限を必要とするかどうかの値を取得します。
isRequiredLowBitAmbient = properties.GetBoolean( PropertyLowBitAmbient, false );
// アンビエントモード時、焼き付き防止を必要とするかどうかの値を取得します。
isReqiredBurnInProtection = properties.GetBoolean( PropertyBurnInProtection, false );
}
/// <summary>
/// 時間を更新した時に実行します。
/// </summary>
/// <remarks>
/// 画面の表示・非表示やモードに関わらず、1分ごとに呼び出されます。
/// </remarks>
public override void OnTimeTick() {
base.OnTimeTick();
// ウォッチフェイスを再描画します。
Invalidate();
}
/// <summary>
/// Android Wearがインタラクティブモードとアンビエントモードを切り替えた時に実行します。
/// </summary>
/// <param name="inAmbientMode">アンビエントモードであるかどうかを示す値</param>
public override void OnAmbientModeChanged( bool inAmbientMode ) {
base.OnAmbientModeChanged( inAmbientMode );
// アンビエントモードが変更されたかどうかを判別します。
if( isAmbient != inAmbientMode ) {
// 現在のアンビエントモードをセットします。
isAmbient = inAmbientMode;
// デバイスがLow-Bit制限を必要とするかどうかを判別します。
if( isRequiredLowBitAmbient ) {
bool antiAlias = !inAmbientMode;
// アンビエントモードの時は、針のPaintオブジェクトのアンチエイリアスを無効にし、
// そうでなければ有効にします。
hourHandPaint.AntiAlias = antiAlias;
minuteHandPaint.AntiAlias = antiAlias;
secondHandPaint.AntiAlias = antiAlias;
// ウォッチフェイスを再描画します。
Invalidate();
}
// タイマーを更新します。
UpdateTimer();
}
}
/// <summary>
/// Interruptionフィルターが変更された時に実行します。
/// </summary>
/// <param name="interruptionFilter">Interruptionフィルター</param>
public override void OnInterruptionFilterChanged( int interruptionFilter ) {
base.OnInterruptionFilterChanged( interruptionFilter );
// Interruptionフィルターが変更されたかどうか判別します。
bool inMuteMode = ( interruptionFilter == InterruptionFilterNone );
// ミュートモードが変更されたかどうか判別します。
if( isMute != inMuteMode ) {
isMute = inMuteMode;
// ウォッチフェイスを再描画します。
Invalidate();
}
}
/// <summary>
/// ユーザーがウォッチフェイスをタップした時に実行されます。
/// </summary>
/// <param name="tapType">タップの種類</param>
/// <param name="xValue">タップのX位置</param>
/// <param name="yValue">タップのY位置</param>
/// <param name="eventTime">タップイベントが発生している時間(ミリ秒)</param>
/// <remarks>
/// Android Wear 1.3以上に対応しています。
/// このメソッドが呼び出させるには、<see cref="WatchFaceStyle.Builder"/>の生成において、SetAcceptsTapEvents( true )を呼び出す必要があります。
/// インタラクティブモードのみ有効です。
/// </remarks>
public override void OnTapCommand( int tapType, int xValue, int yValue, long eventTime ) {
// タップの種類を判別します。
switch( tapType ) {
case TapTypeTouch:
// TODO : ユーザーが画面をタッチした時の処理を入れます。
break;
case TapTypeTouchCancel:
// TODO : ユーザーが画面をタッチしたまま、指を動かした時の処理を入れます。
break;
case TapTypeTap:
// TODO : ユーザーがタップした時の処理を入れます。
break;
}
}
/// <summary>
/// ウォッチフェイスの描画時に実行されます。
/// </summary>
/// <param name="canvas">ウォッチフェイスに描画するためのキャンバスオブジェクト</param>
/// <param name="bounds">画面のサイズを格納するオブジェクト</param>
public override void OnDraw( Canvas canvas, Rect bounds ) {
// 現在時刻を取得します。
nowTime = Java.Util.Calendar.GetInstance( nowTime.TimeZone );
// 背景を描画します。
// アンビエントモードであるかどうか判別します。
if( isAmbient ) {
// アンビエントモードの時は、黒色で塗りつぶします。
canvas.DrawColor( Color.Black );
}
else {
// そうでない時は、背景画像を描画します。
canvas.DrawRect( 0, 0, canvas.Width, canvas.Height, backgroundPaint );
}
// 中心のXY座標を求めます。
float centerX = bounds.Width() / 2.0f;
float centerY = bounds.Height() / 2.0f;
// 針の長さを求めます。
float hourHandLength = centerX - 80;
float minuteHandLength = centerX - 40;
float secondHandLength = centerX - 20;
// 時針の先端のXY座標を求めます。
float hourHandRotation = ( ( nowTime.Get( Java.Util.CalendarField.Hour ) + ( nowTime.Get( Java.Util.CalendarField.Minute ) / 60f ) ) / 6f ) * ( float )Math.PI;
float hourHandX = ( float )Math.Sin( hourHandRotation ) * hourHandLength;
float hourHandY = ( float )-Math.Cos( hourHandRotation ) * hourHandLength;
// 時針を描画します。
canvas.DrawLine( centerX, centerY, centerX + hourHandX, centerY + hourHandY, hourHandPaint );
// 分針の先端のXY座標を求めます。
float minuteHandRotation = nowTime.Get( Java.Util.CalendarField.Minute ) / 30f * ( float )Math.PI;
float minuteHandX = ( float )Math.Sin( minuteHandRotation ) * minuteHandLength;
float minuteHandY = ( float )-Math.Cos( minuteHandRotation ) * minuteHandLength;
// 分針を描画します。
canvas.DrawLine( centerX, centerY, centerX + minuteHandX, centerY + minuteHandY, minuteHandPaint );
// アンビエントモードでないかどうかを判別します。
if( !isAmbient ) {
// 秒針の先端のXY座標を求めます。
float secondHandRotation = nowTime.Get( Java.Util.CalendarField.Second ) / 30f * ( float )Math.PI;
float secondHandX = ( float )Math.Sin( secondHandRotation ) * secondHandLength;
float secondHandY = ( float )-Math.Cos( secondHandRotation ) * secondHandLength;
// 分針を描画します。
canvas.DrawLine( centerX, centerY, centerX + secondHandX, centerY + secondHandY, secondHandPaint );
}
}
/// <summary>
/// ウォッチフェイスの表示・非表示が切り替わった時に実行します。
/// </summary>
/// <param name="visible">ウォッチフェイスの表示・非表示</param>
public override void OnVisibilityChanged( bool visible ) {
base.OnVisibilityChanged( visible );
// ウォッチフェイスの表示・非表示を判別します。
if( visible ) {
// TimeZoneReceiverが未初期化の時、ここで初期化します。
if( timeZoneReceiver == null ) {
timeZoneReceiver = new TimeZoneReceiver(
intent => {
nowTime.TimeZone = Java.Util.TimeZone.Default;
}
);
}
if( !timeZoneReceiverRegistered ) {
// タイムゾーン用のレシーバーを登録します。
var intentFilter = new IntentFilter( Intent.ActionTimezoneChanged );
Application.Context.RegisterReceiver( timeZoneReceiver, intentFilter );
timeZoneReceiverRegistered = true;
}
// ウォッチフェイスが描画されていない時にタイムゾーンが変化した場合の備え、現在タイムゾーンの時の現在時刻を取得します。
nowTime = Java.Util.Calendar.GetInstance( Java.Util.TimeZone.Default );
}
else {
if( timeZoneReceiverRegistered ) {
// タイムゾーン用のレシーバーを登録解除します。
Application.Context.UnregisterReceiver( timeZoneReceiver );
timeZoneReceiverRegistered = false;
}
}
// タイマーの動作を更新します。
UpdateTimer();
}
/// <summary>
/// UpdateTimeHandlerの動作を更新します。
/// </summary>
private void UpdateTimer() {
// UpdateTimeHandlerからMessageUpdateTimeメッセージを取り除きます。
updateTimeHandler.RemoveMessages( MessageUpdateTime );
// UpdateTimeHandlerを動作させるかどうかを判別します。
if( ShouldTimerBeRunning ) {
// UpdateTimeHandlerにMessageUpdateTimeメッセージをセットします。
updateTimeHandler.SendEmptyMessage( MessageUpdateTime );
}
}
/// <summary>
/// UpdateTimeHandlerを動作させるかどうかを表す値を取得します。
/// </summary>
private bool ShouldTimerBeRunning =>
IsVisible && !IsInAmbientMode;
}
}
/// <summary>
/// タイムゾーンを変更した時に通知を受け取るレシーバーを提供します。
/// </summary>
public class TimeZoneReceiver : BroadcastReceiver {
/// <summary>
/// タイムゾーンを変更した通知を受け取った時に実行するデリゲートを表します。
/// </summary>
private Action<Intent> receiver;
/// <summary>
/// 指定したデリゲートから、<see cref="TimeZoneReceiver"/>クラスの新しいインスタンスを生成します。
/// </summary>
/// <param name="_receiver">タイムゾーンを変更した通知を受け取った時に実行するデリゲート</param>
public TimeZoneReceiver( Action<Intent> _receiver ) {
receiver = _receiver;
}
/// <summary>
/// タイムゾーンを変更した通知を受け取った時に実行します。
/// </summary>
/// <param name="context">レシーバーを登録した<see cref="Context"/></param>
/// <param name="intent">ブロードキャストされた<see cref="Intent"/>オブジェクト</param>
public override void OnReceive( Context context, Intent intent ) {
receiver?.Invoke( intent );
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment