首页 > 解决方案 > 在 kotlin 中启动一系列计时器

问题描述

我正在尝试创建一个 tabata 计时器。我设法从 editText 获取用户输入并启动一个计时器,它代表准备时间。

当准备时间结束时,我想开始工作时间,然后是休息时间。稍后,当用户输入时,我需要重复工作时间和休息时间 x 次。但我无法弄清楚。

MainActivity.kt:

        btn_Start_Timer.setOnClickListener() {
            val prepTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
            val workTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
            val restTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
            val numberOfRepetitions = Integer.parseInt(eT_Number_Repetitions.text.toString().trim());

            val Timer = object : CountDownTimer(prepTimeMillis, 1000) {

                override fun onTick(millisUntilFinished: Long) {
                    tV_Total_Duration.setText("Preparation 00:00: " + millisUntilFinished / 1000)
                }

                override fun onFinish() {
                    tV_Total_Duration.setText("Preparation done!")
                }
            }

            timer.start()
        }

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">


    <ScrollView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:fillViewport="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/tV_Workout_Name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:editable="false"
                android:text="Name of the Workout"
                android:textSize="18sp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tV_Total_Repetitions" />

            <EditText
                android:id="@+id/eT_WorkoutName"
                android:layout_width="290dp"
                android:layout_height="40dp"
                android:layout_marginTop="16dp"
                android:ems="10"
                android:inputType="textPersonName"
                android:text="Name"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.495"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tV_Workout_Name" />

            <TextView
                android:id="@+id/tV_Prepare"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:editable="false"
                android:text="Preparation"
                android:textSize="18sp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/eT_WorkoutName" />

            <EditText
                android:id="@+id/eT_PrepTime"
                android:layout_width="290dp"
                android:layout_height="40dp"
                android:layout_marginTop="16dp"
                android:ems="10"
                android:inputType="number"
                android:text="0"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tV_Prepare" />

            <Button
                android:id="@+id/btn_Decrement_PrepTime"
                android:layout_width="50dp"
                android:layout_height="40dp"
                android:layout_marginTop="56dp"
                android:text="-"
                app:layout_constraintEnd_toStartOf="@+id/eT_PrepTime"
                app:layout_constraintTop_toBottomOf="@+id/eT_WorkoutName" />

            <Button
                android:id="@+id/btn_Increment_PrepTime"
                android:layout_width="50dp"
                android:layout_height="40dp"
                android:layout_marginTop="56dp"
                android:text="+"
                app:layout_constraintStart_toEndOf="@+id/eT_PrepTime"
                app:layout_constraintTop_toBottomOf="@+id/eT_WorkoutName" />

            <TextView
                android:id="@+id/tV_WorkTime"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:editable="false"
                android:text="Working"
                android:textSize="18sp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/eT_PrepTime" />

            <EditText
                android:id="@+id/eT_Work_Time"
                android:layout_width="290dp"
                android:layout_height="40dp"
                android:layout_marginTop="16dp"
                android:ems="10"
                android:text="0"
                android:inputType="number"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tV_WorkTime" />

            <Button
                android:id="@+id/btn_Decrement_WorkTime"
                android:layout_width="50dp"
                android:layout_height="40dp"
                android:layout_marginTop="56dp"
                android:text="-"
                app:layout_constraintEnd_toStartOf="@+id/eT_Work_Time"
                app:layout_constraintTop_toBottomOf="@+id/btn_Decrement_PrepTime" />

            <Button
                android:id="@+id/btn_Increment_WorkTime"
                android:layout_width="50dp"
                android:layout_height="40dp"
                android:layout_marginTop="56dp"
                android:text="+"
                app:layout_constraintStart_toEndOf="@+id/eT_Work_Time"
                app:layout_constraintTop_toBottomOf="@+id/btn_Increment_PrepTime" />

            <TextView
                android:id="@+id/tv_Repetitions"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:editable="false"
                android:text="Number of Repetitions"
                android:textSize="18sp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/eT_Rest_Time" />

            <Button
                android:id="@+id/btn_Decrement_Repetitions"
                android:layout_width="50dp"
                android:layout_height="40dp"
                android:layout_marginTop="56dp"
                android:text="-"
                app:layout_constraintEnd_toStartOf="@+id/eT_Number_Repetitions"
                app:layout_constraintTop_toBottomOf="@+id/btn_Decrement_Rest" />

            <Button
                android:id="@+id/btn_Increment_Repetitions"
                android:layout_width="50dp"
                android:layout_height="40dp"
                android:layout_marginTop="56dp"
                android:text="+"
                app:layout_constraintStart_toEndOf="@+id/eT_Number_Repetitions"
                app:layout_constraintTop_toBottomOf="@+id/btn_Increment_Rest" />

            <EditText
                android:id="@+id/eT_Number_Repetitions"
                android:layout_width="290dp"
                android:layout_height="40dp"
                android:layout_marginTop="16dp"
                android:ems="10"
                android:inputType="number"
                android:text="1"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.495"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tv_Repetitions" />

            <TextView
                android:id="@+id/tV_Rest_Time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:editable="false"
                android:text="Resting"
                android:textSize="18sp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/eT_Work_Time" />

            <Button
                android:id="@+id/btn_Decrement_Rest"
                android:layout_width="50dp"
                android:layout_height="40dp"
                android:layout_marginTop="56dp"
                android:text="-"
                app:layout_constraintEnd_toStartOf="@+id/eT_Rest_Time"
                app:layout_constraintTop_toBottomOf="@+id/btn_Decrement_WorkTime" />

            <EditText
                android:id="@+id/eT_Rest_Time"
                android:layout_width="290dp"
                android:layout_height="40dp"
                android:layout_marginTop="16dp"
                android:ems="10"
                android:text="0"
                android:inputType="number"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.495"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tV_Rest_Time" />

            <Button
                android:id="@+id/btn_Increment_Rest"
                android:layout_width="50dp"
                android:layout_height="40dp"
                android:layout_marginTop="56dp"
                android:text="+"
                app:layout_constraintStart_toEndOf="@+id/eT_Rest_Time"
                app:layout_constraintTop_toBottomOf="@+id/btn_Increment_WorkTime" />

            <TextView
                android:id="@+id/tV_Total_Duration"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:editable="false"
                android:text="Duration 00:00:00"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <TextView
                android:id="@+id/tV_Total_Repetitions"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:editable="false"
                android:text="Repeated for x times"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tV_Total_Duration" />

            <Button
                android:id="@+id/btn_Start_Timer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="52dp"
                android:layout_marginEnd="110dp"
                android:layout_marginBottom="52dp"
                android:text="Start now"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/eT_Number_Repetitions" />

            <Button
                android:id="@+id/btn_Stop_Timer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="110dp"
                android:layout_marginTop="52dp"
                android:layout_marginBottom="52dp"
                android:text="STOP"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/eT_Number_Repetitions" />

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

