Chronoir_net.Chronica.WatchfaceExtensionを、実際のWatch Faceアプリのプログラムで使ってみる

本記事は、[初心者さん・学生さん大歓迎!] Xamarin その2 Advent Calendar 2016の21日目の記事です。

前日 : Xamarinを始めてみよう – aodag6 by aodag

明日 : Xamarinハンズオンの感想とxamlを使わないレイアウトの覚書 by aokym

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

今回は、先日私が作成したライブラリ「Chronoir_net.Chronica.WatchfaceExtension」を、実際のウォッチフェイスアプリのプログラムで使用してみたいと思います。

1. 本ライブラリでできること(Ver. 1.0.1の時点)

Chronoir_net.Chronica.WatchfaceExtensionの開発目的は、XamarinでAndroid Wearのウォッチフェイスアプリを開発する時の不便なところを解消し、開発しやすくなるようにサポートすることです。

  1. タイムゾーンやバッテリーの状態などの変更の通知を受け取るために、BroadcastReceiverの派生クラスを一々作成しなくても済むようになります。
  2. Android API 23以降で推奨されているContextCompat.GetColorメソッドの戻り値(int型)から、PaintクラスのColorプロパティで使用するColor型へ簡単に変換できます。
  3. AndroidのTimeクラス、JavaのCalenderクラス、.NETのDateTime構造体のインスタンスを指定するだけで、アナログ時計の時針、分針、秒針の先端のXY座標を簡単に求めることができます。

2. Chronoir_net.Chronica.WatchfaceExtensionのインストール

Chronoir_net.Chronica.WatchfaceExtensionはNuGet Galleryで公開しているので、NuGetパッケージマネージャーからインストールします。

検索ボックスに「Chronoir_net.Chronica.WatchfaceExtension」と入力して(※記事執筆時点では、「AndroidWear」でもすぐに見つかります)、検索結果からChronoir_net.Chronica.WatchfaceExtensionを選択し、インストールボタンを押してインストールします。

◆ ライブラリ名変更について

ウォッチフェイスアプリ開発用ツールキットのブランド名の決定に伴い、Chronoir_net.Chronoface.UtilityはChronoir_net.Chronica.WatchfaceExtensionに変更しました。

コード上での変更点は名前空間のみで、クラス名やメソッド名などに変更はありません。

Chronoir_net.Chronoface.Utilityは後日削除予定です。

3. Chronoir_net.Chronica.WatchfaceExtensionを使う

今回は、「XamarinでAndroid WearのWatch Faceアプリを作ってみよう!(Vol.2 : プログラム作成編)」( → Gist )で作成したアナログウォッチフェイスで使用してみます。

3.1. 名前空間の追加

ソースコードの上部に以下のusingディレクティブを追加します。

using Chronoir_net.Chronica.WatchfaceExtension;

3.2. タイムゾーン変更通知用のレシーバーの実装をリファクタリング

// 中略
public class AnalogWatchFaceService : CanvasWatchFaceService {
	
	// 中略

	private class AnalogWatchFaceEngine : CanvasWatchFaceService.Engine {

		// 中略

		private TimeZoneReceiver timeZoneReceiver;

		private bool timeZoneReceiverRegistered = false;

		public AnalogWatchFaceEngine( CanvasWatchFaceService owner ) : base( owner ) {
			
			// 中略

			timeZoneReceiver = new TimeZoneReceiver(
				intent => {
					nowTime.TimeZone = Java.Util.TimeZone.Default;
				}
			);
		}

		// 中略
		
		public override void OnVisibilityChanged( bool visible ) {
			base.OnVisibilityChanged( visible );

			if( visible ) {
				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();
		}

		// 中略
	}

	// 中略
}

public class TimeZoneReceiver : BroadcastReceiver {

	private Action<Intent> receiver;

	public TimeZoneReceiver( Action<Intent> _receiver ) {
		receiver = _receiver;
	}

