JTF > Tutorial > Animation and Interactivity

Chapter 3
Animation and Interactivity


Even though the programs in Chapter 2 offer a reasonably complete survey to the classes in the acm.graphics package, they do so using examples that are entirely static. Running those programs causes a picture to appear in its final form. For students to get excited about graphics, it is essential to add animation so that the pictures evolve as the program runs and interactivity to give the user control over the program. This chapter introduces several strategies for implementing each of those capabilities.

  As with almost every programming task, however, there are many different ways to animate a program or to get it to respond to mouse events. Some instructors will strongly prefer one style, while others will argue equally strongly for a different approach. To reach the widest possible audience, the Java Task Force chose to support several of the most popular styles and allow individual instructors to make their own choices.

  Although the decision to support multiple styles seems appropriate in terms of the overall package design, it carries with it some pedagogical risks. Giving students several options for accomplishing the same task often confuses them to the point that they learn none of the strategies well. In general, it is more successful to teach one approach in detail, bringing up the possibility of alternative strategies only when students have mastered a particular approach. To avoid the same pitfalls for readers of this tutorial, we have chosen to foreground one strategy for animation and one for mouse-based interaction and to place the discussion of alternative strategies in an optional section. As you read this chapter for the first time, it probably makes sense to focus on sections 3.1 and 3.2, leaving the discussion of alternative strategies in section 3.3 for a subsequent rereading.

3.1 Graphical animation

In computer graphics, the process of updating a graphical display so that it changes over time is called animation. Implementing animation typically involves displaying an initial version of the picture and then changing it slightly over time so that the individual changes appear continuous from one version of the picture to the next. This strategy is analogous to classical film animation in which cartoonists break up the motion of the scene into a series of separate frames. The difference in time between each frame is called a time step and is typically very short. Movies, for example, typically run at 30 frames a second, which makes the time step approximately 33 milliseconds. If you want to obtain smooth motion in Java, you need to use a time step around this scale or even faster.

A simple example of animation

The easiest way to animate graphical programs is to include a loop in your run method that updates the picture from one frame to the next and then pauses for the duration of the time step. An example of this style of animation appears in Figure 3-1, which moves a GLabel across the screen from right to left, just the way the headline displays in New York’s Times Square do.

Figure 3-1. Code to move text across the screen [applet]
/*
 * File: TimesSquare.java
 * ----------------------
 * This program displays the text of the string HEADLINE on the
 * screen in an animated way that moves it across the display
 * from left to right.
 */
 
import acm.graphics.*;
import acm.program.*;
 
public class TimesSquare extends GraphicsProgram {
 
/** Runs the program */
   public void run() {
      GLabel label = new GLabel(HEADLINE);
      label.setFont("Serif-72");
      add(label, getWidth(), (getHeight() + label.getAscent()) / 2);
      while (label.getX() + label.getWidth() > 0) {
         label.move(-DELTA_X, 0);
         pause(PAUSE_TIME);
      }
   }
 
/* The number of pixels to shift the label on each cycle */
   private static final int DELTA_X = 2;
 
/* The number of milliseconds to pause on each cycle */
   private static final int PAUSE_TIME = 20;
 
/* The string to use as the value of the label */
   private static final String HEADLINE =
     "When in the course of human events it becomes necessary " +
     "for one people to dissolve the political bands which " +
     "connected them with another . . .";
 
} 

  The TimesSquare program in Figure 3-1 begins by creating a GLabel object and positioning it so that it is centered vertically in the window. Its starting point in the horizontal dimension, however, is just at the right edge of the canvas, which means that the entire label is outside the visible area of the canvas. The animation is accomplished by the following lines:

 

while (label.getX() + label.getWidth() > 0) {
   label.move(-DELTA_X, 0);
   pause(PAUSE_TIME);
}

This code loops until the label has moved entirely past the left edge of the display, shifting it DELTA_X pixels to the left on every time step. The call to pause(PAUSE_TIME) inside the loop causes the program to suspend operation for PAUSE_TIME milliseconds. This call is necessary to achieve the effect of animation, because computers run so quickly that the label would instantly zip off the left side of the window if you didn’t slow things down.

Bouncing a ball

A slightly more sophisticated application of animation appears in Figure 3-2. This program bounces a ball around the walls of the graphics window and forms the foundation for such classic video games as Pong or Breakout. Because a static picture in this text would offer little insight into how such an animated program works, it is useful to run this as an applet. If you are reading this tutorial on the JTF web site, you can bring up the applet in a separate window by clicking on the applet marker in the caption, but you can also run any of the applets from the demo site at

http://www.acm.org/jtf/demos/index.html

Figure 3-2. Program to bounce a ball off the boundaries of the canvas [applet]
/*
 * File: BouncingBall.java
 * -----------------------
 * This file implements a simple bouncing ball using the run method
 * to drive the animation.
 */
 
import acm.graphics.*;
import acm.program.*;
 
