ITMISS
我只想开始,动画并不容易,好的动画也很难。有很多理论可以让动画“看起来”很好,我不打算在这里介绍,有更好的人和资源。我要讨论的是如何在基本级别上用 Swing 制作“好的”动画。第一个问题似乎是您对 Swing 中的绘画工作原理没有很好的理解。您应该首先阅读在 Swing 中执行自定义绘画和在 Swing中绘画接下来,您似乎没有意识到 Swing 实际上不是线程安全的(并且是单线程的)。这意味着您不应该从事件调度线程的上下文之外更新 UI 或 UI 所依赖的任何状态。有关详细信息,请参阅Swing 中的并发。解决此问题的最简单方法是使用 Swing ,有关详细信息Timer,请参阅如何使用 Swing 计时器。现在,您可以简单地运行 aTimer并进行直线、线性进展,直到您的所有点都达到目标,但这并不总是最好的解决方案,因为它不能很好地扩展,并且会根据个人的不同在不同的 PC 上显示不同能力。在大多数情况下,基于持续时间的动画效果更好。这允许算法在 PC 无法跟上时“丢弃”帧。它可以更好地扩展(时间和距离)并且可以高度配置。我喜欢生成可重复使用的代码块,所以我将从一个简单的“基于持续时间的动画引擎”开始......// 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(); }}好吧,这个“看起来”很复杂,其实很简单。我们创建一个“源”点然后我们创建随机数量的“目标”然后我们创建一个动画引擎并启动它。然后动画引擎将循环遍历所有Pings 并根据当前进度值更新它们并触发新的绘制通道,然后绘制源点和目标点之间的线,绘制Pings,最后绘制源和所有目标点。简单的。如果我想让动画以不同的速度运行怎么办?啊,好吧,这要复杂得多,需要更复杂的动画引擎。一般来说,您可以建立一个“可动画化”的概念。然后,这将由一个中央“引擎”更新,该引擎不断“滴答作响”(本身不受持续时间的限制)。然后,每个“可动画化的”都需要决定如何更新或报告其状态,并允许更新其他对象。在这种情况下,我会寻找更现成的解决方案,例如......Super Simple Swing Animation Framework - 这是我写的一个实验API,所以我只认为它是一个学习点通用补间引擎计时框架三叉戟可运行的例子....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; } }}有没有更简单的😓正如我所说,好的动画很难。需要付出很多努力和计划才能做好。我什至没有谈到地役权、链式或混合算法,所以请相信我,这实际上是一个简单、可重用的解决方案不信,看看Java Swing 中的 JButton 悬停动画