Я создаю 20-минутное приложение таймера обратного отсчета. Я использую JavaFX SceneBuilder для этого. Таймер состоит из двух меток (одна для минут, одна для секунд - каждая состоит из объекта класса CountdownTimer
) и индикатора выполнения (таймер выглядит как this ). Каждый из этих компонентов является отдельным и работает в отдельных потоках одновременно, чтобы предотвратить зависание пользовательского интерфейса. И это работает.
Проблема:
Три потока (minutesThread
, secondsThread
, progressBarUpdaterThread
) Мне нужно иметь возможность приостановить и возобновить регулярные. java классы. Когда пользователь нажимает кнопку воспроизведения (запуска), этот сигнал сигнализирует FXMLDocumentController
(класс, который управляет обновлением компонентов в пользовательском интерфейсе) startTimer()
для работы с таймером.
Вправо теперь единственная функциональность startTimer()
в FXMLDocumentController
: пользователь нажимает кнопку воспроизведения (запуска) -> таймер начинает обратный отсчет.
Я хочу, чтобы пользователь мог приостановить и возобновить таймер с этим та же кнопка. Я попытался использовать синхронизацию между классом FXMLDocumentController
и другими тремя потоками, но безрезультатно несколькими разными способами (правда, у меня почти нет опыта программирования для параллелизма). Я просто хочу иметь возможность сделать паузу и включить таймер!
Может кто-нибудь предложить мне совет, как go об этом? Заранее спасибо.
startTimer () в FXMLDocumentController. java (используется для запуска таймера обратного отсчета):
@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 (используется для приостанавливать / возобновлять потоки таймера, когда пользователь нажимает кнопку воспроизведения / паузы в пользовательском интерфейсе):
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);
}
}