在科特林中启动一系列计时器
start a sequence of timer in kotlin
我正在尝试创建一个 tabata 计时器。
我设法从 editText 中获取用户输入并启动一个计时器,它代表准备时间。
当准备时间结束后,我想开始工作时间,然后是休息时间。
稍后我需要在用户输入时重复 Worktime 和 Resttime 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,并且我的代码中没有更多错误。
但是当我尝试启动项目时,我得到了一系列这样的错误:
用现有的 class 执行此操作的方法是在第一个 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 中,该 ViewModel 更新一个 LiveData<String>
的值,该值可以在 Activity 中观察到以应用于 TextView。但这是一个巨大的话题。您可以阅读有关如何使用 ViewModel 和 LiveData 的文档。
编辑:
要支持取消,您需要 属性 来保持协程作业。
private var countdownJob: Job? = null
当您想要取消当前倒计时(如果存在)时,请使用空安全取消:
countdownJob?.cancel()
启动协程时,将其作业分配给此变量:
countdownJob = lifecycleScope.launch {
// ... code from above example
}
我正在尝试创建一个 tabata 计时器。 我设法从 editText 中获取用户输入并启动一个计时器,它代表准备时间。
当准备时间结束后,我想开始工作时间,然后是休息时间。 稍后我需要在用户输入时重复 Worktime 和 Resttime 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,并且我的代码中没有更多错误。 但是当我尝试启动项目时,我得到了一系列这样的错误:
用现有的 class 执行此操作的方法是在第一个 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 中,该 ViewModel 更新一个 LiveData<String>
的值,该值可以在 Activity 中观察到以应用于 TextView。但这是一个巨大的话题。您可以阅读有关如何使用 ViewModel 和 LiveData 的文档。
编辑: 要支持取消,您需要 属性 来保持协程作业。
private var countdownJob: Job? = null
当您想要取消当前倒计时(如果存在)时,请使用空安全取消:
countdownJob?.cancel()
启动协程时,将其作业分配给此变量:
countdownJob = lifecycleScope.launch {
// ... code from above example
}