Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

本記事は、Vol.2 : プログラム作成編(Page-1)の続きです。

3. Watch FaceのEngineクラスを作成(続き)

ここからは、AnalogWatchFaceEngineクラスのメソッドを実装していきます。

3.5. OnCreateメソッドのオーバーライド

OnCreateメソッドは、AnalogWatchFaceEngineのインスタンスが生成された時に実行します。
ここでは主に、以下の処理を行います。

  • リソースから画像の読み込み
  • Paintなどのグラフィックスオブジェクトを生成
  • 時刻を格納するオブジェクトの初期化
  • ウォッチフェイスのスタイル(ステータスアイコンややOK Googleの表示など)を設定
OnCreateメソッドの実装
public override void OnCreate( ISurfaceHolder holder ) {

    // ウォッチフェイスのスタイルを設定します。
    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 );
}

3.5.1. 無慈悲なDeprecation

Android.Content.Res.Resources.GetColorメソッドは、Android SDK Level 23以降では非推奨となっており、代わりにAndroid.Support.V4.Content.ContextCompat.GetColorメソッドが推奨されています。

しかし、そのメソッドの戻り値はColor型でなく、ARGB値を格納したint型であり、PaintオブジェクトのColorプロパティ(Color型)に代入するには、一工夫が必要です。

リソースから背景色の読み込み
// 非推奨となった方法
backgroundPaint.Color = owner.Resources.GetColor( Resource.Color.background );

// 代わりに推奨された方法
int argb = ContextCompat.GetColor( owner, Resource.Color.background );
backgroundPaint.Color = Color.Argb( ( argb >> 24 ) & 0xFF, ( argb >> 16 ) & 0xFF, ( argb >> 8 ) & 0xFF, argb & 0xFF );

// 注 : ContextCompat.GetColorメソッドの戻り値を、Paint.Colorプロパティに直接代入できません。
backgroundPaint.Color = ContextCompat.GetColor( owner, Resource.Color.background );

3.6. OnDestroyメソッドのオーバーライド

OnDestroyメソッドは、AnalogWatchFaceEngineのインスタンスが破棄される時1に実行します。

OnDestroyメソッドの実装
public override void OnDestroy() {
    // UpdateTimeHandlerにセットされているメッセージを削除します。
    updateTimeHandler.RemoveMessages( MessageUpdateTime );

    base.OnDestroy();
}

3.7. OnApplyWindowInsetsメソッドのオーバーライド

OnApplyWindowInsetsメソッドは、現在のWindowInsetsを適用する時に実行します。

例えば、Android Wearデバイスのウィンドウの形状(丸形 or 四角形)を判別する時に利用します。

OnApplyWindowInsetsメソッドの実装
public override void OnApplyWindowInsets( WindowInsets insets ) {
    base.OnApplyWindowInsets( insets );

    // Android Wearのウィンドウが丸形かどうかを判別します。
    //bool isRound = insets.IsRound;
}

3.8. OnPropertiesChangedメソッドのオーバーライド

OnPropertiesChangedメソッドは、Android Wearデバイスのプロパティが定められた時に実行します。

主に、Low-Bit制限及び焼き付き防止を必要とするかどうかのフラグを設定します。

OnPropertiesChangedメソッドの実装
public override void OnPropertiesChanged( Bundle properties ) {
    base.OnPropertiesChanged( properties );

    // アンビエントモード時、Low-Bit制限を必要とするかどうかの値を取得します。
    isRequiredLowBitAmbient = properties.GetBoolean( PropertyLowBitAmbient, false );
    // アンビエントモード時、焼き付き防止を必要とするかどうかの値を取得します。
    isReqiredBurnInProtection = properties.GetBoolean( PropertyBurnInProtection, false );
}

3.9. OnTimeTickメソッドのオーバーライド

OnTimeTickメソッドは、Android Wearのモードにかかわらず、1分ごとに実行します。

OnTimeTickメソッドの実装
public override void OnTimeTick() {
    base.OnTimeTick();

    // ウォッチフェイスを再描画します。
    Invalidate();
}

3.10. OnAmbientModeChangedメソッドのオーバーライド

OnAmbientModeChangedメソッドは、Android Wearデバイスで「インタラクティブモード」と「アンビエントモード」を切り替えた時に実行します。

引数のinAmbientModeから、アンビエントモードかどうかを判別します。

OnAmbientModeChangedメソッドの実装
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();
    }
}
UpdateTimerメソッド
private void UpdateTimer() {
    // UpdateTimeHandlerからMessageUpdateTimeメッセージを取り除きます。
    updateTimeHandler.RemoveMessages( MessageUpdateTime );
    // UpdateTimeHandlerを動作させるかどうかを判別します。
    if( ShouldTimerBeRunning ) {
        // UpdateTimeHandlerにMessageUpdateTimeメッセージをセットします。
        updateTimeHandler.SendEmptyMessage( MessageUpdateTime );
    }
}

3.11. OnInterruptionFilterChangedメソッドのオーバーライド

OnInterruptionFilterChangedメソッドは、Interruptionフィルターが変更された時に実行します。

