摇摆动画优化

Swing animation optimization

我一直在使用 JComponent 上的 Timer 制作一个简单的动画。但是,当我观看动画时,我会体验到难以置信的波动。我应该采取哪些步骤来优化此代码?

MyAnimationFrame

import javax.swing.*;

public class MyAnimationFrame extends JFrame {
    public MyAnimationFrame() {
        super("My animation frame!");
        setSize(300,300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        add(new AnimationComponent(0,0,50,50));
        setVisible(true);
    }

    public static void main(String[] args) {
        MyAnimationFrame f = new MyAnimationFrame();
    }
}

AnimationComponent

import javax.swing.*;
import java.awt.*;

public class AnimationComponent extends JComponent implements ActionListener {
    private Timer animTimer;
    private int x;
    private int y;
    private int xVel;
    private int yVel;
    private int width;
    private int height;
    private int oldX;
    private int oldY;

    public AnimationComponent(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.height = height;
        this.width = width;

        animTimer = new Timer(25, this);
        xVel = 5;
        yVel = 5;

        animTimer.start();
    }

    @Override
    public void paintComponent(Graphics g) {
        g.fillOval(x,y,width,height);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        oldX = x;
        oldY = y;

        if(x + width > getParent().getWidth() || x < 0) {
             xVel *= -1;
        }

        if(y + height > getParent().getHeight() || y < 0) {
            yVel *= -1;
        }

        x += xVel;
        y += yVel;

        repaint();
    }
}

不确定这是否重要,但我使用的是 OpenJDK 1.8 版。0_121。

感谢任何帮助。

Timing Framework 提供了一种提供高度优化的动画的方法,这在这种情况下可能会有所帮助。

MyAnimationFrame

import javax.swing.*;

public class MyAnimationFrame extends JFrame {
    public MyAnimationFrame() {
        super("My animation frame!");
        setSize(300,300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        add(new AnimationComponent(0,0,50,50));
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                MyAnimationFrame f = new MyAnimationFrame();
            }
        });
    }
}

动画组件

import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.TimeUnit;
import org.jdesktop.core.animation.rendering.*;
import org.jdesktop.core.animation.timing.*;
import org.jdesktop.core.animation.timing.interpolators.*;
import org.jdesktop.swing.animation.rendering.*;
import org.jdesktop.swing.animation.timing.sources.*;

@SuppressWarnings("serial")
public class AnimationComponent extends JRendererPanel {

    protected int x;
    protected int y;
    protected int width;
    protected int height;
    protected Animator xAnimator;
    protected Animator yAnimator;

