XamarinでAndroid WearのWatch Faceアプリを作ってみよう!(Vol.2 : プログラム作成編(Page-1))

More than 1 year has passed since last update.

こんにちはー!ニアです。

今回はWatch Faceアプリのプログラムをコーディングしていきます。


※Vol.2はコンテンツの量が多いので、2記事に分割しています。



1. Watch Faceアプリのクラス構成

Watch Faceアプリのクラスは、CanvasWatchFaceServiceクラスを継承し、CanvasWatchFaceService.Engineの派生クラスの定義と、WallpaperService.Engineを作成するメソッドをオーバーライドしたもので構成されています。


クラス構成

public class MyWatchFaceService : CanvasWatchFaceService {

public override WallpaperService.Engine OnCreateEngine();

private class MyWatchFaceEngine : CanvasWatchFaceService.Engine {

public MyWatchFaceEngine( CanvasWatchFaceService owner );

public override void OnCreate( ISurfaceHolder holder );

public override void OnDestroy();

public override void OnApplyWindowInsets( WindowInsets insets );

public override void OnPropertiesChanged( Bundle properties );

public override void OnTimeTick();

public override void OnAmbientModeChanged( bool inAmbientMode );

public override void OnInterruptionFilterChanged( int interruptionFilter );

public override void OnTapCommand( int tapType, int xValue, int yValue, long eventTime );

public override void OnDraw( Canvas canvas, Rect bounds );

public override void OnVisibilityChanged( bool visible );
}
}



1.1. Watch Faceアプリのプログラム作成でよく使用する名前空間

以下に、今回作成するプログラムでよく使用する名前空間を示します。よく使用する名前空間はusingディレクティブを利用すると、コードを短く記述することができ、便利です。


WatchFaceアプリのプログラム作成でよく使用する名前空間

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;



2. Watch FaceのServiceクラスの作成

まず、Watch Faceのサービスクラスを定義します。


Serviceクラスの宣言

public class AnalogWatchFaceService : CanvasWatchFaceService {

// 中略
}


※本記事では「AnalogWatchFaceService」と名付けています。



2.1. クラスの属性で、AndroidのマニフェストのService要素を構成

先ほど宣言したAnalogWatchFaceServiceクラスに、クラスの属性を追加して、AndroidのマニフェストのService要素を構成します。


2.1.1. サービスの名前を設定

Service」属性を追加し、LabelフィールドにWatch Faceの名前を、Permissionフィールドに「android.permission.BIND_WALLPAPER1を設定します。


サービスの名前を設定

[Service( Label = "@string/my_watch_name", Permission = "android.permission.BIND_WALLPAPER" )]



2.1.2. メタデータを設定

MetaData」属性を3つ追加します。

1つは、引数に「android.service.wallpaper」を、Resourceフィールドに「@xml/watch_face」(※Vol2.で作成した、Resources/xml/watch_face.xmlが参照されます。)を指定します。

[MetaData( "android.service.wallpaper", Resource = "@xml/watch_face" )]

残りの2つは、プレビュー画面に表示する画像のパスを指定します。


プレビュー画面用の画像を指定

// 四角形

[MetaData( "com.google.android.wearable.watchface.preview", Resource = "@drawable/preview" )]
// 丸形
[MetaData( "com.google.android.wearable.watchface.preview_circular", Resource = "@drawable/preview_circular" )]


※リソースのパスを指定する時、「-nodpi」などの修飾子は不要です。



2.1.3. 追加のIntentフィルターを設定

WallpaperServiceクラスに、1つのIntentフィルターを追加します。


WallpaperServiceクラスにIntentフィルターを追加

[IntentFilter( new[] { "android.service.wallpaper.WallpaperService" }, Categories = new[] { "com.google.android.wearable.watchface.category.WATCH_FACE" } )]



2.1.4. クラス属性のまとめ

2.1.1.~2.1.3.のクラス属性をまとめると、以下のようなコードになります。


Androidのマニフェスト、Service要素を構成

[Service( Label = "@string/watch_name", Permission = "android.permission.BIND_WALLPAPER" )]

[MetaData( "android.service.wallpaper", Resource = "@xml/watch_face" )]
[MetaData( "com.google.android.wearable.watchface.preview", Resource = "@drawable/preview" )]
[MetaData( "com.google.android.wearable.watchface.preview_circular", Resource = "@drawable/preview_circular" )]
[IntentFilter( new[] { "android.service.wallpaper.WallpaperService" }, Categories = new[] { "com.google.android.wearable.watchface.category.WATCH_FACE" } )]
public class AnalogWatchFaceService : CanvasWatchFaceService {
// 中略
}

設定したクラスの属性は、ビルド時にAndroidManufest.xmlへ統合されます。


AndroidManufest.xml(ビルド時)

<manufest>