主に、Android Wearデバイスの通知のON / OFF状態を判別する時に利用します。

OnInterruptionFilterChangedメソッドの実装
public override void OnInterruptionFilterChanged( int interruptionFilter ) {
    base.OnInterruptionFilterChanged( interruptionFilter );

    // InterruptionフィルターがInterruptionFilterNoneであるかどうか判別します。
    bool inMuteMode = ( interruptionFilter == InterruptionFilterNone );

    // ミュートモードが変更されたかどうか判別します。
    if( isMute != inMuteMode ) {
        isMute = inMuteMode;

        // ウォッチフェイスを再描画します。
        Invalidate();
    }
}
InterruptionFilterの値 通知
InterruptionFilterAll(1) すべての通知を表示します。
InterruptionFilterPriority(2) 優先度の高い通知のみ表示します。
InterruptionFilterNone(3) すべての通知を表示しません。
InterruptionFilterAlarm(4) アラームの通知のみ表示します。
InterruptionFilterUnknown(0) Interruptionフィルターが利用できません。

3.12. OnTapCommandメソッドのオーバーライド

OnTapCommandメソッドは、ユーザーが画面をタップした時に実行します。

※このメソッドを実行するには、WatchFaceStyle.Builderにて、SetAcceptsTapEventsメソッドの引数にtrueを指定し、タップイベントを有効にする必要があります。
※このメソッドは、Android Wear 1.3以上に対応しています。

引数 概要
tapType タップの種類
xValue タップした位置のX座標
yValue タップした位置のY座標
eventTime タップイベントが発生している時間(ミリ秒)
OnTapCommandメソッドの実装
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;
    }
}

3.13. OnDrawメソッドのオーバーライド

OnDrawメソッドは、現在時刻を取得し、Watch Faceの画面を描画します。

※このメソッド内の処理は、できるだけ短く終えるようにし、リソースの読み込みやグラフィックスオブジェクトの生成などは、なるべくOnCreateメソッドで行うようにします。

引数 概要
canvas 画面に描画するためのキャンパスオブジェクト
bounds 画面のサイズなどを格納するオブジェクト
OnDrawメソッドの実装
public override void OnDraw( Canvas canvas, Rect bounds ) {
    // 中略
}

3.13.1. 現在時刻を取得

AndroidのTimeクラスの場合
nowTime.SetToNow();
JavaのCalendarクラスの場合
nowTime = Java.Util.Calendar.GetInstance( nowTime.TimeZone );
.NETのDateTime構造体の場合
nowTime = DateTime.Now;

3.13.2. 背景を描画

画面全体を背景色で塗りつぶし、前回描画した内容を上書きします。

インタラクティブモードの時は、リソースから読み込んだ背景色で描画し、アンビエントモードの時は、黒色で塗りつぶします。

DrawColorメソッドは、画面全体を指定した色で塗りつぶします。
DrawRectメソッドは、指定した左上と右下のXY座標とPaintオブジェクトからなる矩形を描画します。

背景を描画
// アンビエントモードであるかどうか判別します。
if( IsInAmbientMode ) {
    // アンビエントモードの時は、黒色で塗りつぶします。
    canvas.DrawColor( Color.Black );
}
else {
    // そうでない時は、背景画像を描画します。
    canvas.DrawRect( 0, 0, canvas.Width, canvas.Height, backgroundPaint );
}

3.13.3. 時針、分針、秒針を描画

秒針は、1秒あたり$2\pi / 60$ [rad]なので、$s$秒の時の秒針の角度$\theta_s$は、以下の式で求められます。

\theta_s = s \times \pi / 30

分針も同様に、$m$分の時の分針の角度$\theta_m$は、以下の式で求められます。

\theta_m = m \times \pi / 30

時針は、1時間あたり$2\pi / 6$ [rad]ですが、時の値$h$だけでなく分の値$m$も考慮して、角度$\theta_h$を求めていきます。

\theta_h = ( ( h + m / 60 ) \times \pi / 6

12時の位置を基準に、現在の針の角度を$\theta$ [rad]、針の長さを$l$とすると、針の先端のXY座標は以下の式で求められます。

x = l \times \sin( \theta )
y = -l \times \cos( \theta )

あとは、DrawLineメソッドで描画します。

時針、分針、秒針を描画
// 中心の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 );
}

3.14. OnVisibilityChangedメソッドのオーバーライド

OnVisibilityChangedメソッドは、Android Wearデバイスの画面の表示・非表示が切り替わった時に実行します。

※Android Wearデバイスの設定で「常に画面表示」をOFFにした時、インタラクティブモードで一定時間が経つと、一旦アンビエントモードに移行してから画面を消灯します。従って、OnVisibilityChangedメソッドを実行する前にOnAmbientModeChangedメソッドを実行します。(逆に、画面消灯からインタラクティブモードへの移行では、OnVisibilityChangedメソッドの実行後にOnAmbientModeChangedメソッドを実行します。)

OnVisibilityChangedメソッドの実装
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();
}

4. 完成後のコード

完成後のコードを以下に示します。