	public override void OnReceive( Context context, Intent intent ) {
		receiver?.Invoke( intent );
	}
}

◆ Chronoir_net.Chronica.WatchfaceExtensionを使った、タイムゾーンの変更通知用レシーバーの実装

TimeZoneReceiverクラスの定義と、AnalogWatchFaceEngineのメンバー「timeZoneReceiverRegistered」を削除し、AnalogWatchFaceEngineのメンバー「timeZoneReceiver」の型をTimeZoneReceiverクラスからActionExecutableBroadcastReceiverクラスに変更します。

// AnalogWatchFaceEngineクラスのメンバー
private TimeZoneReceiver timeZoneReceiver;
private bool timeZoneReceiverRegistered = false;

// -------------------------------------------------------------------------

// レシーバークラスの定義
public class TimeZoneReceiver : BroadcastReceiver {

	private Action<Intent> receiver;

	public TimeZoneReceiver( Action<Intent> _receiver ) {
		receiver = _receiver;
	}

	public override void OnReceive( Context context, Intent intent ) {
		receiver?.Invoke( intent );
	}
}

private ActionExecutableBroadcastReceiver timeZoneReceiver;

AnalogWatchFaceEngineのコンストラクター及び、OnVisibilityChangedメソッドの「if( timeZoneReceiver == null )」の中で呼び出している、インスタンスの初期化にて、型をTimeZoneReceiverからActionExecutableBroadcastReceiverに変更し、コンストラクターの第2引数に「Intent.ActionTimezoneChanged」を指定します。

timeZoneReceiver = new TimeZoneReceiver(
	intent => {
		nowTime.TimeZone = Java.Util.TimeZone.Default;
	}
);

timeZoneReceiver = new ActionExecutableBroadcastReceiver(
	intent => {
		nowTime.TimeZone = Java.Util.TimeZone.Default;
	},
	Intent.ActionTimezoneChanged
);

OnVisibilityChangedメソッドにて、レシーバーのApplication.Contextへの登録及び削除は、ActionExecutableBroadcastReceiverRegisterToContextメソッド及びUnregisterFromContextメソッドを呼び出して行います。

Application.Contextへの登録状態はライブラリの中で判別するので、利用側では単にメソッドを呼ぶだけでOK!

if( visible ) {
	// 中略
	
	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;
	}
}

if( visible ) {
	// 中略
	
	timeZoneReceiver.RegisterToContext();

	nowTime = Java.Util.Calendar.GetInstance( Java.Util.TimeZone.Default );
}
else {
	timeZoneReceiver.UnregisterFromContext();
}

変更後のコードを以下に示します。

// 中略
public class AnalogWatchFaceService : CanvasWatchFaceService {
	
	// 中略

	private class AnalogWatchFaceEngine : CanvasWatchFaceService.Engine {

		// 中略

		private ActionExecutableBroadcastReceiver timeZoneReceiver;

		public AnalogWatchFaceEngine( CanvasWatchFaceService owner ) : base( owner ) {
			
			// 中略

			timeZoneReceiver = new ActionExecutableBroadcastReceiver(
				intent => {
					nowTime.TimeZone = Java.Util.TimeZone.Default;
				},
				Intent.ActionTimezoneChanged
			);
		}

		// 中略
		
		public override void OnVisibilityChanged( bool visible ) {
			base.OnVisibilityChanged( visible );

			if( visible ) {
				if( timeZoneReceiver == null ) {
					timeZoneReceiver = new ActionExecutableBroadcastReceiver(
						intent => {
							nowTime.TimeZone = Java.Util.TimeZone.Default;
						},
						Intent.ActionTimezoneChanged
					);
				}
				timeZoneReceiver.RegisterToContext();

				nowTime = Java.Util.Calendar.GetInstance( Java.Util.TimeZone.Default );
			}
			else {
				timeZoneReceiver.UnregisterFromContext();
			}

			UpdateTimer();
		}

		// 中略
	}

	// 中略
}

◆ もう1つの方法

ActionExecutableBroadcastReceiverクラスの代わりにEventExecutableBroadcastReceiverクラスで実装するすこともできます。

その場合、BroadcastReceiverのOnReceiveメソッドで呼び出されるデリゲート処理を、BroadcastedIntentRecievedイベントハンドラーに追加します。