public class BouncingBall extends GraphicsProgram {
 
/** Initialize the ball and its velocity components */
   public void init() {
      ball = new GBall(BALL_RADIUS);
      add(ball, getWidth() / 2, getHeight() / 2);
      dx = 2;
      dy = 1;
   }
 
/** Run forever bouncing the ball */
   public void run() {
      waitForClick();
      while (true) {
         advanceOneTimeStep();
         pause(PAUSE_TIME);
      }
   }
 
/* Check for bounces and advance the ball */
   private void advanceOneTimeStep() {
      double bx = ball.getX();
      double by = ball.getY();
      if (bx < BALL_RADIUS || bx > getWidth() - BALL_RADIUS) dx = -dx;
      if (by < BALL_RADIUS || by > getHeight() - BALL_RADIUS) dy = -dy;
      ball.move(dx, dy);
   }
 
/* Private constants */
   private static final double BALL_RADIUS = 10;
   private static final int PAUSE_TIME = 20;
 
/* Private instance variables */
   private GBall ball;     /* The ball object                   */
   private double dx;      /* Velocity delta in the x direction */
   private double dy;      /* Velocity delta in the y direction */

  The code in Figure 3-2 uses the GBall class presented in Figure 2-18 to create a ball whose reference point is at the center. Doing so makes the geometric calculation simpler when checking whether a bounce occurs because all four edges can be treated symmetrically. The program code is also divided between the init method, which creates the ball and adds it to the window, and the run method, which runs the animation. The code for the run method is

 

public void run() {
   waitForClick();
   while (true) {
      advanceOneTimeStep();
      pause(PAUSE_TIME);
   }
}

which is almost precisely the paradigmatic for an animation loop. The new statement is the call to the waitForClick method, which is implemented by GraphicsProgram and suspends the program until a mouse click occurs in the graphics canvas. This call means that the program does not start up immediately, but instead waits for a mouse click before proceeding.

  The code that implements the underlying physics of the animation appears in the private method advanceOneTimeStep. This method checks to see whether the ball has reached one of the edges of the canvas, in which case it changes the sign of the appropriate component of the ball’s velocity, which is stored in the variables dx and dy. It then moves the ball by those displacements to update its position on the display.

Simulating randomness in animations

As written, the bouncing ball program from the preceding section is altogether too predictable. The ball begins with a constant velocity and then makes perfectly reflective bounces off the edges of the canvas, tracing the same trajectory each time. Many animated programs will involve some kind of random behavior, and students will quickly want to know how they can implement random processes in their own code.

Although it is certainly possible to use either the Math.random method or the Random class in java.util for this purpose, there are a couple of pedagogical advantages to using the RandomGenerator class in the acm.util package instead:

  1. The name of the class emphasizes that a RandomGenerator object is a generator for random values and not a random value in itself. When students use the Random class, they are much more likely to create a new Random instance for each value they wish to generate.
  2. The RandomGenerator class offers several additional methods that are often much easier to use than those in the base class. These extended methods are listed in Figure 3-5.

Figure 3-5. Useful methods in the RandomGenerator class
 Factory method
 
static RandomGenerator getInstance()
  Returns a standard random generator.

 Methods inherited from the Random class in java.util
 
int nextInt(int n)
  Returns a random integer chosen from the n values in the range 0 to n - 1, inclusive.
double nextDouble()
  Returns a random double d in the range 0 ² d< 1.
void nextBoolean()
  Returns a random boolean that is true approximately 50% of the time.
void setSeed(long seed)
  Sets a “seed” to indicate a starting point for the pseudorandom sequence.

 Additional methods defined by RandomGenerator
 
int nextInt(int low, int high)
  Returns a random integer in the specified range (inclusive).
double nextDouble(double low, double high)
  Returns a random double in the specified range.
boolean nextBoolean(double p)
  Returns a random boolean that is true with probability p (0 = never, 1 = always).
Color nextColor()
  Returns a random opaque color.
   

  The conventional pattern for using the RandomGenerator class is to declare and initialize an instance variable to hold the generator using the line

 

private RandomGenerator rgen = RandomInteger.getInstance();

Once this declaration is made, every method in this class can then generate new random values by invoking the appropriate method on the rgen variable. For example, you could use this strategy in the BouncingBall program to initialize each velocity component of the ball to a random value between –3 and 3:

 

dx = rgen.nextDouble(-3, 3);
dy = rgen.nextDouble(-3, 3);

  The RandomShapes program in Figure 3-6 makes more extensive use of the facilities of the RandomGenerator class. The program generates ten shapes and positions them on the canvas using randomness in each of the following ways:

A sample run of the RandomShapes program might look like this:

  Most of RandomShapes program in Figure 3-6 is reasonably straightforward, but there are nonetheless a few aspects of the code that are easier to understand with some additional explanation:

  The RandomGenerator class from the java.util class has applications in a wide variety of contexts beyond graphical animation. In our experience it is far and away the most widely used class in the java.util package.

Figure 3-6. Program to generate random shapes [applet]
/*
 * File: RandomShapes.java
 * -----------------------
 * This file creates ten boxes, ovals, and stars at random locations
 * on the screen, pausing for a suitable interval between each one.
 */
 
import acm.graphics.*;
import acm.program.*;
import acm.util.*;
 
public class RandomShapes extends GraphicsProgram {
 
/** Runs the program */
   public void run() {
      while (true) {
         for (int i = 0; i < NOBJECTS; i++) {
            addOneRandomShape();
            pause(PAUSE_TIME);
         }
         waitForClick();
         removeAll();
      }
   }
 
/* Adds a random shape to the canvas */
   private void addOneRandomShape() {
      GObject gobj = createRandomShape();
      gobj.setColor(rgen.nextColor());
      if (gobj instanceof GFillable) ((GFillable) gobj).setFilled(true);
      double x = rgen.nextDouble(0, getWidth() - gobj.getWidth())
                   - gobj.getBounds().getX();
      double y = rgen.nextDouble(0, getHeight() - gobj.getHeight())
                   - gobj.getBounds().getY();
      add(gobj, x, y);
   }
 
/* Generates a random shape whose reference point is the origin */
   private GObject createRandomShape() {
      double width = rgen.nextDouble(MIN_SIZE, MAX_SIZE);
      double height = rgen.nextDouble(MIN_SIZE, MAX_SIZE);
      switch (rgen.nextInt(3)) {
        case 0: return new GRect(width, height);
        case 1: return new GOval(width, height);
        case 2: return new GStar(width);
        default: throw new ErrorException("Illegal shape index");
      }
   }
 
/* Private constants */
   private static final int NOBJECTS = 10;
   private static final int PAUSE_TIME = 1000;
   private static final double MIN_SIZE = 25;
   private static final double MAX_SIZE = 150;
 
/* Private instance variables */
   private RandomGenerator rgen = RandomInteger.getInstance();
} 

3.2 Interactivity

The animation capability presented in section 3.1 certainly helps to make graphical programs more exciting, but it is not in itself sufficient to implement the kind of interactive graphical applications that today’s students have come to expect. Interactive programs must also respond to actions taken by the user. The sections that follow outline the Java event model and describe one strategy for responding to those events. Several other paradigms for event handling are described in section 3.3.

The Java event model

Programs like Add2Console that request input from the user are interactive programs of a sort. Console programs, however, ask the user for input only at certain well-defined points in the program’s execution history when the program makes an explicit call to an input method like readInt. This style of interaction is called synchronous, because it is always in sync with the program operation. Modern user interfaces, however, are asynchronous in that they allow the user to intercede at any point, typically by using the mouse or the keyboard to trigger a particular action.

  Events that occur asynchronously with respect to the program operation—mouse clicks, key strokes, and the like—are represented using a structure called an event. When an event occurs, the response is always the invocation of a method in some object that is waiting to hear about that event. Such an object is called a listener. In Java, objects that listen for user-interface events do so by implementing the methods in a specific listener interface, which is typically defined in the package java.awt.event. This package contains several interfaces that allow clients to respond to mouse clicks, button presses, keystrokes, changes in component sizes, and other asynchronous events. The examples in the next several examples concentrate on the interfaces that define how programs respond to mouse events, which are described in the following section.

Responding to mouse events

The java.awt.event package defines two separate interfaces—MouseListener and MouseMotionListener—that specify how a program responds to mouse events. The MouseListener methods are called in response to actions that occur relatively infrequently, such as pressing a mouse button or moving the mouse entirely outside the boundary in which the listener is active. The MouseMotionListener methods are called whenever the mouse moves, which happens much more frequently. Moving the mouse without pressing the button results in calls to mouseMoved; dragging the mouse with the button down results in calls to mouseDragged. The methods in each interface are listed in Figure 3-3.

Figure 3-3. Methods in the MouseListener and MouseMotionListener interfaces
 The MouseListener interface
 
void mousePressed(MouseEvent e)
  Called whenever the mouse button is pressed.
void mouseReleased(MouseEvent e)
  Called whenever the mouse button is released.
void mouseClicked(MouseEvent e)
  Called when the mouse button is “clicked” (pressed and released within a short span of time).
void mouseEntered(MouseEvent e)
  Called whenever the mouse enters the canvas.
void mouseExited(MouseEvent e)
  Called whenever the mouse exits the canvas.

 The MouseMotionListener interface
 
void mouseMoved(MouseEvent e)
  Called whenever the mouse is moved with the button up.
void mouseDragged(MouseEvent e)
  Called whenever the mouse is moved with the button down.

  Each of the methods listed in Figure 3-3 takes as its argument an object of type MouseEvent, which is defined in the package java.awt.event, just as the listener interfaces are. The MouseEvent class includes a rich set of methods for designing sophisticated user interfaces. For most applications, however, you can get away with using only two of those methods. Given a MouseEvent stored in a variable named e, you can determine the location at which the mouse even occurred by calling e.getX() and e.getY().

