如何在Kotlin中创建一个简单的倒计时器?

80
我知道如何在Java中创建一个简单的倒计时器。但是我想用Kotlin创建这个倒计时器。
package android.os;

new CountDownTimer(20000, 1000) {
    public void onTick(long millisUntilFinished) {
        txtField.setText("seconds remaining: " + millisUntilFinished / 1000);
    }
    public void onFinish() {
        txtField.setText("Time's finished!");
    }
}.start();

怎样用Kotlin来实现呢?

2
倒计时器可以很容易使用,但是并不真正准确。如果输入时间足够长,您将能够看到计时器跳过秒数随着时间的流逝。如果系统有延迟,则每个滴答的毫秒数会有延迟,并最终导致跳过。要进行更准确的计时器实现,请查看此帖子:https://dev59.com/amnWa4cB1Zd3GeqPy0h9。 - WasabiTea
10个回答

169
您可以使用Kotlin对象:
val timer = object: CountDownTimer(20000, 1000) {
    override fun onTick(millisUntilFinished: Long) {...}

    override fun onFinish() {...}
}
timer.start()

15
在Kotlin中使用计时器会泄露本地内存。 - Akash Chaudhary
5
我认为安卓建议使用这种方法。我记得在一个代码实验中看到过它。 - Hanako

25

我这样使用 Kotlin 解决了定时器问题:

class Timer {

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Default + job)

    private fun startCoroutineTimer(delayMillis: Long = 0, repeatMillis: Long = 0, action: () -> Unit) = scope.launch(Dispatchers.IO) {
        delay(delayMillis)
        if (repeatMillis > 0) {
            while (true) {
                action()
                delay(repeatMillis)
            }
        } else {
            action()
        }
    }

    private val timer: Job = startCoroutineTimer(delayMillis = 0, repeatMillis = 20000) {
        Log.d(TAG, "Background - tick")
        doSomethingBackground()
        scope.launch(Dispatchers.Main) {
            Log.d(TAG, "Main thread - tick")
            doSomethingMainThread()
        }
    }

    fun startTimer() {
        timer.start()
    }

    fun cancelTimer() {
        timer.cancel()
    }
//...
}

我使用了协程作为一个计时器。


2
这应该是被接受的答案,因为它可以很好地控制工作将在哪个调度程序上执行。 - abd3lraouf
如果应用程序崩溃会发生什么? - obey
非常有用的解决方案。可以作为对象注入,也可通过适当的注入独立使用。干得好@Dima - A S M Sayem
1
KMM的最佳答案。 - Vít Kapitola
这个计时器不准确。它取决于action()阻塞线程的时间长短。 此外,action()调用应该是主线程安全的。 - Peter
显示剩余3条评论

13

对于未来的读者,您可以在Kotlin中使用内置的timer内联函数

示例:

import kotlin.concurrent.timer
....
....
timer(initialDelay = 1000L, period = 1000L ) {
     launch {
        executeTask()
     }
}

请注意,与CountdownTimer不同,回调函数是在与您启动它的线程不同的线程上调用的。 - Tenfour04

11

计时器可以设置为倒计时,这似乎是最简单的方法。

在您的布局xml中添加Chronometer视图,例如:

<Chronometer  
 android:id="@+id/view_timer"   
 tools:targetApi="24"  
 android:layout_width="wrap_content"  
 android:layout_height="wrap_content"/>

然后在你的活动或片段中:

   view_timer.isCountDown = true
   view_timer.base = SystemClock.elapsedRealtime() + 20000
   view_timer.start()

13
isCountDown方法的最低API级别为24(Android 7),因此并非适用于每个项目。 - Andrei Drynov
那么,你如何将计数器重置为零? - Asuquo12

11

如果您想展示一个包含天、小时、分钟和秒的倒计时

