在java中为快速移动的对象平滑动画

我正在创建一个简单的动画,球从屏幕的一侧移动到另一侧以不同的速度。 问题是,随着球的速度越来越快,我可以看到球的明显闪烁,实际上很难解释,但是当球的一部分还在前一步时,我可以看到重绘的东西。

我尝试了很多东西,包括:

  1. 使用第一个线程/ sleep / repain的原生摇摆动画,然后转移到计时器

  2. 在swing jframe内切换到javafx canvas / pane。 尝试过渡和AnimationTimer

  3. 修补CreateBufferStrategy,1,2,3 – 说实话没有看到任何差异(也许我做错了什么……)

我的问题是如何提高平滑度以及我想用本机java实现的目标是否可能或者使用一些外部库更好? 如果可以的话,你能推荐什么吗?

下面显示了我的第二次/第三次尝试的示例代码。

import java.awt.Dimension; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import javafx.animation.Interpolator; import javafx.animation.Timeline; import javafx.animation.TranslateTransition; import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.util.Duration; import javax.swing.JFrame; public class FXTrackerPanel extends JFrame { /** * */ private static final long serialVersionUID = 1L; public int crSize = 30; public double xPos = crSize; public double yPos = 100; public int xSize = 100; public int ySize = 100; public Circle r; int dir = 1; public void updateScreenSize() { int screen = 0; GraphicsEnvironment ge = GraphicsEnvironment .getLocalGraphicsEnvironment(); GraphicsDevice[] gs = ge.getScreenDevices(); if( screen > -1 && screen  0 ) { xSize = gs[0].getDisplayMode().getWidth(); ySize = gs[0].getDisplayMode().getHeight(); } else { throw new RuntimeException( "No Screens Found" ); } yPos = ySize / 2; } private void initFXPanel(JFXPanel fxPanel) { updateScreenSize(); xPos = crSize; Group root = new Group(); double speed = 5; int repeats = Timeline.INDEFINITE; r = new javafx.scene.shape.Circle(xPos, yPos, crSize / 2, Color.RED); TranslateTransition tt = new TranslateTransition(Duration.seconds(speed), r); tt.setFromX(xPos); tt.setToX(xSize - crSize * 3); tt.setCycleCount(repeats); tt.setAutoReverse(true); tt.setInterpolator(Interpolator.EASE_BOTH); tt.play(); root.getChildren().add(r); // new AnimationTimer() { // // @Override // public void handle(long now) { // double speed = 20; // try { // speed = Double.valueOf(TETSimple.mp.speedSinus.getText()); // } // catch (Exception ex) { // speed = 20; // } // double xMov = (speed * 4 * Math.sin( xPos * Math.PI / xSize ) ); // if (xMov = xSize - crSize) // dir = 0; // xPos += xMov; // } else { // if (xPos <= 1) // dir = 1; // xPos -= xMov; // } // // r.setTranslateX(xPos); // } // }.start(); fxPanel.setScene(new Scene(root)); } public FXTrackerPanel() { updateScreenSize(); this.setSize(new Dimension(xSize, ySize)); this.setPreferredSize(new Dimension(xSize, ySize)); this.setVisible(true); this.setDefaultCloseOperation(DISPOSE_ON_CLOSE); JFXPanel fxPanel = new JFXPanel(); this.add(fxPanel); this.createBufferStrategy(3); Platform.runLater(new Runnable() { @Override public void run() { initFXPanel(fxPanel); } }); } public static void main(String[] args) { new FXTrackerPanel(); } } 

这里是摆码的例子:

 import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.Ellipse2D; import javax.swing.JPanel; import javax.swing.Timer; import java.lang.Math; import java.util.Random; public class TrackerPanel extends JPanel implements ActionListener { /** * */ private static final long serialVersionUID = 1L; Shape cr; Color c; public int crSize = 30; public double xPos = crSize; public double yPos = 100; public double xPosPrev = crSize; public double yPosPrev = 100; public int xSize = 100; public int ySize = 100; int dir = 1; // left int timer = 50; // 50 - sins, 1500 - linear int method = 1; // 1 - jump, 2 - sinus int timeToChange = 1000; int passes = 0; Timer tt; boolean clickedClose = false; private int repeats = 0; // t - timer interval, m - method of ball movement: 1 - jump, 2 - sinus public TrackerPanel(int t, int m) { this.setPreferredSize(new Dimension(300, 200)); this.timer = t; this.method = m; c = Color.red; repaint(); this.updateScreenSize(); tt = new Timer(t, null); tt.addActionListener(this); tt.start(); } public void updateScreenSize() { int screen = TETSimple.suppMonitor; GraphicsEnvironment ge = GraphicsEnvironment .getLocalGraphicsEnvironment(); GraphicsDevice[] gs = ge.getScreenDevices(); if (screen > -1 && screen  0) { xSize = gs[0].getDisplayMode().getWidth(); ySize = gs[0].getDisplayMode().getHeight(); } else { throw new RuntimeException("No Screens Found"); } yPos = ySize / 2; yPosPrev = ySize / 2; } public void actionPerformed(ActionEvent arg0) { if (method == 1) lineMovement(); else sinusMovement(); repaint(0, ySize / 2, xSize, crSize); } private Double parseText2Int(String literal) { try { return Double.valueOf(literal); } catch (Exception ex) { ex.printStackTrace(); } return 10.0; } private void checkFinishCondition() { if (passes + 1 > repeats && repeats != 0) { if (!clickedClose) { TETSimple.mp.bStop.doClick(); clickedClose = true; } return; } } private void sinusMovement() { this.updateScreenSize(); this.repeats = parseText2Int(TETSimple.mp.repeatsCount.getText()).intValue(); checkFinishCondition(); double speed = parseText2Int(TETSimple.mp.speedSinus.getText()); double xMov = (speed * Math.sin(xPos * Math.PI / xSize)); if (xMov = xSize - crSize) dir = 0; xPosPrev = xPos; xPos += xMov; } else { if (xPos <= 1 + crSize) { dir = 1; passes++; } xPosPrev = xPos; xPos -= xMov; } } private void lineMovement() { this.repeats = parseText2Int(TETSimple.mp.repeatsCount.getText()).intValue(); checkFinishCondition(); double left = crSize; double center = xSize / 2 - crSize * 1.5; double right = xSize - crSize * 2; Random r = new Random(); if (timeToChange <= 0) { passes++; if (xPos == left || xPos == right) { timeToChange = 300 + r.nextInt(12) * 100; xPos = center; } else if (xPos == center) { timeToChange = 300 + r.nextInt(7) * 100; if (r.nextBoolean()) xPos = left; else xPos = right; } } else { timeToChange -= 100; } } public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g.create(); 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); g2d.setColor(Color.green); g2d.fill(new Ellipse2D.Double(xPos, yPos, crSize, crSize)); g2d.dispose(); } } 

如果没有可运行的示例,很难确切地知道可能出现什么问题,但我会尽可能避免混合使用JavaFX和Swing,因为它们具有不同的渲染机制。

以下是一个非常简单的例子,通过简单地改变每次更新时移动的量来简单地增加球的动画速度……

球

 import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.List; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; public class BouncyBall { public static void main(String[] args) { new BouncyBall(); } public BouncyBall() { 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 ControlPane()); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); } }); } public class ControlPane extends JPanel { private JSlider speed; private JSlider quanity; private BallPitPane ballPitPane; public ControlPane() { setLayout(new BorderLayout()); ballPitPane = new BallPitPane(); add(ballPitPane); JPanel controls = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST; speed = new JSlider(1, 100, 4); quanity = new JSlider(1, 100, 1); controls.add(new JLabel("Speed:"), gbc); gbc.gridy++; controls.add(new JLabel("Quanity:"), gbc); gbc.gridx++; gbc.gridy = 0; gbc.weightx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; controls.add(speed, gbc); gbc.gridy++; controls.add(quanity, gbc); add(controls, BorderLayout.SOUTH); speed.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { ballPitPane.setSpeed(speed.getValue()); } }); quanity.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { ballPitPane.setQuanity(quanity.getValue()); } }); } } public class BallPitPane extends JPanel { private List balls; private int speed; public BallPitPane() { balls = new ArrayList<>(25); setSpeed(2); setQuanity(1); Timer timer = new Timer(40, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { for (Ball ball : balls) { ball.update(getWidth(), speed); } repaint(); } }); timer.start(); } public void setSpeed(int speed) { this.speed = speed; } public void setQuanity(int quanity) { while (balls.size() > quanity) { balls.remove(0); } while (balls.size() < quanity) { int radius = 4 + (int) (Math.random() * 48); Ball ball = new Ball( randomColor(), (int) Math.abs(Math.random() * getWidth() - radius), (int) Math.abs(Math.random() * getHeight() - radius), radius ); balls.add(ball); } } protected Color randomColor() { int red = (int) Math.abs(Math.random() * 255); int green = (int) Math.abs(Math.random() * 255); int blue = (int) Math.abs(Math.random() * 255); return new Color(red, green, blue); } @Override public Dimension getPreferredSize() { return new Dimension(400, 200); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g.create(); 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); for (Ball ball : balls) { ball.paint(g2d); } g2d.dispose(); } public class Ball { private Color color; private int x; private int y; private int radius; private int delta; public Ball(Color color, int x, int y, int radius) { this.color = color; this.x = x; this.y = y; this.radius = radius; delta = Math.random() > 0.5 ? 1 : -1; } public void update(int width, int speed) { x += speed * delta; if (x + radius >= width) { x = width - radius; delta *= -1; } else if (x < 0) { x = 0; delta *= -1; } } public void paint(Graphics g) { g.setColor(color); g.fillOval(x, y, radius, radius); } } } } 