  The GraphicsProgram class declares itself to be both a MouseListener and a MouseMotionListener by defining implementations for each of the listener methods in those interfaces. Those implementations, however, do nothing at all. For example, the default definition of mouseClicked is simply

 

public void mouseClicked(MouseEvent e) {
   /* Empty */
}

Thus, unless you override the definition of mouseClicked in your GraphicsProgram subclass, it will simply ignore mouse clicks, just as it ignores all the other mouse events. If, however, you define a new mouseClicked method, the event handling system will call your version instead of the empty one. Because any methods that you don’t override continue to do what they did by default (i.e., nothing), you only have to override the listener methods you need.

  Whenever you write event-handling code in Java, it is important to remember that defining the listener methods is not sufficient in itself to establish the listener relationship. You also need to make sure that the object that is listening for events adds itself as a listener to the object that is generating the events. In the case of a GraphicsProgram, the program is doing the listening, and the embedded GCanvas is generating the events. You therefore need to have the program register its interest in events generated by the canvas by executing the following lines in the context of the program:

 

getCanvas().addMouseListener(this);
getCanvas().addMouseMotionListener(this);

To make this operation just a little bit simpler—and to avoid having to explain the getCanvas method and the keyword this—the GraphicsProgram class includes a method addMouseListeners that performs precisely these two steps. The examples in the subsections that follow make use of this simplified form.

A line-drawing program

The first example of mouse interaction is a simple line-drawing program that operates—at least for straight lines—in the way that painting programs like Adobe Illustratorª does. To create a line on the canvas, you press the mouse at its starting point. From there, you hold the mouse button down and drag it to the other endpoint. As you do so, the line keeps itself updated on the canvas so that it connects the starting point with the current position of the mouse.

  As an example, suppose that you press the mouse button somewhere on the screen and then drag it rightward an inch, holding the button down. What you’d like to see is the following picture:

If you then move the mouse downward without releasing the button, the displayed line will track the mouse, so that you might see the following picture:

When you release the mouse, the line stays where it is, and you can go ahead and draw additional lines using the same sequence of operations.

  Because the line joining the initial point and the mouse stretches and contracts as you move the mouse, this technique is called rubber-banding. The code for a line-drawing program that uses rubber-banding appears in Figure 3-4. Despite the fact that the program seems to perform a reasonably interesting task, the code is surprisingly short. The bodies of the three methods in the program contain a grand total of four lines. Even so, it is worth going through each of the methods in turn.

Figure 3-4. Program to create a line drawing on the screen [applet]
/*
 * File: DrawLine.java
 * -------------------
 * This program allows users to create lines on the graphics
 * canvas by clicking and dragging with the mouse.  The line
 * is redrawn from the original point to the new endpoint, which
 * makes it look as if it is connected with a rubber band.
 */
 
import acm.graphics.*;
import acm.program.*;
import java.awt.event.*;
 
/** This class allows users to draw lines on the canvas */
public class DrawLine extends GraphicsProgram {
 
/** Initializes the program by enabling the mouse listeners */
   public void init() {
      addMouseListeners();
   }
 
/** Called on mouse press to create a new line */
   public void mousePressed(MouseEvent e) {
      line = new GLine(e.getX(), e.getY(), e.getX(), e.getY());
      add(line);
   }
 
/** Called on mouse drag to reset the endpoint */
   public void mouseDragged(MouseEvent e) {
      line.setEndPoint(e.getX(), e.getY());
   }
 
/* Private instance variables */
   private GLine line;
 
} 

  The first thing to notice is that this program contains an init method rather than a run method. In this particular case, you could call the method by either name and have the program run in exactly the same way, but you will soon encounter situations in which you need to be clear about the role of these two methods. Even though both are called as part of the program startup process, the two methods serve different conceptual purposes. The init method is intended for startup operations that are executed before the program starts; the run method is executed as part of the program operation. In the examples of animation earlier in the chapter, the run method implemented the animation. In this program, nothing is actually running after the program starts up. The only time things happen is when the user presses the mouse button and begins to drag it across the screen. Such programs are said to be event-driven. Event-driven programs tend to operate by performing some amount of initialization and then waiting for events to occur. In this case, the only initialization necessary is to enable the program as a listener for events, which is accomplished through the call to addMouseListeners.

  The mousePressed method is called whenever the user presses the mouse button and overrides the empty definition implemented by the GraphicsProgram class itself. In the line-drawing program, the body of the mousePressed method simply creates a new GLine object that starts and ends at the current mouse position. This GLine appears on the canvas as a dot.

