Check out the new USENIX Web site.
FeatureUSENIX

 

 

Active Content in Java

rao_prithvi by Prithvi Rao

<prithvi+@ux4.sp.cs.cmu.edu>

Prithvi Rao is the co-founder of KiwiLabs, which specializes in software engineering methodology and Java/CORBA training. He has also worked on the development of the MACH OS and a real-time version of MACH. He is an adjunct faculty at Carnegie Mellon and teaches in the Heinz School of Public Policy and Management.

Drawing and animation are integral parts of writing Java applications. Applets frequently contain various AWT (Abstract Windowing Toolkit) Components that are part of a graphics context. When they need to be redrawn, the AWT starts with the top Component in the hierarchy and works its way down to the bottom.

The purpose of this article is to expose the reader to various capabilities that are part of the "Active Content" in Java. I'll focus on issues related to the drawing model, drawing shapes, the graphics context, drawing text, measuring text images, and loading them both synchronously and asynchronously. I'll also touch briefly on eliminating flashing and animating images. This information provides for a richer knowledge base from which to venture into writing more interesting Java programs with active content.

The Drawing Model

Redrawing is performed when the AWT starts with the topmost Component in the hierarchy and works its way down to the bottom Component. Given that containers are Components as well, this applies equally to applets and panels.

Each Component draws itself before it draws any of the CComponent that it contains. This ensures that a Panel's background is visible only where it isn't covered by one of the Components embedded in it.

Programs draw only when the AWT tells them to do so. For instance, this can happen when a Component becomes uncovered by the user. Another example is when there is a change in the data being reflected by the Component.

The AWT requests that a Component draw itself by invoking the Component's update() method. The default Component implementation of update() clears its clipping area in the current background color and then calls the paint() method. The default implementation of paint() does nothing.

Another way to look at this is that a programmer causes a refresh that's due to some change in state of the control by calling repaint(), which in turn calls update(). It is usually the case that the paint() method is overridden to provide component-specific drawing. It is also possible to override update() for more sophisticated results. However, it is possible to call paint() directly without calling update(), so paint() must always be implemented.

Note that update() sets the clipping region, and so if the object being drawn changes between drawings, it is possible to end up with half of one and half of the other.

As can be anticipated, the paint() and update() methods must execute very quickly to ensure acceptable performance. (Note: This is where it becomes advantageous to use threads with applets to achieve better performance. My December 1999 "Using Java" article, "Using Threads Within Applets," discusses this topic.)

The Graphics Context

The argument to the paint() and update() methods is a Graphics object that represents the context in which the Component can perform its drawing. The Graphics class provides for drawing and filling rectangles, arcs, lines, ovals and polygons, text and images.

It is also possible to "set" and "get" the current color, font, or clipping area, and to set the paint mode.

The Color class encapsulates an "r.g.b" triplet, and color is set by:

 Graphics.setColor(Color color);

How text is drawn is defined by the Font class that encapsulates a font (e.g., Times Roman or Helvetica). It also supports point size and a style, although currently only bold, italic, and plain are supported. An example of setting the font is:

 Graphics.setFont(Font font);

Drawing Shapes and Text

The Graphics class has a multitude of methods that permit the drawing of various shapes, including:

Graphics.drawRect(int x, int y, int width, int height);
for drawing rectangles
Graphics.fillRect(int x, int y, int width, int height);
for filling a rectangle
Graphics.drawRoundRect(int x, int y, int width, int height, int arcwidth, int archeight);
Graphics.fillRoundRect(int x, int y, int width, int height, int arcwidth, int archeight);

When drawing text it is better to first consider whether it is possible to use a text-oriented Component such as the Label, TextField, or TextArea class. The alternative is to use drawBytes(), drawChars(), or drawString(). For example you can "draw" a string as follows:

 Graphics.drawString("This is an example of drawing a string", x, y);

where x and y specify the coordinates for "drawing" the text.

Measuring Text

In order to determine if text can fit inside a certain area, it is necessary to query the characteristics of the Font using getFontMetrics(), which returns a FontMetrics object corresponding to a given Font:

 FontMetrics foo = Graphics.getFontMetrics(Font f);

Some of the most commonly used methods that are part of the FontMetrics class are charWidth(), getHeight(), getAscent(), getDescent(), and stringWidth().

Images and Loading Images Asynchronously

