smooth animation in java for fast moving objects

2019-01-29 09:11发布

I am creating simple animation of ball moving from one side of the screen to the other with different speed. The problem is that with higher speeds of the ball I can see noticeable flickering of the ball, actually it is hard to explain but something like I could see repaints when part of ball is still in previous step.

I have tried number of things including:

  1. native swing animation using first thread/sleep/repain, then moved to timers

  2. switched to javafx canvas/pane inside swing jframe. Tried both transitions and AnimationTimer

  3. tinkering with CreateBufferStrategy, for 1,2,3 - to be honest haven't seen any difference (maybe I was doing something wrong...)

My question how can I improve smoothness and whether what I want to achieve is possible with native java or maybe it is better to use some external libraries ? and if so could you recommend something ?

below shown my example code for 2nd/3rd attempt.

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 < gs.length )
        {           
            xSize = gs[screen].getDisplayMode().getWidth();
            ySize = gs[screen].getDisplayMode().getHeight();
        }
        else if( gs.length > 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 <= 0) {
//                  xMov = 1;
//              }
//              if (dir == 1) {
//                  if (xPos >= 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();
    }
}

And here example for swing code:

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 < gs.length) {
            xSize = gs[screen].getDisplayMode().getWidth();
            ySize = gs[screen].getDisplayMode().getHeight();
        } else if (gs.length > 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 <= 0) {
            xMov = 1;
        }
        if (dir == 1) {
            if (xPos >= 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();
    }
}

2条回答
时光不老,我们不散
2楼-- · 2019-01-29 09:45

I think the issue is caused by the rendering of the JFXPanel in AWT: there is some complex stuff happening behind the scenes to synchronize between the two different system threads (the AWT event dispatch thread and the FX Application Thread).

If you can write this as a "pure" JavaFX application (i.e. with no Swing/AWT code), it runs more smoothly:

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);
    }
}

If you have to have JavaFX embedded in a Swing application, and you're using Java 8 (I tested this on 8u20), then there is a system property that runs both UI toolkits on the same thread. I think this is still currently experimental, so use at your own risk, but try

java -Djavafx.embed.singleThread=true FXTrackerPanel

This improves things a bit, but it's still pretty flickery and not as good as the "pure JavaFX" version.

查看更多
Rolldiameter
3楼-- · 2019-01-29 10:04

It's difficult to know exactly what might be going wrong without a runnable example, but I would, where you can, avoid mixing JavaFX and Swing, as they have different rendering mechanisms.

The following is a VERY simple example, which simply increases the speed of the balls been animated by simply changing the amount by which they are moved on each update...

Balls

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<Ball> 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);
            }

        }

    }

}

This example works within the confines of Swing's painting process, if you need more control over the painting process you will need to use a BufferStrategy (to work within Swing)

查看更多
登录 后发表回答