  The GLine is stored in the private instance variable line, which means that other methods in the class have access to it. In particular, dragging the mouse with the button down calls the mouseDragged method, which resets the endpoint of the line to the current mouse position.

Dragging objects on the canvas

The second example is a bit more sophisticated but still fits easily on a single page. The DragObjects program in Figure 3-7 illustrates how to use mouse listeners to support dragging graphical objects around on the canvas. The code in the init method should seem familiar, given that its effect is to create two graphical objects—the red rectangle and the green oval from the FeltBoard program in Chapter 2—and then add them to the canvas.

Figure 3-7. Object-dragging program using the addMouseListeners method [applet]
/*
 * File: DragObjects.java
 * ----------------------
 * This implementation illustrates the technique of using the
 * addMouseListeners method to register the program itself as
 * a listeners for events in the underlying GCanvas.
 */
 
import java.awt.*;
import java.awt.event.*;
import acm.graphics.*;
import acm.program.*;
 
/** This class displays a mouse-draggable rectangle and oval */
public class DragObjects extends GraphicsProgram {
 
/** Initializes the program */
   public void init() {
      GRect rect = new GRect(100, 100, 150, 100);
      rect.setFilled(true);
      rect.setColor(Color.RED);
      add(rect);
      GOval oval = new GOval(300, 115, 100, 70);
      oval.setFilled(true);
      oval.setColor(Color.GREEN);
      add(oval);
      addMouseListeners();
   }
 
/** Called on mouse press to record the coordinates of the click */
   public void mousePressed(MouseEvent e) {
      last = new GPoint(e.getPoint());
      gobj = getElementAt(last);
   }
 
/** Called on mouse drag to reposition the object */
   public void mouseDragged(MouseEvent e) {
      if (gobj != null) {
         gobj.move(e.getX() - last.getX(), e.getY() - last.getY());
         last = new GPoint(e.getPoint());
      }
   }
 
/** Called on mouse click to move this object to the front */
   public void mouseClicked(MouseEvent e) {
      if (gobj != null) gobj.sendToFront();
   }
 
/* Private instance variables */
   private GObject gobj;           /* The object being dragged */
   private GPoint last;            /* The last mouse position  */

  The code in Figure 3-7 overrides three of the event methods. The first of these is mousePressed, which is called when the mouse button first goes down. That method looks like this:

 

public void mousePressed(MouseEvent e) {
   lastX = e.getX();
   lastY = e.getY();
   gobj = getElementAt(lastX, lastY);
}

The first two statements simply record the x and y coordinates of the mouse in the instance variables lastX and lastY. The final statement in mousePressed checks to see what object on the canvas contains the current mouse position. Here, it is important to recognize that there are two possibilities. First, you could be pressing the mouse button on top of an object, which means that you want to start dragging it. Second, you could be pressing the mouse button somewhere else on the canvas where there is no object to drag. The getElementAt method looks at the specified position and returns the object it finds there. If there is more than one object covering that space, it chooses the one that is in front of the others in the stacking order. If there are no objects at all at the specified location, getElementAt returns the value null.

  The mouseDragged method consists of the following code:

 

public void mouseDragged(MouseEvent e) {
   if (gobj != null) {
      gobj.move(e.getX() - lastX, e.getY() - lastY);
      lastX = e.getX();
      lastY = e.getY();
   }
}

The if statement simply checks to see whether there is an object to drag. If the value of gobj is null, no object is being dragged, so the rest of the method is skipped. If an object has been selected by a previous call to mousePressed, the mouseDragged method needs to move that object by some displacement in each direction. That displacement, however, does not depend on the absolute location of the mouse but rather in how far it has moved from the point at which you last updated the location of the object. Thus, the arguments to the move method are—for both the x and y components—the location where the mouse is now minus where it used to be. Once you have updated the location of the object being dragged, you have to record the mouse coordinates again so that the location will update correctly on the next mouseDragged call.

  The final listener method specified in Figure 3-7 is mouseClicked, which looks like this:

 

public void mouseClicked(MouseEvent e) {
   if (gobj != null) gobj.sendToFront();
}

The effect of this method is to allow the user to move an object to the front by clicking on it, thereby bringing it out from under the other objects on the canvas. The only subtlety in this method is the question of whether it is appropriate to rely on the proper initialization of the variable gobj, which holds the current object. As it happens, the mouseClicked event is always generated in conjunction with a mousePressed and a mouseReleased event, both of which precede the mouseClicked event. The gobj variable is therefore set by mousePressed, just as if you were going to drag it.

3.3 Alternative strategies for animation and interactivity (optional)

One of the interesting discoveries that we made during the period of review and comment on the intermediate Java Task Force designs was that people teaching introductory programming courses have strongly held beliefs about how Java programs should be coded and how those programs should be presented to students. To the extent that our approach differs from the style that someone has grown accustomed to, our designs are often seen as being contrary to the spirit of Java—at least in that person’s mind. Unfortunately, those reactions did not point in a single direction because those strongly held views diverge widely. For example, some people argue that the only appropriate way to declare a listener method is to use an anonymous inner class, while others have held that exposing Java’s listener mechanism at all will be too confusing for students.

