Android Studio + kotlin + SQLiteで音符フラッシュカードアプリを作る　⑨タイムカウンターの実装（handler）

連載も半ばに差し掛かり、若干モチベーションが落ち気味でしたが、先日初めてフォローしていただけたので、俄然執筆意欲が湧きました！
あと半分、頑張って更新していきます。


#１．今回のテーマ

前回は回答ボタンを押したときの処理を実装しました。
今回は、タイムカウンターの実装です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619303/4c474602-70c1-92e4-aa80-c1a71e2237f4.png)

xmlの定義は普通のTextViewです。

```xml:activity_game
    <TextView
        android:id="@+id/tv_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:layout_marginLeft="120dp"
        android:text="00:00.0"
        android:textSize="48dp"/>
```

idは「**tv_time**」です。


最初にコード全量を載せておきます。

```kotlin:GameActivity.kt
class GameActivity : AppCompatActivity() {

    //タイマー用初期処理
    val handler = Handler()
    var timeValue = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_game)

        //タイマー開始
        //テキストとボタンの変数定義
        val timeText = findViewById<TextView>(R.id.tv_time)

        /* 100msごとに処理を実行する */
        val runnable = object : Runnable {
            override fun run() {
                timeValue++
                // TextViewを更新
                // ?.letを用いて、nullではない場合のみ更新
                timeToText(timeValue)?.let{
                    // timeToText(timeValue)の値がlet内ではitとして使える
                    timeText.text = it
                }
                handler.postDelayed(this, 100)
            }
        }
        handler.post(runnable)

    // 数値を00:00.00形式の文字列に変換する関数
    // 引数timeにはデフォルト値0を設定、返却する型はnullableなString?型
    private fun timeToText(time: Int = 0): String? {
        return if (time < 0) {
            null
        } else if (time == 0) {
            "00:00.0"
        } else {
            val m = time / 600
            val s = time % 600 / 10
            val ms = time % 10
            "%1$02d:%2$02d.%3$01d".format(m,s,ms)
        }
    }
```

##２．実装方法

「xxミリ秒後に処理を動かしたい」という場合は、**Handler**クラスの**postDelayed**というメソッドを使います。
TimerやTimerTaskを使って実装する方法もあるのですが、今回のような1画面内で単純にカウントアップするタイマーを実装するだけの場合は、Handlerを使った実装方法の方が簡潔に書けるみたいなので、今回はHandlerを使って実装していくことにしました。

また、このカウントアップの処理は、これまで書いてきた「ボタンを押したら正誤判定をする」「正誤判定をしたら正解音を鳴らして次の問題を出題する」という処理とは別に、同一UIスレッド内で非同期処理として実装していく必要があります。

**～～非同期処理とは～～～～～～～～～～～～～～～～～～～～～～～～～～**
非同期処理とは、一つのタスクを実行中であっても他のタスクを実行できる実行方式をいいます。 
**～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～**

この非同期処理を実現するのが、**Runnable**クラスになります。
Runnableクラスのオブジェクトを生成する際は、run()メソッドをオーバーライドする必要があり、非同期で行いたい処理はこのrun()メソッド内に記載していきます。


##３．処理概要

① Handlerクラスのオブジェクトを生成(**handler**)
② 100ミリ秒ごとにカウントアップした時間を格納する変数を宣言(**timeValue**)
③ タイマーを表示するTextViewを取得（**timeText**）
④ Runnableクラスのオブジェクトを生成してrun()メソッドをオーバーライド
⑤ run()メソッド内の処理
　⑤-1 **timeValue**をカウントアップ
　⑤-2 **timeValue**を「分:秒.ミリ秒」の形式に変換
　⑤-3 変換した**timeValue**を③で取得したTextViewに設定
　⑤-4 100ミリ秒後にもう一度run()メソッドを実行する
⑥ 初回のrun()メソッドを実行
です。
run()を定義しただけだとどこからも初回実行されないので、⑥は重要です。

⑤-2の表示形式変換は**timeToText()**メソッド内で実装しました。
こちらは後述します。（フォーマット変換だけですが、意外と苦労しました・・・）


