Search code examples
javaswingtimeractionlistener

JLabel text not being displayed correctly after some quickly repeated action event calls


I'm coding a GUI for my Tic Tac Toe project and I'm having fun toying with little visual effects like the one below. Every time the user sets a cpu player and clicks on a done button forgetting to set cpu strength level, I display a little warning message in the same way my code sample below does.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;

public class TestingStuff implements ActionListener {

JLabel myLabel;
JFrame myFrame;
JButton myButton;
JPanel myPanel;

public TestingStuff() {
    myFrame = new JFrame("Hello");
    myFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    myPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
    myPanel.setBackground(Color.DARK_GRAY);
    myFrame.add(myPanel, BorderLayout.CENTER);

    myLabel = new JLabel("Hi dear stackoverflow coders!");
    myLabel.setFont(new Font("MV Boli", Font.BOLD, 15));
    myLabel.setForeground(Color.GREEN);
    myPanel.add(myLabel);

    myButton = new JButton("Click me like there's no tomorrow!");
    myButton.addActionListener(this);
    myFrame.add(myButton, BorderLayout.SOUTH);

    myFrame.pack();
    myFrame.setLocationRelativeTo(null);
    myFrame.setVisible(true);
}

    @Override
public void actionPerformed(ActionEvent e) {
    int timerDelay = 1;
    Timer blinkTimer = new Timer(timerDelay, new ActionListener() {
        private int count = 0;
        private int maxTime = 30;
        private String hello = myLabel.getText();
        private int len = hello.length();
        private String s;

        public void actionPerformed(ActionEvent e) {
            if (count * timerDelay >= maxTime) {
                ((Timer) e.getSource()).stop();
            } else {
                myLabel.setVisible(true);
                if (count < len) {
                    s = hello.substring(0, count + 1);
                    myLabel.setText(s);
                }
                count++;
            }
        }
    });
    blinkTimer.start();
}

    public static void main(String[] args) {
    TestingStuff foo = new TestingStuff();
    }
}

Try to repeatedly click on the button.

Before clicking: enter image description here

After clicking: enter image description here

At least on my machine. Since there's no reason for the user to repeatedly click on the done button, I'm not too worried, but still... I think I'll end up disabling every button the user can click during that short animation in order to avoid unexpected behaviour. My question: can anybody explain what is happening and why I get to see that truncated text just after let's say 7-8 fast clicks?


