Using the Swing API Timers
(To download a zip containing the source files for this article, click here.)
Swing provides a Timer class (located in the javax.swing package) that fires an action event to one or more listeners after a delay. (The ToolTip manager uses this class to determine when to show and hide a ToolTip.) Because a timer's task (code executed in the timer's actionPerformed method) is executed in the event-handling thread, components can be safely manipulated. However, this task shouldn't take too long to execute—otherwise a GUI's performance will suffer.
Call Timer (int delay, ActionListener l) to create a timer that notifies action listener l every delay milliseconds. (You can add more action listeners by calling Timer's addActionListener method.) The newly created timer is in its stopped state. To start the timer, call its start method. Conversely, you would call stop to terminate the timer. By calling its isRunning method, you can find out whether a timer is running. To get a sense of how to use a Swing timer, examine the Listing 1 source code to the TimerDemo1 application.
Listing 1 The TimerDemo1 application source code
// TimerDemo1.java import javax.swing.*; import java.awt.*; import java.awt.event.*; class TimerDemo1 extends JFrame { Timer timer; int counter; TimerDemo1 (String title) { super (title); addWindowListener (new WindowAdapter () { public void windowClosing (WindowEvent e) { System.exit (0); } }); ActionListener a = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Counter = " + counter); if (++counter > 10) { timer.stop (); System.exit (0); } } }; timer = new Timer (300, a); timer.start (); pack (); setVisible (true); } public static void main (String [] args) { new TimerDemo1 ("Timer Demo1"); } }
Timers can simplify Swing-based animation. When the next frame is to be drawn, the timer's actionPerformed method increments the frame number and calls the repaint method on the component whose paintComponent method draws the next frame. Listing 2 presents the source code to a TimerDemo2 application, which shows how to use a timer to animate an image.
Listing 2 The TimerDemo2 application source code
// TimerDemo2.java import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.image.*; class TimerDemo2 extends JFrame { final static int WIDTH = 200; final static int HEIGHT = 200; final static int DIRECTCOLORBACK = 0; final static int INDEXCOLORBACK = 1; int backType = INDEXCOLORBACK; Timer timer; int frameNumber = -1; TimerDemo2 (String title) { super (title); addWindowListener (new WindowAdapter () { public void windowClosing (WindowEvent e) { System.exit (0); } }); int [] pixels = new int [WIDTH * HEIGHT]; Toolkit tk = Toolkit.getDefaultToolkit (); Image imBack; if (backType == DIRECTCOLORBACK) { MkDirectColorBackground (pixels, WIDTH, HEIGHT); imBack = tk.createImage (new MemoryImageSource (WIDTH, HEIGHT, pixels, 0, WIDTH)); } else { IndexColorModel icm; icm = MkIndexColorBackground (pixels, WIDTH, HEIGHT); imBack = tk.createImage (new MemoryImageSource (WIDTH, HEIGHT, icm, pixels, 0, WIDTH)); } Image imFront = tk.getImage ("bullet.gif"); final AnimPane ap = new AnimPane (imBack, imFront); setContentPane (ap); ActionListener a = new ActionListener () { public void actionPerformed (ActionEvent e) { frameNumber++; ap.repaint (); } }; timer = new Timer (300, a); timer.start (); setSize (WIDTH, HEIGHT); setVisible (true); } void MkDirectColorBackground (int [] pixels, int w, int h) { int index = 0; for (int y = 0; y < h; y++) { int numerator = y * 255; int b = numerator / h; int r = 255 - numerator / h; for (int x = 0; x < w; x++) { int g = x * 255 / w; pixels [index++] = (255 << 24) | (r << 16) | (g << 8) | b; } } } IndexColorModel MkIndexColorBackground (int [] pixels, int w, int h) { Color [] colors = { Color.magenta, Color.green, Color.blue }; byte [] reds = new byte [colors.length]; byte [] greens = new byte [colors.length]; byte [] blues = new byte [colors.length]; for (int i = 0; i < colors.length; i++) { reds [i] = (byte) colors [i].getRed (); greens [i] = (byte) colors [i].getGreen (); blues [i] = (byte) colors [i].getBlue (); } int stripeSize = w / colors.length; int colorIndex; int index = 0; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { if (x < stripeSize) colorIndex = 0; else if (x < stripeSize * 2) colorIndex = 1; else colorIndex = 2; pixels [index++] = colorIndex; } IndexColorModel icm; icm = new IndexColorModel (8, colors.length, reds, greens, blues); return icm; } class AnimPane extends JPanel { Image back, front; AnimPane (Image back, Image front) { this.back = back; this.front = front; } //Draw the current frame of animation. public void paintComponent (Graphics g) { super.paintComponent (g); // Paint space not covered // by background image. int compWidth = getWidth (); int compHeight = getHeight (); int imgWidth, imgHeight; // If you have a valid width and height for the // background image, draw this image - centered // horizontally and vertically. imgWidth = back.getWidth (this); imgHeight = back.getHeight (this); if (imgWidth > 0 && imgHeight > 0) g.drawImage (back, (compWidth - imgWidth) / 2, (compHeight - imgHeight) / 2, this); // If you have a valid width and height for the // front image, draw it. imgWidth = front.getWidth (this); imgHeight = front.getHeight (this); if (imgWidth > 0 && imgHeight > 0) { // Compute new horizontal position to fall in component // bounds. The larger the multiplier (such as 10), the // greater the horizontal distance that's traveled. int x = (frameNumber * 10) % (imgWidth + compWidth) - imgWidth; // Center front image vertically. int y = (compHeight - imgHeight) / 2; // Draw front image. g.drawImage (front, x, y, this); } } } public static void main (String [] args) { new TimerDemo2 ("Timer Demo2"); } }
TimerDemo2 creates a background image in memory. This image uses either a direct color model (if the backType variable is set to DIRECTCOLORBACK) or an index color model (if backType is set to INDEXCOLORBACK). An animation pane, which is nothing more than a subclassed JPanel component, is created and its paintComponent method overridden to paint the background and foreground images. Every 300 milliseconds, the timer calls its actionPerformed method. Each call increments the frameNumber variable and issues a call to the animation pane's repaint method. This call eventually results in a call back to the animation pane's paintComponent method, which uses frameNumber's value to determine the next horizontal location. This value is multiplied by 10 to increase the distance covered during each call. Figure 1 shows TimerDemo2's output when the index color background is used.
Timers can be used to achieve slick-looking Swing animations.
Because Swing painting is double-buffered, the flickering problems that are prevalent with AWT animations don't exist.
About the Author
Geoff Friesen is a co-author of Special Edition Using Java 2, Standard Edition (Que, 2001, ISBN 0-7897-2468-5). His contribution consists of nine chapters that explore the Java Foundation Classes and the Java Media Framework. Geoff also writes the monthly Java 101 column for JavaWorld and is the former moderator of ITworld.com's Java Beginner discussion forum.