Mar 11, 2008 2:00 AM PT

Open source Java projects: AnimatingCardLayout

Animated transitions for your Java GUIs

Animated transitions are key to creating a comfortable, seamless user experience in your Java desktop and Web applications. In this installment of Open source Java projects, Jeff Friesen introduces the AnimatingCardLayout API and its six built-in transition effects: cube, dashboard, fade, iris, radial, and slide. You'll see these effects in action in a slideshow application, and also follow along as Jeff builds a custom zoom effect to enhance the slideshow.

Animated transitions are part of modern user interfaces. If you've ever used the Mac OS X dashboard to launch an application, you are probably familiar with its spin-around transition effect. Or you may have played with the various effects you can create for slideshows on a site like slide.com. Sony.com and Ford.com present examples of Flash-based animated transitions. Several libraries also exist to enable animated transitions in Java development.

Chet Haase and Romaine Guy popularized animated transitions for Java developers via their book Filthy Rich Clients. In tandem with the book, the authors created a library that you can use to explore animated transitions in Java application development.

Less known to many Java developers is the open source Java project AnimatingCardLayout, created by Dmitry Markman, Luca Lutterotti, and Sam Berlin. This open source extension to Java's standard CardLayout manager uses animated transitions to replace a card's components with another card's components.

This installment of Open source Java projects introduces AnimatingCardLayout. I'll start by introducing the distribution archive for the AnimatingCardLayout project. Next, I'll show you how to run the archive's example so that you can see for yourself the manager's six transition effects. I'll then guide you through an exploration of AnimatingCardLayout's API and show you how to use the API in a more realistic example. Finally, we'll create a custom transition effect class (ZoomAnimation) that you can use in your Java GUIs.

The BSD license
Each of the open source Java projects covered in this series is subject to a license, which you should understand before integrating the project with your own projects. AnimatingCardLayout is subject to the Berkeley Software Distribution (BSD) license.

AnimatingCardLayout's distribution archive

The animatingcardlayout project, hosted on Java.net, introduces AnimatingCardLayout and provides access to its executable and source code. To obtain this code, point your browser to the site's "Documents & files" section and download the most recent version of AnimatingCardLayout's distribution archive. For example, I downloaded animatingcardlayout_12_24_04.zip.

After downloading and unzipping the archive, I discovered an animatingcardlayout_12_24_04 home directory containing dist, __MACOSX, org, and www subdirectories. Of these four subdirectories, only dist and org are meaningful. The most important portion of their directory structure appears below:

dist
   AnimatingCardLayout.html
   AnimatingCardLayout.jar
org
   javadev
      AnimatingCardLayout.java
      effects
         Animation.java
         AnimationListener.java
         CubeAnimation.java
         DashboardAnimation.java
         FadeAnimation.java
         IrisAnimation.java
         RadialAnimation.java
         SlideAnimation.java
      test
         AnimatingCardLayoutTest.java

The dist directory's HTML file is used to run the layout manager example in an applet context. Both the example's and the layout manager's classfiles are stored in the JAR file. The org directory serves as the parent directory in a package hierarchy of source files. These source files describe AnimatingCardLayout, its six transition effect classes, and the AnimatingCardLayoutTest example.

Test AnimatingCardLayout

AnimatingCardLayoutTest lets you test AnimatingCardLayout in applet and application contexts. To test this layout manager in an applet context, point your browser to AnimatingCardLayout.html. After loading the AnimatingCardLayoutTest class files from AnimatingCardLayout.jar, the browser renders six applet instances demonstrating six transition effects. The screenshot in Figure 1 reveals this page (click to enlarge).

Figure 1. Click the Rotate button or the square between the scroll bars to explore each of the cube, dashboard, fade (which is in progress), iris, radial, and slide transition effects.

At the command line, invoke java -jar AnimatingCardLayout.jar to test this layout manager in an application context. In contrast to the applet context, in which AnimatingCardLayoutTest interrogates an applet parameter to determine which transition effect to use, only the cube transition effect (shown in Figure 2) is demonstrated in the application context.

Figure 2. Cube is one of two three-dimensional transition effects. The other effect is dashboard.

Explore the AnimatingCardLayout API