<!-- 中略 -->
<application android:label="@string/app_name" android:allowEmbedded="true" android:taskAffinity="" android:allowBackup="true" android:supportsRtl="true" android:theme="@android:style/Theme.DeviceDefault" android:name="android.app.Application">
<service android:label="@string/watch_name" android:permission="android.permission.BIND_WALLPAPER" android:name="md[GUID].AnalogWatchFaceService">
<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" />
<meta-data android:name="com.google.android.wearable.watchface.preview_circular" android:resource="@drawable/preview_circular" />
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
<provider android:name="mono.MonoRuntimeProvider" android:exported="false" android:initOrder="2147483647" android:authorities="[パッケージ名].mono.MonoRuntimeProvider.__mono_init__" />
</application>
</manufest>


2.2. メンバーを定義

AnalogWatchFaceServiceクラスに、以下のようにメンバーを定義します。


AnalogWatchFaceServiceクラスのメンバーを定義

// 中略

public class AnalogWatchFaceService : CanvasWatchFaceService {

// インタラクティブモードにおける更新間隔(ミリ秒単位)を表します。
// Java.Util.Concurrent.TimeUnit.Seconds.ToMillisメソッドは、指定した秒の値をミリ秒に変換します。(※)
private static readonly long InteractiveUpdateRateMilliseconds = Java.Util.Concurrent.TimeUnit.Seconds.ToMillis( 1 );

// インタラクティブモードにて、定期的に時刻を更新するための、ハンドラー用のメッセージのIDを表します。
// 値は何でもOKです。
private const int MessageUpdateTime = 0;

// 中略
}



(※)JavaのTimeUnitクラスの代わりに、.NETのTimeSpan構造体を利用する場合

private static readonly long InteractiveUpdateRateMilliseconds = ( long )TimeSpan.FromSeconds( 1 ).TotalMilliseconds;



3. Watch FaceのEngineクラスを作成

次に、Watch Faceの動作の要となるEngineクラスをAnalogWatchFaceServiceクラスの中に定義し、作成していきます。


Engineクラスの宣言

// 中略

public class AnalogWatchFaceService : CanvasWatchFaceService {
// 中略

private class AnalogWatchFaceEngine : CanvasWatchFaceService.Engine {
}
}



※本記事では「AnalogWatchFaceEngine」と名付けています。



3.1. OnCreateEngineメソッドをオーバーライド

CanvasWatchFaceServiceクラスのOnCreateEngineメソッドを、AnalogWatchFaceServiceクラスでオーバーライドします。メソッド内では、AnalogWatchFaceEngineクラスのコンストラクター(※)を呼び出し、生成したインスタンスを返します2


OnCreateEngineメソッドをオーバーライド

// 中略

public class AnalogWatchFaceService : CanvasWatchFaceService {
// 中略

public override WallpaperService.Engine OnCreateEngine() {
return new AnalogWatchFaceEngine( this );
}

private class AnalogWatchFaceEngine : CanvasWatchFaceService.Engine {
}
}



※コンストラクターは3.4節で作成します。



3.2. メンバー変数を定義

AnalogWatchFaceEngineクラスに、以下のようにメンバーを定義します。


AnalogWatchFaceEngineクラスのメンバーを定義

private class AnalogWatchFaceEngine : CanvasWatchFaceService.Engine {

// CanvasWatchFaceServiceオブジェクトの参照を格納します。
private CanvasWatchFaceService owner;

// 時刻を更新する時の処理を行うハンドラーを表します。
private readonly Handler updateTimeHandler;

// 現在時刻を表します。
private Java.Util.Calendar nowTime;

// 背景用のペイントオブジェクトを表します。
private Paint backgroundPaint;

// 時針、分針、秒針用のオブジェクトを表します。
private Paint hourHandPaint;
private Paint minuteHandPaint;
private Paint secondHandPaint;

// アンビエントモードであるかどうかを表します。
private bool isAmbient;

// アンビエントモード時、デバイスがLow-Bitの制限を必要としているかどうかを表します。
private bool isRequiredLowBitAmbient;

// アンビエントモード時、デバイスが焼き付け防止を必要としているかどうかを表します。
private bool isReqiredBurnInProtection;

// ミュート状態であるかどうかを表します。
private bool isMute;

// タイムゾーンを変更した時に通知を受け取るレシーバーを表します。(※)
private TimeZoneReceiver timeZoneReceiver;

// 中略
}



※TimeZoneReceiverクラスは3.3節で作成します。



3.2.1. Xamarin.Androidで利用できる、日付・時刻クラス

Xamarin.Androidでは、以下の3つの日付・時刻のクラス及び構造体を利用できます。



  • Android.Text.Format.Timeクラス(Androidのライブラリ)


  • Java.Util.Calendarクラス(Javaのライブラリ)


  • System.DateTime構造体(.NET(Mono.Android)のライブラリ)

但し、Android.Text.Format.TimeクラスはAndroid API Level 22以降では非推奨3となっているので、後者2つのいずれかを利用することをオススメします。