All images are encapsulated by the image class. There are two ways to load images, Applet.getImage(URL) and Toolkit.getImage(filename/URL). Both of these load an image, and return an Image object. The method getImage() returns immediately without checking whether the image data exists or whether it's been successfully loaded. This is done to improve performance, since it is not necessary to wait for an image to be loaded before going on to perform other functions in the application. Some examples of loading images are:

 Image image = Applet.getImage(getDocumentBase(), "Image.jpeg");
 Image image = Toolkit.getDefaultToolkit().getImage( new/URL
   ("https://www.sample.com/images/sample.gif"));

In order to draw the image it is necessary to use one of the Graphics.drawImage() variants. In other words, drawImage() allows you to specify the image to draw and the positioning and scaling of the image. It also allows the specification of the color to draw underneath the image. This is useful if the image contains transparent pixels.

The ImageObserver parameter specifies an object that implements the ImageObserver interface. This object will be notified whenever new information regarding the image becomes available. The Component class implements the ImageObserver interface to invoke the repaint() method as the image data is loaded. The method drawImage() returns immediately even if the image data has not been completely loaded; this results in the image being displayed partially or incrementally. The easiest way to track the loading of images and to make sure that drawImage() draws only complete images is to use the MediaTracker class. The sequence is:

1. Create a MediaTracker instance.
2. Tell it to track one or more images.
3. Ask the MediaTracker the status of images.

The following code examples elucidates this point:

 tracker = new MediaTracker(this);
 images = new Image(num_images);
 for (int i=0; i  images[i] = getImage(this.getDocumentBase()."image" + i);
 tracker.addImage(images[i],i);
 }

and then later in the program

 for (int i=0; i   this.showStatus("Loading image: "+i);
  tracker.waitForID(i);
  if(tracker.isErrorID(i)) {
    showStatus("Error loading image"+i);
    return;
  }
 }
 showStatus("Loading images done.");

Animation and Thread Management

Animation is perceived motion accomplished by sequencing rapidly through frames. Frames can be incrementally different images or graphics operations. A good rule of thumb is that animation should run on a separate thread in order not to adversely affect event handling. The typical sequence is:

1. Implement the run() method to increment a frame.
2. Perform a repaint() operation.
3. Perform a sleep() operation to delay the frame.

Incorporating threading with animation permits the suspension and resumption of animation with button or mouse events.

To suspend or resume animation in an applet when the user leaves the page, it is necessary to reimplement the applet's stop() and start() methods. The following code example shows how to handle a button event:

 public boolean handleEvent(Event e) {
  if (e.id == Event.ACTION_EVENT) {
    if (e.target == start)
     start();
   else
   if (target == stop)
     stop();
    }
    return super.handleEvent(e);
 }
 private Thread thread = null;
 public void start() {
  if (thread == null) {
    thread = new Thread(this);
   start.disable();
   stop.enable();
   }
 }
 public void stop() {
   if ((thread != null) && thread.isAlive())
     thread.stop();
   thread = null;
   start.enable();
   stop.disable();
 }

A common problem with animation is "flashing," which manifests itself as animation with jitter. One solution to this is to use "double buffering." This involves performing multiple graphics operations on an undisplayed graphics buffer and then displaying the completed image on the screen. Besides preventing incomplete images from being drawn to the screen, double buffering improves drawing performance. This is because drawing to an offscreen image is more efficient than drawing to the screen.

Animating Images

It is likely that loading images will take a long time regardless of whether MediaTracker is used or not. In other words, whenever you load an image using a URL, it will be time-consuming. Most of the time is taken up by initiating HTTP connections. Each image file requires a separate HTTP connection, and each connection can take several seconds to initiate.

One way to avoid this performance degradation is to include multiple images in a single file. Performance can be further improved by using some sort of compression scheme, especially one that is designed for moving images. A simple way to do this is to create an image strip, which is a file that contains several images in a row. To draw an image from the strip it is necessary to first set the size of one image, then perform a draw operation on the image strip shifted to the left so that only the image desired appears within the clipping area.

Conclusion

Writing Java programs that contain active content can be very frustrating but also very rewarding. While it may be simple to rapidly prototype an applet with animation and images, fine-tuning it to meet stringent performance requirements requires more than a cursory knowledge of Java.

The JDK provides a versatile and powerful collection of packages to facilitate writing Java programs with active content. The common theme is always to work with the current graphics context and to use classes such as the FontMetrics class to determine sizing of text regions.