AnimatingCardLayout presents a simple API consisting of org.javadev.AnimatingCardLayout. Because this class extends java.awt.CardLayout, it inherits and overrides various CardLayout methods such as public void show(Container parent, String name) (which replaces a card with another card via a transition effect). Its API also includes the following constructors and methods:

  • public AnimatingCardLayout() creates an AnimatingCardLayout instance without an associated transition effect. An effect can be added later by invoking setAnimation().
  • public AnimatingCardLayout(Animation anim) creates an AnimatingCardLayout instance with the transition effect identified by anim.
  • public Animation getAnimation() returns a reference to the transition effect, or null if no effect is associated with this AnimatingCardLayout instance.
  • public void setAnimation(Animation anim) associates the transition effect identified by anim with this AnimatingCardLayout instance.
  • public void setAnimationDuration(int animationDuration) sets the duration (in milliseconds) of the transition effect's animation. If the argument is less than 500, the duration is set to 500. The duration defaults to 2000 milliseconds.

This API works with six transition effect classes located in the org.javadev.effects package: CubeAnimation, DashboardAnimation, FadeAnimation, IrisAnimation, RadialAnimation, and SlideAnimation. Each of these classes implements the org.javadev.effects.Animation interface.

Suppose that you are designing a shopping-cart application whose GUI consists of order and payment screens, and you want to transition the user from the order screen to the payment screen after the user fills out the order details. In the following code fragment, this GUI is represented via two cards. One of these cards is for the order screen and the other card is for the payment screen:

JPanel cards = new JPanel ();
acl = new AnimatingCardLayout (new FadeAnimation ());
cards.setLayout (acl);

JPanel order = createOrderScreen ();
cards.add (order, "order");
JPanel payment = createPaymentScreen ();
cards.add (payment, "payment");

acl.show (cards, "order");

After creating a cards container and an AnimatingCardLayout instance associated with a fade transition effect, and after associating this layout manager with the container, the code fragment creates order and payment card panels populated with appropriate components, and invokes the manager's show() method to ensure that the order screen is displayed before payment.

Presumably, you would include a component such as a button on the order screen for switching to the payment screen when the component's listener is invoked. For example, when the button's action listener is invoked, it could execute the following statement to replace the order screen with the payment screen via the previously specified fade transition effect:

acl.show (cards, "payment");

A slideshow with animated transitions

To demonstrate the usefulness of AnimatingCardLayout, I have created a SlideShow application that presents a slideshow. This application reads GIF and JPEG images from a specific directory, displays each image slide in sequence, and employs this layout manager with randomly chosen transition effects to transition between successive slides. Listing 1 presents SlideShow's source code.

Listing 1. SlideShow.java

// SlideShow.java

import java.awt.*;
import java.awt.event.*;

import java.io.*;

import java.util.ArrayList;

import javax.swing.*;

import org.javadev.*;
import org.javadev.effects.*;

public class SlideShow extends JFrame
{
   final static int ANIM_DUR = 2500;
   final static int DEFAULT_WINDOW_SIZE = 500;
   final static int TIMER_DELAY = 6000;

   AnimatingCardLayout acl;

   Animation [] animations =
   {
      new CubeAnimation (),
      new DashboardAnimation (),
      new FadeAnimation (),
      new IrisAnimation (),
      new RadialAnimation (),
      new SlideAnimation ()
   };

   static ArrayList<ImageIcon> images = new ArrayList<ImageIcon> ();

   boolean showStartupMessage = true;

   int index;

   JPanel pictures;

   Timer timer;

   public SlideShow ()
   {
      super ("Slide Show");
      setDefaultCloseOperation (EXIT_ON_CLOSE);

      pictures = new JPanel ();
      pictures.setBackground (Color.black);

      acl = new AnimatingCardLayout ();
      acl.setAnimationDuration (ANIM_DUR);
      pictures.setLayout (acl);

      JLabel picture = new JLabel ();

      picture.setHorizontalAlignment (JLabel.CENTER);
      pictures.add (picture, "pic1");

      picture = new JLabel ();
      picture.setHorizontalAlignment (JLabel.CENTER);
      pictures.add (picture, "pic2");

      ActionListener al;
      al = new ActionListener ()
           {
               public void actionPerformed (ActionEvent ae)
               {
                  if (index == images.size ())
                  {
                      timer.stop (); // End the slideshow
                      return;
                  }

                  acl.setAnimation (animations [(int) (Math.random ()*
                                                animations.length)]);

                  if ((index & 1) == 0) // Even indexes
                  {
                      JLabel pic = (JLabel) pictures.getComponent (1);
                      pic.setIcon (images.get (index++));
                      try
                      {
                          acl.show (pictures, "pic2");
                      }
                      catch (IllegalStateException  ise)
                      {
                          index--; // Retry picture on next timer invocation
                      }
                  }
                  else // Odd indexes
                  {
                      JLabel pic = (JLabel) pictures.getComponent (0);
                      pic.setIcon (images.get (index++));
                      try
                      {
                          acl.show (pictures, "pic1");
                      }
                      catch (IllegalStateException  ise)
                      {
                          index--; // Retry picture on next timer invocation
                      }
                  }
               }
           };

      setContentPane (pictures);

      timer = new Timer (TIMER_DELAY, al);
      timer.start ();

      setSize (DEFAULT_WINDOW_SIZE, DEFAULT_WINDOW_SIZE);
      setVisible (true);
   }

