Ssjava.java — Animation with a Timer

 

Download Ssjava.java source file

// S.S. Java 1 - an animation demo adapted from Sun's Java Tutorial.
// This version uses a swing timer to send action events at regular
// intervals, which are used to move and repaint the animation.
// The space ship S.S. Java is moved across a star field, and the
// user can click the mouse to start/stop the animation.
//
// The animation uses a "frame number" to advance the image
// a certain number of pixels per repaint update (5).  This number
// is used modulo the width of the applet (which is the width of
// the background image).
//
// Notice how when used as an appet a MediaTracker is used to
// preload the image, and how the Applet start/stop methods are
// used in conjunction with the boolean "frozen" to control the
// animation.  Also a main has been included so this program can
// be run as a stand-alone as well.  Notice no media tracker is
// needed in this case, but certain window event handler methods
// are used to control the animation, just like the applet start
// and stop.
//
// Notice too the new style of showing multiple closing braces
// on a single line.  I feel this is just as readable, see if you do.
//
// Written 3/2001 by Wayne Pollock, Tampa Florida USA.

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

public class Ssjava1 extends JApplet implements ActionListener
{
   int frameNumber = -1;
   static final int pelsPerUpdate = 2;  // a "pel" is a "pixel"
   boolean frozen = false;
   Timer timer;
   AnimationPane animationPane;

   // Should probably use PARAM tags (and cmd line args) to set these:
   static String fgFile = "rocketship.gif";
   static String bgFile = "starfield.gif";

   //Invoked only when run as an applet, don't invoke from main:
   public void init()
   {
      // Get and load the images:
      Image bgImage = getImage( getCodeBase(), bgFile );
      Image fgImage = getImage( getCodeBase(), fgFile );
      MediaTracker mt = new MediaTracker( this );
      mt.addImage( bgImage, 0 );
      mt.addImage( fgImage, 1 );
      try { mt.waitForAll(); }
      catch ( InterruptedException ignored ) {}

      buildUI( getContentPane(), bgImage, fgImage );
   }

   void buildUI ( Container container, Image bgImage, Image fgImage )
   {
      int fps = 25;  // Frames per Second, should be >28 to appear smooth.

      // How many milliseconds between frames?
      //     1000 / (frames per second) = milliseconds delay per frame.
      int delay = ( fps > 0 ) ? ( 1000 / fps ) : 40;

      // Set up a swing timer that calls this object's action handler:
      timer = new Timer( delay, this );
      timer.setInitialDelay( 0 ); // Start immediately.
      timer.setCoalesce( true );  // ok to skip updates if falling behind.

      animationPane = new AnimationPane( bgImage, fgImage );
      container.add( animationPane, BorderLayout.CENTER );

      animationPane.addMouseListener( new MouseAdapter()
        {  public void mousePressed( MouseEvent e )
           {  if ( frozen )
              {  frozen = false;
                 startAnimation();
              } else
              {  frozen = true;
                 stopAnimation();
        }  }  }
      );
   }

   // Invoked by a browser only:
   public void start ()
   {  startAnimation();
   }

   // Invoked by a browser only:
   public void stop ()
   {  stopAnimation();
   }

   // Can be invoked from any thread, for instance if a user clicks then
   // immediately  clicks "Back" then this might be called simultaneously
   // from main and the AWT Event Dispatch Thread.
   // Since "frozen" and "timer" are shared resources make method synchronized:
   public synchronized void startAnimation ()
   {  if ( ! frozen )
      {  // Start animating only if not frozen and not already running:
         if ( ! timer.isRunning() )
         {  timer.start();
   }  }  }

   // Can be invoked from any thread like startAnimation:
   public synchronized void stopAnimation ()
   {  // Stop the animating thread:
      if ( timer.isRunning() )
      {  timer.stop();
   }  }

   public void actionPerformed ( ActionEvent e )
   {  // Advance animation frame counter and repaint:
      frameNumber++;
      animationPane.repaint();
   }

   // An inner class, this is all that actually gets repainted:
   class AnimationPane extends JPanel
   {
      Image background, foreground;

      public AnimationPane ( Image background, Image foreground )
      {  this.background = background;
         this.foreground = foreground;
      }

      // Draw the current frame of animation:
      public void paintComponent ( Graphics g )
      {
         super.paintComponent( g );  //paint any space not covered
                                     //by the background image
         int compWidth = getWidth();
         int compHeight = getHeight();

         // Safty check:  Only draw if we have a valid image:
         int imageWidth = background.getWidth( this );
         int imageHeight = background.getHeight( this );
         if ( (imageWidth > 0) && (imageHeight > 0) )
         {  g.drawImage( background, (compWidth - imageWidth)/2,
                           (compHeight - imageHeight)/2, this );
         } 
         imageWidth = foreground.getWidth( this );
         imageHeight = foreground.getHeight( this );
         if ( (imageWidth > 0) && (imageHeight > 0) )
         {  int pos = ( (frameNumber * pelsPerUpdate) % 
               (imageWidth + compWidth) ) - imageWidth;
            g.drawImage( foreground, pos, (compHeight - imageHeight)/2, this );
      }  }
   }  // End of AnimationPane class.

   // Invoked only when run as an application:
   public static void main ( String [] args )
   {
      Toolkit tk = Toolkit.getDefaultToolkit();
      Image bgImage = tk.getImage( bgFile );
      Image fgImage = tk.getImage( fgFile );

      JFrame f = new JFrame( "S. S. Java 1" );

      // Anonymous inner classes can only access final variables:
      final Ssjava1 controller = new Ssjava1();
      controller.buildUI( f.getContentPane(), bgImage, fgImage );

      f.addWindowListener( new WindowAdapter()
        {  public void windowIconified ( WindowEvent e )
           {  controller.stopAnimation();
           }
           public void windowDeiconified ( WindowEvent e )
           {  controller.startAnimation();
           }
           public void windowClosing ( WindowEvent e )
           {  System.exit(0);
           }
        }
      );
      f.setSize( new Dimension( 500, 100 ) );  
      f.setVisible( true );
      controller.startAnimation();
   }
}