Using Swing and Util Timers

This set of notes demonstrates how one can use Swing and Util timers to create different animation effects in Java. The following screen shot shows the interface for the application that these notes will discuss:

The two clocks start after a four second delay and update themselves every second. The clock on the left is controlled by a Util timer and the clock on the right is controlled by a Swing timer. The Util timer updates at a fixed execution rate of once per second, meaning that even if an execution is delayed for some reason, the next execution will occur at the next second (e.g., the actual execution might be 1.25, 2, 3, 4.5, 5, 6.05, 7, ...) while the Swing timer can drift (e.g., the actual execution might be 1.25, 2.25, 3.25, 4.75, 5.75, 6.8, 7.8, ...). The animation in the center of the window interchanges the positions of the "1" and the "6" boxes in the array using a Swing timer.

Before looking at the files it would probably be helpful to compile the application and run it yourself using the command:

java animation.AnimationDemo
The files can be found in /home/bvz/gui/notes/animation/ and belong to a package called animation. You can cause the array animation to start by clicking on it with the mouse at any time.

Here are the links to the four classes that comprise the application:

  1. AnimationDemo: Sets up the demo and establishes the timers for the clocks
  2. Clock: Draws a pie-shaped arc depending on the current "hour".
  3. animation: Animates the motion of the "1" and the "6" boxes in the array and handles the timer for the animation.
  4. LabeledBox: The labeled boxes used in the array animation.
The interesting code in this demo revolves about the use of timers to perform animations. We will first discuss the clock timers. To make the discussion simpler, we will aggregate the clock code associated with the Util timer and the clock code associated with the Swing timer.


Util Timer Clock

Here is the relevant code from the AnimationDemo.java file for creating the util timer clock:

Clock fixedClock = new Clock(); // create an instance of a clock
java.util.Timer fixedTimer;     // declare a util timer

public AnimationDemo() {
    // add the clock to the animation window
    this.getContentPane().add(fixedClock, BorderLayout.WEST);

    // create an instance of a util timer 
    fixedTimer = new java.util.Timer();

    // schedule the timer as a fixed rate timer with a delay of 4 seconds
    // (4000ms) and a repetition rate of 1 second (1000ms). 
    fixedTimer.scheduleAtFixedRate(new TimerTask() {
	int hour = 0; // hour keeps track of the current hour
	public void run() {
	    hour++;
	    SwingUtilities.invokeLater(new Runnable() {
		public void run() {
		   fixedClock.setPercentDone((double)hour / 12);
		}
            });
	    // stop the timer when the clock has cycled through all of the
	    // hours
	    if (hour == 12) fixedTimer.cancel();
	 }}, 4000, 1000); 
         ...
}
The comments should be self-explanatory. A TimerTask, which is defined in the Util package, must be created to perform the repetitive task to be completed by the timer. In this case the task is to advance the hour hand by one hour. The task to be executed should be defined in the run method. However, while the hour can be advanced within the TimerTask it is not safe to call the Clock's setPercentDone method within the TimerTask. The reason is that the setPercentDone method calls repaint, a Swing method, and Swing methods should only be executed within the event dispatch thread. Util TimerTasks are not executed on the event dispatch thread, so in order to ensure that the setPercentDone method gets executed on the event dispatch thread, we must call the SwingUtilities invokeLater method. The invokeLater method requires a Runnable object to perform its task, which is why we create and pass a Runnable object.


Swing Timer Clock

Here is the relevant code from the AnimationDemo.java file for creating the Swing timer clock:


Clock variableClock = new Clock();  // create an instance of a clock
Timer variableTimer;                // declare a Swing timer
int swingHour = 0;                  // keep track of the current hour

public AnimationDemo() {

    // add the clock to the animation window
    this.getContentPane().add(variableClock, BorderLayout.EAST);

    // create the ActionListener that will advance the hour hand and notify
    // the clock of the new hour
    ActionListener taskPerformer = new ActionListener() {
	    public void actionPerformed(ActionEvent evt) {
		swingHour++;
		variableClock.setPercentDone((double)swingHour / 12);
	    }};

    // create a Swing timer that repeats its task every second (1000ms)
    variableTimer = new Timer(1000, taskPerformer);

    // after the start method is called, delay the initial execution of the
    // timer's task for 4 seconds (4000ms)
    variableTimer.setInitialDelay(4000);

    // start the timer
    variableTimer.start();
}
The comments should be self-explanatory. The only reason for declaring swingHour as an instance variable of AnimationDemo rather than the ActionListener was to again drive home the point that inner classes can access instance variables in their outer classes. If I weren't interested in making this point then it would have been preferable to declare swingHour within the ActionListener class, which is what I did with the hour variable in the Util TimerTask.


Array Animation