   public void paint (Graphics g)
   {
      super.paint (g);

      if (showStartupMessage)
      {
          g.setColor (Color.yellow);
          g.drawString ("One moment please...", 30, 60);
          showStartupMessage = false;
      }
   }

   public static void main (String [] args)
   {
      if (args.length != 1)
      {
          System.err.println ("usage: java SlideShow imagePath");
          return;
      }

      final File imagePath = new File (args [0]);
      if (!imagePath.isDirectory ())
      {
          System.err.println (args [0]+" is not a directory path");
          return;
      }

      Runnable r = new Runnable ()
                   {
                       public void run ()
                       {
                          // Load all GIF and JPEG images in the imagePath.

                          File [] filePaths = imagePath.listFiles ();
                          for (File filePath: filePaths)
                          { 
                               if (filePath.isDirectory ())
                                   continue;

                               String name;
                               name = filePath.getName ().toLowerCase ();
                               if (name.endsWith (".gif") ||
                                   name.endsWith (".jpg"))
                               {
                                   System.out.println ("Loading "+filePath);

                                   ImageIcon ii;
                                   ii = new ImageIcon (filePath.toString ());
                                   images.add (ii);
                               }
                          }

                          if (images.size () < 2)
                          {
                              System.err.println ("too few images");
                              System.exit (0);
                          }

                          new SlideShow ();
                       }
                   };
      EventQueue.invokeLater (r);
   }
}

About SlideShow.java

SlideShow requires a single command-line argument, which is the path to a directory that contains at least two GIF and/or JPEG files. At startup, this application creates an ImageIcon object for each GIF/JPEG, and stores these objects in an ArrayList. During the slideshow, each of these objects is read from this collection, and its image is displayed via a transition effect.

Images are displayed from within the action listener of a timer that controls the slideshow. Each timer pulse invokes the listener, which randomly assigns a transition effect to the layout manager, assigns the next ImageIcon to one of the manager's pic1 and pic2 card label components, and invokes show() to transition from the current card label image to the other card label image.

Images associated with even ArrayList indexes are displayed via the pic2 card's label; images with odd indexes are displayed via pic1's label. This allows the very first image (at index 0) to be displayed via a transition effect. If I associated pic1 with even indexes (and so on), the first image would appear without a transition effect (a layout manager idiosyncrasy).

The timer pulses every few milliseconds as set by TIMER_DELAY. At least some of these milliseconds (set by ANIM_DUR) are used by the transition effect -- setAnimationDuration() assigns this value to the AnimatingCardLayout instance. Although ANIM_DUR is much smaller than TIMER_DELAY, it is possible for the transition effect to occupy too much time.

On slow machines (or machines with limited amounts of RAM -- not much of a problem these days), a transition effect might occasionally take much more time to complete than the value specified by ANIM_DUR. In fact, it is possible that another timer pulse might occur before the effect finishes. If this happens, AnimatingCardLayout's overridden show() method will throw an IllegalStateException.

This exception is thrown because show() is not re-entrant: A transition effect must end before this method can be re-invoked. On my Windows XP platform, this has not been a problem. However, to be on the safe side, I wrap each of the timer action listener's show() method calls in a try/catch construct. If the exception is thrown, the slideshow retries the image on the next timer pulse.

On with the show

Now that you understand how SlideShow works, let's try out this application. At the command line, invoke the following command to compile SlideShow.java:

javac -cp animatingcardlayout.jar SlideShow.java

This command assumes that animatingcardlayout.jar and SlideShow.java are located in the same directory.