这个例子在Swing绘画过程的范围内工作,如果你需要更多控制绘画过程,你需要使用BufferStrategy (在Swing中工作)

我认为这个问题是由AWT中的JFXPanel渲染引起的:在幕后发生了一些复杂的事情,需要在两个不同的系统线程(AWT事件调度线程和FX应用程序线程)之间进行同步。

如果你可以把它写成一个“纯粹的”JavaFX应用程序(即没有Swing / AWT代码),它运行得更顺畅:

 import javafx.animation.Interpolator; import javafx.animation.Timeline; import javafx.animation.TranslateTransition; import javafx.application.Application; import javafx.geometry.Rectangle2D; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.stage.Screen; import javafx.stage.Stage; import javafx.util.Duration; public class FXAnimationTest extends Application { @Override public void start(Stage primaryStage) { Group root = new Group(); double speed = 5; int repeats = Timeline.INDEFINITE; Screen screen = Screen.getPrimary(); Rectangle2D screenBounds = screen.getBounds(); double xSize = screenBounds.getWidth(); double ySize = screenBounds.getHeight(); double crSize = 30 ; double xPos = crSize ; double yPos = ySize / 2 ; Circle r = new Circle(xPos, yPos, crSize / 2, Color.RED); TranslateTransition tt = new TranslateTransition(Duration.seconds(speed), r); tt.setFromX(xPos); tt.setToX(xSize - crSize * 3); tt.setCycleCount(repeats); tt.setAutoReverse(true); tt.setInterpolator(Interpolator.EASE_BOTH); tt.play(); root.getChildren().add(r); Scene scene = new Scene(root, xSize, ySize); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } } 

如果你必须在Swing应用程序中嵌入JavaFX,并且你正在使用Java 8(我在8u20上测试过它),那么有一个系统属性可以在同一个线程上运行两个UI工具包。 我认为这仍然是实验性的,所以使用风险自负,但请尝试

 java -Djavafx.embed.singleThread=true FXTrackerPanel 

这稍微改善了一些事情,但它仍然相当闪烁,不如“纯JavaFX”版本好。