【はじめてのKotlinプログラミング(25)】CountDownTimer(後編)

前回、簡単なカウントダウンタイマーを作りました。(おまけ編はこちら

スタートボタンをおしたらカウントが開始され、
スタートボタンをおしたら停止される、というものです。

今回はその続きです。

リスタートボタンを押したら、続きから再スタートされるようにしていきましょう。

動画

【目次】

▼00:30~
7)リスタートボタン(の準備)
8)関数

▼06:15~ warningの修正(lateinitの解説ほか)
▼13:35~ 9)残り時間の取得
▼16:30~ 10)スタート / リスタート
▼19:15~ 11)リセット

コード

▼activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="00:00"
        android:textSize="50sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="START"
        app:layout_constraintEnd_toStartOf="@+id/btnStop"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv" />

    <Button
        android:id="@+id/btnStop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="STOP"
        app:layout_constraintEnd_toStartOf="@+id/btnRestart"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btnStart"
        app:layout_constraintTop_toBottomOf="@+id/tv" />

    <Button
        android:id="@+id/btnRestart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="RESTART"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btnStop"
        app:layout_constraintTop_toBottomOf="@+id/tv" />

    <Button
        android:id="@+id/btnReset"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="RESET"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

▼MainActivity.kt

package com.example.simplecountdown

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.CountDownTimer
import android.widget.Button
import android.widget.TextView

class MainActivity : AppCompatActivity() {
    //8)横断的に使うやつを用意
    private lateinit var timer:CountDownTimer
    private var remainingTime:Long = 0 //9)残り時間(後で代入するので一旦0)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //1)開始時間を設定
        val startTime:Long = 10000 //10秒

        //1)viewを取得+tvに開始時間を表示
        val tv:TextView =findViewById(R.id.tv)
        val btnStart:Button =findViewById(R.id.btnStart)
        val btnStop:Button =findViewById(R.id.btnStop)
        val btnRestart:Button =findViewById(R.id.btnRestart)
        val btnReset:Button =findViewById(R.id.btnReset)
        tv.text ="${startTime/1000}" //開始時間を秒で表示

        //2)ボタンの有効・無効
        btnStart.isEnabled = true
        btnStop.isEnabled = false
        btnRestart.isEnabled = false

        //4)カウントダウンタイマーのオブジェクトを用意
        //val timer =

        //3)スタートボタンを押したらカウントダウン
        btnStart.setOnClickListener {
            //5)「4」のtimerをスタート
            //timer.start()
            //10)startTimeでカウントダウン開始
            timer = countDownTimer(startTime).start()

            btnStart.isEnabled = false
            btnStop.isEnabled = true
            btnRestart.isEnabled = false
        }

        //6)ストップボタンを押した時の処理
        btnStop.setOnClickListener {
            timer.cancel()

            //ストップボタンを無効化
            btnStart.isEnabled = true
            btnStop.isEnabled = false
            btnRestart.isEnabled = true
        }

        //7)リスタートボタンを押した時の処理
        btnRestart.setOnClickListener {
            //10)remainingTimeでカウントダウン開始
            timer = countDownTimer(remainingTime).start()

            btnStart.isEnabled = false
            btnStop.isEnabled = true
            btnRestart.isEnabled = false
        }

        //11)リセットボタン
        btnReset.setOnClickListener {
            timer.cancel()
            remainingTime = 0
            tv.text ="${startTime/1000}" //開始時間を秒で表示

            //2)ボタンの有効・無効
            btnStart.isEnabled = true
            btnStop.isEnabled = false
            btnRestart.isEnabled = false
        }
    }

    //8)「4」を関数でここに用意
    private fun countDownTimer(st:Long):CountDownTimer{
        val tv:TextView =findViewById(R.id.tv)
        val btnStart:Button =findViewById(R.id.btnStart)
        val btnStop:Button =findViewById(R.id.btnStop)
        val btnRestart:Button =findViewById(R.id.btnRestart)

        return object :CountDownTimer(st,100){
            //[4-1]途中経過・残り時間
            override fun onTick(p0: Long) {
                //TODO("Not yet implemented")
                //残り時間を表示
                tv.text ="${p0/1000}" //秒単位
                remainingTime = p0 //9)残り時間を代入
            }
            //[4-2]終了設定
            override fun onFinish() {
                //TODO("Not yet implemented")
                tv.text ="タイムアップ"

                //終了時は3つ全部無効(リセットボタンを押すため)
                btnStart.isEnabled = false
                btnStop.isEnabled = false
                btnRestart.isEnabled = false
            }

        }
    }
}

