So I'm making this game which gives the player 30 seconds to choose an answer. If they choose an option, the timer resets back to the 30 seconds.
@Override
public void actionPerformed(ActionEvent e) {
//the idea is that when this button is clicked, the timer starts.
if(e.getSource() == button) {
//random gui stuff which i won't include
//calls the method for timer
runner();
}
This is my runner() method:
public void runner() {
final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
final Runnable runnable = new Runnable() {
int countdownStarter = 30;
public void run() {
//this is my JLabel that prints out the timer
label.setText("" countdownStarter);
countdownStarter--;
if (countdownStarter < 0) {
label.setText("");
/*Random GUI stuff here I won't show*/
scheduler.shutdown();
}
}
My problem is, the timer does run when the button is clicked however it creates a new "timer" so on my GUI it just flickers between the two different times. For example if the already existing timer is at 5 seconds (left) and I click the button, the JLabel text flickers between 5/30, 4/29, 3/28, 2/27 .. ect but in reality I just want it to stop the existing timer.
Not sure what I can do and I could really appreciate any help since i'm very new to swing and gui stuff.
Thanks!
CodePudding user response:
Start by separating your concerns.
You have a mechanism which needs to keep track of when the button was trigged and the amount of time it should be counting down from AND you have some mechanism which updates the UI.
The first part is pretty simple and you can simply take advantage of the java.time
API, for example...
public class CountdownTimer {
private Instant startedAt;
private Duration duration;
public CountdownTimer(Duration duration) {
this.duration = duration.plusSeconds(1);
}
public boolean isRunning() {
return startedAt != null;
}
public void start() {
if (startedAt != null) {
return;
}
startedAt = Instant.now();
}
public void stop() {
startedAt = null;
}
public Duration getTimeRemaining() {
return duration.minus(getRunTime());
}
public Duration getRunTime() {
if (startedAt == null) {
return null;
}
return Duration.between(startedAt, Instant.now());
}
}
So, based on a "anchor" time, you can calculate the "run time" from which you can then calculate the "remaining time", simple. Now, to reset it, you just call stop
and start
again.
Next, we need some way to update the UI. Swing is single threaded AND is not thread safe, this means you can't run long running or blocking operations from within the context of the Event Dispatching Thread and you shouldn't update the UI, or any state the UI relies on, from out side of the Event Dispatching Thread.
The best option is to make use of Swing Timer
, for example...
public class CountdownPane extends JPanel {
private CountdownTimer countdownTimer;
private Timer timer;
public CountdownPane() {
countdownTimer = new CountdownTimer(Duration.ofSeconds(5));
JButton btn = new JButton("Destory the world");
timer = new Timer(500, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Duration duration = countdownTimer.getTimeRemaining();
long seconds = duration.toSecondsPart();
if (seconds <= 0) {
countdownTimer.stop();
timer.stop();
btn.setText("Boom");
} else {
btn.setText(format(duration));
}
}
});
timer.setInitialDelay(0);
setLayout(new GridBagLayout());
setBorder(new EmptyBorder(36, 36, 36, 36));
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (countdownTimer.isRunning()) {
countdownTimer.stop();
timer.stop();
btn.setText("Destory the world");
} else {
countdownTimer.start();
timer.start();
}
}
});
add(btn);
}
protected String format(Duration duration) {
long seconds = duration.toSecondsPart();
return String.format("d seconds", seconds);
}
}
Runnable example...
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.border.EmptyBorder;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new CountdownPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class CountdownPane extends JPanel {
private CountdownTimer countdownTimer;
private Timer timer;
public CountdownPane() {
countdownTimer = new CountdownTimer(Duration.ofSeconds(5));
JButton btn = new JButton("Destory the world");
timer = new Timer(500, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Duration duration = countdownTimer.getTimeRemaining();
long seconds = duration.toSecondsPart();
if (seconds <= 0) {
countdownTimer.stop();
timer.stop();
btn.setText("Boom");
} else {
btn.setText(format(duration));
}
}
});
timer.setInitialDelay(0);
setLayout(new GridBagLayout());
setBorder(new EmptyBorder(36, 36, 36, 36));
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (countdownTimer.isRunning()) {
countdownTimer.stop();
timer.stop();
btn.setText("Destory the world");
} else {
countdownTimer.start();
timer.start();
}
}
});
add(btn);
}
protected String format(Duration duration) {
long seconds = duration.toSecondsPart();
return String.format("d seconds", seconds);
}
}
public class CountdownTimer {
private Instant startedAt;
private Duration duration;
public CountdownTimer(Duration duration) {
this.duration = duration.plusSeconds(1);
}
public boolean isRunning() {
return startedAt != null;
}
public void start() {
if (startedAt != null) {
return;
}
startedAt = Instant.now();
}
public void stop() {
startedAt = null;
}
public Duration getTimeRemaining() {
return duration.minus(getRunTime());
}
public Duration getRunTime() {
if (startedAt == null) {
return null;
}
return Duration.between(startedAt, Instant.now());
}
}
}