#４．実装内容（Handler & Runnable）
それではコードの解説です。

① Handlerクラスのオブジェクトを生成(**handler**)
② 100ミリ秒ごとにカウントアップした時間を格納する変数を宣言(**timeValue**)

```kotlin:GameActivity.kt
class GameActivity : AppCompatActivity() {

    //タイマー用初期処理
    val handler = Handler()　・・・①
    var timeValue = 0　・・・②
```

特に説明は不要かと思います。



③ タイマーを表示するTextViewを取得（**timeText**）

```kotlin:GameActivity.kt
        val timeText = findViewById<TextView>(R.id.tv_time)
```

これもいいですね。



④ Runnableクラスのオブジェクトを生成してrun()メソッドをオーバーライド


```kotlin:GameActivity.kt
        val runnable = object : Runnable {
            override fun run() {}
```

runnableという変数を実行可能なRunnableクラスのオブジェクトとして宣言して、run()メソッドをオーバーライドします。

⑤ run()メソッド内の処理
　⑤-1 **timeValue**をカウントアップ
　⑤-2 **timeValue**を「分:秒.ミリ秒」の形式に変換
　⑤-3 変換した**timeValue**を③で取得したTextViewに設定
　⑤-4 100ミリ秒後にもう一度run()メソッドを実行する

```kotlin:GameActivity.kt
            override fun run() {
                timeValue++　・・・⑤-1
                // TextViewを更新
                // ?.letを用いて、nullではない場合のみ更新
                timeToText(timeValue)?.let{　・・・⑤-2
                    // timeToText(timeValue)の値がlet内ではitとして使える
                    timeText.text = it　・・・⑤-3
                }
                handler.postDelayed(this, 100)　・・・⑤-4
            }
```

⑤-2では、⑤-1でカウントアップしたtimeValueをtimeToTextの引数で渡して表示形式を変換しています。
その後、戻り値がnullではない場合を判定して、⑤-3で形式変換後のtimeValueをTextView(timeText)のtext値に設定しています。
⑤-4でpostDelayedメソッドに指定した2つの引数は、それぞれ
this：自分自身を
100：100ms秒後に
再度実行する、という意味です。


⑥ 初回のrun()メソッドを実行

```kotlin:GameActivity.kt
        handler.post(runnable)
```

postメソッドでrunnableクラスを呼び出すことでrun()メソッドが実行されます。



#５．実装内容（タイマー表示形式変換）
それでは次に、timeToText()メソッドについて解説します。

run()メソッド内でカウントアップされるtimeValueは、100ミリ秒ごとにカウントアップされる数値型の変数です。
これを「分:秒.ミリ秒」形式に変換していきます。

```kotlin:GameActivity.kt
    private fun timeToText(time: Int = 0): String? {
        return if (time < 0) {
            null
        } else if (time == 0) {
            "00:00.0"
        } else {
            val m = time / 600
            val s = time % 600 / 10
            val ms = time % 10
            "%1$02d:%2$02d.%3$01d".format(m,s,ms)
        }
    }
```

if文の最初の二つの判定は、
渡されたtimeValueが
　・0以下の時はnullをリターン　⇒　まずありえないケース
　・0の時は「00:00.0」をリターン　⇒　初期表示
です。
3つ目のelse内がメインの処理です。


run()メソッドは100msごとに実行されるので、timeValueも100msごとにカウントアップされます。

つまり、timeValueの値が
・10で1秒
・600で1分です。
ということで、
分換算　⇒　timeValueを600で割った商
秒換算　⇒　timeValueを600で割った余りをさらに10で割った商
100ミリ秒換算　⇒　tieValueを10で割った余り
で変換することができます。


具体的な値で考えると、、、
timeValueが5664の時
分：5664 ÷ 600 = 9 余り264　⇒　9分
秒：264 ÷ 10 = 26 余り 4　⇒　26秒
100ミリ秒：5664 ÷ 10 = 566 余り4　⇒ 4
となります。

除算の演算子については以下の通りです。

