Я просто хочу начать, анимация - это не просто, а хорошая анимация - это сложно. Существует много теорий, которые делают анимацию «хорошо выглядящей», которую я не буду здесь освещать, для этого есть лучшие люди и ресурсы.
Что я собираюсь обсудить, так это то, как вы можете сделать «хорошую» анимацию в Swing на базовом уровне.
Первая проблема, кажется, в том, что у вас нет хорошего понимания того, как рисование работает в Swing. Вы должны начать с чтения Выполнение нестандартной живописи в Swing и Рисование в Swing
Далее вы, похоже, не понимаете, что Swing на самом деле НЕ является потокобезопасным (и является однопоточным). Это означает, что вы никогда не должны обновлять пользовательский интерфейс или любое состояние, на которое полагается пользовательский интерфейс, вне контекста потока диспетчеризации событий. См. Параллельность в Swing для получения более подробной информации.
Самое простое решение этой проблемы - использовать Swing Timer
, подробнее см. Как использовать Swing Timers .
Теперь вы можете просто запустить Timer
и делать прямую, линейную прогрессию, пока все ваши очки не достигнут своей цели, но это не всегда лучшее решение, так как оно плохо масштабируется и будет выглядеть по-разному на разных ПК, основанные на индивидуальных возможностях.
В большинстве случаев анимация на основе длительности дает лучший результат. Это позволяет алгоритму «сбрасывать» кадры, когда ПК не может успевать. Он масштабируется намного лучше (по времени и расстоянию) и может быть легко настраиваемым.
Мне нравится создавать повторно используемые блоки кода, поэтому я начну с простого "механизма анимации на основе длительности" ...
// 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);
}
}
Это не слишком сложно, у него есть duration
времени, в течение которого он будет работать. Он будет tick
через регулярный интервал (не менее 5 миллисекунд) и будет генерировать tick
событий, сообщая о текущем ходе анимации (как нормализованное значение от 0 до 1).
Идея в том, что мы отделяем «движок» от тех элементов, которые его используют. Это позволяет нам использовать его для гораздо более широкого диапазона возможностей.
Далее мне нужен способ отслеживать положение моего движущегося объекта ...
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;
}
}
Это простой объект, который берет начальную и конечную точки, а затем вычисляет положение объекта между этими точками на основе прогрессии. Может покрасить сам по запросу.
Теперь нам просто нужен какой-то способ собрать это вместе ...
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();
}
}
Хорошо, это "выглядит" сложно, но это действительно просто.
- Мы создаем «исходную» точку
- Затем мы создаем случайное количество «целей»
- Затем мы создаем анимационный движок и запускаем его.
Движок анимации затем перебирает все Ping
s и обновляет их на основе текущего значения прогресса и запускает новый проход рисования, который затем рисует линии между исходной и целевой точками, рисует Ping
s и, наконец, источник и все целевые точки. Простой.
Что если я хочу, чтобы анимация работала на разных скоростях?
А, ну, это намного сложнее и требует более сложного движка анимации.
Вообще говоря, вы могли бы создать концепцию чего-то, что было бы "оживляемым". Затем он будет обновляться центральным «двигателем», который постоянно «тикает» (сам по себе не ограничен длительностью).
Затем каждому «анимируемому» необходимо будет принять решение о том, как он собирается обновить или сообщить о своем состоянии, и разрешить обновление других объектов.
В этом случае я бы искал более готовое решение, например ...
Пример выполнения ....
![Ping me](https://i.stack.imgur.com/MFQt4.gif)
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;
}
}
}
Неужели нет ничего проще?
Как я уже сказал, хорошая анимация - это сложно.Требуется много усилий и планирование, чтобы преуспеть.Я даже не говорил об алгоритмах упрощения, связывания или смешивания, поэтому, поверьте мне, когда я говорю, это на самом деле простое, многократно используемое решение
Не верьте мне, посмотрите Анимация парения JButton вJava Swing