The trick here is to keep track of the elapsed time, check it frequently, and update the GUI when the actual time required ('one second' in this case) has passed.
Here is an example of doing that. Pay attention to the comments in the code.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
public class NonDriftingCountdownTimer {
private JComponent ui = null;
private Timer timer;
private JLabel outputLabel;
NonDriftingCountdownTimer() {
initUI();
}
/** Keeps track of the start time and adjusts the count
* based on the ELAPSED time.
* This should be used with a short time between listener calls. */
class TimerActionListener implements ActionListener {
long start = -1l;
int duration;
int count = 0;
TimerActionListener(int duration) {
this.duration = duration;
}
@Override
public void actionPerformed(ActionEvent e) {
long time = System.currentTimeMillis();
if (start<0l) {
start = time;
} else {
long next = start+(count*1000);
if (time>next) {
count++;
outputLabel.setText((duration-count)+"");
if (count==duration) {
timer.stop();
JOptionPane.showMessageDialog(
outputLabel, "Time Is Up!");
}
}
}
}
}
public final void initUI() {
if (ui!=null) return;
ui = new JPanel(new BorderLayout(4,4));
ui.setBorder(new EmptyBorder(4,4,4,4));
JPanel controlPanel = new JPanel();
ui.add(controlPanel, BorderLayout.PAGE_START);
final SpinnerNumberModel durationModel =
new SpinnerNumberModel(10, 1, 1200, 1);
JSpinner spinner = new JSpinner(durationModel);
controlPanel.add(spinner);
JButton startButton = new JButton("Start");
ActionListener startListener = (ActionEvent e) -> {
int duration = durationModel.getNumber().intValue();
TimerActionListener timerActionListener =
new TimerActionListener(duration);
if (timer!=null) { timer.stop(); }
// Note the short time of fire. This will allow accuracy
// to within 1/50th of a second (without gradual drift).
timer = new Timer(20, timerActionListener);
timer.start();
};
startButton.addActionListener(startListener);
controlPanel.add(startButton);
outputLabel = new JLabel("0000", SwingConstants.TRAILING);
outputLabel.setFont(new Font(Font.MONOSPACED, Font.BOLD, 200));
ui.add(outputLabel);
}
public JComponent getUI() {
return ui;
}
public static void main(String[] args) {
Runnable r = () -> {
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
} catch (Exception useDefault) {
}
NonDriftingCountdownTimer o = new NonDriftingCountdownTimer();
JFrame f = new JFrame(o.getClass().getSimpleName());
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setLocationByPlatform(true);
f.setContentPane(o.getUI());
f.pack();
f.setMinimumSize(f.getSize());
f.setVisible(true);
};
SwingUtilities.invokeLater(r);
}
}