I just want to start with, animation is not easy and good animation is hard. There is a lot of theory that goes into making animation "look" good, which I'm not going to cover here, there are better people and resources for that.
What I am going to discuss is how you can do "good" animation in Swing at a basic level.
The first problem seems to be the fact that you don't have a good understanding of how painting works in Swing. You should start by reading Performing Custom Painting in Swing and Painting in Swing
Next, you don't seem to realise that Swing is actually NOT thread safe (and is single threaded). This means you should never update the UI or any state the UI relies on from outside the context of the Event Dispatching Thread. See Concurrency in Swing for more details.
The simplest solution to solving this problem is to use a Swing Timer
, see How to Use Swing Timers for more details.
Now, you could simply run a Timer
and do a straight, linear progression until all your points meet their target, but this is not always the best solution, as it doesn't scale well and will appear different on different PC's, based on there individual capabilities.
In most cases, a duration based animation gives a better result. This allows the algorithm to "drop" frames when the PC is unable to keep up. It scales much better (of time and distance) and can be highly configurable.
I like to produce re-usable blocks of code, so I will start with a simple "duration based animation engine"...
// Self contained, duration based, animation engine...
public class AnimationEngine {
private Instant startTime;
private Duration duration;
private Timer timer;
private AnimationEngineListener listener;
public AnimationEngine(Duration duration) {
this.duration = duration;
}
public void start() {
if (timer != null) {
return;
}
startTime = null;
timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
tick();
}
});
timer.start();
}
public void stop() {
timer.stop();
timer = null;
startTime = null;
}
public void setListener(AnimationEngineListener listener) {
this.listener = listener;
}
public AnimationEngineListener getListener() {
return listener;
}
public Duration getDuration() {
return duration;
}
public double getRawProgress() {
if (startTime == null) {
return 0.0;
}
Duration duration = getDuration();
Duration runningTime = Duration.between(startTime, Instant.now());
double progress = (runningTime.toMillis() / (double) duration.toMillis());
return Math.min(1.0, Math.max(0.0, progress));
}
protected void tick() {
if (startTime == null) {
startTime = Instant.now();
}
double rawProgress = getRawProgress();
if (rawProgress >= 1.0) {
rawProgress = 1.0;
}
AnimationEngineListener listener = getListener();
if (listener != null) {
listener.animationEngineTicked(this, rawProgress);
}
// This is done so if you wish to expand the
// animation listener to include start/stop events
// this won't interfer with the tick event
if (rawProgress >= 1.0) {
rawProgress = 1.0;
stop();
}
}
public static interface AnimationEngineListener {
public void animationEngineTicked(AnimationEngine source, double progress);
}
}
It's not overly complicated, it has a duration
of time, over which it will run. It will tick
at a regular interval (of no less then 5 milliseconds) and will generate tick
events, reporting the current progression of the animation (as a normalised value of between 0 and 1).
The idea here is we decouple the "engine" from those elements which are using it. This allows us to use it for a much wider range of possibilities.
Next, I need some way to keep track of the position of my moving object...
public class Ping {
private Point point;
private Point from;
private Point to;
private Color fillColor;
private Shape dot;
public Ping(Point from, Point to, Color fillColor) {
this.from = from;
this.to = to;
this.fillColor = fillColor;
point = new Point(from);
dot = new Ellipse2D.Double(0, 0, 6, 6);
}
public void paint(Container parent, Graphics2D g2d) {
Graphics2D copy = (Graphics2D) g2d.create();
int width = dot.getBounds().width / 2;
int height = dot.getBounds().height / 2;
copy.translate(point.x - width, point.y - height);
copy.setColor(fillColor);
copy.fill(dot);
copy.dispose();
}
public Rectangle getBounds() {
int width = dot.getBounds().width;
int height = dot.getBounds().height;
return new Rectangle(point, new Dimension(width, height));
}
public void update(double progress) {
int x = update(progress, from.x, to.x);
int y = update(progress, from.y, to.y);
point.x = x;
point.y = y;
}
protected int update(double progress, int from, int to) {
int distance = to - from;
int value = (int) Math.round((double) distance * progress);
value += from;
if (from < to) {
value = Math.max(from, Math.min(to, value));
} else {
value = Math.max(to, Math.min(from, value));
}
return value;
}
}
This is a simply object which takes the start and end points and then calculates the position of the object between these points based on the progression. It can the paint itself when requested.
Now, we just need some way to put it together...
public class TestPane extends JPanel {
private Point source;
private Shape sourceShape;
private List<Ping> pings;
private List<Shape> destinations;
private Color[] colors = new Color[]{Color.BLACK, Color.BLUE, Color.CYAN, Color.DARK_GRAY, Color.GREEN, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.RED, Color.YELLOW};
private AnimationEngine engine;
public TestPane() {
source = new Point(10, 10);
sourceShape = new Ellipse2D.Double(source.x - 5, source.y - 5, 10, 10);
Dimension size = getPreferredSize();
Random rnd = new Random();
int quantity = 1 + rnd.nextInt(10);
pings = new ArrayList<>(quantity);
destinations = new ArrayList<>(quantity);
for (int index = 0; index < quantity; index++) {
int x = 20 + rnd.nextInt(size.width - 25);
int y = 20 + rnd.nextInt(size.height - 25);
Point toPoint = new Point(x, y);
// Create the "ping"
Color color = colors[rnd.nextInt(colors.length)];
Ping ping = new Ping(source, toPoint, color);
pings.add(ping);
// Create the destination shape...
Rectangle bounds = ping.getBounds();
Shape destination = new Ellipse2D.Double(toPoint.x - (bounds.width / 2d), toPoint.y - (bounds.height / 2d), 10, 10);
destinations.add(destination);
}
engine = new AnimationEngine(Duration.ofSeconds(10));
engine.setListener(new AnimationEngine.AnimationEngineListener() {
@Override
public void animationEngineTicked(AnimationEngine source, double progress) {
for (Ping ping : pings) {
ping.update(progress);
}
repaint();
}
});
engine.start();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
// This is probably overkill, but it will make the output look nicer ;)
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
// Lines first, these could be cached
g2d.setColor(Color.LIGHT_GRAY);
double fromX = sourceShape.getBounds2D().getCenterX();
double fromY = sourceShape.getBounds2D().getCenterY();
for (Shape destination : destinations) {
double toX = destination.getBounds2D().getCenterX();
double toY = destination.getBounds2D().getCenterY();
g2d.draw(new Line2D.Double(fromX, fromY, toX, toY));
}
// Pings, so they appear above the line, but under the points
for (Ping ping : pings) {
ping.paint(this, g2d);
}
// Destination and source
g2d.setColor(Color.BLACK);
for (Shape destination : destinations) {
g2d.fill(destination);
}
g2d.fill(sourceShape);
g2d.dispose();
}
}
Okay, this "looks" complicated, but it's really simple.
- We create a "source" point
- We then create a random number of "targets"
- We then create a animation engine and start it.
The animation engine will then loop through all the Ping
s and update them based on the current progress value and trigger a new paint pass, which then paints the lines between the source and target points, paints the Ping
s and then finally the source and all the target points. Simple.
What if I want the animation to run at different speeds?
Ah, well, this is much more complicated and requires a more complex animation engine.
Generally speaking, you could establish a concept of something which is "animatable". This would then be updated by a central "engine" which continuously "ticked" (wasn't itself constrained to duration).
Each "animatable" would then need to make decisions about how it was going to update or report its state and allow other objects to be updated.
In this case, I'd be looking towards a more ready made solution, for example...
Runnable example....

import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
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.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class JavaApplication124 {
public static void main(String[] args) {
new JavaApplication124();
}
public JavaApplication124() {
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 Point source;
private Shape sourceShape;
private List<Ping> pings;
private List<Shape> destinations;
private Color[] colors = new Color[]{Color.BLACK, Color.BLUE, Color.CYAN, Color.DARK_GRAY, Color.GREEN, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.RED, Color.YELLOW};
private AnimationEngine engine;
public TestPane() {
source = new Point(10, 10);
sourceShape = new Ellipse2D.Double(source.x - 5, source.y - 5, 10, 10);
Dimension size = getPreferredSize();
Random rnd = new Random();
int quantity = 1 + rnd.nextInt(10);
pings = new ArrayList<>(quantity);
destinations = new ArrayList<>(quantity);
for (int index = 0; index < quantity; index++) {
int x = 20 + rnd.nextInt(size.width - 25);
int y = 20 + rnd.nextInt(size.height - 25);
Point toPoint = new Point(x, y);
// Create the "ping"
Color color = colors[rnd.nextInt(colors.length)];
Ping ping = new Ping(source, toPoint, color);
pings.add(ping);
// Create the destination shape...
Rectangle bounds = ping.getBounds();
Shape destination = new Ellipse2D.Double(toPoint.x - (bounds.width / 2d), toPoint.y - (bounds.height / 2d), 10, 10);
destinations.add(destination);
}
engine = new AnimationEngine(Duration.ofSeconds(10));
engine.setListener(new AnimationEngine.AnimationEngineListener() {
@Override
public void animationEngineTicked(AnimationEngine source, double progress) {
for (Ping ping : pings) {
ping.update(progress);
}
repaint();
}
});
engine.start();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
// This is probably overkill, but it will make the output look nicer ;)
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
// Lines first, these could be cached
g2d.setColor(Color.LIGHT_GRAY);
double fromX = sourceShape.getBounds2D().getCenterX();
double fromY = sourceShape.getBounds2D().getCenterY();
for (Shape destination : destinations) {
double toX = destination.getBounds2D().getCenterX();
double toY = destination.getBounds2D().getCenterY();
g2d.draw(new Line2D.Double(fromX, fromY, toX, toY));
}
// Pings, so they appear above the line, but under the points
for (Ping ping : pings) {
ping.paint(this, g2d);
}
// Destination and source
g2d.setColor(Color.BLACK);
for (Shape destination : destinations) {
g2d.fill(destination);
}
g2d.fill(sourceShape);
g2d.dispose();
}
}
// Self contained, duration based, animation engine...
public static class AnimationEngine {
private Instant startTime;
private Duration duration;
private Timer timer;
private AnimationEngineListener listener;
public AnimationEngine(Duration duration) {
this.duration = duration;
}
public void start() {
if (timer != null) {
return;
}
startTime = null;
timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
tick();
}
});
timer.start();
}
public void stop() {
timer.stop();
timer = null;
startTime = null;
}
public void setListener(AnimationEngineListener listener) {
this.listener = listener;
}
public AnimationEngineListener getListener() {
return listener;
}
public Duration getDuration() {
return duration;
}
public double getRawProgress() {
if (startTime == null) {
return 0.0;
}
Duration duration = getDuration();
Duration runningTime = Duration.between(startTime, Instant.now());
double progress = (runningTime.toMillis() / (double) duration.toMillis());
return Math.min(1.0, Math.max(0.0, progress));
}
protected void tick() {
if (startTime == null) {
startTime = Instant.now();
}
double rawProgress = getRawProgress();
if (rawProgress >= 1.0) {
rawProgress = 1.0;
}
AnimationEngineListener listener = getListener();
if (listener != null) {
listener.animationEngineTicked(this, rawProgress);
}
// This is done so if you wish to expand the
// animation listener to include start/stop events
// this won't interfer with the tick event
if (rawProgress >= 1.0) {
rawProgress = 1.0;
stop();
}
}
public static interface AnimationEngineListener {
public void animationEngineTicked(AnimationEngine source, double progress);
}
}
public class Ping {
private Point point;
private Point from;
private Point to;
private Color fillColor;
private Shape dot;
public Ping(Point from, Point to, Color fillColor) {
this.from = from;
this.to = to;
this.fillColor = fillColor;
point = new Point(from);
dot = new Ellipse2D.Double(0, 0, 6, 6);
}
public void paint(Container parent, Graphics2D g2d) {
Graphics2D copy = (Graphics2D) g2d.create();
int width = dot.getBounds().width / 2;
int height = dot.getBounds().height / 2;
copy.translate(point.x - width, point.y - height);
copy.setColor(fillColor);
copy.fill(dot);
copy.dispose();
}
public Rectangle getBounds() {
int width = dot.getBounds().width;
int height = dot.getBounds().height;
return new Rectangle(point, new Dimension(width, height));
}
public void update(double progress) {
int x = update(progress, from.x, to.x);
int y = update(progress, from.y, to.y);
point.x = x;
point.y = y;
}
protected int update(double progress, int from, int to) {
int distance = to - from;
int value = (int) Math.round((double) distance * progress);
value += from;
if (from < to) {
value = Math.max(from, Math.min(to, value));
} else {
value = Math.max(to, Math.min(from, value));
}
return value;
}
}
}
Isn't there something simpler
As I said, good animation, is hard. It takes a lot of effort and planning to do well. I've not even talked about easement, chained or blending algorithms, so trust me when I say, this is actually a simple, reusable solution
Don't believe me, check out JButton hover animation in Java Swing