Following a successful compilation, invoke the command below to run the application.

java -cp animatingcardlayout.jar;. SlideShow \windows

Note that this command assumes a Windows XP platform, and that the \windows directory contains at least two GIF and/or JPEG files.

As with AnimatingCardLayoutTest, you might notice some flicker during SlideShow's transition effects. I discuss this problem and present a solution in the next section.

Roll your own transition effects

Although AnimatingCardLayout's six transition effects are sufficient for many applications, you might want to implement your own effects. The first thing you need to know about creating a new transition effect class is that this class must implement AnimatingCardLayout's Animation interface, in terms of these three methods:

  • public Component animate(Component first, Component last, AnimationListener listener) returns a component on which a sequence of images (ranging from an image of first to an image of last, or vice-versa) are rendered by an internal thread. When the animation ends, the thread notifies the layout manager via listener.
  • public void setAnimationDuration(int duration) establishes the length of the animation, in milliseconds. The argument passed to setAnimationDuration() is the maximum of the duration passed to AnimatingCardLayout's setAnimationDuration() method and 500.
  • public void setDirection(boolean direction) sets the direction of the animation sequence. By convention, the animation sequence ranges from first to last (previously passed to animate()) when the direction argument is true, and vice-versa when this argument is false.

You also need to understand the context in which the aforementioned methods are used. In other words, you need to understand how they interact with AnimatingCardLayout. The following steps outline this interaction from the moment that show() is called until the moment that the layout manager receives notification about the animation finishing:

  1. AnimatingCardLayout's show() method, which is the entry point into working with the layout manager, invokes a private animate() method to perform setup tasks and indirectly begin the animation. One of this private method's arguments determines the direction in which animation proceeds (current component to the show()-specified component, or vice-versa).
  2. The private animate() method invokes Animation's setDirection() method to set the direction. By convention, true means animate from the current component to the show()-specified component; false means animate from the show()-specified component to the current component.
  3. The private animate() method invokes Animation's setAnimationDuration() method to establish the length of the animation, followed by this interface's animate() method to animate a sequence of images and notify the layout manager when the animation finishes.
  4. Animation's animate() method creates and returns a component (typically an instance of a JPanel subclass), passing its arguments to this component class's constructor. In turn, the constructor takes snapshots of the first and last components, and installs a thread (or a timer) that animates from the first/last snapshot image to the last/first snapshot image.
  5. When the thread/timer finishes the animation, it notifies AnimatingCardLayout to perform cleanup by invoking listener's public void animationFinished() method -- given that AnimatingCardLayout implements the AnimationListener interface. This method must be invoked on the event-dispatching thread.

I've created a ZoomAnimation class that renders a zoom transition similar to the zoom transition found in Apple's iPhone product. After the current card's component shrinks to the upper-left corner of the containing window, the next card's component grows in size from this corner until it fills the window. Listing 2 presents ZoomAnimation's source code.

Naming convention
In keeping with the naming convention established by the six example transition effect classes, ZoomAnimation includes Animation in its name.

Listing 2. ZoomAnimation.java

// ZoomAnimation.java

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;

import javax.swing.*;

import org.javadev.effects.*;

public class ZoomAnimation implements Animation
{
   boolean direction = true;
   int animationDuration = 2000;
   SpecialPanel animationPanel;

   public Component animate (Component first, Component last,
                             AnimationListener listener)
   {
      return new SpecialPanel (first, last, listener);

/*
      For many effects, you would employ direction logic such as that shown
      below -- see CubeAnimation.java, DashboardAnimation.java, and
      SlideAnimation.java for examples. This logic does not work for
      ZoomAnimation because SlideShow keeps alternating between a pair of
      components, which causes AnimatingCardLayout to keep switching
      direction. To see the result, comment out the return statement above,
      and uncomment the return statement below.
*/

/*
      return new SpecialPanel ((direction) ? first : last,
                               (direction) ? last : first, listener);
*/
   }

   public void setAnimationDuration (int duration)
   {
      animationDuration = duration;
   }

   public void setDirection (boolean direction)
   {
      this.direction = direction;
   }

   class SpecialPanel extends JPanel
   {
      final static int STEP_TIME = 50;

      BufferedImage firstImage, secondImage;
      double incr, xscale, yscale;
      int maxsteps, step;
      Timer timer;