  From these reactions, it became clear that the Java Task Force packages had to support multiple coding styles and allow individual instructors to choose the strategy that seems most closely aligned with their overall pedagogical approach. The purpose of this section is to describe several different approaches to animation and interactivity so that you can have a better sense of the range of options. Those alternative strategies are illustrated by recoding two of the example programs presented earlier in this chapter—the BouncingBall program from Figure 3-2 and the DragObjects program from Figure 3-7—using several different strategies. The code for each of these version is available on the JTF website.

  Although we believe that it is important for the Java Task Force packages to support a range of coding strategies, it is probably not a good idea to try to cover all of these strategies in an introductory course. Many students find that having multiple strategies to accomplish the same task is more confusing than liberating. Thus, it is probably best to choose a particular approach to animation or event handling and then stick with that model until students gain enough experience to appreciate the strengths and weaknesses of the alternative styles.

Alternative strategies for implementing animation

The animated applications in section 3.1 use the run defined in the Program class to drive the animation. The task of dividing the animation into discrete time steps is accomplished by making periodic calls to pause. Because the run method runs in a thread of its own, calling pause does not disable system tasks that run, for example, on Java’s event-handling thread. The remainder of this section describes two alternative animation strategies using the BouncingBall program from Figure 3-2 as a common point of departure.

  The first of these strategies involves giving the ball a thread of its own. The pedagogical foundation for this approach lies in the belief that students of modern programming need to learn about concurrency at a much earlier stage. Giving the ball its own thread makes it easy to see the ball as an active entity in a concurrent world. In this conceptual model, the ball is moving of its own accord rather than being moved by the program.

  One possible implementation of this strategy appears in Figures 3-8 and 3-9. Figure 3-8 shows the main program, but all the real work takes place in the RunnableGBall class shown in Figure 3-9, which extends the GBall class. The RunnableGBall class implements Java’s Runnable interface so that it can serve as the basis for an independent thread of control. The code for that run method has the same steps as in the original implementation:

 

public void run() {
   while (true) {
      advanceOneTimeStep();
      pause(PAUSE_TIME);
   }
}

Figure 3-8. Ball bouncing program using a separate thread
/*
 * File: BouncingBallUsingThreads.java
 * -----------------------------------
 * This file implements a simple bouncing ball by creating
 * a RunnableBall class and executing it in its own thread.
 */
 
import acm.graphics.*;
import acm.program.*;
 
public class BouncingBallUsingThreads extends GraphicsProgram {
 
/** Initialize the ball and its velocity components */
   public void init() {
      ball = new RunnableGBall(BALL_RADIUS);
      ball.setEnclosureSize(getWidth(), getHeight());
      ball.setVelocity(2, 1);
      add(ball, getWidth() / 2, getHeight() / 2);
   }
 
/** Create a thread to bounce the ball */
   public void run() {
      waitForClick();
      new Thread(ball).start();
   }
 
/* Private constants */
   private static final double BALL_RADIUS = 10;
 
/* Private instance variables */
   private RunnableGBall ball;
 
} 

Figure 3-9. The RunnableGBall class
/*
 * File: RunnableGBall.java
 * ------------------------
 * This file defines an extension to the GBall class that is
 * designed to run as a separate thread of control.
 */
 
import acm.graphics.*;
 
public class RunnableGBall extends GBall implements Runnable {
 
/** Creates a new ball centered at the origin */
   public RunnableGBall(double r) {
      super(r);
   }
 
/** Sets the size of the enclosure */
   public void setEnclosureSize(double width, double height) {
      enclosureWidth = width;
      enclosureHeight = height;
   }
 
/** Sets the velocity of the ball */
   public void setVelocity(double vx, double vy) {
      dx = vx;
      dy = vy;
   }
 
/** Run forever bouncing the ball */
   public void run() {
      while (true) {
         advanceOneTimeStep();
         pause(PAUSE_TIME);
      }
   }
 
/* Check for bounces and advance the ball */
   private void advanceOneTimeStep() {
      double bx = getX();
      double by = getY();
      double r = getWidth() / 2;
      if (bx < r || bx > enclosureWidth - r) dx = -dx;
      if (by < r || by > enclosureHeight - r) dy = -dy;
      move(dx, dy);
   }
 
/* Private constants */
   private static final int PAUSE_TIME = 20;
 
/* Private instance variables */
   private double enclosureWidth;
   private double enclosureHeight;
   private double dx;
   private double dy;
 
} 

In this case, however, the thread that executes this method is associated with the ball as opposed to being part of the main program. All the BouncingBallUsingThreads program does on its own behalf is to create the runnable ball, initialize various properties such as the speed and dimensions of the boundary enclosure, and then start up a separate thread for the ball by calling

 

new Thread(ball).start();

