- JGoodies Animation Library Intro
- Animation Library Tour
- Conclusion
Animation Library Tour
The JGoodies animation library provides a powerful framework for making animations part of the user interface. This framework relies on several concepts that are presented below:
- An animation is the time-based change of some attribute (property) of an animation target (object) over a specific duration (length of time). For example, the radius (attribute) of a circle (animation target) expands from 10 pixels to 100 pixels over 20 seconds (duration).
- Each animation defines an animation function that maps time to target attribute values. During the animation, the animation function is periodically evaluated to obtain the attribute’s next value. Because animation functions are continuous over time, an animation maintains the same overall appearance when rendered at different frame rates (the number of animation frames presented per second).
- A composed animation comprises multiple animations and applies the animation effect (a combination of all animation functions with timing and anything else related to a composed animation...for each non-negative time, the animation effect describes all attributes values for all targets) to multiple animation targets and their attributes. For example, a circle’s radius can grow from 10 to 100 pixels (1 animation) and then stay at 100 pixels while its fill-color cycles through all shades of red (a second animation) during the first 15 seconds of this composed animation. The animations that form a composed animation can run sequentially or run in parallel.
The animation library is contained in animation-1.1.3.jar. That Jar file organizes its classes and interfaces into several packages, with com.jgoodies.animation serving as the top-level package. That package contains the animation library’s core types, including the Animation and AnimationListener interfaces, and the AbstractAnimation, AnimationEvent, and Animator classes:
- The Animation interface describes time-based animations. Its public void animate(long time) method performs the animation at the given time by applying the animation effect to the animation target. It also fires an appropriate event when the animation starts or stops, and resets the animation effect if the animation’s duration has been exceeded. There is also a public void addAnimationListener(AnimationListener listener) method for registering an object to receive animation start and stop events.
- The AnimationListener interface describes animation listener objects in terms of two methods: public void animationStarted(AnimationEvent evt) is invoked when the animation starts and public void animationStopped(AnimationEvent evt) is invoked when the animation stops.
- The AbstractAnimation class minimizes the effort required to
implement Animation. It provides a protected AbstractAnimation(long
duration, boolean frozen) constructor for specifying the animation’s
duration and whether or not the animation effect is frozen
when the animation completes. A true value means that the animation
effect is retained (frozen) once the animation finishes; a false value
means that the animation effect is reset to whatever it would be at time 0. A
simpler protected AbstractAnimation(long duration) constructor is also
provided, which invokes the former constructor with false being passed
to frozen.
In addition to the aforementioned constructors, AbstractAnimation provides a protected abstract void applyEffect(long time) method to apply the animation effect at the given time to the animation target—and also maintains a list of animation listeners.
- The AnimationEvent class describes animation start and stop events. You might want to call its public com.jgoodies.animation.AnimationEvent.Type type() method to return the event type—AnimationEvent.STARTED or AnimationEvent.STOPPED.
- The Animator class starts and stops an animation at a given frame
rate (the number of frames per second). After invoking its public
Animator(Animation animation, int framesPerSecond) constructor to identify
the animation to animate, and the desired frame rate, invoke
Animator’s public void start() method to start the
animation and (if necessary) Animator’s public void
stop() method to stop the animation.
Behind the scenes, Animator’s constructor creates a javax.swing.Timer object to take care of timing. That timer is started in response to invoking start(). Once per time period (as determined by the frame rate), the timer invokes Animator’s public void actionPerformed(ActionEvent e) method on the event-dispatching thread. In turn, that method invokes the animation’s animate(long time) method with the elapsed time as the value of time. If you were to examine AbstractAnimation’s overridden animate() method’s source code, you would discover that method invoking the applyEffect() method. It’s within applyEffect() where the animation code resides.
Let’s put these interfaces and classes into context by employing them in a simple animation application. That application’s source code appears in Listing 1.
Listing 1 AnimDemo1.java
// AnimDemo1.java import com.jgoodies.animation.*; public class AnimDemo1 { final static int DEFAULT_RATE = 30; final static int DURATION = 10000; public static void main (String [] args) { int fps = DEFAULT_RATE; if (args.length > 0) try { fps = Integer.parseInt (args [0]); } catch (NumberFormatException e) { System.err.println ("Unable to parse the frame rate"); return; } System.out.println ("fps = " + fps); Animation animation = new SimpleAnimation (DURATION); AnimationListener al; al = new AnimationListener () { public void animationStarted (AnimationEvent e) { System.out.println ("Started"); } public void animationStopped (AnimationEvent e) { System.out.println ("Stopped"); synchronized ("A") { "A".notify (); } } }; animation.addAnimationListener (al); Animator animator = new Animator (animation, fps); animator.start (); synchronized ("A") { try { "A".wait (); } catch (InterruptedException e) { } } } static class SimpleAnimation extends AbstractAnimation { SimpleAnimation (long duration) { super (duration); } protected void applyEffect (long time) { System.out.println ("It is now " + time + " milliseconds."); } } }
Listing 1’s public static void main(String [] args) method first establishes the frame rate (which is either the default setting or a command-line value). It next creates an animation object from the SimpleAnimation class, with a 10000-millisecond (10-second) duration. That class’s constructor forwards this duration to its AbstractAnimation superclass. Continuing, main() creates an animation listener—for detecting when an animation starts and stops—and attaches that listener to the animation object. Finally, main() creates and starts an Animator to execute the animation at the specified frame rate.
If the thread executing the main() method exits before the internal timer’s thread is started, the animation will not occur. Rather than force the main thread to sleep for a specific number of seconds (the value possibly needing to be changed on different platforms), I elected to have the main thread wait until it receives notification, via the event-dispatching thread, from within the animationStopped() method.
If you run AnimDemo1 with no command-line arguments, you might expect a default frame rate of 30 frames per second. Given a 10-second duration, you might also expect the applyEffect() method to be called 300 times, every 33 and 1/3 milliseconds. This isn’t quite what happens, as the output below reveals:
fps = 30 Started It is now 3730 milliseconds. It is now 3840 milliseconds. It is now 3900 milliseconds. It is now 3950 milliseconds. It is now 4010 milliseconds. It is now 4060 milliseconds. It is now 4120 milliseconds. It is now 4170 milliseconds. It is now 4230 milliseconds. It is now 4280 milliseconds. It is now 4340 milliseconds. It is now 4390 milliseconds. It is now 4450 milliseconds. It is now 4500 milliseconds. It is now 4560 milliseconds. It is now 4610 milliseconds. It is now 4670 milliseconds. It is now 4720 milliseconds. It is now 4780 milliseconds. It is now 4830 milliseconds. It is now 4890 milliseconds. It is now 4940 milliseconds. It is now 5000 milliseconds. It is now 5050 milliseconds. It is now 5110 milliseconds. It is now 5160 milliseconds. It is now 5220 milliseconds. It is now 5270 milliseconds. It is now 5330 milliseconds. It is now 5380 milliseconds. It is now 5440 milliseconds. It is now 5490 milliseconds. It is now 5550 milliseconds. It is now 5600 milliseconds. It is now 5650 milliseconds. It is now 5710 milliseconds. It is now 5760 milliseconds. It is now 5820 milliseconds. It is now 5870 milliseconds. It is now 5930 milliseconds. It is now 5980 milliseconds. It is now 6040 milliseconds. It is now 6090 milliseconds. It is now 6150 milliseconds. It is now 6200 milliseconds. It is now 6260 milliseconds. It is now 6310 milliseconds. It is now 6370 milliseconds. It is now 6420 milliseconds. It is now 6480 milliseconds. It is now 6530 milliseconds. It is now 6590 milliseconds. It is now 6640 milliseconds. It is now 6700 milliseconds. It is now 6750 milliseconds. It is now 6810 milliseconds. It is now 6860 milliseconds. It is now 6920 milliseconds. It is now 6970 milliseconds. It is now 7030 milliseconds. It is now 7080 milliseconds. It is now 7140 milliseconds. It is now 7190 milliseconds. It is now 7250 milliseconds. It is now 7300 milliseconds. It is now 7360 milliseconds. It is now 7410 milliseconds. It is now 7470 milliseconds. It is now 7520 milliseconds. It is now 7580 milliseconds. It is now 7630 milliseconds. It is now 7690 milliseconds. It is now 7740 milliseconds. It is now 7800 milliseconds. It is now 7850 milliseconds. It is now 7910 milliseconds. It is now 7960 milliseconds. It is now 8020 milliseconds. It is now 8070 milliseconds. It is now 8130 milliseconds. It is now 8180 milliseconds. It is now 8240 milliseconds. It is now 8290 milliseconds. It is now 8350 milliseconds. It is now 8400 milliseconds. It is now 8460 milliseconds. It is now 8510 milliseconds. It is now 8570 milliseconds. It is now 8620 milliseconds. It is now 8680 milliseconds. It is now 8730 milliseconds. It is now 8790 milliseconds. It is now 8840 milliseconds. It is now 8900 milliseconds. It is now 8950 milliseconds. It is now 9010 milliseconds. It is now 9060 milliseconds. It is now 9120 milliseconds. It is now 9170 milliseconds. It is now 9230 milliseconds. It is now 9280 milliseconds. It is now 9330 milliseconds. It is now 9390 milliseconds. It is now 9440 milliseconds. It is now 9500 milliseconds. It is now 9550 milliseconds. It is now 9610 milliseconds. It is now 9660 milliseconds. It is now 9720 milliseconds. It is now 9770 milliseconds. It is now 9830 milliseconds. It is now 9880 milliseconds. It is now 9940 milliseconds. It is now 9990 milliseconds. It is now 0 milliseconds. Stopped
The output (that I consistently produced during several AnimDemo1 executions) reveals the first time applyEffect() is called to be 3730 milliseconds—not 0 milliseconds. Furthermore, there are only 115 lines of output between the Started and Stopped messages—not 300. Finally, the number of milliseconds between successive method invocations varies (50 milliseconds one time; 110 milliseconds another time).
These numbers are not surprising when you consider that Java’s threading model often relies on the underlying platform’s threading mechanism, that it can take time to start the internal timer thread running, and that thread scheduling affects the order in which threads execute. You will probably notice different numbers on your platform. Just keep in mind that the actual frame rate might be lower than the frame rate you specify, and that you might not want to rely on the actual time values passed to applyEffect().
Earlier, I introduced the concept of and stated that each animation defines an animation function. Looking back at Listing 1, you might be wondering what animation function is being used. For this simple application, think of the animation function as being implicit—the act of printing out the time value is the animation function. But for more practical applications, the animation library’s com.jgoodies.animation package contains the AnimationFunction interface and the AbstractAnimationFunction class for creating explicit animation functions.
AnimationFunction describes a time-based animation function in terms of that function’s duration and a mapping from time to an animation effect (such as a fade or dissolve effect). The public long duration() method returns the duration of the effect; the public Object valueAt(long time) method returns an Object that represents the value of the effect at time time. For a text-fade animation function, valueAt() could return an Object representing text that is less faded at a smaller time value and an Object representing text that is more faded at a larger time value. To save you from having to implement AnimationFunction, the animation library supplies AbstractAnimationFunction for you to subclass.
You might never need to subclass AbstractAnimationFunction. Instead, you are more likely to make use of com.jgoodies.animation.AnimationFunctions. That class provides various static methods that each return a specific kind of AnimationFunction. For example, public static AnimationFunction discrete(long duration, Object[] values) creates and returns a noninterpolating animation function for a given duration—and a given values array whose entries are equally distributed over the duration.
To demonstrate an animation function (specifically, the AnimationFunction returned by the discrete() method), I created a Swing application that animates a bullet moving in a sinusoidal fashion from left to right on a panel. Listing 2 presents that application’s source code.
Listing 2 AnimDemo2.java
// AnimDemo2.java import com.jgoodies.animation.*; import java.awt.*; import java.util.*; import javax.swing.*; public class AnimDemo2 extends JFrame { final static int DURATION = 30000; final static int FRAME_RATE = 30; final static int HEIGHT = 200; final static int WIDTH = 300; AnimPanel ap; public AnimDemo2 (String title) { super (title); setDefaultCloseOperation (EXIT_ON_CLOSE); getContentPane ().add (ap = new AnimPanel (WIDTH, HEIGHT)); Animation animation = new SimpleAnimation (DURATION); Animator animator = new Animator (animation, FRAME_RATE); animator.start (); pack (); setVisible (true); } public static void main (String [] args) { new AnimDemo2 ("Animation Demo"); } class AnimPanel extends JPanel { Image imBullet; int height, width, xPos, yPos; Vector<Point> trail; AnimPanel (int width, int height) { this.width = width; this.height = height; xPos = 0; yPos = height / 2; imBullet = Toolkit.getDefaultToolkit ().getImage ("bullet.gif"); trail = new Vector<Point> (); } public Dimension getPreferredSize () { return new Dimension (width, height); } public void paintComponent (Graphics g) { super.paintComponent (g); for (Point pt : trail) g.drawLine (pt.x, pt.y, pt.x, pt.y); g.drawImage (imBullet, xPos++, yPos, this); if (xPos >= width) xPos = 0; } void setPosition (int pos) { yPos = pos; trail.add (new Point (xPos, yPos)); repaint (); } } class SimpleAnimation extends AbstractAnimation { AnimationFunction af; SimpleAnimation (long duration) { super (duration); Integer [] positions = new Integer [3600]; for (int degree = 0; degree < positions.length; degree++) positions [degree] = new Integer (HEIGHT / 2 + (int) (Math.sin (Math.toRadians (degree)) * 50)); af = AnimationFunctions.discrete (DURATION, positions); } protected void applyEffect (long time) { Integer position = (Integer) af.valueAt (time); ap.setPosition (position.intValue ()); } } }
Listing 2 establishes a GUI consisting of a single animation panel—an AnimPanel instance. That class’s AnimPanel(int width, int height) constructor establishes the animation panel’s width and height, loads the image of a bullet, and establishes a data structure to hold the trail the bullet makes as it moves across the panel. Whenever the bullet must be moved, void setPosition(int pos) is invoked to move the bullet’s image one position to the right, and either upward (if pos is positive) or downward (if pos is negative).
The setPosition() method is invoked from within SimpleAnimation’s applyEffect() method. Each invocation receives an argument value obtained by calling an animation function’s valueAt() method. This method returns one of the sinusoidal values that were precomputed in SimpleAnimation’s constructor.
Figure 1 illustrates the bullet’s sinusoidal movement in terms of the trail that follows the bullet. The noise in the lower part of the leftmost portion of the trail is due to the initial delay before the first call to applyEffect() and other issues related to the underlying platform’s threading mechanism.
Figure 1 A bullet moves in a sinusoidal path.
You might never need to work directly with animation functions. Instead, you will probably work at a higher level with predefined animations and components to which those animations apply. Behind the scenes, these animations make use of animation functions.
The com.jgoodies.animation.animations package supplies several animation classes, including the BasicTextAnimation and FanAnimation classes. Similarly, the com.jgoodies.animation.components package supplies several specially designed Swing component classes, such as BasicTextLabel and FanComponent, which are referenced by the animation classes. To illustrate these classes for you, I created an application that presents a splash screen. Check out Listing 3 for the source code.
Listing 3 AnimDemo3.java
// AnimDemo3.java import com.jgoodies.animation.*; import com.jgoodies.animation.animations.*; import com.jgoodies.animation.components.*; import java.awt.*; import javax.swing.*; public class AnimDemo3 extends JWindow { final static int DURATION = 30000; final static int FRAME_RATE = 30; int duration; BasicTextLabel label; FanComponent fan; public void showSplashScreen () { JPanel content = (JPanel) getContentPane (); content.setLayout (new GridLayout (2, 1)); content.setBackground (Color.white); content.setBorder (BorderFactory.createLineBorder (Color.blue, 10)); int width = 500; int height= 200; Dimension screen = Toolkit.getDefaultToolkit ().getScreenSize (); int x = (screen.width - width) / 2; int y = (screen.height -height) / 2; setBounds (x, y, width, height); label = new BasicTextLabel (""); label.setFont (new Font ("Tahoma", Font.BOLD, 18)); content.add (label); fan = new FanComponent (10, Color.green); content.add (fan); setVisible (true); Animation animation = createAnimation (); AnimationListener al; al = new AnimationListener () { public void animationStarted (AnimationEvent e) { } public void animationStopped (AnimationEvent e) { synchronized ("A") { "A".notify (); } } }; animation.addAnimationListener (al); Animator animator = new Animator (animation, 30); animator.start (); synchronized ("A") { try { "A".wait (); } catch (InterruptedException e) { } } } private Animation createAnimation () { Animation a1 = BasicTextAnimation.defaultFade (label, 10000, "Welcome To", Color.orange); Animation a2 = BasicTextAnimation.defaultScale (label, 10000, "The JGoodies", Color.cyan); Animation a3 = BasicTextAnimation.defaultSpace (label, 10000, "Animation Demonstration!", Color.magenta); Animation allSeq = Animations.sequential (new Animation [] { Animations.pause (1000), a1, Animations.pause (1000), a2, Animations.pause (1000), a3, Animations.pause (1000), }); Animation a4 = FanAnimation.defaultFan (fan, 33000); return Animations.parallel (allSeq, a4); } public static void main (String [] args) { new AnimDemo3 ().showSplashScreen (); System.exit (0); } }
Listing 3 creates and displays a splash screen. The splash screen is based on a JWindow so that there are no distracting decorations (such as a minimize box). After building the splash screen, which includes the creation of BasicTextLabel and FanComponent components, the showSplashScreen() method takes care of animation setup.
From an animation perspective, Listing 3 is similar to Listing 1, but with one important exception: the createAnimation() method. That method creates a composed animation from several animations, where some animations apply to a BasicTextLabel component, and another animation applies to a FanComponent. For example, Animation a1 = BasicTextAnimation.defaultFade (label, 10000, "Welcome To", Color.orange); returns a text-fade Animation, where label identifies the BasicTextLabel animation target, 10000 identifies the duration of this animation, Welcome To is the text to fade, and Color.orange serves as the base color for the text fade.
You will notice calls to Animations’ public static Animation pause(long duration), public static Animation sequential(Animation[] animations), and public static Animation parallel(Animation animation1, Animation animation2) methods. The pause() method creates a pausing animation that does not affect the animation effect, but exists only to provide a delay between two other animations. The sequential() method concatenates several animations into a single animation, where the component animations execute in sequence. Finally, the parallel() method returns an animation whose two component animations execute in parallel. Hence, a fan can rotate concurrently with a sequence of text animations. One frame from this composed animation is shown in Figure 2.
Figure 2 A splash screen with two concurrent animations