      SpecialPanel (Component component1, Component component2,
                    final AnimationListener listener)
      {
         // Take a snapshot of the first component.

         firstImage = new BufferedImage (component1.getSize ().width,
                                         component1.getSize ().height,
                                         BufferedImage.TYPE_INT_RGB);
         Graphics g = firstImage.createGraphics ();
         component1.paint (g);
         g.dispose ();

         // Take a snapshot of the second component.

         secondImage = new BufferedImage (component2.getSize ().width,
                                          component2.getSize ().height,
                                          BufferedImage.TYPE_INT_RGB);
         g = secondImage.createGraphics ();
         component2.paint (g);
         g.dispose ();

         // Calculate the maximum number of steps in the animation sequence
         // and the scaling increment used to modify the x and y scale factors
         // during each step in the sequence.

         maxsteps = animationDuration/STEP_TIME;
         incr = 1.0/(maxsteps >> 1);

         // Create an action listener whose logic, which runs on the
         // event-dispatching thread, paints each step's image and terminates
         // the animation sequence after the last step has run.

         ActionListener al;
         al = new ActionListener ()
              {
                  public void actionPerformed (ActionEvent ae)
                  {
                     repaint ();

                     if (++step >= maxsteps)
                     {
                         timer.stop ();
                         listener.animationFinished ();
                     }
                  }
              };

         // Create and start a timer that invokes the action listener at a 
         // specific interval.

         timer = new Timer (STEP_TIME, al);
         timer.start ();
      }

      public void paint (Graphics g)
      {
         Graphics2D g2d = (Graphics2D) g;

         // Paint the background to remove any artifacts of previously
         // displayed image.

         g2d.setColor (Color.black);
         g2d.fillRect (0, 0, getWidth (), getHeight ());

         // Advance the animation sequence by calculating the next scale
         // factors and drawing either the first or the second image.

         if (step < (maxsteps >> 1)) // faster than maxsteps/2
         {
             if (step == 0)
             {
                 xscale = 1.0;
                 yscale = 1.0;
             }
             else
             {
                 xscale -= incr;
                 yscale -= incr;
             }
             g2d.scale (xscale, yscale);
             g2d.drawImage (firstImage, 0, 0, null);
         }
         else
         {
             if (step == (maxsteps >> 1))
             {
                 xscale = incr;
                 yscale = incr;
             }
             else
             {
                 xscale += incr;
                 yscale += incr;
             }
             g2d.scale (xscale, yscale);
             g2d.drawImage (secondImage, 0, 0, null);
         }
      }
   }
}

Listing 2 should be fairly easy to understand. Instead of following the pattern laid out by the six example transition effect classes, I simplified ZoomAnimation.java because I found the code for these classes to be somewhat hard to follow. Furthermore, I wanted to prevent the flickering problem that I encountered on my platform when viewing the example transition effects.

The flicker problem in AnimatingCardLayout's six given transition effects appears to result from a race condition between the animating and event-dispatching threads. If firstImage and secondImage are set to null before the paint() method is invoked, the window is momentarily blanked. Changing invokeLater() to invokeAndWait() seems to fix this problem.

Double-buffering for older Java versions
Although not a problem under Java SE 6, ZoomAnimation's paint() method might also exhibit flicker under older versions of Java. If this is the case, you will need to employ double-buffering within this method to avoid this problem.

Expanding the slideshow

You will probably want to play with ZoomAnimation in the context of the SlideShow application. Complete the following steps to add this transition effect class to SlideShow:

  1. Copy ZoomAnimation.java to the same directory as SlideShow.java.
  2. Insert a new ZoomAnimation () entry into SlideShow's animations array.
  3. Compile the source files via javac -cp animatingcardlayout.jar;. SlideShow.java.

After running SlideShow (as described earlier) and seeing the zoom transition effect in action, you can make this effect more dramatic by commenting out the background-painting code in ZoomAnimation's paint() method.

In conclusion

AnimatingCardLayout is an interesting open source Java project for integrating animated transitions into Java GUIs. One downside of the project is its lack of API documentation, which means you must review the source code to learn about the API. I also found the source code for the layout manager's transition-effect class somewhat hard to follow. Despite these drawbacks, I like this layout manager and enjoyed extending it with the ZoomAnimation class. I think you'll like it, too.

Jeff Friesen is a freelance software developer and educator who specializes in Java technology. Check out his javajeff.mb.ca Website to discover all of his published Java articles and more.

Learn more about this topic