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.AnimationDemoThe 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:
AnimationDemo
: Sets up the demo and establishes the timers for the clocks
Clock
: Draws a pie-shaped arc depending on the current "hour".
animation
: Animates the motion of the "1" and the "6" boxes in the array and handles the timer
for the animation.
LabeledBox
: The labeled boxes used in the array animation.
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.
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.
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 moveThe 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:
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.
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(); } });