【Androidアプリ開発】[Kotlin] Handlerを使ってストップウォッチを作る

今回は、Handlerを使ってストップウォッチを作りたいと思います。

Handlerを使うと、一定周期で処理を繰り返し実行する事が出来るようになります。

サクッと内容みたい方は、目次から「ソースコード」に飛んでください。

実は、他サイトのHandlerの使い方を参考にしたところ、Handler()が非推奨となってしまい困り果てました。

Handler()の非推奨を解決した状態のソースを記載していますので、同じ様に詰まっている方は参考にして下さい。

Handlerを使ったストップウォッチの要件

ザックリと以下の様な機能で作成したいと思います。

要件

  • スタートボタン押下で計測を開始する
  • 計測開始と同時にスタートボタンはストップボタンに変わる
  • ストップボタンで計測を停止する
  • 計測停止した状態でリセットボタン押下で、計測値をクリアする
  • 計測中にラップボタン押下で計測を止めずに途中のタイム(ラップタイム)を記録する

Handlerを使ったストップウォッチの完成画面

まずは、完成画面から紹介します。

計測開始前

計測開始前は、スタートボタンとリセットボタンが表示されています。

スタートボタンで計測を開始します。

リセットボタンで計測データをクリアします。

計測中

計測中は、ストップボタンとラップボタンが表示されます。

ストップボタンで計測を停止します。

ラップボタンで、ラップタイムを保存します。

計測中(ラップタイムあり)

ラップボタンを押してラップタイムが計測されると、ラップタイムがリスト表示されます。

ラップタイムのリストは降順で、スクロール可能です。

Handlerを使ったストップウォッチの作成

プロジェクト作成

プロジェクトの作成は、省略するので、以下のページを参照下さい。

【Android Studio】はじめの一歩 Hello Worldを試す

続きを見る

画面作成

画面はMainActivity単体の一画面だけのアプリとします。

ボタンは、スタート・ストップ用ボタンとラップ・リセット用ボタンの二つを用意します。

経過時間の表示は、「HH:MM:SS:ZZ」となる様にします。

ラップ・リセットボタンの下にラップタイムを表示するListVIewを配置しています。

ソースコード

ソースコード全体です。

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ListView
import android.widget.TextView
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : AppCompatActivity() {

    companion object {
        const val INTERVAL_MILLISEC : Long = 10     // 処理実行周期
    }

    var t : Long = 0    // タイム
    var lap : Long = 0  // ラップタイム計測用前回値
    var laplist = mutableListOf<String>("") // ラップリスト
    val dataformat = SimpleDateFormat("HH:mm:ss.SS", Locale.getDefault())
    var startflag : Boolean = false     // 計測中フラグ

    val handler = Handler(Looper.getMainLooper())
    val timer = object : Runnable {
        override fun run() {
            // 一定間隔で処理実行
            t += INTERVAL_MILLISEC
            val timeView = findViewById<TextView>(R.id.textView)
            timeView.text = dataformat.format(t)
            handler.postDelayed(this, INTERVAL_MILLISEC)
        }
    }

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

        laplist.clear()
    }

    // スタート・ストップボタン押下
    fun onStartStopClick(view : View) {
        val buttonStartStop = findViewById<Button>(R.id.button_startstop)
        val buttonResetLap = findViewById<Button>(R.id.button_resetlap)
        val lapListView = findViewById<ListView>(R.id.listview)

        if (!startflag) {
            // スタートボタン押下
            handler.post(timer)
            buttonStartStop.text = "STOP"
            buttonResetLap.text = "LAP"
        } else {
            // ストップボタン押下
            handler.removeCallbacks(timer)
            buttonStartStop.text = "START"
            buttonResetLap.text = "RESET"
            if (lap != 0L) {
                // 一度でもLAP計測している場合は、STOPボタンで最後のLAPを計算する
                addLapList()
                dispLapList()
            }
        }
        startflag = !startflag
    }

    // ラップ・リセットボタン押下
    fun onResetLapClick(view : View) {
        val timeView = findViewById<TextView>(R.id.textView)

        if (startflag) {
            // LAPボタン押下
            addLapList()
        } else {
            // RESETボタン押下
            // クリアする
            t = 0
            lap = 0
            laplist.clear()
            timeView.text = dataformat.format(t)
        }

        dispLapList()
    }

    // ラップ用リストデータへの追加処理
    fun addLapList() {
        val n : Int = laplist.size + 1
        laplist.add(0,"LAP" + n.toString() + ": " + dataformat.format(t - lap))
        lap = t
    }

    // ラップデータをリスト表示する処理
    fun dispLapList() {
        // lap用リストにアダプターをセット
        val lapListView = findViewById<ListView>(R.id.listview)
        val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, laplist)
        lapListView.adapter = adapter
    }
}

今回重要な部分は、以下のHandlerの部分となります。

    val handler = Handler(Looper.getMainLooper())
    val timer = object : Runnable {
        override fun run() {
            // 一定間隔で処理実行
            t += INTERVAL_MILLISEC
            val timeView = findViewById<TextView>(R.id.textView)
            timeView.text = dataformat.format(t)
            handler.postDelayed(this, INTERVAL_MILLISEC)
        }
    }

「val handler = Handler(Looper.getMainLooper())」の部分ですが、「Handler()」とすると非推奨となります。

注意ポイント

Handlerを使っているサイトを見ていると、「val handler = Handler()」になっているサイトが多いですが、
「Handler()」は非推奨になっています。(いつから非推奨なのかは調べ切れていないです…)

引数無しの「Handler()」を使用すると、暗黙的Looperが選択されて、予期せずクラッシュする恐れがあるそうです。

なので、明示的にLooperを指定してあげない方法は、非推奨って事になっているみたいです。

これで、指定した周期(今回はINTERVAL_MILLISECの値)で実行される様になります。

周期実行を始めたり止めたりは、以下の部分でやっています。

    // スタート・ストップボタン押下
    fun onStartStopClick(view : View) {
        -- 省略 --

        if (!startflag) {
            // スタートボタン押下
            handler.post(timer)
            -- 省略 --
        } else {
            // ストップボタン押下
            handler.removeCallbacks(timer)
            -- 省略 --
        }
        startflag = !startflag
    }

「handler.post()」で周期実行を開始します。

「handler.removeCallbacks()」で周期実行を停止します。

Handlerでストップウォッチを作る まとめ

どうでしょうか?動きましたか?

正直、「Handler()」が非推奨で警告出ちゃった時は、どうしていいか途方に暮れました。

同じところで詰まってしまった人の参考になっていたら嬉しいです。

今後も、基礎的な部分と、実際のアプリを作るのを混ぜ混ぜしながらやっていきたいと思います。

それでは、今回はここまで、お疲れさまでしたー

スポンサーリンク

  • この記事を書いた人

まさじぃ

ダメプログラマ歴17年です。 プログラミング関連の事や、 自分で使って良かったもの等の紹介をメインにやっています。

-プログラミング
-