Java Swing Layout Animation

Articles —> Java Swing Layout Animation

Java Swing does not by default have any built in animation tools available, leaving it up to the programmer to implement any sort of animation themselves. While animation of custom graphics can be quite case specific, animation of the Swing Components themselves* can be generalized into a simple animation framework.

A framework should be general enough to allow for many types of animation - fades, size changes, etc... - of all types of Swing Components. The framework I built is generalized via the simple implementation of an interface. At its core is a Swing Timer and callback system. The SwingAnimator class posted below handles the actual timing of events, calling upon the SwingAnimatorCallback interface to deal with the actual animation.

The Animator Class




import java.awt.event.ActionEvent;

import java.awt.event.ActionListener;

import java.util.ArrayList;

import java.util.List;



import javax.swing.Timer;



/**

 * Class to animate Layouts and Fades for a given component. 

 * @author Greg Cope

 *

 */

public class SwingAnimator {



	//callback object

	private final SwingAnimatorCallback callback;

	//Timer to animate on the EDT

	private Timer timer = null;

	//duration in milliseconds betweeen each firing of the Timer

	private int duration = 60;

	//Listeners

	private List<AnimationStateListener> listeners = new ArrayList<AnimationStateListener>();

	

	/**

	 * Constructs a new SwingAnimator. 

	 * @param callback The object to callback to

	 */

	public SwingAnimator(SwingAnimatorCallback callback){

		this(callback, false);

	}

	

	/**

	 * 

	 * @param callback The object to callback to

	 * @param start true to automatically start the animation, false otherwise

	 */

	public SwingAnimator(SwingAnimatorCallback callback, boolean start){

		this(callback, 60, start);

	}

	

	/**

	 * 

	 * @param callback The object to callback to

	 * @param frameTiming Timing between each call to callback.

	 * @param start true to automatically start the animation, false otherwise

	 */

	public SwingAnimator(SwingAnimatorCallback callback, int frameTiming, boolean start){

		this.callback = callback;

		setAnimationDuration(frameTiming);

		if ( start ){

			start();

		}

	}

	

	/**

	 * 

	 * @param callback The object to callback to

	 * @param frameTiming Timing between each call to callback.

	 */

	public SwingAnimator(SwingAnimatorCallback callback, int frameTiming){

		this(callback, frameTiming, false);

	}

	/**

	 * Adds a listener to be notified of events

	 * @param listener

	 */

	public void addListener(AnimationStateListener listener){

		listeners.add(listener);

	}

	

	/**

	 * Removes the listener.

	 * @param listener

	 */

	public void removeListener(AnimationStateListener listener){

		listeners.remove(listener);

	}

	

	/**

	 * Sets the time (in milliseconds) between each call to callback. 

	 * @param duration

	 */

	public void setAnimationDuration(int duration){

		this.duration = duration;

	}

	/**

	 * Checks if this animator is running.

	 * @return

	 */

	public boolean isRunning(){

		if ( timer == null ){

			return false;

		}

		return timer.isRunning();

	}

	

	/**

	 * Stops the timer

	 */

	public void stop(){

		if ( timer != null ){

			timer.stop();

		}

	}

	/**

	 * Starts the timer to fire. If the current timer is non-null and running, this method will first

	 * stop the timer before beginning a new one. 

	 */

	public void start(){

		if ( timer != null && timer.isRunning() ){

			stop();

		}

		timer = new Timer(duration, new CallbackListener());

		timer.start();

		for ( AnimationStateListener l : listeners ){

			l.animationStarted(this);

		}

	}

	

	/**

	 * ActionListener implements to be passed to the internal timer instance

	 * @author Greg Cope

	 *

	 */

	private class CallbackListener implements ActionListener{



		@Override

		public void actionPerformed(ActionEvent e) {

			if ( callback.hasTerminated() ){

				if ( timer == null ){

					throw new IllegalStateException("Callback listener should not be fired outside of SwingAnimator timer control");

				}

				for ( AnimationStateListener l : listeners ){

					l.animationComplete(SwingAnimator.this);

				}

				timer.stop();

			}

			callback.callback(SwingAnimator.this);

		}

		

	}

	

}

The callback class:


/**

 * Callback interface to be notified by a SwingAnimator of a new time frame. 

 * @author Greg Cope

 *

 */

public interface SwingAnimatorCallback {



	/**

	 * Callback method for the SwingAnimator

	 * @param caller

	 */

	public void callback(Object caller);

	

	/**

	 * Returns true if the SwingAnimator should terminate. 

	 * @return

	 */

	public boolean hasTerminated();

	

}

To be notified upon animation initiation and completion, one can register an implementation of the AnimationStateListener with the SwingAnimator:


/**

 * Listener interface that allows notification when a particular state is reached

 * @author Greg Cope

 *

 */

public interface AnimationStateListener {



	/**

	 * Called by the SwingAnimator when animation has started. 

	 * @param animator

	 */

	public void animationComplete(SwingAnimator animator);

	

	/**

	 * Called by the SwingAnimator when animation is complete. 

	 * @param animator

	 */

	public void animationStarted(SwingAnimator animator);

	

}