|演算子|説明　|
|---|---|---|
|　/　|x / y なら xをyで割る|
|　%　|x % y なら xをyで割った余りを求める|
※整数を「/」を使って割り算した場合、小数点以下は切り捨てられる。


#６．結果画面へデータを渡す

前回、回答ボタンを押したとき時、問題数が10問になっていたら、結果画面に遷移する処理を説明しました。結果画面には今回実装したプレイ時間についても渡します。

コードは以下です。

```kotlin:GameActivity.kt
            //問題数が10だったら結果画面に遷移
            if(questionCount == 10) {
                val intent2Result = Intent(this@GameActivity, ResultActivity::class.java)

                //タイム（テキスト）
                val timeText = findViewById<TextView>(R.id.tv_time).text
                intent2Result.putExtra("finishTime", timeText)
                //タイム（数値）
                intent2Result.putExtra("timeValue", timeValue)
                //正解数
                intent2Result.putExtra("correctCount", correctCount)
                //レベル
                val level = intent.getStringExtra("level")
                intent2Result.putExtra("level", level)

                //結果画面表示
                startActivity(intent2Result)
            }
```

画面表示用にtimeToTextで形式を変換した**timeValue**（＝画面に表示しているプレイ時間）と、100ms秒ごとにカウントアップする数値型の**timeValue**両方とも渡します。
これは次の結果画面で説明しますが、プレイ時間でランキングを表示するために使います。
プレイタイムを比較して早い順にランキング表示する際に文字列型だと比較ができないので、数値としても保持しておく必要があります。

それぞれ、
・文字列：finishTime
・数値：timeValue
という名前でputExtra()メソッドを使って結果画面に渡します。
putExtra()の時点では、数値型と文字列型で記載方法に違いはありません。
getする際に、文字列型はgetStringExtra()、数値型はgetIntExtra()で指定すればOKです。


今回は以上です。
次回はゲーム画面から引き継いだデータを結果画面で表示する処理を実装していきます。



[①概要](https://qiita.com/tomohiro-yamashita-5664/items/e87f90fef9cf34faf9bf)
[②画面デザイン～トップ画面（Constraint Layout）～](https://qiita.com/tomohiro-yamashita-5664/items/72a529e12f292a8e1538)
[③画面デザイン～ゲーム画面（Linear Layout）～](https://qiita.com/tomohiro-yamashita-5664/items/9674fa7c1d0cc2de1c40)
[④画面デザイン～結果画面（Linear Layoutその2）～](https://qiita.com/tomohiro-yamashita-5664/items/08f99be14d2ac2254634)
[⑤トップ画面からの遷移（インテント(putExtra)）](https://qiita.com/tomohiro-yamashita-5664/items/ae86d6273cfc8c13b482)
[⑥トップ画面から引き継いだデータ表示（インテント(getExtra)）](https://qiita.com/tomohiro-yamashita-5664/items/bf50791fa5355818d7fb)
[⑦問題出題（ロジック実装）](https://qiita.com/tomohiro-yamashita-5664/items/a67eeb3fe9cc63eb1c65)
[⑧回答ボタン押下（効果音再生（MediaPlayer、正誤判定、次の問題出題）](https://qiita.com/tomohiro-yamashita-5664/items/8b032855a9377ba9897e)
[⑨タイムカウンターの実装（handler）（本記事）](https://qiita.com/tomohiro-yamashita-5664/items/8f5633e68f0519d6841f)
[⑩ゲーム画面から引き継いだゲーム結果表示（インテント）](https://qiita.com/tomohiro-yamashita-5664/items/0e208a3c1a75c5b13376)
[⑪当日日付データ取得](https://qiita.com/tomohiro-yamashita-5664/items/59513080e5d5f419d6af)
[⑫DB保存（SQLite、Insert）](https://qiita.com/tomohiro-yamashita-5664/items/5a0d967f2e42aa90ee24)
⑬もう一度、トップ画面へ戻るボタン（インテント）
⑭ランキング表示（SQLite、Select）
⑮実機でのテスト
⑯Google Playで公開