private lateinit var countDownTimer:CountDownTimer
.
.
.
    fun printDifferenceDateForHours() {

            val currentTime = Calendar.getInstance().time
            val endDateDay = "03/02/2020 21:00:00"
            val format1 = SimpleDateFormat("dd/MM/yyyy hh:mm:ss",Locale.getDefault())
            val endDate = format1.parse(endDateDay)

            //milliseconds
            var different = endDate.time - currentTime.time
            countDownTimer = object : CountDownTimer(different, 1000) {

                override fun onTick(millisUntilFinished: Long) {
                    var diff = millisUntilFinished
                    val secondsInMilli: Long = 1000
                    val minutesInMilli = secondsInMilli * 60
                    val hoursInMilli = minutesInMilli * 60
                    val daysInMilli = hoursInMilli * 24

                    val elapsedDays = diff / daysInMilli
                    diff %= daysInMilli

                    val elapsedHours = diff / hoursInMilli
                    diff %= hoursInMilli

                    val elapsedMinutes = diff / minutesInMilli
                    diff %= minutesInMilli

                    val elapsedSeconds = diff / secondsInMilli

                    txt_timeleft.text = "$elapsedDays days $elapsedHours hs $elapsedMinutes min $elapsedSeconds sec"
                }

                override fun onFinish() {
                    txt_timeleft.text = "done!"
                }
            }.start()
        }

如果您要导航到另一个活动/片段,请确保取消倒计时

countDownTimer.cancel()

代码输出

51天17小时56分钟5秒


4
请不要教年轻人使用早已过时且臭名昭著的SimpleDateFormat类。至少不要将其作为第一选择。也不要毫无保留地使用它。今天,我们有更好的选择java.time,现代Java日期和时间API,以及它的DateTimeFormatter。是的,您可以在Android上使用它。对于旧版Android,请参见如何在Android项目中使用ThreeTenABP - Ole V.V.
1
DateTimeFormatter需要最低API级别26(Android 8),而使用SimpleDateFormat,我们可以在API 21中使用它,这样可以处理更多的设备。我也会更新答案,以便与DateTimeFormatter一起使用 :) - Gastón Saillén
1
有一个后移版本。要在最小SDK 21(例如)中使用java.time,请将ThreeTenABP添加到您的Android项目中。并确保从org.threeten.bp包及其子包导入日期和时间类。请在我之前评论的链接中阅读更多信息。 - Ole V.V.
@GastónSaillén 我有一个不同的用例,我需要跟踪剩余时间。那么在导航到其他片段时仍然需要取消计数器吗?如果是的话,为什么? - Enos Okello

8

CountDownTimer 在 Kotlin 中:

object: CountDownTimer(3000, 1000){
    override fun onTick(p0: Long) {}
    override fun onFinish() {
        //add your code here
    }
 }.start()

7
class CustomCountDownTimer(var mutableLiveData: MutableLiveData<String>) {

    lateinit var timer: CountDownTimer
    val zone = ZoneId.systemDefault()
    val startDateTime: ZonedDateTime = LocalDateTime.now().atZone(zone)

    fun start(endOn: Long) {
        if (this::timer.isInitialized) {
            return
        }
        timer = object : CountDownTimer(endOn * 1000, 1000) {

            override fun onTick(millisUntilFinished: Long) {

                val stringBuilder = StringBuilder()

                val endDateTime: ZonedDateTime =
                    Instant.ofEpochMilli(millisUntilFinished).atZone(ZoneId.systemDefault())
                        .toLocalDateTime().atZone(zone)

                var diff: Duration = Duration.between(startDateTime, endDateTime)



                if (diff.isZero() || diff.isNegative) {
                    stringBuilder.append("Already ended!")
                } else {
                    val days: Long = diff.toDays()

                    if (days != 0L) {
                        stringBuilder.append("${days}day : ")
                        diff = diff.minusDays(days)
                    }

                    val hours: Long = diff.toHours()
                    stringBuilder.append("${hours}hr : ")
                    diff = diff.minusHours(hours)

                    val minutes: Long = diff.toMinutes()
                    stringBuilder.append("${minutes}min : ")
                    diff = diff.minusMinutes(minutes)

                    val seconds: Long = diff.getSeconds()

                    stringBuilder.append("${seconds}sec")

                }

                mutableLiveData.postValue(stringBuilder.toString())
                //Log.d("CustomCountDownTimer", stringBuilder.toString())
            }

            override fun onFinish() {
            }
        }

        timer.start()
    }


    fun getTimerState(): LiveData<String> {
        return mutableLiveData
    }
}