编辑:

我设法导入了库。

但我现在收到以下错误:

Suspend function 'countDown' should be called only from a coroutine or another suspend function

Unresolved reference: lifecycleScope

Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper 'jvm-target' option

No value passed for parameter 'handler'

代码:

        btn_Start_Timer.setOnClickListener() {
            val prepTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
            val workTimeMillis = Integer.parseInt(eT_Work_Time.text.toString().trim()) * 1000L;
            val restTimeMillis = Integer.parseInt(eT_Rest_Time.text.toString().trim()) * 1000L;
            val numberOfRepetitions = Integer.parseInt(eT_Number_Repetitions.text.toString().trim());

            countdownJob = lifecycleScope.launch {
                repeat(numberOfRepetitions) {
                    countDown(prepTimeMillis, 1000L) { millisUntilFinished ->
                        tV_Total_Duration.setText("Preparation 00:00: " + millisUntilFinished / 1000)
                    }
                    countDown(workTimeMillis, 1000L) { millisUntilFinished ->
                        tV_Total_Duration.setText("Work 00:00: " + millisUntilFinished / 1000)
                    }
                    countDown(restTimeMillis, 1000L) { millisUntilFinished ->
                        tV_Total_Duration.setText("Rest 00:00: " + millisUntilFinished / 1000)
                    }
                }
            }

        }

        btn_Stop_Timer.setOnClickListener(){

        }
    }

    suspend inline fun countDown(millisInFuture: Long, countDownInterval: Long, crossinline onTick: (Long) -> Unit) = withContext(Dispatchers.Main)
    {
        suspendCancellableCoroutine<Unit>
        { continuation ->
            val timer = object: CountDownTimer(millisInFuture, countDownInterval)
            {
                override fun onTick(millisUntilFinished: Long) = onTick(millisUntilFinished)
                override fun onFinish() = continuation.resume(Unit)
            }.start()

            continuation.invokeOnCancellation()
            {
                timer.cancel()
            }
        }
    }

编辑:build.gradle(Module.app):

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.example.instafollow"
        minSdkVersion 24
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation("androidx.lifecycle:lifecycle-viewmodel:2.4.0-rc01")
    implementation("androidx.lifecycle:lifecycle-livedata:2.4.0-rc01")
    implementation("androidx.lifecycle:lifecycle-runtime:2.4.0-rc01")
    implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.0-rc01")
    annotationProcessor("androidx.lifecycle:lifecycle-compiler:2.4.0-rc01")
    implementation("androidx.lifecycle:lifecycle-common-java8:2.4.0-rc01")
    implementation("androidx.lifecycle:lifecycle-service:2.4.0-rc01")
    implementation("androidx.lifecycle:lifecycle-process:2.4.0-rc01")
    implementation("androidx.lifecycle:lifecycle-reactivestreams:2.4.0-rc01")
    testImplementation("androidx.arch.core:core-testing:2.1.0")

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
}

build.gradle(项目)

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.3.72'
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

编辑:

我设法将目标 JVM 更改为 1.8,并且我的代码中没有更多错误。但是当我尝试启动项目时,我得到了一系列这样的错误:

在此处输入图像描述

标签: androidkotlintimer

解决方案


