如何消除Java动画闪烁

8
我正在编写一个Java应用程序,需要平滑地在屏幕上滚动波形。我花费了很长时间研究各种教程,以尽可能使此动画变得流畅。据我所见,我已经做了所有正常的事情来消除闪烁(绘制到离屏缓冲区并在单个步骤中渲染,覆盖更新以使屏幕不被清空),但我的动画仍然会闪烁,屏幕看起来像是在每次更新之前被清空。
我相信我错过了一些基本的东西(可能很简单),但我已经没有更多的想法了。我将在下面发布一个类,说明问题。任何帮助都将不胜感激。
import java.awt.*;
import javax.swing.*;

public class FlickerPanel extends JPanel implements Runnable {

    private float [] pixelMap = new float[0];

    /** Cached graphics objects so we can control our animation to reduce flicker **/
    private Image screenBuffer;
    private Graphics bufferGraphics;

    public FlickerPanel () {
        Thread t = new Thread(this);
        t.start();
    }

    private float addNoise () {
        return (float)((Math.random()*2)-1);
    }

    private synchronized void advance () {
        if (pixelMap == null || pixelMap.length == 0) return;
        float [] newPixelMap = new float[pixelMap.length];
        for (int i=1;i<pixelMap.length;i++) {
            newPixelMap[i-1] = pixelMap[i];
        }

        newPixelMap[newPixelMap.length-1] = addNoise();     

        pixelMap = newPixelMap;
    }

    public void run() {
        while (true) {
            advance();
            repaint();

            try {
                Thread.sleep(25);
            } catch (InterruptedException e) {}

        }
    }

    private int getY (float height) {
        double proportion = (1-height)/2;
        return (int)(getHeight()*proportion);
    }

    public void paint (Graphics g) {

        if (screenBuffer == null || screenBuffer.getWidth(this) != getWidth() || screenBuffer.getHeight(this) != getHeight()) {
            screenBuffer = createImage(getWidth(), getHeight());
            bufferGraphics = screenBuffer.getGraphics();
        }

        if (pixelMap == null || getWidth() != pixelMap.length) {
            pixelMap = new float[getWidth()];
        }

        bufferGraphics.setColor(Color.BLACK);

        bufferGraphics.fillRect(0, 0, getWidth(), getHeight());

        bufferGraphics.setColor(Color.GREEN);

        int lastX = 0;
        int lastY = getHeight()/2;

        for (int x=0;x<pixelMap.length;x++) {
            int y = getY(pixelMap[x]);
            bufferGraphics.drawLine(lastX, lastY, x, y);
            lastX = x;
            lastY = y;
        }

        g.drawImage(screenBuffer, 0, 0, this);
    }

    public void update (Graphics g) {
        paint(g);
    }

    public static void main (String [] args) {
        JFrame frame = new JFrame("Flicker test");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane(new FlickerPanel());
        frame.setSize(500,300);
        frame.setVisible(true);
    }


}

我在你的代码中找不到任何问题。在我的桌面上,它看起来非常流畅。另外,你可以通过只有一个像素地图并将其用作循环缓冲区以更高效的方式完成此操作,并使用同步块保护对其的访问。因为目前每次更新都是创建一个新结构体,这也应该是没问题的。 - Adam
+1 for sscce。在我的平台上它闪烁得非常厉害。 - trashgod
玩了一段时间后,似乎问题的一部分原因是在绘制过程中修改了pixelMap数据结构,导致屏幕上不同部分之间出现撕裂。在paint方法开始时对pixelMap进行clone()操作似乎改善了情况,但闪烁问题仍然存在,所以我认为这并不是完整的答案。 - Simon Andrews
2个回答

8
  1. JPanel中,重写paintComponent(Graphics)方法而不是paint(Graphics)方法。
  2. 不要使用Thread.sleep(n)方法,而是使用Swing Timer重复执行任务或者使用SwingWorker处理长时间运行的任务。更多细节请参考Swing并发性

1
是的,即使sleep()在不同的线程上发生并且repaint()是线程安全的,advance()也必须被同步。 - trashgod
好的,这是一个不错的观点,但我认为它对闪烁没有直接影响 - 无论我覆盖哪种方法,效果都是一样的。 - Simon Andrews
1
@trashgod,我决定删除第2点的第1句话:“不要阻塞EDT(事件分派线程)-当发生这种情况时,GUI将会'冻结'。”因为在这种情况下似乎并不适用。 - Andrew Thompson
进阶已经同步,但这仍然可能是问题的一部分,因为pixelMap可能会在绘制过程中被替换,从而导致剪切。 - Simon Andrews

7

JPanel默认是双缓冲的,“Swing程序应该重写paintComponent()而不是重写paint()。”—摘自AWT和Swing中的绘制方法。与此相反,这个例子重写了paintComponent(),而这个例子则重写了paint()

补充说明:剪切是由于在每次更新时创建并复制整个图像所致。因此,应在GeneralPath上累积点,并在每次迭代时draw() Shape,如此处所示。


我尝试了覆盖paintComponent和设置面板不使用双缓冲,但似乎都没有任何作用,闪烁仍然存在。 - Simon Andrews
这个相关的示例不会闪烁。编辑:它依赖于javax.swing.Timer在EDT上更新。 - trashgod
我已经详细说明了上面的内容。此外,还要考虑使用nextGaussian()来添加噪声。 - trashgod

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接