※コメント付きのバージョンはGistにて公開しています。

MyWatchFace.cs(完成後)
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 {

    [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 {

        private static readonly long InteractiveUpdateRateMilliseconds = Java.Util.Concurrent.TimeUnit.Seconds.ToMillis( 1 );

        private const int MessageUpdateTime = 0;

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

        private class AnalogWatchFaceEngine : CanvasWatchFaceService.Engine {

            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;

            private bool isRequiredLowBitAmbient;

            private bool isReqiredBurnInProtection;

            private bool isMute;

            private TimeZoneReceiver timeZoneReceiver;

            private bool timeZoneReceiverRegistered = false;

            public AnalogWatchFaceEngine( CanvasWatchFaceService owner ) : base( owner ) {
                this.owner = owner;
                updateTimeHandler = new Handler(
                    message => {
                        switch( message.What ) {
                            case MessageUpdateTime:
                                Invalidate();
                                if( ShouldTimerBeRunning ) {
                                    long timeMillseconds = DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond;
                                    long delayMilliseconds = InteractiveUpdateRateMilliseconds - ( timeMillseconds % InteractiveUpdateRateMilliseconds );
                                    updateTimeHandler.SendEmptyMessageDelayed( MessageUpdateTime, delayMilliseconds );
                                }
                                break;
                        }
                    }
                );

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

            public override void OnCreate( ISurfaceHolder holder ) {

                SetWatchFaceStyle(
                    new WatchFaceStyle.Builder( owner )
                        .SetCardPeekMode( WatchFaceStyle.PeekModeShort )
                        .SetBackgroundVisibility( WatchFaceStyle.BackgroundVisibilityInterruptive )
                        .SetShowSystemUiTime( false )
                        .Build()
                );

                base.OnCreate( holder );

                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;

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

            public override void OnDestroy() {
                updateTimeHandler.RemoveMessages( MessageUpdateTime );

                base.OnDestroy();
            }

            public override void OnApplyWindowInsets( WindowInsets insets ) {
                base.OnApplyWindowInsets( insets );

                //bool isRound = insets.IsRound;
            }

            public override void OnPropertiesChanged( Bundle properties ) {
                base.OnPropertiesChanged( properties );

                isRequiredLowBitAmbient = properties.GetBoolean( PropertyLowBitAmbient, false );
                isReqiredBurnInProtection = properties.GetBoolean( PropertyBurnInProtection, false );
            }

            public override void OnTimeTick() {
                base.OnTimeTick();

                Invalidate();
            }

            public override void OnAmbientModeChanged( bool inAmbientMode ) {
                base.OnAmbientModeChanged( inAmbientMode );

                if( isAmbient != inAmbientMode ) {
                    isAmbient = inAmbientMode;
                    if( isRequiredLowBitAmbient ) {
                        bool antiAlias = !inAmbientMode;

                        hourHandPaint.AntiAlias = antiAlias;
                        minuteHandPaint.AntiAlias = antiAlias;
                        secondHandPaint.AntiAlias = antiAlias;
                        Invalidate();
                    }
                    UpdateTimer();
                }
            }

            public override void OnInterruptionFilterChanged( int interruptionFilter ) {
                base.OnInterruptionFilterChanged( interruptionFilter );

                bool inMuteMode = ( interruptionFilter == InterruptionFilterNone );

                if( isMute != inMuteMode ) {
                    isMute = inMuteMode;

                    Invalidate();
                }
            }

            public override void OnTapCommand( int tapType, int xValue, int yValue, long eventTime ) {
                switch( tapType ) {
                    case TapTypeTouch:
                        break;
                    case TapTypeTouchCancel:
                        break;
                    case TapTypeTap:
                        break;
                }
            }

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

                float centerX = bounds.Width() / 2.0f;
                float centerY = bounds.Height() / 2.0f;

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

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

            private void UpdateTimer() {
                updateTimeHandler.RemoveMessages( MessageUpdateTime );
                if( ShouldTimerBeRunning ) {
                    updateTimeHandler.SendEmptyMessage( MessageUpdateTime );
                }
            }

            private bool ShouldTimerBeRunning =>
                IsVisible && !IsInAmbientMode;
        }
    }

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

Watch Faceアプリの基本的なプログラムの作成は以上です。お疲れ様でした。

今回作成したプログラムをエミュレーターやAndroid Wearデバイス上で実行すると、以下のようなアナログ時計を表示します。

今回作成したアナログ時計

Next

次回は、背景画像やアナログ針の画像を描画し、よりリッチなWatch Faceアプリを作っていこうと思います。

それでは、See you next!

参考サイト

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


  1. 例えば、現在のウォッチフェイスから別のウォッチフェイスに切り替えた時に呼び出されます。 

nia_tn1012
湘南生まれのITエンジニア(サーバー・フロント・DB・インフラ(クラウド))です。主にC#、PHP、TypeScript、GO言語、Docker、gRPCを使っています。好物は紅茶とコーヒー、シラス丼、趣味は写真撮影と音ゲーです。よろしくお願いします!
https://chronoir.net/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした