Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
C#とXamarin.Androidで作る、Android Wearのウォッチフェイスアプリのプログラムです。(ライブラリ「Chronoir_net.Chronoface.Utility」を利用)
<?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;
using Chronoir_net.Chronoface.Utility;
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 HourAnalogHandStroke hourHand;
/// <summary>
/// 分針用のオブジェクトを表します。
/// </summary>
private MinuteAnalogHandStroke minuteHand;
/// <summary>
/// 秒針用のオブジェクトを表します。
/// </summary>
private SecondAnalogHandStroke secondHand;
/// <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 ActionExecutableBroadcastReceiver timeZoneReceiver;
/// <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 = new ActionExecutableBroadcastReceiver(
intent => {
nowTime.TimeZone = Java.Util.TimeZone.Default;
},
Intent.ActionTimezoneChanged
);
}
/// <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 = WatchfaceUtility.ConvertARGBToColor( ContextCompat.GetColor( owner, Resource.Color.background ) );
// 時針用のPaintグラフィックスオブジェクトを生成します。
var hourHandPaint = new Paint();
hourHandPaint.Color = WatchfaceUtility.ConvertARGBToColor( ContextCompat.GetColor( owner, Resource.Color.analog_hands ) );
// 時針の幅を設定します。
hourHandPaint.StrokeWidth = resources.GetDimension( Resource.Dimension.hour_hand_stroke );
// アンチエイリアスを有効にします。
hourHandPaint.AntiAlias = true;
// 線端の形は丸形を指定します。
hourHandPaint.StrokeCap = Paint.Cap.Round;
// 時針のオブジェクトを初期化します。
hourHand = new HourAnalogHandStroke( hourHandPaint );
// 分針用のPaintグラフィックスオブジェクトを生成します。
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 );
// 秒針用のPaintグラフィックスオブジェクトを生成します。
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 );
// 時刻を格納するオブジェクトを生成します。
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オブジェクトのアンチエイリアスを無効にし、
// そうでなければ有効にします。
hourHand.Paint.AntiAlias = antiAlias;
minuteHand.Paint.AntiAlias = antiAlias;
secondHand.Paint.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;
// 針の長さを求めます。
hourHand.Length = centerX - 80;
minuteHand.Length = centerX - 40;
secondHand.Length = centerX - 20;
// 時針の先端のXY座標を求めます。
hourHand.SetTime( nowTime );
// 時針を描画します。
canvas.DrawLine( centerX, centerY, centerX + hourHand.X, centerY + hourHand.Y, hourHand.Paint );
// 分針の先端のXY座標を求めます。
minuteHand.SetTime( nowTime );
// 分針を描画します。
canvas.DrawLine( centerX, centerY, centerX + minuteHand.X, centerY + minuteHand.Y, minuteHand.Paint );
// アンビエントモードでないかどうかを判別します。
if( !isAmbient ) {
// 秒針の先端のXY座標を求めます。
secondHand.SetTime( nowTime );
// 分針を描画します。
canvas.DrawLine( centerX, centerY, centerX + secondHand.X, centerY + secondHand.Y, secondHand.Paint );
}
}
/// <summary>
/// ウォッチフェイスの表示・非表示が切り替わった時に実行します。
/// </summary>
/// <param name="visible">ウォッチフェイスの表示・非表示</param>
public override void OnVisibilityChanged( bool visible ) {
base.OnVisibilityChanged( visible );
// ウォッチフェイスの表示・非表示を判別します。
if( visible ) {
// TimeZoneReceiverが未初期化の時、ここで初期化します。
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();
}
/// <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;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.