To apply this framework to a particular type of animation, one must implement the SwingAnimationCallback interface. This implementation is where many of the nuts and bolts come together - the actual animation (whether it be fades or size changes) is handled in this animation, typically by incrementing an instance variable and repainting the animated component for every call to the callback method.

Rate of animation can be controlled in two ways:

  • SwingAnimator: setting the animation duration changes how frequently the timer is fired (and hence, how fast the animation will occur - smaller values fire the timer more frequently).
  • SwingAnimatorCallback implementation: every time the callback method is called, one should increment/decrement a value which defines the current state of the animation - for instance an alpha transparency value. The rate of animation will be affected by how much this field is incremented/decremented.

Of course, this framework makes more sense when placed into context: consider the following example in which the custom graphics of a JPanel can be faded in or out. This is accomplished by creating a single class which extends JPanel and implements the SwingAnimationCallback interface. In this example, the paintComponent method is overridden to allow for alpha transparency changes via the AlphaComposite - the alpha value is then incremented/decremented during the fading process:




import java.awt.AlphaComposite;

import java.awt.Graphics;

import java.awt.Graphics2D;



import javax.swing.JPanel;



/**

 * 

 * @author Greg Cope

 *

 */

public class FadingJPanel extends JPanel{



	private static final long serialVersionUID = 1L;



	private float alpha = 0;

		

	private float increment = 0.01f;

	

	private SwingAnimator fadeInAnimator = null;

	private SwingAnimator fadeOutAnimator = null;

	

	/**

	 * Sets the alpha value

	 * @param a

	 */

	public void setAlpha(float a){

		this.alpha = a;

	}



/**

	 * Fades this JPanel in. 

	 */

	public void fadeIn(){

		stop();

		fadeInAnimator = new SwingAnimator(new FadeInCallback());

		fadeInAnimator.addListener(new AnimationStateListener(){



			@Override

			public void animationComplete(SwingAnimator animator) {

				FadingJPanel.this.firePropertyChange("fade complete", 0, 0);

			}



			@Override

			public void animationStarted(SwingAnimator animator) {

				FadingJPanel.this.firePropertyChange("fade started", 0, 1);

			}

			

		});

		fadeInAnimator.start();

	}

	

	/**

	 * Fades this JPanel out

	 */

	public void fadeOut(){

		stop();

		fadeOutAnimator = new SwingAnimator(new FadeOutCallback());

		fadeOutAnimator.addListener(new AnimationStateListener(){



			@Override

			public void animationComplete(SwingAnimator animator) {

				FadingJPanel.this.firePropertyChange("fade complete", 1, 1);

			}



			@Override

			public void animationStarted(SwingAnimator animator) {

				FadingJPanel.this.firePropertyChange("fade started", 1, 0);

			}

			

		});

		fadeOutAnimator.start();

	}

	

	/**

	 * Stops all animators. 

	 */

	private void stop(){

		if ( fadeOutAnimator != null && fadeOutAnimator.isRunning() ){

			fadeOutAnimator.stop();

		}

		if ( fadeInAnimator != null && fadeInAnimator.isRunning() ){

			fadeInAnimator.stop();

		}

	}

	/**

	 * Sets how quickly each frame will change the alpha transparency

	 * @param increment

	 */

	public void setIncrement(float increment){

		this.increment = increment;

	}



	/**

	 * Sets the animation speed

	 * @param ms

	 */

	public void setAnimationSpeed(int ms){

		this.animationSpeed = ms;

	}

	

	

	@Override

	public void paintComponent(Graphics g){

		super.paintComponent(g);

		Graphics2D g2d = (Graphics2D)g;

		if ( alpha >= 0 && alpha <= 1 ){

			AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);

			g2d.setComposite(ac);

		}

	}

	/**

	 * Callback implementation for fading in

	 * @author Greg Cope

	 *

	 */

	private class FadeInCallback implements SwingAnimatorCallback{



		@Override

		public void callback(Object caller) {

			alpha += increment;

			repaint();

		}



		@Override

		public boolean hasTerminated() {

			if ( alpha >= 1 ){

				alpha = 1;

				return true;

			}

			return false;

		}

		

	}

	/**

	 * Callback implementation to fade out

	 * @author Greg Cope

	 *

	 */

	private class FadeOutCallback implements SwingAnimatorCallback{



		@Override

		public void callback(Object caller) {

			alpha -= increment;

			repaint();

		}



		@Override

		public boolean hasTerminated() {

			if ( alpha <= 0 ){

				alpha = 0;

				return true;

			}

			return false;

		}

		

	}

}

In the above example, animation can be performed simply by calling the fadeIn and fadeOut methods of this class. Timing can be adjusted as needed with the setter methods setIncrement and setAnimationSpeed, and initial visibility using the setAlpha method. Although this class is specific to custom drawing, one could animate child components by carefully overriding the paint method of the Component.

In a similar fashion, one can extend many other Swing components to provide a library of animated components. For instance, to animate the size of a Component (note, the LayoutManager may play a role in these cases), animating the position of a JSlider, animating the divider location of a JSplitPane, animating scrolling of a JScrollPane, etc...

*The idea here being similar to JQuery animation in Javascript.



There are no comments on this article.

Back to Articles


© 2008-2022 Greg Cope