3.2.2. Android Wearのモード

Android Wearには「インタラクティブモード(Interactive mode)」と「アンビエントモード(Ambient mode)」の2つのモードがあります。


◆ インタラクティブモード

インタラクティブモードは、Android Wearにおける通常のモードで、画面に触れたり、デバイスを傾けたりするとこのモードに移行します。


  • Watch Faceのすべての機能を利用できます。

  • すべての色を利用できます。


◆ アンビエントモード

アンビエントモードは、いわゆる省電力モードで、インタラクティブモードの状態から一定時間が経つとこのモードに移行します。


  • 更新間隔は1分となります。

  • 色はグレースケールのみを利用します。


3.2.3. Low-Bitの制限と焼き付き防止

Android Wearのデバイスによっては、アンビエントモード時にアンチエイリアスを無効にしたり、焼き付き防止の工夫をしたりする必要があります。


◆ Low-Bitの制限を必要とする時


  • 色はグレースケールではなく、白黒のみとします。

  • アンチエイリアスを無効にします


◆ 焼き付き防止を必要とする時


  • 画像はなるべく輪郭のみに(黒の領域を可能な限り多く占めるように)します。

  • ディスプレイの端から数ピクセルにはなるべく描画しないようにします。


3.3. タイムゾーンが変更された時の処理を行う、BroadcastReceiverの派生クラスを作成

BroadcastReceiverクラスを継承した派生クラスを、AnalogWatchFaceServiceクラスの外で作成します。

Action<Intent>型のデリゲート1つを定義し、オーバーライドしたOnReceiveメソッドで呼び出すように構成します。


TimeZoneRecieverクラスの定義

// タイムゾーンを変更した時に通知を受け取るレシーバーを提供します。

public class TimeZoneReceiver : BroadcastReceiver {

// タイムゾーンを変更した通知を受け取った時に実行するデリゲートを表します。
private Action<Intent> receiver;

// OnReceiveメソッドで実行する処理を
public TimeZoneReceiver( Action<Intent> _receiver ) {
receiver = _receiver;
}

// タイムゾーンを変更した通知を受け取った時に実行します。
public override void OnReceive( Context context, Intent intent ) {
receiver?.Invoke( intent );
}
}



3.4. コンストラクターを定義

AnalogWatchFaceEngineクラスのコンストラクターを定義します。CanvasWatchFaceService型1つを引数4にとり、基底クラスのコンストラクターの引数に渡します。

コンストラクターの中では、時刻を更新する時の処理を行うハンドラーと、タイムゾーンを変更した時に通知を受け取るレシーバーの初期化をします。


AnalogWatchFaceEngineクラスのコンストラクター

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(
// BroadcastReceiver.OnReceiveメソッドの実行時に実行します。
intent => {
// 新しいタイムゾーンを設定します。
nowTime.TimeZone = Java.Util.TimeZone.Default;
}
);
}



ShouldTimerBeRunningプロパティの定義

// UpdateTimeHandlerを動作させるかどうかを表す値を取得します。

private bool ShouldTimerBeRunning =>
IsVisible && !IsInAmbientMode;


3.4.1. タイムゾーンの変更方法

※変数名は「nowTime」であるとします。


AndroidのTimeクラスの場合

timeZoneReceiver = new TimeZoneReceiver(

intent => {
// GetStringExtraメソッドで、Android Wearとペアリングしているスマートフォンで設定したタイムゾーンのIDを取得します。
nowTime.Clear( intent.GetStringExtra( "time-zone" ) );
nowTime.SetToNow();
}
);


JavaのCalendarクラスの場合

timeZoneReceiver = new TimeZoneReceiver(

intent => {
// TimeZone.Defaultプロパティは、Android Wearとペアリングしているスマートフォンで設定しているタイムゾーンのIDを取得します。
// もちろん、AndroidのTimeクラスのように、GetStringExtraメソッドで取得したタイムゾーンのIDを設定してもOK
nowTime.TimeZone = Java.Util.TimeZone.Default;
}
);


.NETのDateTime構造体の場合

timeZoneReceiver = new TimeZoneReceiver(

intent => {
// DateTime.Nowプロパティで取得する時刻は、Android Wearとペアリングしているスマートフォンで
// 設定しているタイムゾーンが適用されています。
nowTime = DateTime.Now;
}
);


Engineクラスでメソッドのオーバーライド

Vol.2 : プログラム作成編(Page-2)に続きます。


参考サイト


「XamarinでAndroid WearのWatch Faceアプリを作ってみよう!」シリーズ一覧





  1. WallpaperServiceクラスを利用する時に必要です。 



  2. CanvasWatchFaceService.Engineクラスは、WallpaperService.Engineを継承しています。 



  3. Android.Text.Format.Timeクラスでは、2032年までしか扱えないためです。 



  4. Watch Faceアプリにて、実際に受け取っているのは、AnalogWatchFaceServiceクラスのインスタンスです。