对现有类执行此操作的方法是在第一个类中嵌套另一个倒数计时器,onFinish()并使用该嵌套的倒数计时器对工作时间进行倒计时。然后在其余时间,您将在该计时器内嵌套另一个onFinish()计时器。而且,因为你想重复整个事情,你必须把它移到一个单独的函数中,这样你就可以重复调用它。因此,启动三个计时器的函数也需要一个倒计时参数。非常复杂,它看起来像这样:

btn_Start_Timer.setOnClickListener() {
    val prepTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
    val workTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
    val restTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
    val numberOfRepetitions = Integer.parseInt(eT_Number_Repetitions.text.toString().trim());

    doRep(prepTimeMillis, workTimeMillis, restTimeMillis, numberOfRepetitions)
}

private fun doRep(prepTimeMillis: Long, workTimeMillis: Long, restTimeMillis: Long, times: Int) {
    object : CountDownTimer(prepTimeMillis, 1000) {
        override fun onTick(millisUntilFinished: Long) {
            tV_Total_Duration.setText("Preparation 00:00: " + millisUntilFinished / 1000)
        }

        override fun onFinish() {
            object : CountDownTimer(workTimeMillis, 1000) {
                override fun onTick(millisUntilFinished: Long) {
                    tV_Total_Duration.setText("Work 00:00: " + millisUntilFinished / 1000)
                }

                override fun onFinish() {
                    object : CountDownTimer(restTimeMillis, 1000) {
                        override fun onTick(millisUntilFinished: Long) {
                            tV_Total_Duration.setText("Rest 00:00: " + millisUntilFinished / 1000)
                        }

                        override fun onFinish() {
                            if (times == 1) {
                                tV_Total_Duration.setText("All done!")
                            } else {
                                doRep(prepTimeMillis, workTimeMillis, restTimeMillis, times - 1)
                            }
                        }
                    }.start()
                }
            }.start()
        }
    }.start()

}

这就是所谓的“回调地狱”,您必须在其中深度嵌套代码并且很难遵循,因此它是使用协程进行简化的主要候选者。这是 CountdownTimer 的挂起函数版本,可让您按顺序使用它,而不是通过嵌套代码。当您在协程中调用此函数时,您会将通常放入onTick函数中的操作传递给它。它会自动启动计时器,然后协程暂停直到onFinish()发生,因此您的协程代码可以按顺序编写。如果调用它的协程被取消,它将取消计时器,从而onTick()停止被调用。

suspend inline fun countDown(
    millisInFuture: Long,
    countDownInterval: Long,
    crossinline onTick: (Long) -> Unit
) = withContext(Dispatchers.Main) {
    suspendCancellableCoroutine<Unit>{ continuation ->
        val timer = object: CountDownTimer(millisInFuture, countDownInterval) {
            override fun onTick(millisUntilFinished: Long) = onTick(millisUntilFinished)
            override fun onFinish() = continuation.resume(Unit)
        }.start()
        continuation.invokeOnCancellation { timer.cancel() }
    }
}

我还将创建一个简单的辅助函数来从编辑文本中获取用户输入并避免代码重复,如下所示:

fun TextView.inputToInt(): Long = text.toString().trim().toIntOrNull() ?: 0

使用这两个函数,您可以使用启动的协程在按钮侦听器中顺序编写代码:

btn_Start_Timer.setOnClickListener() {
    val prepTimeMillis = eT_PrepTime.inputToInt() * 1000L
    val workTimeMillis = eT_PrepTime.inputToInt() * 1000L // TODO pick correct ET
    val restTimeMillis = eT_PrepTime.inputToInt() * 1000L // TODO pick correct ET
    val numberOfRepetitions = eT_Number_Repetitions.inputToInt()
    
    lifecycleScope.launch {
        repeat(numberOfRepetitions) {
            countDown(prepTimeMillis, 1000L) { millisUntilFinished ->
                tV_Total_Duration.setText("Preparation 00:00: " + millisUntilFinished / 1000)
            }    
            countDown(workTimeMillis, 1000L) { millisUntilFinished ->
                tV_Total_Duration.setText("Work 00:00: " + millisUntilFinished / 1000)
            }    
            countDown(restTimeMillis, 1000L) { millisUntilFinished ->
                tV_Total_Duration.setText("Rest 00:00: " + millisUntilFinished / 1000)
            }    
        }
    }
}

请注意,您的设计很容易受到每次屏幕旋转时重置计时器的影响,因为 Activity 将被销毁并重新创建。我建议将计时器放在 ViewModel 中,该 ViewModelLiveData<String>使用可以在 Activity 中观察到的值更新 a 以应用于 TextView。但这是一个巨大的话题。您可以阅读有关如何使用 ViewModel 和 LiveData 的文档。

编辑: 为了支持取消,您需要一个属性来保存协程作业。

private var countdownJob: Job? = null

如果要取消当前倒计时(如果存在),请使用 null-safe cancel:

countdownJob?.cancel()

当你启动协程时,将它的 Job 分配给这个变量:

countdownJob = lifecycleScope.launch { 
   // ... code from above example
}

推荐阅读