0

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?

Goet
  • 57
  • 1
  • 1
  • 8
  • Your time is set to a 1 millisecond delay, so it will be completed in `30` milliseconds :/ – MadProgrammer Dec 12 '21 at 02:17
  • @MadProgrammer and what's the problem? Try to set a delay time of 100 and a total time of 1000: same result. EDIT: oh, ok, I get what you mean: i'm not giving enough time to complete the text writing... if I set a bigger total time it's working even after a lot of fast clicks – Goet Dec 12 '21 at 02:19
  • @MadProgrammer EDIT 2: not really... tried with delay 100 and maxtime 6000, and the text still get truncated after some fast clicks – Goet Dec 12 '21 at 02:25
  • Not the total time, but the time between updates. You don't really care about the total time, as you just want to keep updating till all the text is displayed – MadProgrammer Dec 12 '21 at 02:25

1 Answers1

1

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();
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thanks for your reply : ) By the way, try to use your code and repeatedly clicks on the button: I stil get the same result. P.s. for the maxTime part... you're right :) – Goet Dec 12 '21 at 02:30
  • 1. Disable the button when the animation is playing or; 2. Stop the current animation before start a new one – MadProgrammer Dec 12 '21 at 02:32
  • Yeah I'll end up disabling buttons I guess... but I was just curious to know what's happening there, why the last call isn't able to correctly display the whole string – Goet Dec 12 '21 at 02:34
  • You're also seeding incomplete text back into the next animation loop - so what ever text the last animation cycle ended on is what will be seeded back into the next animation loop. Use a variable to maintain the "original" text – MadProgrammer Dec 12 '21 at 02:39
  • Thanks for the clarifying edit! Going to study your code now : ) – Goet Dec 12 '21 at 02:42
  • I'm studying your code (once again: thank you, I'm learning a bunch of things!) and hope you don't mind answering some questions I might have from time to time. My first one: I believe I fully understood the `Time based animation` sample. Only one thing: what's the role of that `5` param in that specific algo? Guess it's there just to make compiler happy cause `Timer` constructor wants its delay arg? – Goet Dec 12 '21 at 14:41
  • @Goet The "delay" or "interval" parameter is the time between "ticks" of the `Timer`, in my experience, about 5 milliseconds is the lowest achievable value. Because it's a time based progression, it's generally easier to run the timer "hot", so to speak. You could set it to 16 (milliseconds) and that would equate to roughly 60fps – MadProgrammer Dec 12 '21 at 20:20
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/240082/discussion-between-goet-and-madprogrammer). – Goet Dec 13 '21 at 00:46