timeZoneReceiver = new EventExecutableBroadcastReceiver(
	Intent.ActionTimezoneChanged
);

timeZoneReceiver.BroadcastedIntentRecieved += ( context, intent ) => {
	nowTime.TimeZone = Java.Util.TimeZone.Default;
}

◆ 他のレシーバーの例

new ActionExecutableBroadcastReceiver(
	intent => {
		// バッテリーの残量を取得します。
		int batteryValue = intent.GetIntExtra( BatteryManager.ExtraLevel, 0 );
		// 電源への接続状態を取得します。
		BatteryStatus status = ( BatteryStatus )intent.GetIntExtra( BatteryManager.ExtraStatus, 1 );
		bool isCharging = status == BatteryStatus.Charging || status == BatteryStatus.Full;
	},
	Intent.ActionBatteryChanged
);

3.3. カラーリソースの取得を簡単に

今までは、リソースから色の値を取得する時、CanvasWatchFaceServiceのResources.GetColorメソッドを使用していました。


// ※ownerはCanvasWatchFaceService型とします。

Paint backgroundPaint = new Paint();
backgroundPaint.Color = owner.Resources.GetColor( Resource.Color.background );

しかし、Android API 23からそのメソッドは非推奨となり、代わりにAndroid.Support.V4.ContentのContextCompat.GetColorメソッドが推奨されています。

// ※ownerはCanvasWatchFaceService型とします。
// 注 : このコードはコンパイルエラーになります。

Paint backgroundPaint = new Paint();
backgroundPaint.Color = ContextCompat.GetColor( owner, Resource.Color.background );

ところが、そのメソッドの戻り値はColor型でなく、ARGB値を格納したint型整数なので、PaintクラスのColorプロパティに直接代入することができません。(運命は残酷だ・・・)

そのため、一旦int型変数に格納し、ビット演算とColor.Argbメソッドを使って、Color型に変換する必要があります。

// ※ownerはCanvasWatchFaceService型とします。

Paint backgroundPaint = new Paint();
int argb = ContextCompat.GetColor( owner, Resource.Color.background );
backgroundPaint.Color = Color.Argb( ( argb >> 24 ) & 0xFF, ( argb >> 16 ) & 0xFF, ( argb >> 8 ) & 0xFF, argb & 0xFF );

そこで、WatchfaceUtilityクラスConvertARGBToColorメソッドを利用することで、ARGB値を格納したint型整数をColor型へ簡単に変換することができます。

// ※ownerはCanvasWatchFaceService型とします。

Paint backgroundPaint = new Paint();
paint.Color = WatchfaceUtility.ConvertARGBToColor( ContextCompat.GetColor( owner, Resource.Color.background ) );

では、OnCreateメソッド内のコードをリファクタリングしてみます。

var resources = owner.Resources;

backgroundPaint = new Paint();
backgroundPaint.Color = resources.GetColor( Resource.Color.background );

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;

minuteHandPaint = new Paint();
minuteHandPaint.Color = hourHandPaint.Color;
minuteHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.minute_hand_stroke );
minuteHandPaint.AntiAlias = true;
minuteHandPaint.StrokeCap = Paint.Cap.Round;

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;

var resources = owner.Resources;

backgroundPaint = new Paint();
backgroundPaint.Color = WatchfaceUtility.ConvertARGBToColor( ContextCompat.GetColor( owner, Resource.Color.background ) );

hourHandPaint = new Paint();
hourHandPaint.Color = resources.GetColor( Resource.Color.analog_hands );
hourHandPaint.StrokeWidth = WatchfaceUtility.ConvertARGBToColor( ContextCompat.GetColor( owner, Resource.Color.analog_hands ) );
hourHandPaint.AntiAlias = true;
hourHandPaint.StrokeCap = Paint.Cap.Round;

minuteHandPaint = new Paint();
minuteHandPaint.Color = hourHandPaint.Color;
minuteHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.minute_hand_stroke );
minuteHandPaint.AntiAlias = true;
minuteHandPaint.StrokeCap = Paint.Cap.Round;