テキスト

それでは前回の続きから書いていきましょう。

07) リスタートボタンの準備
08) 関数

てことでストップボタンを押した後、
6番の波カッコ下に、リスタートボタンの処理を書いていきます。

//7)リスタートボタンを押した時の処理
btnRestart.setOnClickListener {
}

で、結論から言うと、この4番のカウントダウンタイマーを
onCreateの外に持っていって、関数を1つ作ります。

何故か。ちょっと頭を整理しましょう。
スタートボタンもリスタートボタンも、カウントダウンが進む、という意味では、同じ処理、同じ回路を通るんですよね。

ただ、スタートの位置が、「最初からなのか」、「途中からなのか」って違いなんです。
以前、四択クイズでも似たようなことをしたんですけど、複数のボタンで同じような処理をさせたい場合は下に関数で1か所にまとめて、それを各ボタンで呼び出す、という風にしてあげます。

まあ口で説明するより見てもらった方が早いですね。
関数の書き方は、ファンクションを意味する
fun と書いて、関数の名前、丸カッコ、波カッコです。

    fun 名前(){
        
    }

名前は今回わかりやすくcountDownTimerとかにしておきます。

    fun countDownTimer(){
        
    }

なんですが、カウントダウンタイマーの場合、()に続けて
:CountDownTimer ってつけてあげてください。
こっちは、Cが大文字の、本家の方なんで間違えないようにしてくだい。

ファンクションの名前の方は、パッチモンで、こちらで勝手に名前を付けてるだけなので、
最初は小文字のcです。

あとは中身を書いて、処理させたいところにcountDownTimer() 丸カッコまでを
ペタペタ貼っていくっていうことになります。

ただ、横断的に使えるようになった反面、このクラスでしか使わないんだったら
アクセス修飾子をつけないよと、後々androidStudio先生に注意されるので、今回は先回りして
アクセス修飾子でprivateと付け加えてあげます。

それから、先ほどと同様、丸カッコの中は、何かしら引数の名前を用意してあげる必要があります。
まあ他と被らなければなんでもいいんですが
習慣的にはmillisInFuture みたいな文字がここに入ってきたりします。
ただ、正直それだと意味がよくわからないので、スタートタイム、略してstとかにしておきましょう。
型はもちろん:Long

   private fun countDownTimer(st:Long):CountDownTimer{
    }

あとは処理させる中身をこの中に入れるわけですが、中身は4番を入れるだけなので
4番のobjectから波カッコ閉じるまでをもろともカットして
8番に貼り付け。

で、この時に、object :CountDownTimer()の中の引数も
stに変えておいてください。つまり一致させておいてください。

今、ここには何も定義されていないLong型のstをとりあえず入れているわけですが、

あとで、この関数をスタートボタンとかストップボタンとかに貼っていったときに
この中に開始時間とか途中時間を入れてあげます。

ここに開始時間が入ると、objectの中も開始時間、今回は10秒から開始されますし
ここに残り時間が入ると、この中も残り時間から開始される、という風になります。

これで中身のほうは完成です。

**************************************
それから4番のところの

val timer = の変数名が残っているので、名前のval timerは、onCreateの上、
まあクラス名の下、という言い方もできますが、ここに入れていきます。
//8)横断的に使うやつを用意
これ、消してもいいんですが、一応後で見返せるように気持ち、残骸だけ残しておきます。

//val timer =

8番に
//8)横断的に使うやつを用
val timer

で、名前を付けたら、その人がどういう型として使っていくのかを指定しなければいけません。
tvならTextViewですよとか
btnStartはButtonですよとか。

この人はCountDownTimerとして使っていくので