  At first glance, it would seem that this strategy is better for applications in which there is more than one animated object. Given that any RunnableBall object can have a thread of its own, it would be simple to create a second ball, add that to the canvas, and start it running as well. As it happens, however, that strategy is difficult to manage because there is no way to ensure that the balls move at the same rate. The pause method is only approximate in its timing. Depending on the system load, it would be possible for one ball to advance through several time steps before the other had a chance to move at all. To avoid this problem, it is often preferable to have a single animation thread that updates the position of all moving objects during each time step.

  The second alternative strategy for animation abandons the idea of pausing a thread altogether. The code for the BouncingBallUsingTimerCode in Figure 3-10 uses Swing’s Timer class to alert the main program at regular intervals. When the timer goes off, the program can advance the ball’s position by one time step. Although the idea behind this strategy is simple enough, a couple of aspects of the code are worth noting:

Figure 3-10. Ball bouncing program using timer events
/*
 * File: BouncingBallUsingTimer.java
 * ---------------------------------
 * This file implements a simple bouncing ball using a Timer to
 * implement the animation.
 */
 
import acm.graphics.*;
import acm.program.*;
import acm.util.*;
import java.awt.event.*;
 
public class BouncingBallUsingTimer extends GraphicsProgram {

/** Initialize the ball and its velocity components */
   public void init() {
      ball = new GBall(BALL_RADIUS);
      add(ball, getWidth() / 2, getHeight() / 2);
      dx = 2;
      dy = 1;
   }
 
/** Create a timer to advance the ball */
   public void run() {
      waitForClick();
      ActionListener listener = new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            advanceOneTimeStep();
         }
      };
      SwingTimer timer = new SwingTimer(TIMER_RATE, listener);
      timer.start();
   }
 
/* Check for bounces and advance the ball */
   private void advanceOneTimeStep() {
      double bx = ball.getX();
      double by = ball.getY();
      if (bx < BALL_RADIUS || bx > getWidth() - BALL_RADIUS) dx = -dx;
      if (by < BALL_RADIUS || by > getHeight() - BALL_RADIUS) dy = -dy;
      ball.move(dx, dy);
   }
 
/* Private constants */
   private static final double BALL_RADIUS = 10;
   private static final int TIMER_RATE = 20;
 
/* Private instance variables */
   private GBall ball;
   private double dx;
   private double dy;
} 

Alternative strategies for responding to mouse events

Just as there is more than one way to implement animation, there are also multiple approaches that one can take to respond to mouse events. In addition to the strategy of calling the addMouseListeners method to register the program itself as a listener, the Java Task Force packages support several additional coding styles, each of which has its own strengths and weaknesses. The next few paragraphs describe three additional approaches in the context of the DragObjects example from Figure 3-7

  The DragUsingInnerClasses program shown in Figure 3-11 offers the most straightforward rewrite of the original version. The only change is that the mouse listeners are now supplied using anonymous inner classes instead of having the program itself assume that role. The advantage of this structure is that it corresponds most closely to the style that has become standard in the Java community. The disadvantage is the additional conceptual overhead involved in presenting inner classes to students. In a way, the situation is even more problematic here than it was in the case of the BouncingBallUsingTimer program presented in the preceding section. In that model, it was possible to use ActionListener as the base class for the listener, because the one method the interface specifies is defined in the body of the inner class. In the object-dragging example, the base classes need to be MouseAdapter and MouseMotionAdapter to ensure that all the methods in the corresponding interfaces are defined.

  The DragUsingGObjectEvents program in Figure 3-12 offers a model that initially seems similar to the original implementation but that actually represents an important change in point of view. In this implementation, the listeners are attached to the individual GObjects and not to the canvas. When a mouse event occurs in the screen area of a GObject, the code for the acm.graphics package generates mouse events that use the GObject itself as the source of the event and which are then forwarded to any listeners registered for that object. The advantage here is that the model supports the notion that objects are active entities that can both generate and accept messages from other objects. The disadvantage lies in the fact that many applications will also need to assign a listener to the canvas to respond to events that occur outside the context of any of the graphical objects currently being displayed. If the canvas listener is required in any case, it seems easiest to use it for all event handling rather than to adopt two separate models.

  The final version of the object-dragging program appears in Figure 3-13. This strategy is derived from the objectdraw package developed at Williams College and uses a simpler model in which the acm.graphics code forwards events to a set of specialized event handlers defined specifically for this purpose. If a GraphicsProgram subclass defines any of the methods

 

mousePressed(GPoint pt)
mouseReleased(GPoint pt)
mouseClicked(GPoint pt)
mouseMoved(GPoint pt)
mouseDragged(GPoint pt)

then that method is called whenever the appropriate event occurs in the GCanvas. The parameter pt in each of these methods is the point at which the mouse event occurred, already translated into the real-valued coordinate space of the acm.graphics package. This model completely hides the details of mouse events and mouse listeners, so that the student need not, for example, import the java.awt.event package or take any special steps to register the program as a listener. All of that comes for free. The primary disadvantage is that students who learn this strategy for event handling will have to learn how standard Java listeners work at some later point.

