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 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.
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.
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.
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();
}
});