secondHandPaint = new Paint();
secondHandPaint.Color = WatchfaceUtility.ConvertARGBToColor( ContextCompat.GetColor( owner, Resource.Color.analog_second_hand ) );
secondHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.second_hand_stroke );
secondHandPaint.AntiAlias = true;
secondHandPaint.StrokeCap = Paint.Cap.Round;

◆ Javaの場合

Javaの場合、PaintクラスのGetColornメソッドの戻り値がColor型、SetColorメソッドの引数がint型と、getとsetで異なる型で設計されていますが、C#の場合、プロパティの仕様上、残念ながらそれができないのです。

SetColorメソッドを独自に追加するという手もありますが、今度はXamarin.Androidのライブラリとしての規則(JavaでGet~/ Set~で対になっているメソッドは、C#ではプロパティで定義する)から外れるという、別の問題が・・・・・・、う~ん、難しいところです。

3.3. アナログ時計の針の描画処理をシンプル&簡単に

アナログ時計では、OnDrawメソッド内で、現在時刻から針の角度、先端のXY座標を求め、直線を描画しています。

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

	nowTime = Java.Util.Calendar.GetInstance( nowTime.TimeZone );

	// 中略

	float hourHandLength = centerX - 80;
	float minuteHandLength = centerX - 40;
	float secondHandLength = centerX - 20;

	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 );

	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 ) {
		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 );
	}
}

上記の例では、JavaのCalendarクラスを使用していますが、これを.NETのDateTime構造体に変えるとしましょう。すると、OnDrawメソッド内の彼方此方にあるCalendar.GetメソッドをHour、Minute、Secondプロパティに変更する必要があり、ちょっと面倒ですね。

そこで、本ライブラリのAnalogHandStrokeを継承したクラスを利用します。

AnalogWatchFaceEngineのメンバー「hourHandPant」、「minuteHandPant」、「secondHandPant」の型をそれぞれPaintクラスからHourAnalogHandStrokeMinuteAnalogHandStrokeSecondAnalogHandStrokeに変更し、さらに変数名から「Paint」を取り除きます(※今回扱うコードの場合、Visual Studioのリファクタリング機能は利用しません)。

private Paint hourHandPaint;
private Paint minuteHandPaint;
private Paint secondHandPaint;

private HourAnalogHandStroke hourHand;
private MinuteAnalogHandStroke minuteHand;
private SecondAnalogHandStroke secondHand;

OnCreateメソッドにて、Paintクラスの新しいインスタンスを代入している変数の前にvarまたはPaintを追加し、ローカルのPaint型変数にします。

Paintクラスでの初期化処理の後に、先ほどのAnalogHandStrokeの変数を初期化する文を追加し、コンストラクターの引数にPaint型変数をそれぞれ指定します。

var resources = owner.Resources;

// 中略

hourHandPaint = new Paint();
hourHandPaint.Color = resources.GetColor( Resource.Color.analog_hands );
hourHandPaint.StrokeWidth = WatchfaceUtility.ConvertARGBToColor( ContextCompat.GetColor( owner, Resource.Color.analog_hands ) );
hourHandPaint.AntiAlias = true;
hourHandPaint.StrokeCap = Paint.Cap.Round;

minuteHandPaint = new Paint();
minuteHandPaint.Color = hourHandPaint.Color;
minuteHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.minute_hand_stroke );
minuteHandPaint.AntiAlias = true;
minuteHandPaint.StrokeCap = Paint.Cap.Round;

secondHandPaint = new Paint();
secondHandPaint.Color = WatchfaceUtility.ConvertARGBToColor( ContextCompat.GetColor( owner, Resource.Color.analog_second_hand ) );
secondHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.second_hand_stroke );
secondHandPaint.AntiAlias = true;
secondHandPaint.StrokeCap = Paint.Cap.Round;

var resources = owner.Resources;

// 中略