    public AnimationComponent(int x, int y, int width, int height) {
        setOpaque(true);

        this.x = x;
        this.y = y;
        this.height = height;
        this.width = width;

        JRendererFactory.getDefaultRenderer(this,
                new JRendererTarget<GraphicsConfiguration, Graphics2D>() {
            @Override
            public void renderSetup(GraphicsConfiguration gc) {
                // Nothing to do
            }
            @Override
            public void renderUpdate() {
              // Nothing to do
            }
            @Override
            public void render(Graphics2D g, int w, int h) {
                Color c = g.getColor();
                g.setColor(g.getBackground());
                g.fillRect(0, 0, w, h);
                g.setColor(c);
                g.fillOval(AnimationComponent.this.x, AnimationComponent.this.y,
                        AnimationComponent.this.width, AnimationComponent.this.height);
            }
            @Override
            public void renderShutdown() {
              // Nothing to do
            }
        }, false);

        this.xAnimator = new Animator.Builder(new SwingTimerTimingSource())
                .addTargets(new TimingTargetAdapter() {
                    @Override
                    public void timingEvent(Animator source, double fraction) {
                        AnimationComponent.this.x = (int) ((getWidth() - AnimationComponent.this.width) * fraction);
                    }})
                .setRepeatCount(Animator.INFINITE)
                .setRepeatBehavior(Animator.RepeatBehavior.REVERSE)
                .setInterpolator(LinearInterpolator.getInstance()).build();

        this.yAnimator = new Animator.Builder(new SwingTimerTimingSource())
                .addTargets(new TimingTargetAdapter() {
                    @Override
                    public void timingEvent(Animator source, double fraction) {
                        AnimationComponent.this.y = (int) ((getHeight() - AnimationComponent.this.height) * fraction);
                    }})
                .setRepeatCount(Animator.INFINITE)
                .setRepeatBehavior(Animator.RepeatBehavior.REVERSE)
                .setInterpolator(LinearInterpolator.getInstance()).build();

        addComponentListener(new ComponentAdapter() {
            private int oldWidth = 0;
            private int oldHeight = 0;
            @Override
            public void componentResized(ComponentEvent event) {
                Component c = event.getComponent();
                int w = c.getWidth();
                int h = c.getHeight();

                if (w != this.oldWidth) {
                    AnimationComponent.this.xAnimator.stop();
                    AnimationComponent.this.xAnimator = new Animator.Builder()
                            .copy(AnimationComponent.this.xAnimator)
                            .setDuration(w * 5, TimeUnit.MILLISECONDS) // Original speed was 200 px/s
                            .build();
                    AnimationComponent.this.xAnimator.start();
                }
                if (h != this.oldHeight) {
                    AnimationComponent.this.yAnimator.stop();
                    AnimationComponent.this.yAnimator = new Animator.Builder()
                            .copy(AnimationComponent.this.yAnimator)
                            .setDuration(h * 5, TimeUnit.MILLISECONDS) // Original speed was 200 px/s
                            .build();
                    AnimationComponent.this.yAnimator.start();
                }

                this.oldWidth = w;
                this.oldHeight = h;
            }
        });
    }

}

我取得了不错的效果,但有一个问题:您调整任何项目的大小,动画都会重置。

在与 Yago 进行了精彩的讨论之后,我发现这个问题围绕着许多方面,很多归结为 Java 与 OS 和硬件同步更新的能力,有些事情你可以控制,有些你不能。

受 Yago 的示例和我的 "memory" 计时框架工作原理的启发,我通过增加帧速率(到 5 毫秒,~= 200fps)和减少变化增量来测试你的代码,这给出了相同的结果结果与使用 Timing Framework 相同,但让您拥有原始设计的灵活性。

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame("Test");
                frame.add(new AnimationComponent(0, 0, 50, 50));
                frame.setSize(300, 300);
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class AnimationComponent extends JComponent implements ActionListener {

        private Timer animTimer;
        private int x;
        private int y;
        private int xVel;
        private int yVel;
        private int width;
        private int height;
        private int oldX;
        private int oldY;

        public AnimationComponent(int x, int y, int width, int height) {
            this.x = x;
            this.y = y;
            this.height = height;
            this.width = width;

            animTimer = new Timer(5, this);
            xVel = 1;
            yVel = 1;

            animTimer.start();
        }

        @Override
        public void paintComponent(Graphics g) {
            Graphics2D g2d = (Graphics2D) g.create();
            RenderingHints hints = new RenderingHints(
                    RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON
            );
            g2d.setRenderingHints(hints);
            g2d.fillOval(x, y, width, height);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            oldX = x;
            oldY = y;

            if (x + width > getParent().getWidth() || x < 0) {
                xVel *= -1;
            }

            if (y + height > getParent().getHeight() || y < 0) {
                yVel *= -1;
            }

            x += xVel;
            y += yVel;

            repaint();
        }
    }
}

如果你需要更慢的速度,然后减少变化增量,这将意味着你必须使用 doubles,这将导致 Shape 的 API 支持双精度值

你应该使用哪个?这取决于你。 Timing Framework 非常适合一段时间内的线性动画,您知道自己想要从一种状态转到另一种状态。对于像游戏这样的东西来说不太好,在游戏中,对象的状态可以从我的周期改变到另一个。我相信你可以做到,但使用简单的 "main loop" 概念会容易得多 - 恕我直言