我正在创建一个20分钟倒计时计时器应用程序。我正在使用JavaFX SceneBuilder来完成这项工作。计时器由两个标签(一个表示分钟,一个表示秒——每个标签由一个CountdownTimer
类对象组成(和一个进度条(计时器看起来是这样的(组成。这些组件中的每一个都是独立的,并在独立的线程上同时运行,以防止UI冻结。它是有效的。
问题:
我需要能够暂停和恢复的三个线程(minutesThread
、secondsThread
、progressBarUpdaterThread
(是常规的.java类。当用户单击播放(开始(按钮时,单击会向FXMLDocumentController
(控制UI中组件如何更新的类(方法startTimer()
发出信号,以完成有关计时器的工作。
现在FXMLDocumentController
中startTimer()
的唯一功能是:用户单击播放(开始(按钮-->计时器开始倒计时。
我希望用户能够用这个按钮暂停和恢复计时器。我尝试过在FXMLDocumentController
类和其他三个线程之间使用同步,但在多种不同的方式中都没有效果(诚然,我几乎没有编写并发代码的经验(。我只想能够暂停并播放计时器!
有人能给我建议怎么做吗?提前谢谢。
FXMLDocumentController.java中的startTimer(((用于启动倒计时计时器(:
@FXML
void startTimer(MouseEvent event) throws FileNotFoundException {
// update click count so user can switch between pause and start
startTimerButtonClickCount++;
// create a pause button image to replace the start button image when the user pauses the timer
Image pauseTimerButtonImage = new Image(new
FileInputStream("/Users/Home/NetBeansProjects/Take20/src/Images/pause2_black_18dp.png"));
// setting imageview to be used when user clicks on start button to pause it
ImageView pauseTimerButtonImageView = new ImageView(pauseTimerButtonImage);
// setting the width and height of the pause image
pauseTimerButtonImageView.setFitHeight(31);
pauseTimerButtonImageView.setFitWidth(28);
// preserving the pause image ratio after resize
pauseTimerButtonImageView.setPreserveRatio(true);
// create a start button image to replace the pause button image when the user unpauses the timer
Image startTimerButtonImage = new Image(new
FileInputStream("/Users/Home/NetBeansProjects/
Take20/src/Images/play_arrow2_black_18dp.png"));
ImageView startTimerButtonImageView = new ImageView(startTimerButtonImage);
startTimerButtonImageView.setFitHeight(31);
startTimerButtonImageView.setFitWidth(28);
startTimerButtonImageView.setPreserveRatio(true);
// progressBar updater
ProgressBarUpdater progressBarUpdater = new ProgressBarUpdater();
TimerThread progressBarThread = new TimerThread(progressBarUpdater);
// minutes timer
CountdownTimer minutesTimer = new CountdownTimer(19);
TimerThread minutesThread = new TimerThread(minutesTimer);
// seconds timer
CountdownTimer secondsTimer = new CountdownTimer(59);
TimerThread secondsThread = new TimerThread(secondsTimer);
// bind our components in order to update them
progressBar.progressProperty().bind(progressBarUpdater.progressProperty());
minutesTimerLabel.textProperty().bind(minutesTimer.messageProperty());
secondsTimerLabel.textProperty().bind(secondsTimer.messageProperty());
// start the threads in order to have them run parallel when the start button is clicked
progressBarThread.start();
minutesThread.start();
secondsThread.start();
// if the start button was clicked, then we set its graphic to the pause image
// if the button click count is divisible by 2, we pause it, otherwise, we play it (and change
// the button images accordingly).
if (startTimerButtonClickCount % 2 == 0) {
startTimerButton.setGraphic(pauseTimerButtonImageView);
progressBarThread.pauseThread();
minutesThread.pauseThread();
secondsThread.pauseThread();
progressBarThread.run();
minutesThread.run();
secondsThread.run();
} else {
startTimerButton.setGraphic(startTimerButtonImageView);
progressBarThread.resumeThread();
minutesThread.resumeThread();
secondsThread.resumeThread();
progressBarThread.run();
minutesThread.run();
secondsThread.run();
}
}
TimerThread(用于在用户单击UI中的播放/暂停按钮时暂停/恢复计时器线程(:
public class TimerThread extends Thread implements Runnable {
public boolean paused = false;
public final Task<Integer> timerObject;
public final Thread thread;
public TimerThread(Task timerObject) {
this.timerObject = timerObject;
this.thread = new Thread(timerObject);
}
@Override
public void start() {
this.thread.start();
System.out.println("TimerThread started");
}
@Override
public void run() {
System.out.println("TimerThread class run() called");
try {
synchronized (this.thread) {
System.out.println("synchronized called");
while (paused) {
System.out.println("wait called");
this.thread.wait();
System.out.println("waiting...");
}
}
} catch (Exception e) {
System.out.println("exception caught in TimerThread");
}
}
synchronized void pauseThread() {
paused = true;
}
synchronized void resumeThread() {
paused = false;
notify();
}
}
CountdownTimer.java(用于创建和更新倒计时计时器的分钟和秒数(:
public class CountdownTimer extends Task<Integer> {
private int time;
private Timer timer;
private int timerDelay;
private int timerPeriod;
private int repetitions;
public CountdownTimer(int time) {
this.time = time;
this.timer = new Timer();
this.repetitions = 1;
}
@Override
protected Integer call() throws Exception {
// we will create a new thread for each time unit (minutes, seconds)
// we start with whatever time is passed to the constructor
// we have threads devoted to each case so both minutes and second cases can run parallel to each other.
switch (time) {
// for our minutes timer
case 19:
// first display should be 19 first since our starting timer time should be 19:59
updateMessage("19");
// set delay and period to change every minute of the countdown
// 60,000 milliseconds in one minute
timerDelay = 60000;
timerPeriod = 60000;
System.out.println("Running minutesthread....");
// use a timertask to loop through time at a fixed rate as set by timerDelay, until the timer reaches 0 and is cancelled
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
//check if the flag is divisible by 2, then we sleep this thread
// if time reaches 0, we want to update the minute label to 00
if (time == 0) {
updateMessage("0" + Integer.toString(time));
timer.cancel();
timer.purge();
// if the time is a single digit, append a 0 and reduce time by 1
} else if (time <= 10) {
--time;
updateMessage("0" + Integer.toString(time));
// otherwise, we we default to reducing time by 1, every minute
} else {
--time;
updateMessage(Integer.toString(time));
}
}
}, timerDelay, timerPeriod);
// exit switch statement once we finish our work
break;
// for our seconds timer
case 59:
// first display 59 first since our starting timer time should be 19:59
updateMessage("59");
// use a counter to count repetitions so we can cancel the timer when it arrives at 0, after 20 repetitions
// set delay and period to change every second of the countdown
// 1000 milliseconds in one second
timerDelay = 1000;
timerPeriod = 1000;
System.out.println("Running seconds thread....");
// use a timertask to loop through time at a fixed rate as set by timerDelay, until the timer reaches 0 and is cancelled
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
--time;
System.out.println("repititions: " + repetitions);
// Use a counter to count repetitions so we can cancel the timer when it arrives at 0, after 1200 repetitions
// We will reach 1200 repetitions at the same time as the time variable reaches 0, since the timer
// loops/counts down every second (1000ms).
// 1200 seconds = 20 minutes * 60 seconds (1 minute)
repetitions++;
if (time == 0) {
if (repetitions == 1200) {
// reset repetitions if user decides to click play again
repetitions = 0;
timer.cancel();
System.out.println("repetitions ran");
}
updateMessage("0" + Integer.toString(time));
// reset timer to 60, so it will countdown again from 60 after reaching 0 (since we have to repeat the seconds timer multiple times,
// unlike the minutes timer, which only needs to run once
time = 60;
System.out.println("time == 00 ran");
} else if (time < 10 && time > 0) {
updateMessage("0" + Integer.toString(time));
} else {
updateMessage(Integer.toString(time));
}
}
}, timerDelay, timerPeriod);
// exit switch statement once we finish our work
break;
}
return null;
}
}
ProgressBarUpdater.java(用于在倒计时计时器倒计时时更新进度条(:
public class ProgressBarUpdater extends Task<Integer> {
private int progressBarPeriod;
private Timer timer;
private double time;
public ProgressBarUpdater() {
this.timer = new Timer();
this.time = 1200000;
}
@Override
protected Integer call() throws Exception {
progressBarPeriod = 10;
System.out.println("Running progressBar thread....");
// using a timer task, we update our progressBar by reducing the filled progressBar every 9.68 milliseconds
// (instead of 10s to account for any delay in program runtime) to ensure that the progressBar ends at the same time our timer reaches 0.
// according to its max (1200000ms or 20 minutes)
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
time -= 9.68;
updateProgress(time, 1200000);
System.out.println("progressBarUpdater is running");
}
}, 0, progressBarPeriod);
return null;
}
@Override
protected void updateProgress(double workDone, double maxTime) {
super.updateProgress(workDone, maxTime);
}
}
正如我在评论中提到的,使用一个后台线程,更不用说三个(!(后台线程了,只会使实现和推理变得更加困难。最好使用JavaFX-It异步提供的动画API,但仍在JavaFX应用程序线程上执行。正如其他人所提到的,您只需要一个值来表示剩余时间,另一个值表示持续时间。从那里可以显示分钟、秒和进度。
就我个人而言,我会使用AnimationTimer
,因为它会给你当前帧的时间戳,你可以用它来计算还剩多少时间。为了更容易使用,我还将AnimationTimer
包装在另一个类中,并让后者公开一个更适合倒计时计时器的API。例如:
package com.example;
import java.util.concurrent.TimeUnit;
import javafx.animation.AnimationTimer;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyLongProperty;
import javafx.beans.property.ReadOnlyLongWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleLongProperty;
public class CountdownTimer {
private static long toMillis(long nanos) {
return TimeUnit.NANOSECONDS.toMillis(nanos);
}
/* *********************************************************************
* *
* Instance Fields *
* *
***********************************************************************/
private final Timer timer = new Timer();
private long cachedDuration;
/* *********************************************************************
* *
* Constructors *
* *
***********************************************************************/
public CountdownTimer() {}
public CountdownTimer(long duration) {
setDuration(duration);
}
/* *********************************************************************
* *
* Public API *
* *
***********************************************************************/
public void start() {
if (getStatus() == Status.READY || getStatus() == Status.PAUSED) {
timer.start();
setStatus(Status.RUNNING);
}
}
public void pause() {
if (getStatus() == Status.RUNNING) {
timer.pause();
setStatus(Status.PAUSED);
}
}
public void stopAndReset() {
timer.stopAndReset();
setStatus(Status.READY);
}
/* *********************************************************************
* *
* Properties *
* *
***********************************************************************/
private final ReadOnlyObjectWrapper<Status> status = new ReadOnlyObjectWrapper<>(this, "status", Status.READY) {
@Override protected void invalidated() {
if (get() == Status.READY) {
cachedDuration = Math.abs(getDuration());
setTimeRemaining(cachedDuration);
}
}
};
private void setStatus(Status status) { this.status.set(status); }
public final Status getStatus() { return status.get(); }
public final ReadOnlyObjectProperty<Status> statusProperty() { return status.getReadOnlyProperty(); }
private final LongProperty duration = new SimpleLongProperty(this, "duration") {
@Override protected void invalidated() {
if (getStatus() == Status.READY) {
cachedDuration = Math.abs(get());
setTimeRemaining(cachedDuration);
}
}
};
public final void setDuration(long duration) { this.duration.set(duration); }
public final long getDuration() { return duration.get(); }
public final LongProperty durationProperty() { return duration; }
private final ReadOnlyLongWrapper timeRemaining = new ReadOnlyLongWrapper(this, "timeRemaining") {
@Override protected void invalidated() {
setProgress((double) (cachedDuration - get()) / (double) cachedDuration);
}
};
private void setTimeRemaining(long timeRemaining) { this.timeRemaining.set(timeRemaining); }
public final long getTimeRemaining() { return timeRemaining.get(); }
public final ReadOnlyLongProperty timeRemainingProperty() { return timeRemaining.getReadOnlyProperty(); }
private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(this, "progress");
private void setProgress(double progress) { this.progress.set(progress); }
public final double getProgress() { return progress.get(); }
public final ReadOnlyDoubleProperty progressProperty() { return progress.getReadOnlyProperty(); }
/* *********************************************************************
* *
* Static Classes *
* *
***********************************************************************/
public enum Status {
READY,
RUNNING,
PAUSED,
FINISHED
}
/* *********************************************************************
* *
* Classes *
* *
***********************************************************************/
private class Timer extends AnimationTimer {
private long triggerTime = Long.MIN_VALUE;
private long pauseTime = Long.MIN_VALUE;
private boolean pausing;
@Override
public void handle(long now) {
if (pausing) {
pauseTime = toMillis(now);
pausing = false;
stop();
} else {
if (triggerTime == Long.MIN_VALUE) {
triggerTime = toMillis(now) + cachedDuration;
} else if (pauseTime != Long.MIN_VALUE) {
triggerTime += toMillis(now) - pauseTime;
pauseTime = Long.MIN_VALUE;
}
long timeRemaining = Math.max(0, triggerTime - toMillis(now));
setTimeRemaining(timeRemaining);
if (timeRemaining == 0) {
setStatus(Status.FINISHED);
stop();
}
}
}
@Override
public void start() {
pausing = false;
super.start();
}
void pause() {
if (triggerTime != Long.MIN_VALUE) {
pausing = true;
} else {
stop();
}
}
void stopAndReset() {
stop();
triggerTime = Long.MIN_VALUE;
pauseTime = Long.MIN_VALUE;
pausing = false;
}
}
}
警告:当AnimationTimer
正在运行时,不能对CountdownTimer
实例进行垃圾回收
此实现将持续时间和剩余时间值解释为毫秒。此外,在启动计时器后更改持续时间直到计时器重置(即调用stopAndReset()
(后才生效。
以下是在基于FXML的应用程序中使用上述CountdownTimer
的示例。请注意,此示例使用不同的按钮来启动、暂停、恢复和重置计时器。这与你在问题中描述的不同,但你应该能够重新设计以满足你的需求。此外,该示例提供了一种切换是否显示当前秒的毫秒的方式。
App.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.CheckBox?>
<?import com.example.CountdownTimer?>
<?import com.example.CountdownTimer.Status?>
<VBox xmlns="http://javafx.com/javafx/14.0.1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.example.Controller" prefHeight="300" prefWidth="500">
<fx:define>
<!-- 90,000ms == 1m 30s -->
<CountdownTimer fx:id="timer" duration="90000"/>
<CountdownTimer.Status fx:id="READY" fx:value="READY"/>
<CountdownTimer.Status fx:id="RUNNING" fx:value="RUNNING"/>
<CountdownTimer.Status fx:id="PAUSED" fx:value="PAUSED"/>
</fx:define>
<ToolBar style="-fx-font: 10pt 'Monospaced';">
<Button text="Start" disable="${timer.status != READY}" focusTraversable="false"
onAction="#handleStartOrResumeTimer"/>
<Button text="Resume" disable="${timer.status != PAUSED}" focusTraversable="false"
onAction="#handleStartOrResumeTimer"/>
<Button text="Pause" disable="${timer.status != RUNNING}" focusTraversable="false" onAction="#handlePauseTimer"/>
<Button text="Reset" disable="${timer.status == READY || timer.status == RUNNING}" focusTraversable="false"
onAction="#handleResetTimer"/>
<Separator/>
<CheckBox fx:id="showMillisBox" text="Show Millis" focusTraversable="false"/>
</ToolBar>
<ProgressBar progress="${timer.progress}" maxWidth="Infinity"/>
<StackPane VBox.vgrow="ALWAYS">
<Label fx:id="timerLabel" style="-fx-font: bold 48pt 'Monospaced';"/>
</StackPane>
</VBox>
Controller.java:
package com.example;
import java.time.Duration;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.paint.Color;
public class Controller {
@FXML private CountdownTimer timer;
@FXML private CheckBox showMillisBox;
@FXML private Label timerLabel;
@FXML
private void initialize() {
timerLabel
.textProperty()
.bind(
Bindings.createStringBinding(
this::formatTimeRemaining,
timer.timeRemainingProperty(),
showMillisBox.selectedProperty()));
timerLabel
.textFillProperty()
.bind(
Bindings.when(timer.statusProperty().isEqualTo(CountdownTimer.Status.FINISHED))
.then(Color.FIREBRICK)
.otherwise(Color.FORESTGREEN));
}
private String formatTimeRemaining() {
Duration d = Duration.ofMillis(timer.getTimeRemaining());
if (showMillisBox.isSelected()) {
return String.format("%02d:%02d:%03d", d.toMinutes(), d.toSecondsPart(), d.toMillisPart());
}
return String.format("%02d:%02d", d.toMinutes(), d.toSecondsPart());
}
@FXML
private void handleStartOrResumeTimer(ActionEvent event) {
event.consume();
timer.start();
}
@FXML
private void handlePauseTimer(ActionEvent event) {
event.consume();
timer.pause();
}
@FXML
private void handleResetTimer(ActionEvent event) {
event.consume();
timer.stopAndReset();
}
}
Main.java:
package com.example;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws IOException {
Parent root = FXMLLoader.load(getClass().getResource("/com/example/App.fxml"));
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Countdown Timer Example");
primaryStage.show();
}
}