How to Create a Delay in Swing

How could I add a simple delay in a Java Swing application?

Here's an example using a javax.swing.Timer

public class TestBlinkingText {

public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
} catch (UnsupportedLookAndFeelException ex) {
}

JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new BlinkPane());
frame.setSize(200, 200);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}

protected static class BlinkPane extends JLabel {

private JLabel label;
private boolean state;

public BlinkPane() {

label = new JLabel("Look at me!");
setLayout(new GridBagLayout());

add(label);

Timer timer = new Timer(500, new ActionListener() {

@Override
public void actionPerformed(ActionEvent ae) {
state = !state;
if (state) {
label.setForeground(Color.RED);
} else {
label.setForeground(Color.BLACK);
}
repaint();
}
});
timer.setRepeats(true);
timer.setCoalesce(true);
timer.setInitialDelay(0);
timer.start();
}
}
}

How to create a delay in Swing

Well, the following code shows a JFrame with a JTextArea and a JButton. When the buttons is clicked, the Timer send the event repeatedly (with a second delay between them) to the actionListener related to the button which appends a line with the current time.

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Calendar;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.Timer;

public class TimerTest extends JFrame implements ActionListener{

private static final long serialVersionUID = 7416567620110237028L;
JTextArea area;
Timer timer;
int count; // Counts the number of sendings done by the timer
boolean running; // Indicates if the timer is started (true) or stopped (false)

public TimerTest() {
super("Test");
setBounds(30,30,500,500);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(null);

area = new JTextArea();
area.setBounds(0, 0, 500, 400);
add(area);

JButton button = new JButton("Click Me!");
button.addActionListener(this);
button.setBounds(200, 400, 100, 40);
add(button);

// Initialization of the timer. 1 second delay and this class as ActionListener
timer = new Timer(1000, this);
timer.setRepeats(true); // Send events until someone stops it
count = 0; // in the beginning, 0 events sended by timer
running = false;
System.out.println(timer.isRepeats());
setVisible(true); // Shows the frame
}

public void actionPerformed(ActionEvent e) {
if (! running) {
timer.start();
running = true;
}
// Writing the current time and increasing the cont times
area.append(Calendar.getInstance().getTime().toString()+"\n");
count++;
if (count == 10) {
timer.stop();
count = 0;
running = false;
}
}

public static void main(String[] args) {
// Executing the frame with its Timer
new TimerTest();
}
}

Well, this code is a sample of how to use javax.swig.Timer objects. In relation with the particular case of the question. The if statement to stop the timer must change, and, obviously, the actions of the actionPerformed. The following fragment is a skeleton of the solution actionPerformed:

public void actionPerformed(ActionEvent e) {
if (e.getComponent() == myDealerComponent()) {
// I do this if statement because the actionPerformed can treat more components
if (! running) {
timer.start();
runnig = true;
}
// Hit a card if it must be hitted
switch (getJBTable(JB.total, JB.aces > 0)) {
case 0:
JB.hit();
break;
case 1:
break done;
case 2:
JB.hit();
JB.bet *= 2;
break done;
}
if (JB.total >= 21) { // In this case we don't need count the number of times, only check the JB.total 21 reached
timer.stop()
running = false;
}

}
}

IMHO this resolves the problem, now @user920769 must think where put the actionListener and the starting/stopping conditions...

@kleopatra: Thanks for show me the existence of this timer class, I don't know nothing about it and it's amazing, make possible a lot of tasked things into a swing application :)

Any way to delay actions in a Java GUI

Not looking to use Thread.sleep() as it doesn't work well with gui.

Good, use a Swing Timer instead

I want the pokerHand.setVisibility(true), and other methods in my code to execute AFTER the delay, ( using a timer doesn't do this ).

Yes it does, you're just not using it properly, but since you've not provided any actual code, I can't say "how" you're not using it properly, only from the sounds of it, you are.

Start by taking a look at How to use Swing Timers for more details

The following is very simple example, it uses a Timer to calculate and print the amount of time between the time you clicked the button.

The example updates the UI AFTER the Timer is started, but until the Timer completes, the calculation and result are not generated

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.LocalTime;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

public static void main(String[] args) {
new Test();
}

public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}

JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}

public class TestPane extends JPanel {

private JTextArea ta;
private JButton btn;

private Timer timer;

private LocalTime startTime, endTime;

public TestPane() {
setLayout(new BorderLayout());
ta = new JTextArea(10, 20);
add(new JScrollPane(ta));
btn = new JButton("Click");
add(btn, BorderLayout.SOUTH);

timer = new Timer(2000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
endTime = LocalTime.now();
Duration duration = Duration.between(startTime, endTime);
ta.append("Ended @ " + endTime + "\n");
ta.append("Took " + (duration.toMillis() / 1000) + " seconds\n");
}
});
timer.setRepeats(false);