val timer:CountDownTimer

*************************************************
さあ。これで一気に色んなものが出来たんですけど、それと同時に、一気に色んなエラーが出ています。

それでは次に、これらのエラーを1つずつ解決していきましょう。

08) warningの修正 

それではワーニングを1つずつ直していきましょう。
正直、ここが今回の動画のメインといってもいいくらいで、
直していくうちにほとんどのものが完成していきます。

1.viewの取得

まずは簡単なやつから。
先ほど移し替えた、8番のところで
onTickの中のtvとか、onFinishの中のbtnStart、ストップ、リスタート。
これらはviewが取得できてませんよってことで赤色になっています。
今は、onCreate{}波カッコの中で宣言したので、

この{}の外では使えませんってことですね。
なのでもう一度、CountDownTimer{ 波カッコの中にも書いてあげましょう。

ここはコピペすればOKです。実はこれでほとんど解決します。
今は12個もあるようですが

1で書いた、tvからbtnRestartまでの4つをコピーして
貼り付け。
そうすると一気に、2つまで減りました。

        val tv: TextView =findViewById(R.id.tv)
        val btnStart:Button = findViewById(R.id.btnStart)
        val btnStop:Button=findViewById(R.id.btnStop)
        val btnRestart:Button =findViewById(R.id.btnRestart)

2.lateinit

さあ、いよいよ本命を直していきましょう。

一度ワーニングのところをクリックしてみましょう。
すると下に色々出てきまして、、、まず1つ目は11行目。

Property must be initialized or be abstract

プロパティー、イニシャライズドうんぬんかんぬん。
これ、前回のBGMのところでも出てきましたね。
11行目を見てみましょう。val timer
なんやよーわかりませんが、これをイニシャライズしなさい的なことが書いてあります。

で結論から言うと、前回の動画をご覧の方はお察しのとおりかと思いますが
このtimerのところにlateinit varてのを書きます。

あ、ついでに、一番下に、timerはprivateじゃないのって言われているので
それも含めて一気に解決しましょう。

private lateinit var timer:CountDownTimer

まず、そもそも論ですが、変数を使うためには
宣言で名前をつけて、
それに中身を入れるまでがセットです。

これが大前提ですよってのを覚えておいてください。

今の話を踏まえたうえで、timerを見てみましょう。
timerって名前は付けてるんだけど、「=」で、その先の中身をまだ何も入れてないんですよね。

実際の中身はこの中(8番)だったりするんですが、今じゃないと
今は名前だけつけて、中身はあとからonCreateの中で書いていくので
今は名前だけ、中身は後から、っていう時に書くのがlateinitです。

3.return

さあ、これであらかた解決しました。
最後にもう1つだけ、赤いワーニングが出ています。

87行目。波カッコのブロックボディには、リターンって書きなさいよと、言っているようです。

波カッコの中にある「object」にreturnとつけてあげてください。これで解決すると思います。

プログラムは上から下に流れていくんですけど
returnを書いてあげることで
下に行ったらもう一度戻ってくる、いわゆる値を返す、っていう言い方をしたりもしますが、
もう一度戻ってきます。

これで経過時間を戻ってきて、取得できたりするんですけど、まあ・・・
こっから{

ここまで}、

ほぼひな形通りなので、今回は解説のために1つずつ作っていきましたけど
これ(return object内)をそっくりそのままコピーして、
必要なところだけ自分なりに変更して使う、っていう風にしてください。

これを一言一句覚えようとすると、頭がパンクしますので。
countDownTimerで検索するとだいたいみなさん同じようなことを書いてますので
最初のうちはそっくりそのまま真似をする。

てか、出来る人に限って、こういうの一言一句書かないですよ。誰が書いたって一緒なので。

誰が書いても一緒なものは、ひな形をコピー。
肝心なのは、自分の必要なところだけ書き換えられるようにしておいてください。

てことで、だいぶ長々としゃべりましたけれども、赤いワーニングが全て消えました。
この黄色いやつはまだ宣言した名前が未使用ですよってことなので、これから使っていくのでOKです。

それではもう完全に峠は越えましたので、仕上げに入っていきましょう。

スタートボタンが押された時には、最初からスタート。
リスタートボタンが押された時には、途中からスタート、っていう命令を書いたら、タイマーは完成です。

09)残り時間の取得

