Java – Create duplicate operations in GUI without ‘time drift’

Create duplicate operations in GUI without ‘time drift’… here is a solution to the problem.

Create duplicate operations in GUI without ‘time drift’

Based on Swing Timer Recommended for updating GUI components – because calls to components are made automatically on the event dispatch thread (the correct thread for updating Swing or AWT-based components).

However, Swing Timer has a tendency to “drift” time. If you create a timer that fires every second, after about an hour, it may drift above or below the elapsed time for a few seconds.

When updating the display with Swing Timer, which must be accurate (e.g. countdown timer/stopwatch), how can we avoid this time drift?

Solution

The trick here is to keep track of the elapsed time, check it often, and update the GUI when the time actually needed (in this case, “one second”) has passed.

This is an example of doing this. 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);
    }
}

Related Problems and Solutions