ta.setEditable(false);

btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
timer.start();
startTime = LocalTime.now();
btn.setEnabled(false);
ta.append("Start @ " + startTime + "\n");
}
});
}

}

}

How do I change Swing Timer Delay inside actionPerformed()

Caveats:

  • Animation is NOT simple. It's complicated. It has a number of important theories around it which are designed to make animation look good
  • Good animation is hard
  • Animation is the illusion of change over time
  • Much of what I'm presenting is based on library code, so it will be slightly convoluted, but is designed for re-use and abstraction

Theory tl;dr

Okay, some really boring theory. But first, things I'm not going to talk about - easement or animation curves. These change the speed at which animation is played over a given period of time, making the animation look more natural, but I could spend the entire answer talking about nothing else :/

The first thing you want to do is abstract your concepts. For example. Animation is typically a change over time (some animation is linear over an infinite amount of time, but, let's try and keep it within the confines of the question).

So immediately, we have two important concepts. The first is duration, the second is the normalised progress from point A to point B over that duration. That is, at half the duration, the progression will be 0.5. This is important, as it allows us to abstract the concepts and make the framework dynamic.

Animation too fast? Change the duration and everything else remains unchanged.

A timeline...

Okay, music is a timeline. It has a defined start and end point (again, keep it simple) and events along that timeline which "do stuff", independent of the music timeline (ie, each note can play for a specified duration, independent of the music timeline, which will have moved on or even finished)

First, we need a note...

public class Note {
private Duration duration;

public Note(Duration duration) {
this.duration = duration;
}

public Duration getDuration() {
return duration;
}
}

And a "event" based timeline, which describes when those notes should be played over a normalised period of time.

public static class EventTimeLine<T> {

private Map<Double, KeyFrame<T>> mapEvents;

public EventTimeLine() {
mapEvents = new TreeMap<>();
}

public void add(double progress, T value) {
mapEvents.put(progress, new KeyFrame<T>(progress, value));
}

public List<T> getValues() {
return Collections.unmodifiableList(mapEvents.values().stream()
.map(kf -> kf.getValue())
.collect(Collectors.toList()));
}

public double getPointOnTimeLineFor(T value) {
for (Map.Entry<Double, KeyFrame<T>> entry : mapEvents.entrySet()) {
if (entry.getValue().getValue() == value) {
return entry.getKey();
}
}

return -1;
}

public List<T> getValuesAt(double progress) {

if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}

return getKeyFramesBetween(progress, 0.01f)
.stream()
.map(kf -> kf.getValue())
.collect(Collectors.toList());
}

public List<KeyFrame<T>> getKeyFramesBetween(double progress, double delta) {

int startAt = 0;

List<Double> keyFrames = new ArrayList<>(mapEvents.keySet());
while (startAt < keyFrames.size() && keyFrames.get(startAt) <= progress - delta) {
startAt++;
}

startAt = Math.min(keyFrames.size() - 1, startAt);
int endAt = startAt;
while (endAt < keyFrames.size() && keyFrames.get(endAt) <= progress + delta) {
endAt++;
}
endAt = Math.min(keyFrames.size() - 1, endAt);

List<KeyFrame<T>> frames = new ArrayList<>(5);
for (int index = startAt; index <= endAt; index++) {
KeyFrame<T> keyFrame = mapEvents.get(keyFrames.get(index));
if (keyFrame.getProgress() >= progress - delta
&& keyFrame.getProgress() <= progress + delta) {
frames.add(keyFrame);
}
}

return frames;

}

public class KeyFrame<T> {

private double progress;
private T value;

public KeyFrame(double progress, T value) {
this.progress = progress;
this.value = value;
}

public double getProgress() {
return progress;
}

public T getValue() {
return value;
}

@Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
}

}

}

Then you could create a music timeline something like...

musicTimeLine = new EventTimeLine<Note>();
musicTimeLine.add(0.1f, new Note(Duration.ofMillis(1000)));
musicTimeLine.add(0.12f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.2f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.21f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.22f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.25f, new Note(Duration.ofMillis(1000)));
musicTimeLine.add(0.4f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.5f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.7f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.8f, new Note(Duration.ofMillis(2000)));

Note, here I've defined the notes as running at a fixed duration. You "could" make them play as a percentage of the duration of the timeline ... but just saying that is hard, so I'll leave that up to you ;)

Animation Engine

The presented (simple) animation engine, uses a single Timer, running at high speed, as a central "tick" engine.

It then notifies Animatable objects which actually perform the underlying animation.