Solution

  • The delay between updates is set to 1 millisecond (got luck getting that to be that accurate 😉), so you whole animation is done in around 30 milliseconds.

    Instead, try setting it to something like 30 milliseconds (~1 second of runtime)

    I'd also do away with the concept of maxTime, you just want to keep looping until you've got all the text, otherwise it's a little bit pointless (IMHO)

    Maybe something like...

    int timerDelay = 30;
    Timer blinkTimer = new Timer(timerDelay, new ActionListener() {
        private int count = 0;
        private String hello = myLabel.getText();
        private String s;
    
        public void actionPerformed(ActionEvent e) {
            if (count >= hello.length()) {
                System.out.println("Done");
                 ((Timer) e.getSource()).stop();
            } else {
                System.out.println("Hello");
                myLabel.setVisible(true);
                s = hello.substring(0, count + 1);
                myLabel.setText(s);
                count++;
            }
        }
    });
    blinkTimer.start();
    

    Time based animation

    Now, if you prefer a time based animation, it might look something like this.

    This is set to run for 1 second and it will use a "progression" calculation to determine how much of the text should be displayed

    Timer blinkTimer = new Timer(5, new ActionListener() {
    
        private String title = myLabel.getText();
        private Duration runtime = Duration.ofSeconds(1);
    
        private Instant startTime;
    
        public void actionPerformed(ActionEvent e) {
            if (startTime == null) {
                startTime = Instant.now();
            }
            Duration between = Duration.between(startTime, Instant.now());
            double progress = between.toMillis() / (double)runtime.toMillis();
    
            if (progress >= 1.0) {
                ((Timer) e.getSource()).stop();
                // Just make sure the text is up-to-date
                myLabel.setText(title);
            } else {
                int count = (int)(title.length() * progress);
                String text = title.substring(0, count);
                myLabel.setText(text);
            }
        }
    });
    blinkTimer.start();
    

    "By the way, try to use your code and repeatedly clicks on the button: I stil get the same result"

    You are constantly creating new Timers, they are going to complete with each other. Instead, either disable the button or stop the currently running Timer, for example...

    Disable the button...

    @Override
    public void actionPerformed(ActionEvent e) {
        myButton.setEnabled(false);
        Timer blinkTimer = new Timer(5, new ActionListener() {
    
            private String title = myLabel.getText();
            private Duration runtime = Duration.ofSeconds(1);
    
            private Instant startTime;
    
            public void actionPerformed(ActionEvent e) {
                if (startTime == null) {
                    startTime = Instant.now();
                }
                Duration between = Duration.between(startTime, Instant.now());
                double progress = between.toMillis() / (double) runtime.toMillis();
    
                if (progress >= 1.0) {
                    ((Timer) e.getSource()).stop();
                    // Just make sure the text is up-to-date
                    myLabel.setText(title);
                    myButton.setEnabled(true);
                } else {
                    int count = (int) (title.length() * progress);
                    String text = title.substring(0, count);
                    myLabel.setText(text);
                }
            }
        });
        blinkTimer.start();
    }
    

    Cancel the active timer

    private Timer blinkTimer;
    
    @Override
    public void actionPerformed(ActionEvent e) {
        if (blinkTimer != null) {
            blinkTimer.stop();
            blinkTimer = null;
        }
        blinkTimer = new Timer(5, new ActionListener() {
    
            private String title = myLabel.getText();
            private Duration runtime = Duration.ofSeconds(1);
    
            private Instant startTime;
    
            public void actionPerformed(ActionEvent e) {
                if (startTime == null) {
                    startTime = Instant.now();
                }
                Duration between = Duration.between(startTime, Instant.now());
                double progress = between.toMillis() / (double) runtime.toMillis();
    
                if (progress >= 1.0) {
                    ((Timer) e.getSource()).stop();
                    // Just make sure the text is up-to-date
                    myLabel.setText(title);
                    myButton.setEnabled(true);
                } else {
                    int count = (int) (title.length() * progress);
                    String text = title.substring(0, count);
                    myLabel.setText(text);
                }
            }
        });
        blinkTimer.start();
    }
    

    And...

    Oh 🤦‍♂️ - don't use the labels text as the initial text to be displayed by the label - use something which you can control, otherwise you're seeding incomplete text back into the animation

    private String greetingText = "Hi dear stackoverflow coders!";
    private Timer blinkTimer;
    
    @Override
    public void actionPerformed(ActionEvent e) {
        if (blinkTimer != null) {
            blinkTimer.stop();
            blinkTimer = null;
        }
        blinkTimer = new Timer(5, new ActionListener() {
    
            private String title = greetingText;
            private Duration runtime = Duration.ofSeconds(5);
    
            private Instant startTime;
    
            public void actionPerformed(ActionEvent e) {
                if (startTime == null) {
                    startTime = Instant.now();
                }
                Duration between = Duration.between(startTime, Instant.now());
                double progress = between.toMillis() / (double) runtime.toMillis();
    
                System.out.println(hashCode() + " " + progress);
    
                if (progress >= 1.0) {
                    ((Timer) e.getSource()).stop();
                    // Just make sure the text is up-to-date
                    myLabel.setText(title);
                    myButton.setEnabled(true);
                } else {
                    int count = (int) (title.length() * progress);
                    String text = title.substring(0, count);
                    myLabel.setText(text);
                }
            }
        });
        blinkTimer.start();
    }