Figure 3-11. Object-dragging program using inner classes to specify the listeners
/*
 * File: DragUsingInnerClasses.java
 * --------------------------------
 * This implementation illustrates the technique of defining
 * listeners as anonymous inner classes.
 */
 
import java.awt.*;
import java.awt.event.*;
import acm.graphics.*;
import acm.program.*;
 
/** This class displays a mouse-draggable rectangle and oval */
public class DragUsingInnerClasses extends GraphicsProgram {
 
/** Initializes the program */
   public void init() {
      GRect rect = new GRect(100, 100, 150, 100);
      rect.setFilled(true);
      rect.setColor(Color.RED);
      add(rect);
      GOval oval = new GOval(300, 115, 100, 70);
      oval.setFilled(true);
      oval.setColor(Color.GREEN);
      add(oval);
      GCanvas canvas = getGCanvas();
      canvas.addMouseListener(new MouseAdapter() {
         public void mousePressed(MouseEvent e) {
            last = new GPoint(e.getPoint());
            gobj = getElementAt(last);
         }

         public void mouseClicked(MouseEvent e) {
            if (gobj != null) gobj.sendToFront();
         }
      });
      canvas.addMouseMotionListener(new MouseMotionAdapter() {
         public void mouseDragged(MouseEvent e) {
            if (gobj != null) {
               gobj.move(e.getX() - last.getX(),
                         e.getY() - last.getY());
               last = new GPoint(e.getPoint());
            }
         }
      });
   }
 
/* Private instance variables */
   private GObject gobj;           /* The object being dragged */
   private GPoint last;            /* The last mouse position  */

Figure 3-12. Object-dragging program that listens to the GObjects
/*
 * File: DragUsingGObjectEvents.java
 * ---------------------------------
 * This implementation illustrates the technique of assigning
 * listeners to GObjects.
 */
 
import java.awt.*;
import java.awt.event.*;
import acm.graphics.*;
import acm.program.*;
 
/** This class displays a mouse-draggable rectangle and oval */
public class DragUsingGObjectEvents extends GraphicsProgram {
 
/** Initializes the program */
   public void init() {
      GRect rect = new GRect(100, 100, 150, 100);
      rect.setFilled(true);
      rect.setColor(Color.RED);
      rect.addMouseListener(this);
      rect.addMouseMotionListener(this);
      add(rect);
      GOval oval = new GOval(300, 115, 100, 70);
      oval.setFilled(true);
      oval.setColor(Color.GREEN);
      oval.addMouseListener(this);
      oval.addMouseMotionListener(this);
      add(oval);
   }
 
/** Called on mouse press to record the coordinates of the click */
   public void mousePressed(MouseEvent e) {
      last = new GPoint(e.getPoint());
   }
 
/** Called on mouse drag to reposition the object */
   public void mouseDragged(MouseEvent e) {
      GObject gobj = (GObject) e.getSource();
      gobj.move(e.getX() - last.getX(), e.getY() - last.getY());
      last = new GPoint(e.getPoint());
   }
 
/** Called on mouse click to move this object to the front */
   public void mouseClicked(MouseEvent e) {
      GObject gobj = (GObject) e.getSource();
      gobj.sendToFront();
   }
 
/* Private instance variables */
   private GPoint last;            /* The last mouse position */

Figure 3-13. Object-dragging program using callback methods in the style of objectdraw
/*
 * File: DragUsingObjectDrawModel.java
 * -----------------------------------
 * This implementation illustrates the technique of using callback
 * methods in the style of the objectdraw package.
 */
 
import java.awt.*;
import acm.graphics.*;
import acm.program.*;
 
/** This class displays a mouse-draggable rectangle and oval */
public class DragUsingObjectDrawModel extends GraphicsProgram {
 
/** Initializes the program */
   public void init() {
      GRect rect = new GRect(100, 100, 150, 100);
      rect.setFilled(true);
      rect.setColor(Color.RED);
      add(rect);
      GOval oval = new GOval(300, 115, 100, 70);
      oval.setFilled(true);
      oval.setColor(Color.GREEN);
      add(oval);
   }
 
/** Called on mouse press to record the coordinates of the click */
   public void mousePressed(GPoint pt) {
      last = pt;
      gobj = getElementAt(last);
   }
 
/** Called on mouse drag to reposition the object */
   public void mouseDragged(GPoint pt) {
      if (gobj != null) {
         gobj.move(pt.getX() - last.getX(), pt.getY() - last.getY());
         last = pt;
      }
   }
 
/** Called on mouse click to move this object to the front */
   public void mouseClicked(GPoint pt) {
      if (gobj != null) gobj.sendToFront();
   }
 
/* Private instance variables */
   private GObject gobj;           /* The object being dragged */
   private GPoint last;            /* The last mouse position  */
}
 



JTF Tutorial—July 31, 2006