Normally, I animated over a range of values (from - to), but in this case, we're actually only interested in the amount of time that the animation has played. From that we can determine what notes should be getting played AND animate the notes, in the case of this example, change the alpha value, but you could equally change the size of the objects representing the notes, but that would be a different Animatable implementation, which I've not presented here.

If you're interested, my SuperSimpleSwingAnimationFramework, which this example is loosely based on, contains "range" based Animatables ... fun stuff.

In the example, an Animatable is used to drive the music EventTimeLine, which simply checks the timeline for any "notes" which need to be played at the specific point in time.

A second BlendingTimeLine is used to control the alpha value (0-1-0). Each note is then provided with it's own Animatable which drives this blending timeline, and uses its values to animate the change in the alpha of the highlighted note.

This is a great example of the decoupled nature of the API - the BlendingTimeLine is used for ALL the notes. The Animatables simply take the amount of time they have played and extract the required value from the timeline and apply it.

This means that each note is only highlighted as long as its own duration specifies, all independently.

Runnable Example...

nb: If I was doing this, I'd have abstracted the solution to a much higher level

Twang my strings

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Test {

public static void main(String[] args) {
new Test();
}

public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}

public class TestPane extends JPanel {

private EventTimeLine<Note> musicTimeLine;
private DefaultDurationAnimatable timeLineAnimatable;

private Double playProgress;

private Set<Note> playing = new HashSet<Note>(5);
private Map<Note, Double> noteAlpha = new HashMap<>(5);

private DoubleBlender blender = new DoubleBlender();
private BlendingTimeLine<Double> alphaTimeLine = new BlendingTimeLine<>(blender);

public TestPane() {
musicTimeLine = new EventTimeLine<Note>();
musicTimeLine.add(0.1f, new Note(Duration.ofMillis(1000)));
musicTimeLine.add(0.12f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.2f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.21f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.22f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.25f, new Note(Duration.ofMillis(1000)));
musicTimeLine.add(0.4f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.5f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.7f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.8f, new Note(Duration.ofMillis(2000)));

alphaTimeLine.add(0.0f, 0.0);
alphaTimeLine.add(0.5f, 1.0);
alphaTimeLine.add(1.0f, 0.0);

timeLineAnimatable = new DefaultDurationAnimatable(Duration.ofSeconds(10),
new AnimatableListener() {
@Override
public void animationChanged(Animatable animator) {
double progress = timeLineAnimatable.getPlayedDuration();
playProgress = progress;
List<Note> notes = musicTimeLine.getValuesAt(progress);
if (notes.size() > 0) {
System.out.println(">> " + progress + " @ " + notes.size());
for (Note note : notes) {
playNote(note);
}
}
repaint();
}
}, null);

timeLineAnimatable.start();
}

protected void playNote(Note note) {
// Note is already playing...
// Equally, we could maintain a reference to the animator, mapped to
// the note, but what ever...
if (playing.contains(note)) {
return;
}
playing.add(note);

DurationAnimatable noteAnimatable = new DefaultDurationAnimatable(note.getDuration(), new AnimatableListener() {
@Override
public void animationChanged(Animatable animator) {
DurationAnimatable da = (DurationAnimatable) animator;
double progress = da.getPlayedDuration();
double alpha = alphaTimeLine.getValueAt((float) progress);
noteAlpha.put(note, alpha);
repaint();
}
}, new AnimatableLifeCycleListenerAdapter() {
@Override
public void animationCompleted(Animatable animator) {
playing.remove(note);
noteAlpha.remove(note);
repaint();
}
});
noteAnimatable.start();
}

@Override
public Dimension getPreferredSize() {
return new Dimension(200, 100);
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();

int startX = 10;
int endX = getWidth() - 10;
int range = endX - startX;

int yPos = getHeight() / 2;

g2d.setColor(Color.DARK_GRAY);
g2d.drawLine(startX, yPos, endX, yPos);

List<Note> notes = musicTimeLine.getValues();
for (Note note : notes) {
double potl = musicTimeLine.getPointOnTimeLineFor(note);
double xPos = startX + (range * potl);
// Technically, this could be cached...
Ellipse2D notePoint = new Ellipse2D.Double(xPos - 2.5, yPos - 2.5, 5, 5);
g2d.fill(notePoint);

if (noteAlpha.containsKey(note)) {
double alpha = noteAlpha.get(note);
// I'm lazy :/
// It's just simpler to copy the current context, modify the
// composite, paint and then dispose of, then trying to
// track and reset the composite manually
Graphics2D alpha2d = (Graphics2D) g2d.create();
alpha2d.setComposite(AlphaComposite.SrcOver.derive((float) alpha));
Ellipse2D playedNote = new Ellipse2D.Double(xPos - 5, yPos - 5, 10, 10);
alpha2d.setColor(Color.RED);
alpha2d.fill(playedNote);
alpha2d.dispose();
}
}

double playXPos = startX + (range * playProgress);
g2d.setColor(Color.RED);
Line2D playLine = new Line2D.Double(playXPos, 0, playXPos, getHeight());
g2d.draw(playLine);

g2d.dispose();
}

}