それでは、スタートボタン、リスタートボタンを使い分けていきましょう。

一旦頭を整理すると
スタートボタンを押したら、(3番を選択して)最初からスタート。
最初からというのはココでいうとstartTimeのことですね。今は10秒という設定になっています。

スタートボタンが押されたら、startTimeからカウントダウンされる。

一方、リスタートボタンを押したら途中、つまり残り時間からカウントダウンされる必要があります。
なのでスタートタイムとは別に変数を用意しておきましょう。
残り時間は、onCreateでも使うし、その外のfun countDownTimerでも使うので
横断的に使える8番のところに書いていきます。

varで宣言して、残り時間ということで、名前を remainingTime
この人の型はもちろんLong
ここは、あとで、経過時間を代入するのでひとまず0を入れておきます。

private var remainingTime:Long =0 //9)残り時間(後で代入するので一旦0)

スタートボタンが押されたら、開始時間(startTime)からカウントダウンタイマー
リスタートボタンが押されたら、残り時間(remainingTime)からカウントダウンタイマー。
という風にしていきましょう。

**************************************
てことはですよ、remainingTimeに残り時間を取得しなければいけません。
今は0ですけど、このままだとずっと0になってしまいます。

前回
onTickが途中経過・残り時間って話をしたと思います。

で、残り時間はp0に入ってくるっていうことだったので
「ここで」、残り時間を代入してあげましょう。

remainingTime = p0 //9)残り時間を代入

**************************************

これで、今何が起きたか。

startTimeは、初期設定で10秒で固定のまま。
残り時間、remainingTimeは、最初は0だけども、タイマーが動くと
あとからp0が入ってくる、ということになりました。

それではカウントダウンタイマーをstartTimeで開始する場合と、
remainingTimeから開始する場合で使い分ければ完成です。

10)スタートボタン・リスタートボタン

それでは、スタートボタン・リスタートボタンを設定していきましょう。

まずはスタートボタンから。5番のやつを消して

//timer.start()

先ほどlateinitで名前だけ付けたtimerをここで使います。で呼び出すのはこの関数なのでcountDownTimer(st:Long) 丸カッコまでをコピーして貼り付け。
中に、開始時間をstartTimeといれて、 これを .start()

          //10)startTimeでカウントダウン開始
            timer = countDownTimer(startTime).start()

*****************************

同じくリスタートボタンは、今のをまるまるコピーして
remainingTimeで開始。

            //10)remainingTimeでカウントダウン開始
            timer =countDownTimer(remainingTime).start()

ボタンの有効無効は、スタートボタンと一緒でいいので、btnStartをそっくりそのままコピー。

いいすか。スタートボタンもリスタートもほぼ一緒。違うのは開始時間が違う、というのが出来ました。これでエミュレータを起動して確認してみましょう。

11)リセットボタン

それでは、リセットボタンを押した時の処理を書いてあげましょう。
リスタートボタンの波カッコに続けて書いていきましょう。
//11)リセットボタン
btnReset.setOnClickListener {

}

ここはもう簡単ですわ。

▼まずはストップボタン同様、タイマーをキャンセルして
timer.cancel()

▼一応残り時間も0に戻しておきましょう
remainingTime = 0

▼で、テキストビューに、最初の開始時間を表示させてあげればいいので
1番の最初のコードを貼り付け
tv.text = “${startTime/1000}”//開始時間を秒で表示

有効・無効も、最初の状態と同じなので
2番目のスタートボタンだけ押せるtrue false falseをコピーして
貼り付け
btnStart.isEnabled = true
btnStop.isEnabled = false
btnRestart.isEnabled= false

 


次回は、ちょっとだけおまけでタイマーが0になった時に、
ちょうど前回MediaPlayerの解説をしたばかりなので復習がてら、MediaPlayerでアラーム音を鳴らしてみる、というのをやってみたいと思います。

    コメントを残す