rksoftware

Visual Studio とか C# とかが好きです

Xamarin.Forms でオリジナルタイマーアプリを作ろう(2)

この記事は からの続きです。

10.タイマーページの作成

画面遷移後の画面を作成します。
これまで作ってきた MainPage と同様に、xaml、xaml.cs による View および ViewModel クラスを作成します。

View の作成

CountDownPage ビュークラスを作成します。

MyTimer プロジェクト(共有コードプロジェクト)に TimerSettings クラスを作成します。
クラスの作成方法は [背景画像の表示] で学んでいます。今回は少し違う部分がありますが、基本的な流れは同じです。思い出しながら作成してください。

Mac の場合
・ソリューションエクスプローラー上の [MyTimer] 上で二本指タップ (右クリック) します。
・[追加 > 新しいファイル] を選択します。
・[新しいファイル] ウィンドウで [Forms > フォーム ContentPage Xaml] を選択します。
・[名前:] に CountDownPage と入力します
・[新規] をクリックします。

Windows の場合
・ソリューションエクスプローラー上の [MyTimer] 上で二本指タップ (右クリック) します。
・[追加 > 新しい項目] を選択します。
・[新しい項目の追加 - MyTimer] ウィンドウの左のペインで [インストール済み > Visual C# アイテム > Xamarin.Forms] を選択します。
・右のペインで [ContentPage] を選択します。
・[名前:] に CountDownPage と入力します。
・[追加] をクリックします。

ViewModel の作成

MyTimer プロジェクト (共有コードプロジェクト) に CountDownPageViewModel クラスを作成します。
クラスの作成方法は [背景画像の表示] で学んでいます。思い出しながら作成してください。

現在のカウントのデータバインディング

カウント画面の現在のカウント (カウントダウン表示) をデータバインディングします。
カウント中の残り時間は ViewModel のプロパティとして保持します。表示は View になります。
データバインディングについては [データバインディング] で学んでいます。思い出しながら次のコードで View、ViewModel それぞれを上書きしてください。

・CountDownPageViewModel.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xamarin.Forms;

namespace MyTimer
{
    // タイマーカウント画面の ViewModel
    class CountDownPageViewModel : BindableBase
    {
        // カウントの残り時間
        private TimeSpan _time;
        public TimeSpan Time
        {
            get { return _time; }
            private set { SetProperty(ref _time, value); }
        }
    }
}

・CountDownPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MyTimer"
             x:Class="MyTimer.CountDownPage"
             xmlns:vm="clr-namespace:MyTimer"
             Title="カウントダウン">
    <!-- ViewModel を設定、構築 -->
    <ContentPage.BindingContext>
        <vm:CountDownPageViewModel/>
    </ContentPage.BindingContext>
    <!-- ページの表示内容 -->
    <ContentPage.Content>
        <Grid>
            <!-- 背景画像 -->
            <Image Source="{local:ImageResource MyTimer.Resources.Background.png}" Aspect="AspectFit" />
            <!-- 文字が背景に埋もれないよう、背景画像に半透明色を乗せる -->
            <Grid BackgroundColor="#80FFFFFF"/>

            <!-- 残り時間の表示 -->
            <Label Text="{Binding Time, StringFormat='{0:mm\\:ss\\.fff}'}"
                HorizontalOptions="Center" VerticalOptions="Center"
                FontSize="96" TextColor="Black" FontAttributes="Bold"/>
            <!-- Back ボタン -->
            <Button Text=" &lt; Back "
                HorizontalOptions="Start" VerticalOptions="Start"
                BackgroundColor="#80808080"/>
        </Grid>
    </ContentPage.Content>
</ContentPage>

戻る遷移

カウント画面からタイマー設定画面 (メイン画面・最初の画面) に戻る処理は、タイマーをスタートした時と同様に、Viewに記述します。
ページを戻るには Navigation オブジェクトの Pop~ メソッドを呼び出します。Pop~ メソッドは複数ありますが、このページへの遷移をするときの Push~ と対になるものを使えば OK です。
今回は PushModalAsync メソッドで遷移したので PopModalAsync でを使用します。

// タイマー設定画面へ遷移する
this.Navigation.PopModalAsync();

また、View へ遷移した時と同様に戻る処理も View に記述します。そのため今回もメッセージングを使用します。メッセージングについては [メッセージング] で、 画面遷移については [画面遷移] で学んでいます。思い出しながら次のコードで View、ViewModel のコードを上書きしてください。

・CountDownPageViewModel.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xamarin.Forms;

namespace MyTimer
{
    // タイマーカウント画面の ViewModel
    class CountDownPageViewModel : BindableBase
    {
        // カウントの残り時間
        private TimeSpan _time;
        public TimeSpan Time
        {
            get { return _time; }
            private set { SetProperty(ref _time, value); }
        }

        // 戻るボタンが押された
        public Command GoBackCommand { get; }

        // 画面を閉じ、カウント設定画面へ戻る
        private void GoBack()
        {
            // 画面を「戻る」メッセージを送信
            MessagingCenter.Send(this, "GoBack");
        }

        // コンストラクタ
        public CountDownPageViewModel()
        {
            // コマンドの設定
            // readonly プロパティの初期化は、コンストラクタ内でも行える
            GoBackCommand = new Command(GoBack);
        }
    }
}

・CountDownPage.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace MyTimer
{
    // タイマーカウントダウン画面
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class CountDownPage : ContentPage
    {
        // コンストラクタ
        public CountDownPage ()
        {
            InitializeComponent ();
        }

        // 画面が表示されたタイミングでの処理
        private void CountDownPageAppearing(object sender, EventArgs e)
        {
            // 戻るメッセージを購読する
            MessagingCenter.Subscribe<CountDownPageViewModel>(this, "GoBack", GoBack);
        }

        // 画面が表示されなくなったタイミングでの処理
        private void CountDownPageDisappearing(object sender, EventArgs e)
        {
            // メッセージの購読を解除する
            MessagingCenter.Unsubscribe<CountDownPageViewModel>(this, "GoBack");
        }

        // 画面を閉じ、タイマー設定画面へ戻る
        private void GoBack<T>(T sender)
        {
            // タイマー設定画面へ遷移する
            this.Navigation.PopModalAsync();
        }
    }
}

・CountDownPage.xaml.cs

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MyTimer"
             x:Class="MyTimer.CountDownPage"
             xmlns:vm="clr-namespace:MyTimer"
             Appearing="CountDownPageAppearing"
             Disappearing="CountDownPageDisappearing"
             Title="カウントダウン">
    <!-- ViewModel を設定、構築 -->
    <ContentPage.BindingContext>
        <vm:CountDownPageViewModel/>
    </ContentPage.BindingContext>
    <!-- ページの表示内容 -->
    <ContentPage.Content>
        <Grid>
            <!-- 背景画像 -->
            <Image Source="{local:ImageResource MyTimer.Resources.Background.png}" Aspect="AspectFit" />
            <!-- 文字が背景に埋もれないよう、背景画像に半透明色を乗せる -->
            <Grid BackgroundColor="#80FFFFFF"/>

            <!-- 残り時間の表示 -->
            <Label Text="{Binding Time, StringFormat='{0:mm\\:ss\\.fff}'}"
                HorizontalOptions="Center" VerticalOptions="Center"
                FontSize="96" TextColor="Black" FontAttributes="Bold"/>
            <!-- Back ボタン -->
            <Button Text=" &lt; Back " Command="{Binding GoBackCommand}"
                HorizontalOptions="Start" VerticalOptions="Start"
                BackgroundColor="#80808080"/>
        </Grid>
    </ContentPage.Content>
</ContentPage>

確認

デバッグ実行し、[タイマー開始] で二つ目の View に遷移し、[< Back] で一つ目の画面に戻れたらこのステップは完了です。

・一つ目の View
f:id:rksoftware:20180520125858j:plain

・二つ目のView
f:id:rksoftware:20180520125918j:plain

11.タイマー処理

このアプリのメイン機能である時間をカウントするためのタイマー処理を作成します。
Xamarin.Forms では Xamarin.Forms.Device クラスの StartTimer メソッドでタイマーを実装できます。

時間計測機能の作成

タイマーを定期的に実行すれば、その実行回数とタイマー間隔で経過時間がわかるという考えもあるかもしれません。あまり精度を必要としないアプリであれば、それで構いません。しかしタイマーの実行間隔の精度はそれほど高くありません。
正確に時間を計りたい場合は、System.Diagnostics.Stopwatch クラスが利用できます。
ただし、Stopwatch クラスにはタイマーのように定期的に何らかの処理を起動する機能はありませんので、これらのタイマーとストップウォッチを組み合わせて実装します。

タイマーの実行

CountDownPageViewModel クラスに Stopwatch オブジェクトをフィールドとして保持します。
次のコードを CountDownPageViewModel.cs に追加してください。

// タイマーカウント用のストップウォッチ
private System.Diagnostics.Stopwatch _stopwatch = new System.Diagnostics.Stopwatch();

ストップウォッチとタイマーを起動します。
次のコードを CountDownPageViewModel.cs のコンストラクタ public CountDownPageViewModel() に追加してください。

// ストップウォッチをスタート
_stopwatch.Start();
// カウントダウン更新タイマーをスタート
Xamarin.Forms.Device.StartTimer(TimeSpan.FromMilliseconds(33), OnTimerTick);

タイマー間隔ごとの処理

タイマーを使ったコードでは一定間隔ごとに設定したメソッドが呼ばれます。
一定間隔ごとに実行されるメソッドを作成します。
次のコードを CountDownPageViewModel.cs に追加してください。

// 画面表示を更新
// カウントダウン中の残り時間の画面表示を更新する
// タイマー処理のたびに呼び出される
private void UpdateTime()
{
    var time = Math.Max(0, (TimerSettings.Instance.CountMilliseconds - _stopwatch.ElapsedMilliseconds));
    Console.WriteLine(time);
    Time = TimeSpan.FromMilliseconds(time);
}

// タイマー処理
// 毎回のタイマーイベントの処理
private bool OnTimerTick()
{
    // 残り時間を更新
    UpdateTime();
    // 時間が残っている場合、タイマーを続行
    if (Time.TotalMilliseconds > 0)
        return true;

    // 時間が経過しきった場合、一回だけ動かすタイマーをスタートし本タイマーは終了する
    Xamarin.Forms.Device.StartTimer(TimeSpan.FromMilliseconds(100),OnTimerEnd);
    return false;
}

UpdateTime メソッド内で遷移前の View で設定したパラメータを参照していることも確認しておきましょう。

設定時間経過時の処理

カウントダウンが終了した後に最後に一度だけ実行する処理を作成します。
今回のアプリにはカウントダウン終了後に音声で通知する機能があります。音声を再生する処理は後の手順で作成しますので、ここでは空のメソッドだけ用意しておきます。
次のコードを CountDownPageViewModel.cs に追加してください。

// 時間が経過しきった後の、一度だけ動かすタイマーの処理
private bool OnTimerEnd()
{
    // タイマー設定を確認し
    // テキストの読み上げ、または音声ファイルの再生を行う
    if (TimerSettings.Instance.UseSpeechText)
        TextToSpeech();
    else
        PlayAudio();
    return false;
}

// テキストの読み上げ
private async void TextToSpeech()
{
}

// 音声ファイルの再生
private void PlayAudio()
{
}

ここまでの手順で CountDownPageViewModel.cs のコードは次のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xamarin.Forms;

namespace MyTimer
{
    // タイマーカウント画面の ViewModel
    class CountDownPageViewModel : BindableBase
    {
        // タイマーカウント用のストップウォッチ
        private System.Diagnostics.Stopwatch _stopwatch = new System.Diagnostics.Stopwatch();

        // カウントの残り時間
        private TimeSpan _time;
        public TimeSpan Time
        {
            get { return _time; }
            private set { SetProperty(ref _time, value); }
        }

        // 戻るボタンが押された
        public Command GoBackCommand { get; }

        // 画面を閉じ、カウント設定画面へ戻る
        private void GoBack()
        {
            // 画面を「戻る」メッセージを送信
            MessagingCenter.Send(this, "GoBack");
        }

        // コンストラクタ
        public CountDownPageViewModel()
        {
            // コマンドの設定
            // readonly プロパティの初期化は、コンストラクタ内でも行える
            GoBackCommand = new Command(GoBack);

            // ストップウォッチをスタート
            _stopwatch.Start();
            // カウントダウン更新タイマーをスタート
            Xamarin.Forms.Device.StartTimer(TimeSpan.FromMilliseconds(33), OnTimerTick);
        }

        // 画面表示を更新
        // カウントダウン中の残り時間の画面表示を更新する
        // タイマー処理のたびに呼び出される
        private void UpdateTime()
        {
            var time = Math.Max(0, (TimerSettings.Instance.CountMilliseconds - _stopwatch.ElapsedMilliseconds));
            Console.WriteLine(time);
            Time = TimeSpan.FromMilliseconds(time);
        }

        // タイマー処理
        // 毎回のタイマーイベントの処理
        private bool OnTimerTick()
        {
            // 残り時間を更新
            UpdateTime();
            // 時間が残っている場合、タイマーを続行
            if (Time.TotalMilliseconds > 0)
                return true;

            // 時間が経過しきった場合、一回だけ動かすタイマーをスタートし本タイマーは終了する
            Xamarin.Forms.Device.StartTimer(TimeSpan.FromMilliseconds(100), OnTimerEnd);
            return false;
        }

        // 時間が経過しきった後の、一度だけ動かすタイマーの処理
        private bool OnTimerEnd()
        {
            // タイマー設定を確認し
            // テキストの読み上げ、または音声ファイルの再生を行う
            if (TimerSettings.Instance.UseSpeechText)
                TextToSpeech();
            else
                PlayAudio();
            return false;
        }

        // テキストの読み上げ
        private async void TextToSpeech()
        {
        }

        // 音声ファイルの再生
        private void PlayAudio()
        {
        }

    }
}

12.DependencyService

タイマーアプリの機能としてタイマー設定時間が経過したことを音で伝えます。
音声ファイルの再生などのプラットフォームごとの機能は、Xamarin.Forms では機能が用意されていません (Xamarin.Android や Xamarin.iOS では対応しています) 。
このような機能は DependencyService を使うことで簡単に実装できます。

DependencyService 概要

プロジェクト構成上、プラットフォーム機能は MyTimer.Android や MyTimer.iOS といったプラットフォーム毎のプロジェクトに作成します。そしてそれら作成した処理は、MyTimer プロジェクトといった共有コードのプロジェクトから呼び出して起動します。
しかしプロジェクト間の依存の方向として、共有コードプロジェクトからプラットフォームのプロジェクトを参照することはできません。この制約を越えるための手段の一つが DependencyService です。

共有コードプロジェクト上の実装

共有コードプロジェクト (MyTimer プロジェクト) にインターフェイスを作成します。今回は DependencyService を使って画面の自動スリープを止める機能を作成するので、それを前提とした名前で作成して行きます。

インターフェイスの作成方法はクラスや View の作成と基本的に同じ流れになります。思い出しながら作成してください。

Mac の場合
・ソリューションエクスプローラー上の [MyTimer] 上で二本指タップ (右クリック) します。
・[追加 > 新しいファイル] を選択します。
・[新しいファイル] ウィンドウで [General > 空のインターフェイス] を選択します。
・[名前:] に IKeepScreenOn と入力します。
・[新規] をクリックします。

Windows の場合
・ソリューションエクスプローラー上の [MyTimer] 上で二本指タップ(右クリック)します。
・[追加 > 新しい項目] を選択します。
・[新しい項目の追加 - MyTimer] ウィンドウの左のペインで [インストール済み > Visual C# アイテム > コード] を選択します。
・右のペインで [インターフェイス] を選択します。
・[名前:] に IKeepScreenOn と入力します。
・[追加] をクリックします。

共通
機能を呼び出すメソッドを定義します。今回は画面スリープを止める機能の ON/OFF を設定する Set メソッドを定義します。
次のコードで IKeepScreenOn.cs を上書きしてください。

using System;
using System.Collections.Generic;
using System.Text;

namespace MyTimer
{
    // / KeepScreenOn の DependencyService 用のインターフェイス
    public interface IKeepScreenOn
    {
        // KeepScreenOn を設定
        void Set(bool keepOn);
    }
}

Android プロジェクト上の実装

Android プロジェクト (MyTimer.Android または MyTimer.Droid) に実装クラスを作成します。
※注意※ これまでの MyTimer プロジェクトではない点に注意してください
クラスの作成方法は [背景画像の表示] で学んでいます。思い出しながら作成してください。
※最初の二本指タップ(右クリック)が、ソリューションエクスプローラー上の [MyTimer.Android] または [MyTimer.Droid] になる点に注意してください

  • KeppScreenOn クラス

作成した KeppScreenOnIKeepScreenOn を実装します。
次のコードを KeppScreenOn.cs ファイルのクラス名の宣言を次のように書き換えます。

// KeepScreenOn の Android の実装
class KeppScreenOn : IKeepScreenOn

インターフェイスで定義されたメソッドも実装します。ここではまだ機能は作成しないので空の実装です。
次のコードを KeppScreenOn クラスに追加します。

// KeepScreenOn を設定
public void Set(bool keepOn){ ; }

DependencyService の実装クラスは assembly
KeppScreenOn.cs ファイルの namespace 指定の前に追加します。

// Dependency Service に Android の実装を登録
[assembly: Xamarin.Forms.Dependency(typeof(MyTimer.Droid.KeppScreenOn))]

ここまでの KeppScreenOn.cs のコードは次のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;

// Dependency Service に Android の実装を登録
[assembly: Xamarin.Forms.Dependency(typeof(MyTimer.Droid.KeppScreenOn))]
namespace MyTimer.Droid
{
    // KeepScreenOn の Android の実装
    class KeppScreenOn : IKeepScreenOn
    {
        // KeepScreenOn を設定
        public void Set(bool keepOn){ ; }
    }
}

iOS プロジェクト上の実装

iOS プロジェクト (MyTimer.iOS) に実装クラスを作成します。
※注意※ これまでの MyTimer プロジェクトではない点に注意してください
クラスの作成方法は [背景画像の表示] で学んでいます。思い出しながら作成してください。
※最初の二本指タップ(右クリック)が、ソリューションエクスプローラー上の [MyTimer.iOS] になる点に注意してください

  • KeppScreenOn クラス

作成した KeppScreenOnIKeepScreenOn を実装します。次のコードを KeppScreenOn.cs ファイルのクラス名の宣言を次のように書き換えます。

// KeepScreenOn の iOS の実装
class KeppScreenOn : IKeepScreenOn

インターフェイスで定義されたメソッドも実装します。ここではまだ機能は作成しないので空の実装です。
次のコードを KeppScreenOn クラスに追加します。

// KeepScreenOn を設定
public void Set(bool keepOn){ ; }

DependencyService の実装クラスは assembly 属性の設定も必要です。
次のコードを KeppScreenOn.cs ファイルの namespace 指定の前に追加します。

// Dependency Service に iOS の実装を登録
[assembly: Xamarin.Forms.Dependency(typeof(MyTimer.iOS.KeppScreenOn))]

ここまで手順で KeppScreenOn.cs は次のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Foundation;
using UIKit;

// Dependency Service に iOS の実装を登録
[assembly: Xamarin.Forms.Dependency(typeof(MyTimer.iOS.KeppScreenOn))]
namespace MyTimer.iOS
{
    // KeepScreenOn の iOS の実装
    class KeppScreenOn : IKeepScreenOn
    {
        // KeepScreenOn を設定
        public void Set(bool keepOn){ ; }
    }
}

機能の実行

共有コードプロジェクト (MyTimer プロジェクト) からプラットフォームのプロジェクトの KeppScreenOn クラスを呼び出します。
Xamarin.Forms.DependencyService クラスの Get<T>() メソッドで先ほど作成したクラスを呼び出せます。

共有コードプロジェクト (MyTimer プロジェクト) の CountDownPageViewModel クラスのコンストラクターに次のコードを追加します。

// 画面をスリープしない設定
DependencyService.Get<IKeepScreenOn>().Set(true);

CountDownPageViewModel クラスのコンストラクターは次のようになります。

// コンストラクタ
public CountDownPageViewModel()
{
    // コマンドの設定
    // readonly プロパティの初期化は、コンストラクタ内でも行える
    GoBackCommand = new Command(GoBack);

    // ストップウォッチをスタート
    _stopwatch.Start();
    // カウントダウン更新タイマーをスタート
    Xamarin.Forms.Device.StartTimer(TimeSpan.FromMilliseconds(33), OnTimerTick);

    // 画面をスリープしない設定
    DependencyService.Get<IKeepScreenOn>().Set(true);
}

13.画面スリープしないようにする

スマートデバイスには触らないでいると自動で画面 OFF になる機能があり、一般的に端末の所有者はこの設定を行っています。
しかし、今回作成しているアプリはタイマーアプリです。この画面が自動で OFF になってはあまり使い勝手の良いものではありません。

DependencyService

アプリ起動中に画面が自動で OFF にならないようにするにはプラットフォーム毎の個別の実装が必要です。ここで、前ページの DependencyService が利用できます。

共通コードプロジェクト上の実装

前ページの手順で既に完了しています。

Android プロジェクト上の実装

Android で画面スリープを OFF にするのは少し厄介です。理由は設定に Activity の参照が必要となっているためです。Android の過去のバージョンでは Activity を取得する static メソッドが用意されていましたが現在は非推奨となっています (Xamarin ではなく Android の SDK です) 。
今回は起動時に Activity の参照を記録する方針で作成します。

Android プロジェクト (MyTimer.Android または MyTimer.Droid) の KeppScreenOn クラスに Activity を保存するフィールドとメソッドを作成します。
※注意※ iOS プロジェクトにも同名のクラスがあります。間違えないよう注意してください

次のコードを KeppScreenOn クラスに追加してください。

// Activity を static フィールドに確保
private static Activity _mainActivity;
public static void SetActivity(Activity activity) => _mainActivity = activity;

以前の手順で空実装にしておいた Set メソッドを実装します。
次のコードで KeppScreenOn クラスの Set メソッドを上書きしてください。

// KeepScreenOn を設定
public void Set(bool keepOn) => _mainActivity.RunOnUiThread(() =>
{
    if (keepOn)
        _mainActivity.Window.AddFlags(WindowManagerFlags.KeepScreenOn);
    else
        _mainActivity.Window.ClearFlags(WindowManagerFlags.KeepScreenOn);
});

KeppScreenOn.cs は次のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;

// Dependency Service に Android の実装を登録
[assembly: Xamarin.Forms.Dependency(typeof(MyTimer.Droid.KeppScreenOn))]
namespace MyTimer.Droid
{
    // KeepScreenOn の Android の実装
    class KeppScreenOn : IKeepScreenOn
    {
        // Activity を static フィールドに確保
        private static Activity _mainActivity;
        public static void SetActivity(Activity activity) => _mainActivity = activity;

        // KeepScreenOn を設定
        public void Set(bool keepOn) => _mainActivity.RunOnUiThread(() =>
        {
            if (keepOn)
                _mainActivity.Window.AddFlags(WindowManagerFlags.KeepScreenOn);
            else
                _mainActivity.Window.ClearFlags(WindowManagerFlags.KeepScreenOn);
        });
    }
}

Android プロジェクトでは MainActivity クラスへもコード追加が必要になります。
Android プロジェクトの MainActivity.cs 内の MainActivity クラスの OnCreate メソッドに次のコードを追加します。追加する位置は global::Xamarin.Forms.Forms.Init(this, bundle); の次の行です。

KeppScreenOn.SetActivity(this);

MainActivity.cs は次のようになります。

using System;

using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;

namespace MyTimer.Droid
{
    [Activity(Label = "MyTimer", Icon = "@drawable/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation, ScreenOrientation = ScreenOrientation.Landscape)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle bundle)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(bundle);

            global::Xamarin.Forms.Forms.Init(this, bundle);
            KeppScreenOn.SetActivity(this);
            LoadApplication(new App());
        }
    }
}

iOS プロジェクト上の実装

iOS は Android と比べて手順が少なくて済みます。

以前の手順で空実装にしておいた Set メソッドを実装します。
次のコードで KeppScreenOn クラスの Set メソッドを上書きしてください。
※注意※ Android プロジェクトにも同名のクラスがあります。間違えないよう注意してください

// KeepScreenOn を設定
public void Set(bool keepOn) => UIApplication.SharedApplication.InvokeOnMainThread(() =>
                UIApplication.SharedApplication.IdleTimerDisabled = keepOn);

KeppScreenOn.cs は次のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Foundation;
using UIKit;

// Dependency Service に iOS の実装を登録
[assembly: Xamarin.Forms.Dependency(typeof(MyTimer.iOS.KeppScreenOn))]
namespace MyTimer.iOS
{
    // KeepScreenOn の iOS の実装
    class KeppScreenOn : IKeepScreenOn
    {
        // KeepScreenOn を設定
        public void Set(bool keepOn) => UIApplication.SharedApplication.InvokeOnMainThread(() =>
                        UIApplication.SharedApplication.IdleTimerDisabled = keepOn);
    }
}

14.TextToSpeech

タイマー設定時間の経過を音声で知らせる機能を実装します。
スマートデバイスに設定したテキストをしゃべらせる機能は、プラットフォーム毎の機能になり Xamrin.Forms の機能にはありません。前述の DependencyService を利用することで自身で実装することもできますが、今回は NuGet からライブラリを取得し利用します。

NuGet の概要

パッケージ管理のサービスです。
世界中の開発者が多くのライブラリを登録してくれています。今回のテキストをしゃべる機能のように、良く使いそうな機能の多くは NuGet 上でライブラリが見つかります。
今回利用する TextToSpeech もその一つです。作者に感謝しながらプロジェクトに追加しましょう。

TextToSpeech の追加 (Mac の場合)

・ソリューションエクスプローラー上の [MyTimer] プロジェクト上で二本指タップ (右クリック) します。
・[追加 > NuGet パッケージの追加] を選択します。
f:id:rksoftware:20180520120050j:plain

・検索ボックスに xam.plugins.texttospeech と入力します。
・検索結果欄で Xam.Plugins.TextToSpeech を選択します。
・[パッケージを追加] ボタンをクリックします。
f:id:rksoftware:20180520120108j:plain

・[ライセンスの同意] ダイアログで [同意する] をクリックします。

同様の手順で、[MyTimer.Droid]、[MyTimer.iOS] プロジェクトにもパッケージを追加します。

TextToSpeech の追加 (Windows の場合)

・ソリューションエクスプローラー上の [MyTimer] 上で二本指タップ (右クリック) します。
※プロジェクトではなくソリューションを二本指タップ (右クリック) することに注意してください
・[ソリューションの NuGet パッケージの管理] を選択します。
f:id:rksoftware:20180520120132j:plain

・[参照] タブを選択します。
・検索ボックスに xam.plugins.texttospeech と入力します。
・検索結果欄で Xam.Plugins.TextToSpeech を選択します。
・右ペインのチェックボックスを全て ([MyTimer] [MyTimer\MyTimer.Android~] [MyTimer\MyTimer.iOS~]) ON にします。
・[インストール] ボタンをクリックします。
f:id:rksoftware:20180520120152j:plain

・[変更のプレビュー] ダイアログで [OK] ボタンをクリックします。

TextToSpeech の利用

TextToSpeech を利用するには、Plugin.TextToSpeech.CrossTextToSpeech クラスの static プロパティ Current で取得できるオブジェクトの Speak メソッドを実行するだけです。しゃべるテキストはメソッドの引数で指定します。
今回のしゃべるテキストは、全画面から渡されたパラメーターの中に含まれています。

CountDownPageViewModel クラスの TextToSpeech メソッドを次のコードで上書きしてください。

// テキストの読み上げ
private async void TextToSpeech()
{
    await Plugin.TextToSpeech.CrossTextToSpeech.Current.Speak(TimerSettings.Instance.SpeechText, volume:0.5f);
}

15.AudioPlayer

タイマー設定時間の経過を音声で知らせる機能を実装します。
音声ファイルを再生する機能は、プラットフォーム毎の機能になり Xaamrin.Forms の機能にはありません。前述の DependencyService を利用することで自身で実装することもできますが、今回は TextToSpeech と同様に NuGet からライブラリを取得し利用します。

AudioPlayer の追加

NuGet パッケージの追加は [TextToSpeech] で学んでいます。思い出しながら Xam.Plugin.SimpleAudioPlayer を追加してください。

AudioPlayer の利用

AudioPlayer を利用するには、Plugin.SimpleAudioPlayer.CrossSimpleAudioPlayer クラスの static プロパティ Current で取得できるオブジェクトの Load メソッドと Play メソッドをメソッドを実行するだけです。再生する音声データはLoadメソッドの引数で設定します。
今回再生する音声ファイルは [背景画像の表示] の手順ですでに追加しています。

CountDownPageViewModel クラスの PlayAudio メソッドを次のコードで上書きしてください。

// 音声ファイルの再生
private void PlayAudio()
{
    try
    {
        var player = Plugin.SimpleAudioPlayer.CrossSimpleAudioPlayer.Current;
        var assembly = typeof(App).Assembly;
        var name = "MyTimer.Resources.voice.m4a";
        // name 確認
        if (!IsContainsResource(assembly, name)) return;
        using (var stream = assembly.GetManifestResourceStream(name))
            player.Load(stream);
        player.Play();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }
}

※ファイル名の指定などでミスをしやすいためエラーハンドリングなどを行っているためコードが長くなっています。コードは後日お時間のある時にゆっくり確認してみてください

音声ファイル名の指定は特に間違えやすくエラーもわかりにくいため専用のチェックメソッドを作成します。
次のコードを CountDownPageViewModel クラスに追加してください。

// 音声ファイルのリソース名指定に誤りがないかチェックをする
private bool IsContainsResource(System.Reflection.Assembly assembly, string name)
{
    if (assembly.GetManifestResourceNames().Contains(name)) return true;
    Console.WriteLine($"name : {name} はリソースに存在しません。");
    Console.WriteLine("リソースに含まれるファイル:");
    foreach (var resoureName in assembly.GetManifestResourceNames())
        Console.WriteLine(resoureName);
    return false;
}

ここまでの手順で CountDownPageViewModel.cs は次のようになっています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xamarin.Forms;

namespace MyTimer
{
    // タイマーカウント画面の ViewModel
    class CountDownPageViewModel : BindableBase
    {
        // タイマーカウント用のストップウォッチ
        private System.Diagnostics.Stopwatch _stopwatch = new System.Diagnostics.Stopwatch();

        // カウントの残り時間
        private TimeSpan _time;
        public TimeSpan Time
        {
            get { return _time; }
            private set { SetProperty(ref _time, value); }
        }

        // 戻るボタンが押された
        public Command GoBackCommand { get; }

        // 画面を閉じ、カウント設定画面へ戻る
        private void GoBack()
        {
            // 画面を「戻る」メッセージを送信
            MessagingCenter.Send(this, "GoBack");
        }

        // コンストラクタ
        public CountDownPageViewModel()
        {
            // コマンドの設定
            // readonly プロパティの初期化は、コンストラクタ内でも行える
            GoBackCommand = new Command(GoBack);

            // ストップウォッチをスタート
            _stopwatch.Start();
            // カウントダウン更新タイマーをスタート
            Xamarin.Forms.Device.StartTimer(TimeSpan.FromMilliseconds(33), OnTimerTick);

            // 画面をスリープしない設定
            DependencyService.Get<IKeepScreenOn>().Set(true);
        }

        // 画面表示を更新
        // カウントダウン中の残り時間の画面表示を更新する
        // タイマー処理のたびに呼び出される
        private void UpdateTime()
        {
            var time = Math.Max(0, (TimerSettings.Instance.CountMilliseconds - _stopwatch.ElapsedMilliseconds));
            Console.WriteLine(time);
            Time = TimeSpan.FromMilliseconds(time);
        }

        // タイマー処理
        // 毎回のタイマーイベントの処理
        private bool OnTimerTick()
        {
            // 残り時間を更新
            UpdateTime();
            // 時間が残っている場合、タイマーを続行
            if (Time.TotalMilliseconds > 0)
                return true;

            // 時間が経過しきった場合、一回だけ動かすタイマーをスタートし本タイマーは終了する
            Xamarin.Forms.Device.StartTimer(TimeSpan.FromMilliseconds(100), OnTimerEnd);
            return false;
        }

        // 時間が経過しきった後の、一度だけ動かすタイマーの処理
        private bool OnTimerEnd()
        {
            // タイマー設定を確認し
            // テキストの読み上げ、または音声ファイルの再生を行う
            if (TimerSettings.Instance.UseSpeechText)
                TextToSpeech();
            else
                PlayAudio();
            return false;
        }

        // テキストの読み上げ
        private async void TextToSpeech()
        {
            await Plugin.TextToSpeech.CrossTextToSpeech.Current.Speak(TimerSettings.Instance.SpeechText, volume: 0.5f);
        }

        // 音声ファイルの再生
        private void PlayAudio()
        {
            try
            {
                var player = Plugin.SimpleAudioPlayer.CrossSimpleAudioPlayer.Current;
                var assembly = typeof(App).Assembly;
                var name = "MyTimer.Resources.voice.m4a";
                // name 確認
                if (!IsContainsResource(assembly, name)) return;
                using (var stream = assembly.GetManifestResourceStream(name))
                    player.Load(stream);
                player.Play();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }
        }

        // 音声ファイルのリソース名指定に誤りがないかチェックをする
        private bool IsContainsResource(System.Reflection.Assembly assembly, string name)
        {
            if (assembly.GetManifestResourceNames().Contains(name)) return true;
            Console.WriteLine($"name : {name} はリソースに存在しません。");
            Console.WriteLine("リソースに含まれるファイル:");
            foreach (var resoureName in assembly.GetManifestResourceNames())
                Console.WriteLine(resoureName);
            return false;
        }

    }
}

実行 - 完成

これでアプリは完成です。
実行してタイマーアプリとして動作することを確認しましょう!

16.オリジナルタイマーアプリにしてみよう

これでタイマーアプリは完成です。まずは少し動かして自分の手でアプリを作った感動を味わいましょう!

独自カスタマイズ

ひとまず動かしてみたら今度は独自のカスタマイズを加えてオリジナルアプリにしてみましょう!

カスタマイズポイント

次のカスタマイズポイントは簡単にカスタマイズでき高い自分のアプリ感を味わえるポイントです。

  • 背景画像を変更してみよう!
  • 再生する音声を変更してみよう!

時間があれば

自分の使い方に合わせてカスタマイズをしてみましょう。

  • 良く計る時間のある方は、例えば 3 分・4 分・5 分といった決まった時間を 1 タップで設定できる機能を作ってみましょう。
  • 正確な時間間隔を鍛えたい方は、秒だけでなくミリ秒まで設定できるようにしてみましょう。
  • タイマーのカウント中に途中経過を知りたい方は、途中経過を音声で知らせてくれる機能を作ってみましょう。

よいスマートデバイスアプリ開発を!