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 2 seconds
    // (2000ms) 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();
	 }}, 2000, 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);
		// when the clock reaches 12, restart the animation.
		// The restart command restarts the timer after
		// the initial delay, which in this case is 4 seconds
                if (swingHour == 12) { 
		    swingHour = 0;
                    // variableTimer is an instance variable of the outer class and
                    // hence is accessible to this method
		    variableTimer.restart();
		}
	    }};

    // 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 2 seconds (2000ms)
    variableTimer.setInitialDelay(2000);

    // 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 50 pixels (defined as the constant VERTICAL_DISTANCE) 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 - VERTICAL_DISTANCE; // 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. In order to determine how fast the boxes should move, we need to know:

  1. the total distance each box will move.
  2. the frame rate per second, which I have decided should be 25, since it evenly divides 1000 milliseconds. Movies use a frame rate of 24 per second, so I in essence rounded up to get an even number for the interval between frames, which will be 1000/25 = 40 milliseconds.
  3. the number of seconds the animation should take
The number of pixels that each box moves per frame is the total distance traveled by a box divided by the total number of frames. The total number of frames is the product of the frames per second and the number of seconds in the animation. Here is the Java code from the animation class's constructor:
totalDist = 2*VERTICAL_DISTANCE + (endX - startX);
pixels_per_move = totalDist / (FRAMES_PER_SECOND * num_seconds);
If the boxes moved along a straight line, I could have simplified the code by calculating the total number of frames, and then calculating the fraction of the way to the finish by dividing the current frame number by the total frames. This corresponds to our class discussion of using a variable t that ranges from 0 to 1, and calculating the x and y positions as a function of t. In this case the box moves along a path that is defined by a piece-wise function, so it is easier to keep track of the number of pixels that a box should move in each frame.


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, the upBox, 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) {
        int tentativeTop = upBox.getTop() - pixels_per_move;
	if (tentativeTop > endY) {
	  upBox.setPosition(upBox.getLeft(), tentativeTop);
	  downBox.setPosition(downBox.getLeft(), 
			      downBox.getTop() + pixels_per_move);
	  repaint();
	  return;
	}
	else {
 	  // start moving to the right by pegging the top and
	  // bottom boxes to their maximum vertical extents and
	  // take any additional pixels and add them to the x
	  // direction
	  action = RIGHT;
	  slack = endY - tentativeTop; // the boxes must still move right this many pixels
	  upBox.setPosition(startX, startY - VERTICAL_DISTANCE);
	  downBox.setPosition(endX, startY + VERTICAL_DISTANCE);
        }
      }
Right Action: As box 1 moves to the right only its left variable needs to be modified. If there are leftover pixels from moving the box up, we must add the pixels to the x coordinate, to start moving the box right. As we move box 1 right, we need to ensure 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) {
	int tentativeUpLeft;
	int tentativeDownLeft;
        // we can perform both the UP and the RIGHT actions in the same step. If we
        // do, then the slack will be non-zero
	if (slack != 0) {
	  tentativeUpLeft = upBox.getLeft() + slack;
	  tentativeDownLeft = downBox.getLeft() - slack;
        }
        // only a RIGHT action is performed. Compute the new tentative X positions
	else {
	  tentativeUpLeft = upBox.getLeft() + pixels_per_move;
	  tentativeDownLeft = downBox.getLeft() - pixels_per_move;
        }
        // make sure we do not move past the ending X position
	if (tentativeUpLeft < endX) {
	  upBox.setPosition(tentativeUpLeft, upBox.getTop());
	  downBox.setPosition(tentativeDownLeft, downBox.getTop());
	  repaint();
	  return;
	}
	else { // we have reached the ending X position
 	  // peg the rectangles to their final Left positions and
	  // start them moving vertically to their final vertical
	  // positions
	  action = DOWN;
	  slack = tentativeUpLeft - endX; // the boxes must still move down by this many pixels
	  upBox.setPosition(endX, upBox.getTop());
	  downBox.setPosition(startX, downBox.getTop());
	}
      }
Down Action: As box 1 moves down only its Y position needs to be modified and this Y position will need to be increased. We must be careful to ensure that box 1 does not overshoot its final Y position, so we calculate a candidate Y position and then check whether this candidate Y position is less than the final Y position. When we have reached the final Y position, or exceeded it, we peg the boxes to their final Y positions and we stop the timer because the animation is complete. Here is the code for the down case:
      if (action == DOWN) {
 	int tentativeTop;
	int tentativeBottom;
        // calculate a candidate Y position
        // if the slack is zero, then we are only moving down so add the pixels_per_move
        // to the current Y position to get the candidate Y position
	if (slack == 0) {
	    tentativeTop = upBox.getTop() + pixels_per_move;
	    tentativeBottom = downBox.getTop() - pixels_per_move;
	}
        // we can move both right and down in the same frame. If the slack is non-zero,
        // then we moved right and we want to move down with however many pixels are left
	else {
	    tentativeTop = upBox.getTop() + slack;
	    tentativeBottom = downBox.getTop() - slack;
        }
        // if the candidate Y position is still above the final Y position, set
        // the upBox to the new Y position and do likewise with the downBox.
	if (tentativeTop < startY) {
	  upBox.setPosition(upBox.getLeft(), tentativeTop);
	  downBox.setPosition(downBox.getLeft(), tentativeBottom);
	  repaint();
	  return;
	}
	else {
	  // we are done--peg the boxes to their new vertical positions and stop the timer
	  upBox.setPosition(upBox.getLeft(), startY);
	  downBox.setPosition(downBox.getLeft(), startY);
	  repaint();
	  animationTimer.stop();
	}
      }
Finally, we can create the animation by creating an instance of a Swing Timer and setting its frequency to the frame interval (40ms). 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(FRAME_INTERVAL, taskPerformer);
addMouseListener(new MouseAdapter() {
	public void mouseClicked(MouseEvent me) {
	    animationTimer.start();
	}
      });