nickkislak
nickkislak

Reputation: 27

Swing animation flickers and makes GUI slow to respond

I'm trying to write a simple program: a bouncing ball that appears and starts bouncing after you press the "Start" button on the screen. The program should be closed by pressing "X".

For some reason, it runs very slowly. The ball is blinking, and I have to wait for a long time after I press the "X" for program to close.

Here is the code:

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;

public class Bounce
{
    public static void main(String[] args)
    {
        JFrame frame = new BounceFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.show();
    }
}

class BounceFrame extends JFrame
{
    public BounceFrame()
    {
        setSize(WIDTH, HEIGHT);
        setTitle("Bounce");
        Container contentPane = getContentPane();
        canvas = new BallCanvas();
        contentPane.add(canvas, BorderLayout.CENTER);
        JPanel buttonPanel = new JPanel();
        addButton(buttonPanel, "Start", new ActionListener()
        {
            public void actionPerformed(ActionEvent evt)
            {
                addBall();
            }
        });
        contentPane.add(buttonPanel, BorderLayout.SOUTH);
    }
    public void addButton(Container c, String title, ActionListener listener)
    {
        JButton button = new JButton(title);
        c.add(button);
        button.addActionListener(listener);
    }
    public void addBall()
    {
        try
        {
            Ball b = new Ball(canvas);
            canvas.add(b);
            for (int i = 1; i <= 10000; i++)
            {
                b.move();
                Thread.sleep(10);
            }
        }
        catch (InterruptedException exception)
        {
        }
    }
    private BallCanvas canvas;
    public static final int WIDTH = 300;
    public static final int HEIGHT = 200;
}

class BallCanvas extends JPanel
{
    public void add(Ball b)
    {
        balls.add(b);
    }
    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g;
        for (int i = 0; i < balls.size(); i++)
        {
            Ball b = (Ball)balls.get(i);
            b.draw(g2);
        }
    }
    private ArrayList balls = new ArrayList();
}

class Ball
{
    public Ball(Component c) { canvas = c; }
    public void draw(Graphics2D g2)
    {
        g2.fill(new Ellipse2D.Double(x, y, XSIZE, YSIZE));
    }
    public void move()
    {
        x += dx;
        y += dy;
        if (x < 0)
        {
            x = 0;
            dx = -dx;
        }
        if (x + XSIZE >= canvas.getWidth())
        {
            x = canvas.getWidth() - XSIZE;
            dx = -dx;
        }
        if (y < 0)
        {
            y = 0;
            dy = -dy;
        }
        if (y + YSIZE >= canvas.getHeight())
        {
            y = canvas.getHeight() - YSIZE;
            dy = -dy;
        }
        canvas.paint(canvas.getGraphics());
    }
    private Component canvas;
    private static final int XSIZE = 15;
    private static final int YSIZE = 15;
    private int x = 0;
    private int y = 0;
    private int dx = 2;
    private int dy = 2;
}

Upvotes: 1

Views: 434

Answers (2)

Kevin J. Chase
Kevin J. Chase

Reputation: 3956

The slowness comes from two related problems, one simple and one more complex.

Problem #1: paint vs. repaint

From the JComponent.paint docs:

Invoked by Swing to draw components. Applications should not invoke paint directly, but should instead use the repaint method to schedule the component for redrawing.

So the canvas.paint() line at the end of Ball.move must go.

You want to call Component.repaint instead... but just replacing the paint with repaint will reveal the second problem, which prevents the ball from even appearing.

Problem #2: Animating inside the ActionListener

The ideal ActionListener.actionPerformed method changes the program's state and returns as soon as possible, using lazy methods like repaint to let Swing schedule the actual work for whenever it's most convenient.

In contrast, your program does basically everything inside the actionPerformed method, including all the animation.

Solution: A Game Loop

A much more typical structure is to start a javax.swing.Timer when your GUI starts, and just let it run "forever", updating your simulation's state every tick of the clock.

public BounceFrame()
{
    // Original code here.
    // Then add:
    new javax.swing.Timer(
        10,  // Your timeout from `addBall`.
        new ActionListener()
        {
            public void actionPerformed(final ActionEvent ae)
            {
                canvas.moveBalls();  // See below for this method.
            }
        }
    ).start();
}

In your case, the most important (and completely missing) state is the "Have we started yet?" bit, which can be stored as a boolean in BallCanvas. That's the class that should do all the animating, since it also owns the canvas and all the balls.

BallCanvas gains one field, isRunning:

private boolean isRunning = false;  // new field

// Added generic type to `balls` --- see below.
private java.util.List<Ball> balls = new ArrayList<Ball>();

...and a setter method:

public void setRunning(boolean state)
{
    this.isRunning = state;
}

Finally, BallCanvas.moveBalls is the new "update all the things" method called by the Timer:

public void moveBalls()
{
    if (! this.isRunning)
    {
        return;
    }
    for (final Ball b : balls)
    {
        // Remember, `move` no longer calls `paint`...  It just
        // updates some numbers.
        b.move();
    }
    // Now that the visible state has changed, ask Swing to
    // schedule repainting the panel.
    repaint();
}

(Note how much simpler iterating over the balls list is now that the list has a proper generic type. The loop in paintComponent could be made just as straightforward.)

Now the BounceFrame.addBall method is easy:

public void addBall()
{
    Ball b = new Ball(canvas);
    canvas.add(b);
    this.canvas.setRunning(true);
}

With this setup, each press of the space bar adds another ball to the simulation. I was able to get over 100 balls bouncing around on my 2006 desktop without a hint of flicker. Also, I could exit the application using the 'X' button or Alt-F4, neither of which responded in the original version.

If you find yourself needing more performance (or if you just want a better understanding of how Swing painting works), see "Painting in AWT and Swing: Good Painting Code Is the Key to App Performance" by Amy Fowler.

Upvotes: 3

Naveen Chahar
Naveen Chahar

Reputation: 571

I would suggest you to use 'Timer' class for running your gameloop.It runs infinitely and you can stop it whenever you want using timer.stop() You can also set its speed accordingly.

Upvotes: 0

Related Questions