public class Note {

private Duration duration;

public Note(Duration duration) {
this.duration = duration;
}

public Duration getDuration() {
return duration;
}
}

public static class EventTimeLine<T> {

private Map<Double, KeyFrame<T>> mapEvents;

public EventTimeLine() {
mapEvents = new TreeMap<>();
}

public void add(double progress, T value) {
mapEvents.put(progress, new KeyFrame<T>(progress, value));
}

public List<T> getValues() {
return Collections.unmodifiableList(mapEvents.values().stream()
.map(kf -> kf.getValue())
.collect(Collectors.toList()));
}

public double getPointOnTimeLineFor(T value) {
for (Map.Entry<Double, KeyFrame<T>> entry : mapEvents.entrySet()) {
if (entry.getValue().getValue() == value) {
return entry.getKey();
}
}

return -1;
}

public List<T> getValuesAt(double progress) {

if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}

return getKeyFramesBetween(progress, 0.01f)
.stream()
.map(kf -> kf.getValue())
.collect(Collectors.toList());
}

public List<KeyFrame<T>> getKeyFramesBetween(double progress, double delta) {

int startAt = 0;

List<Double> keyFrames = new ArrayList<>(mapEvents.keySet());
while (startAt < keyFrames.size() && keyFrames.get(startAt) <= progress - delta) {
startAt++;
}

startAt = Math.min(keyFrames.size() - 1, startAt);
int endAt = startAt;
while (endAt < keyFrames.size() && keyFrames.get(endAt) <= progress + delta) {
endAt++;
}
endAt = Math.min(keyFrames.size() - 1, endAt);

List<KeyFrame<T>> frames = new ArrayList<>(5);
for (int index = startAt; index <= endAt; index++) {
KeyFrame<T> keyFrame = mapEvents.get(keyFrames.get(index));
if (keyFrame.getProgress() >= progress - delta
&& keyFrame.getProgress() <= progress + delta) {
frames.add(keyFrame);
}
}

return frames;

}

public class KeyFrame<T> {

private double progress;
private T value;

public KeyFrame(double progress, T value) {
this.progress = progress;
this.value = value;
}

public double getProgress() {
return progress;
}

public T getValue() {
return value;
}

@Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
}

}

}

public static class BlendingTimeLine<T> {

private Map<Float, KeyFrame<T>> mapEvents;

private Blender<T> blender;

public BlendingTimeLine(Blender<T> blender) {
mapEvents = new TreeMap<>();
this.blender = blender;
}

public void setBlender(Blender<T> blender) {
this.blender = blender;
}

public Blender<T> getBlender() {
return blender;
}

public void add(float progress, T value) {
mapEvents.put(progress, new KeyFrame<T>(progress, value));
}

public T getValueAt(float progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}

List<KeyFrame<T>> keyFrames = getKeyFramesBetween(progress);

float max = keyFrames.get(1).progress - keyFrames.get(0).progress;
float value = progress - keyFrames.get(0).progress;
float weight = value / max;

T blend = blend(keyFrames.get(0).getValue(), keyFrames.get(1).getValue(), 1f - weight);
return blend;
}

public List<KeyFrame<T>> getKeyFramesBetween(float progress) {

List<KeyFrame<T>> frames = new ArrayList<>(2);
int startAt = 0;
Float[] keyFrames = mapEvents.keySet().toArray(new Float[mapEvents.size()]);
while (startAt < keyFrames.length && keyFrames[startAt] <= progress) {
startAt++;
}

startAt = Math.min(startAt, keyFrames.length - 1);

frames.add(mapEvents.get(keyFrames[startAt - 1]));
frames.add(mapEvents.get(keyFrames[startAt]));

return frames;

}

protected T blend(T start, T end, float ratio) {
return blender.blend(start, end, ratio);
}

public static interface Blender<T> {

public T blend(T start, T end, float ratio);
}

public class KeyFrame<T> {

private float progress;
private T value;

public KeyFrame(float progress, T value) {
this.progress = progress;
this.value = value;
}

public float getProgress() {
return progress;
}

public T getValue() {
return value;
}

@Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
}

}

}

public class DoubleBlender implements BlendingTimeLine.Blender<Double> {

@Override
public Double blend(Double start, Double end, float ratio) {
double ir = (double) 1.0 - ratio;
return (double) (start * ratio + end * ir);
}

}


Related Topics



Leave a reply



Submit