The array animation is accomplished by means of a Swing Timer. The two boxes are supposed to move smoothly along linear paths. The "1" box initially moves upward, then to the right, and finally downward into its new location. The "6" box initially moves downward, then to the left, and finally upward to its new location. The actions of the two boxes are always opposite so if we know the action being performed on one of the boxes, we can perform the opposite action on the other box. We have chosen to keep track of the actions on the "1" box. To keep track of the actions we have defined a variable named action and defined three actions:
final int UP = 0;
final int RIGHT = 1;
final int DOWN = 2;
int action = UP;
The initial value of action is UP because that is the initial direction of box 1.

In order to perform the calculations of where each box should lie on its path, we must know the beginning and ending X positions, so that the two boxes can be appropriatelyh moved to their new positions, the beginning Y position, so that the two boxes will know when to stop, and the maximal Y position on box 1's path, so that box 1 will know when to stop moving upward and start moving to the right. The following four variables store this information:

int startX;
int endX;
int startY;
int endY;
The beginning X position corresponds to the initial left side of box 1 and the ending X position corresponds to the initial left side of box 6. The beginning Y position is the initial top of box 1 and the maximal Y position has been arbitrarily defined to be 10 moves above the initial Y position. The variables are thus initialized in the constructor as follows:
startX = upBox.getLeft();  // upBox is box 1
endX = downBox.getLeft();  // downBox is box 6
startY = upBox.getTop();
endY = startY - pixels_per_move * 10; // pixels_per_move represents the number
                                      // of pixels to move each box per move
The last important piece of information is how fast the animation should proceed. We could specify the number of seconds the animation should take but to simplify the presentation we have arbitrarily decided to move the boxes four pixels per "frame" and to run at a rate of 5 frames per second.


Animating the Box Paths

The Swing Timer will require an ActionListener to animate the box paths. Because of the complexity of the calculations it makes sense to declare the ActionListener as a named inner class rather than an anonymous inner class. The calculations are separated into three parts, depending on whether box 1 is moving up, to the right, or down.

Up Action: As box 1 moves up only its Y value gets modified. You should note in the following code that we actually subtract pixels from box 1's top because the "up" direction in window coordinates is toward 0. When box 1 reaches its maximal vertical position the code changes the current action to RIGHT. Here is the code for the up case:

if (action == UP) {
    if (upBox.getTop() > endY) {
        upBox.setPosition(upBox.getLeft(), upBox.getTop() - pixels_per_move);
	downBox.setPosition(downBox.getLeft(), downBox.getTop() + pixels_per_move);
	repaint();
	return;
    }
    else
        action = RIGHT;
}
Right Action: As box 1 moves to the right only its left variable needs to be modified. We need to ensure that if the initial distance between box 1 and box 6 is not divisible by 4, that box 1 does not overshoot the ending X position by a couple pixels. As a result we calculate a candidate left position for box 1 and then modify it if that candidate position exceeds the final X position. Once box 1 reaches its final X position the code changes the current action to DOWN. Here is the code for the right case:
if (action == RIGHT) {
    if (upBox.getLeft() < endX) {
        int next_up_left = upBox.getLeft() + pixels_per_move;
	int next_down_left = downBox.getLeft() - pixels_per_move;
	if (next_up_left > endX) {
	    next_up_left = endX;
	    next_down_left = startX;
	}
	upBox.setPosition(next_up_left, upBox.getTop());
	downBox.setPosition(next_down_left, downBox.getTop());
	repaint();
	return;
    }
    else 
        action = DOWN;
}
Down Action: As box 1 moves down only its Y position needs to be modified and this Y position will need to be increased. If we wanted to be absolutely careful we would include code that ensures that box 1 does not overshoot its final Y position. However, we know that we terminated the UP action after an integral number of 4 pixel moves and hence we know that overshooting is not a possibility. When we have reached the final Y position we stop the timer because the animation is complete. Here is the code for the down case:
if (action == DOWN) {
    if (upBox.getTop() < startY) {
        upBox.setPosition(upBox.getLeft(), upBox.getTop() + pixels_per_move);
	downBox.setPosition(downBox.getLeft(), downBox.getTop() - pixels_per_move);
	repaint();
	return;
    }
    else
	animationTimer.stop();
}
Finally, we can create the animation by creating an instance of a Swing Timer and setting its frequency to 200ms, which corresponds to 5 frames per second. We do not want to start the animation until the user actually mouse clicks on the window containing the array, so we create a mouse listener that listens for a mouse click and then starts the timer. The following code in the animation class's constructor sets up and starts the animation:
ActionListener taskPerformer = new mover(boxes[1], boxes[6]);
animationTimer = new Timer(200, taskPerformer);
addMouseListener(new MouseAdapter() {
	public void mouseClicked(MouseEvent me) {
	    animationTimer.start();
	}
      });