var hourHandPaint = new Paint();
hourHandPaint.Color = resources.GetColor( Resource.Color.analog_hands );
hourHandPaint.StrokeWidth = WatchfaceUtility.ConvertARGBToColor( ContextCompat.GetColor( owner, Resource.Color.analog_hands ) );
hourHandPaint.AntiAlias = true;
hourHandPaint.StrokeCap = Paint.Cap.Round;
hourHand = new HourAnalogHandStroke( hourHandPaint );

var minuteHandPaint = new Paint();
minuteHandPaint.Color = hourHandPaint.Color;
minuteHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.minute_hand_stroke );
minuteHandPaint.AntiAlias = true;
minuteHandPaint.StrokeCap = Paint.Cap.Round;
minuteHand = new MinuteAnalogHandStroke( minuteHandPaint );

var secondHandPaint = new Paint();
secondHandPaint.Color = WatchfaceUtility.ConvertARGBToColor( ContextCompat.GetColor( owner, Resource.Color.analog_second_hand ) );
secondHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.second_hand_stroke );
secondHandPaint.AntiAlias = true;
secondHandPaint.StrokeCap = Paint.Cap.Round;
secondHand = new SecondAnalogHandStroke( secondHandPaint );

コンストラクターで指定したPaintクラスのインスタンスは、AnalogHandStorokeのメンバーのPaintプロパティにセットされます。

OnAmbientModeChangedメソッドにあるアンチエリアスの設定処理では、そのPaintプロパティを通して行います。

hourHandPaint.AntiAlias = antiAlias;
minuteHandPaint.AntiAlias = antiAlias;
secondHandPaint.AntiAlias = antiAlias;

hourHand.Paint.AntiAlias = antiAlias;
minuteHand.Paint.AntiAlias = antiAlias;
secondHand.Paint.AntiAlias = antiAlias;

OnDrawメソッドでは、AnalogHandStrokeのLengthプロパティに針の長さをセットし、SetTimeメソッドの引数にCalendarクラス変数を指定して、針の先端のXY座標を求めます。XYプロパティからXY座標、PaintプロパティからPaintクラスのインスタンスを取得し、時針、分針、秒針を描画します。

nowTime = Java.Util.Calendar.GetInstance( nowTime.TimeZone );

// 中略

float hourHandLength = centerX - 80;
float minuteHandLength = centerX - 40;
float secondHandLength = centerX - 20;

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 );

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 ) {
	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 );
}

nowTime = Java.Util.Calendar.GetInstance( nowTime.TimeZone );

// 中略

hourHand.Length = centerX - 80;
minuteHand.Length = centerX - 40;
secondHand.Length = centerX - 20;

hourHand.SetTime( nowTime );
canvas.DrawLine( centerX, centerY, centerX + hourHand.X, centerY + hourHand.Y, hourHand.Paint );

minuteHand.SetTime( nowTime );
canvas.DrawLine( centerX, centerY, centerX + minuteHand.X, centerY + minuteHand.Y, minuteHand.Paint );

if( !isAmbient ) {
	secondHand.SetTime( nowTime );
	canvas.DrawLine( centerX, centerY, centerX + secondHand.X, centerY + secondHand.Y, secondHand.Paint );
}

リファクタリング後のコードは、Gistにて公開しています。

4. Choroface / Chronicaシリーズの今後のロードマップ

最近、Chronoir.netの新たなプロジェクトして始めた「Chronoface(クロノフェイス)」及び「Chronica(クロニカ)」ですが、今後は、

  • ウォッチフェイスアプリ開発用のテンプレートの作成
  • ウォッチフェイスアプリを作成し、Google Playにリリース

をしていきたいと思います。

テンプレートについては、まず、Wearアプリ単体のプロジェクトテンプレート(※)をリリースし、最終的には設定用のActivityやコンパニオン用のモバイルアプリのひな形、専用のウィザードなどを統合したVSIXパッケージを作っていきたいなと思います。

(※)プロジェクトを作成した時点のコードで、エミュレーターや実機でウォッチフェイスアプリを実行できる形で提供します。2017年1月にリリース予定です。

それでは、See you next!

コメント

タイトルとURLをコピーしました