Essentially, I want each mouse click to spawn a new Timer,
No, I wouldn't do that, more Timer
's isn't going to help, it's going to have a degrading effect on performance. Remember, each Timer
will still schedule it's callbacks on the Event Dispatching Thread.
that will not begin until the previous has finished.
Sounds more like you want some kind of "stack" where a centralised loop can pop of the next task and animate it.
Cavets
First things first. Animation is hard, good animation is very hard. There's a lot that goes into making animation "look" good.
Centralised engine
Typically, you want to start with a concept of a "main" or "animation" loop. This will repeat, at as regular an interval as it can and will "tick" you animations.
For this, I've started with a Swing Timer
. It's simple and safe to use. When there are no more things to animate, it will stop, so it's not wasting CPU cycles doing nothing.
public class AnimationEngine {
private Timer timer;
private List<Animatable> animatables = new ArrayList<>(8);
private Animatable current;
public AnimationEngine() {
this.timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
didTick();
}
});
}
public void add(Animatable animatable) {
animatables.add(animatable);
start();
}
public void remove(Animatable animatable) {
animatables.remove(animatable);
if (current == animatable) {
current = null;
}
if (animatables.isEmpty()) {
stop();
}
}
public void start() {
if (timer.isRunning()) {
return;
}
timer.start();
}
public void stop() {
timer.stop();
}
protected void didTick() {
if (current == null) {
if (animatables.isEmpty()) {
stop();
return;
}
current = animatables.remove(0);
}
if (current.tick(this)) {
remove(current);
}
}
}
Something to animate
This is probably a little bit of overkill for what you need, but the following describes something which can be animated.
public interface Animatable {
public double getProgress();
public Duration getDuration();
public boolean tick(AnimationEngine engine);
}
This is "duration" based, meaning that it will animate some change over a specified period of time. Generally speaking, a time based animation will give better results over a linear delta based animation. It also makes it much easier to change the speed of the animation.
I then created a RotationAnimation
, which just animates the rotation of a Box
from one angle to other (over a period of time)
public class RotationAnimation implements Animatable {
private float from;
private float to;
private Box box;
private Duration duration;
private Instant startedAt;
public RotationAnimation(Box box, float from, float to, Duration duration) {
this.from = from;
this.to = to;
this.box = box;
this.duration = duration;
}
public Box getBox() {
return box;
}
public float getFrom() {
return from;
}
public float getTo() {
return to;
}
protected float getDistance() {
return getTo() - getFrom();
}
protected float valueAt(double progress) {
float distance = getDistance();
float value = distance * (float) progress;
value += getFrom();
return value;
}
@Override
public double getProgress() {
if (startedAt == null) {
startedAt = Instant.now();
}
Duration duration = getDuration();
Duration runningTime = Duration.between(startedAt, Instant.now());
double progress = (runningTime.toMillis() / (double) duration.toMillis());
return Math.min(1.0, Math.max(0.0, progress));
}
@Override
public Duration getDuration() {
return duration;
}
@Override
public boolean tick(AnimationEngine engine) {
boolean didComplete = false;
double progress = getProgress();
if (progress >= 1.0f) {
didComplete = true;
progress = 1.0f;
}
float angle = valueAt(progress);
box.setAngle(angle);
box.repaint();
return didComplete;
}
}
Relatively speaking, this is pretty simple. On each tick
it determines the amount of time it's been running, converts this value to a normalised progress (0-1) and calculates the resulting angle to be applied.
Runnable example...

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.border.EmptyBorder;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
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 AnimationEngine engine = new AnimationEngine();
public TestPane() {
setBorder(new EmptyBorder(32, 32, 32, 32));
setLayout(new GridLayout(10, 10));
for (int index = 0; index < 100; index++) {
Box box = new Box();
add(box);
box.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
float startAngle = box.getAngle();
float endAngle = startAngle + 90;
engine.add(new RotationAnimation(box, startAngle, endAngle, Duration.ofSeconds(1)));
}
});
}
}
}
public class Box extends JPanel {
public static final int BOX_WIDTH = 50;
public static final int BOX_HEIGHT = 50;
protected static final List<Color> COLORS = new ArrayList<>(Arrays.asList(new Color[] {
Color.BLACK, Color.BLUE, Color.CYAN, Color.DARK_GRAY, Color.GRAY, Color.GREEN,
Color.LIGHT_GRAY, Color.MAGENTA, Color.PINK, Color.RED
}));
private float angle = 0;
private Map<RenderingHints.Key, Object> renderingHints = new HashMap<>();
public Box() {
Collections.shuffle(COLORS);
setForeground(COLORS.get(0));
renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
public void setAngle(float angle) {
this.angle = angle;
}
public float getAngle() {
return angle;
}
@Override
public Dimension getPreferredSize() {
return new Dimension(BOX_WIDTH, BOX_HEIGHT);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHints(renderingHints);
g2d.rotate(Math.toRadians(getAngle()), getWidth() / 2, getHeight() / 2);
g2d.setColor(getForeground());
int x = (getWidth() - BOX_WIDTH) / 2;
int y = (getHeight() - BOX_HEIGHT) / 2;
g2d.drawRect(x, y, BOX_WIDTH - 1, BOX_HEIGHT - 1);
g2d.drawLine(x + BOX_WIDTH / 2, y + BOX_HEIGHT / 2, x + BOX_WIDTH / 2, y + BOX_HEIGHT);
g2d.dispose();
}
}
public interface Animatable {
public double getProgress();
public Duration getDuration();
public boolean tick(AnimationEngine engine);
}
public class AnimationEngine {
private Timer timer;
private List<Animatable> animatables = new ArrayList<>(8);
private Animatable current;
public AnimationEngine() {
this.timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
didTick();
}
});
}
public void add(Animatable animatable) {
animatables.add(animatable);
start();
}
public void remove(Animatable animatable) {
animatables.remove(animatable);
if (current == animatable) {
current = null;
}
if (animatables.isEmpty()) {
stop();
}
}
public void start() {
if (timer.isRunning()) {
return;
}
timer.start();
}
public void stop() {
timer.stop();
}
protected void didTick() {
if (current == null) {
if (animatables.isEmpty()) {
stop();
return;
}
current = animatables.remove(0);
}
if (current.tick(this)) {
remove(current);
}
}
}
public class RotationAnimation implements Animatable {
private float from;
private float to;
private Box box;
private Duration duration;
private Instant startedAt;
public RotationAnimation(Box box, float from, float to, Duration duration) {
this.from = from;
this.to = to;
this.box = box;
this.duration = duration;
}
public Box getBox() {
return box;
}
public float getFrom() {
return from;
}
public float getTo() {
return to;
}
protected float getDistance() {
return getTo() - getFrom();
}
protected float valueAt(double progress) {
float distance = getDistance();
float value = distance * (float) progress;
value += getFrom();
return value;
}
@Override
public double getProgress() {
if (startedAt == null) {
startedAt = Instant.now();
}
Duration duration = getDuration();
Duration runningTime = Duration.between(startedAt, Instant.now());
double progress = (runningTime.toMillis() / (double) duration.toMillis());
return Math.min(1.0, Math.max(0.0, progress));
}
@Override
public Duration getDuration() {
return duration;
}
@Override
public boolean tick(AnimationEngine engine) {
boolean didComplete = false;
double progress = getProgress();
if (progress >= 1.0f) {
didComplete = true;
progress = 1.0f;
}
float angle = valueAt(progress);
box.setAngle(angle);
box.repaint();
return didComplete;
}
}
}
I've not even started talking about things like "easement", which could make the animation look "more natural".
if you're interested, you could have a look at How can I implement easing functions with a thread (which I also implemented here)
I need. In you're example here, it only allows you to select one box at a time. I did not mean that, by "synchronization". I meant, prevent them from overlapping, and using the same resource simultaneously
This is relatively simple to fix, simply modify the didTick
method of the AnimationEngine
to loop through the List
and call each Animatable
s tick
method, for example...
protected void didTick() {
List<Animatable> copy = new ArrayList<>(animatables);
for (Animatable animated : copy) {
if (animated.tick(this)) {
animatables.remove(animated);
}
}
if (animatables.isEmpty()) {
stop();
}
}
The mouse-click is part of the score, and the player may want to quickly double-click a box, or triple-click. They'll expect to see an animation for each click
This is a more complicated, but could be accomplished through a number of different methods.
You could "interrupt" the current animation, take the Box
s current angle, project the new angle and start again. This, however, is going to make the animation look ... well, weird. Since the time doesn't change, the speed at which the box moves will change, so click it enough times and you can have a very fast spin.
Alternatively, you could build a "chain" of animations. This would basically be a Animatable
container which contains a List
of animations to be played. It would simply play through each animation in sequence until it has no more animations.
For example...
public class ChainedRotationAnimation implements Animatable {
private List<RotationAnimation> animations = new ArrayList<>(8);
private RotationAnimation active;
private Box box;
public ChainedRotationAnimation(Box box) {
this.box = box;
}
public Box getBox() {
return box;
}
public Float getTargetAngle() {
// If nothing is avaliable, return `null`
if (animations.isEmpty() && active == null) {
return null;
}
// If no other animations are avaliable, use the active animation
// instead
if (animations.isEmpty() && active != null) {
return active.getTo();
}
// Otherwise, find the last animation and use it's target angle
RotationAnimation last = animations.get(animations.size() - 1);
return last.getTo();
}
public void add(RotationAnimation animatable) {
animations.add(animatable);
}
public void remove(RotationAnimation animatable) {
animations.remove(animatable);
if (active == animatable) {
active = null;
}
}
// I would remove these properties fom the `Animatable` class
// and instead move them to a `DurationAnimatable` class
@Override
public double getProgress() {
return 0;
}
@Override
public Duration getDuration() {
return Duration.ZERO;
}
@Override
public boolean tick(AnimationEngine engine) {
if (active == null) {
if (animations.isEmpty()) {
return true;
}
active = animations.remove(0);
}
if (active.tick(engine)) {
active = null;
// Save us a whole tick
return animations.isEmpty();
}
return false;
}
}
Then we'd need to update the Box
mouse click handler...
box.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// Get all the current animations...
List<Animatable> animations = engine.getAnimations();
boolean exists = false;
for (Animatable animatable : animations) {
// Ignore anything which isn't a chained rotation
if (!(animatable instanceof ChainedRotationAnimation)) {
continue;
}
// Ignore the animation if it's not for our Box
ChainedRotationAnimation chained = (ChainedRotationAnimation) animatable;
if (chained.getBox() != box) {
continue;
}
exists = true;
// Determine the angle from which the box will start
// Either this is the current angle if it's not been
// animated or the target angle for the last animation
// in the chain
float startAngle = box.getAngle();
if (chained.getTargetAngle() != null) {
startAngle = chained.getTargetAngle();
}
float endAngle = startAngle + 90;
chained.add(new RotationAnimation(box, startAngle, endAngle, Duration.ofMillis(250)));
}
// If no chained animation exists, we need to create one
if (exists) {
return;
}
float startAngle = box.getAngle();
float endAngle = startAngle + 90;
ChainedRotationAnimation chained = new ChainedRotationAnimation(box);
chained.add(new RotationAnimation(box, startAngle, endAngle, Duration.ofMillis(250)));
engine.add(chained);
}
});
This is just an example and the concepts are loosely based on my personal Super Simple Swing Animation Framework library