如何使用:

val liveData: MutableLiveData<String> = MutableLiveData()
val customCountDownTimer = CustomCountDownTimer(liveData)
customCountDownTimer.start(1631638786) //Epoch timestamp
customCountDownTimer.mutableLiveData.observe(this, Observer { counterState ->
            counterState?.let {
                println(counterState)
            }
        })

输出:

22小时:42分钟:51秒 //剩余时间不足4小时时

1天:23小时:52分钟:44秒 //其他情况下


7

尝试使用对象,像这样:

var countDownTimer = object : CountDownTimer(2000, 1000) {
    // override object functions here, do it quicker by setting cursor on object, then type alt + enter ; implement members
}

尝试访问这个网站: https://try.kotlinlang.org/#/Kotlin%20Koans/Introduction/Java%20to%20Kotlin%20conversion/Task.kt。你会发现页面右上角有一个“从Java转换”的小按钮,可能对你很有用。
编辑提示:在需要使用此对象时,请通过在声明末尾或代码的任何位置添加.start()来启动它,例如在activity / fragment中。
countDownTimer.start()

5

我知道我来晚了,但这可能会帮助想要构建倒计时应用程序的人。


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_timer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="60"
        android:textSize="30dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:padding="20dp"
        />

    <TextView
        android:id="@+id/startBtn"
        android:layout_width="160dp"
        android:layout_height="50dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_timer"
        android:text="START"
        android:gravity="center"
        android:background="@color/grey"
        android:textColor="@color/black"
        android:textStyle="bold"
        android:layout_margin="12dp"
        />
    <TextView
        android:id="@+id/pauseBtn"
        android:layout_width="160dp"
        android:layout_height="50dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/startBtn"
        android:text="PAUSE"
        android:gravity="center"
        android:background="@color/grey"
        android:textColor="@color/black"
        android:textStyle="bold"
        android:layout_margin="12dp"
        />
    <TextView
        android:id="@+id/resetBtn"
        android:layout_width="160dp"
        android:layout_height="50dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/pauseBtn"
        android:text="RESET"
        android:gravity="center"
        android:background="@color/grey"
        android:textColor="@color/black"
        android:textStyle="bold"
        android:layout_margin="12dp"
        />

</androidx.constraintlayout.widget.ConstraintLayout><br/>

MainActivity.kt 文件:

    package com.example.countdownapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.CountDownTimer
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private var countdown_timer: CountDownTimer? = null
    private var time_in_milliseconds = 60000L
    private var pauseOffSet = 0L

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

        tv_timer.text= "${(time_in_milliseconds/1000).toString()}"

        startBtn.setOnClickListener{
                starTimer(pauseOffSet)
        }

        pauseBtn.setOnClickListener{
            pauseTimer()
        }

        resetBtn.setOnClickListener{
                resetTimer()
        }
    }
    private fun starTimer(pauseOffSetL : Long){
        countdown_timer = object : CountDownTimer(time_in_milliseconds - pauseOffSetL, 1000){
            override fun onTick(millisUntilFinished: Long) {
                pauseOffSet = time_in_milliseconds - millisUntilFinished
                tv_timer.text= (millisUntilFinished/1000).toString()
            }

            override fun onFinish() {
                Toast.makeText(this@MainActivity, "Timer finished", Toast.LENGTH_LONG).show()
            }
        }.start()
    }

    private fun pauseTimer(){
        if (countdown_timer!= null){
            countdown_timer!!.cancel()
        }
    }

    private fun resetTimer(){
        if (countdown_timer!= null){
            countdown_timer!!.cancel()
            tv_timer.text = " ${(time_in_milliseconds/1000).toString()}"
            countdown_timer = null
            pauseOffSet =0
        }
    }
}

4

对于minapi=24,请使用计时器(Chronometer)

  <Chronometer
            android:id="@+id/timer_expire_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/spacing_m"
            android:countDown="true"
            android:textColor="@color/white"
            android:textSize="@dimen/text_size_huge"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:targetApi="24" />

在 Kotlin 中:

    binding.timerExpireTime.apply {
        base = SystemClock.elapsedRealtime()
        start()
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接