Multimedia is a powerful technique that can be used to really intrigue your users into spending hours in front of their computers. With intense graphics and sounds, your users can be captivated.
The preceding chapter covered how to use the Graphics class to draw various shapes and images in programs. This chapter goes farther and discusses how to start to animate the graphics and images and how to add sound to your programs.
Animation is the process by which images are displayed over time, giving the appearance of movement. This task can be accomplished in several ways. This section covers using the MediaTracker class to load multiple images into a program, some techniques to polish animations, and an alternative method of animation using a single image.
The MediaTracker class can be used to load various media. This sounds good, but, unfortunately, like most things in life, it sounds better than it is. Currently, the only media it tracks is images. It does a really good job, though.
Why would a MediaTracker instance be used? If you did not use MediaTracker, every program written would have to implement an ImageObserver. MediaTracker uses an internal ImageObserver to monitor the loading of images.
The MediaTracker constructor takes a single argument:
public MediaTracker(Component comp)
The Component parameter is the component for which the images are being loaded. Since Applet is derived from Component, this is typically passed to a MediaTracker instance inside of an Applet derived class.
After an instance has been allocated, images can be added. To add an image, one of the following methods is used:
public void addImage(Image, int) public void addImage(Image, int, int, int)
Both addImage methods take an image and an identifier. The image is created via Applet.getImage. The following code illustrates the use of this method, which is a code fragment from the preceding chapter:
myImage = getImage(getDocumentBase(), "savannah.jpg");
The identifier passed as the second argument is used to identify and track either a single image or a group of images. The value of the identifier indicates the priority given to the loading of the images. Images with a lower value have a higher priority when loading. If several images are given the same identifier, all references to that identifier apply to all the images with that identifier. Therefore, when the status of the images is referred to, that is, by using statusID, the value returned should be applied to all images with that identifier.
After an image, or group of images, has been added, the status of the loading of the image can be monitored with the following methods:
public int statusID(int, boolean) public int statusAll(boolean)
These methods both return a value that is the combination of the following flags found in the MediaTracker class:
ABORTED
COMPLETE
ERRORED
LOADING
statusID is used to return the status associated with a single identifier. Alternatively, statusAll is used to return the overall status of all the images being monitored. After adding an image, the MediaTracker class does nothing to initiate the actual loading of the images. Both of these methods, along with several other methods of the class, take an additional Boolean parameter. This parameter indicates that the images should be loaded, if they haven't been already.
Table 15.1 shows additional methods of the MediaTracker
class that can be used to monitor the loading progress.
| Member | Purpose |
| checkAll() | Returns TRUE if all images associated with the MediaTracker instance have been loaded. |
| checkAll(boolean) | Returns TRUE if all images associated with the MediaTracker instance have been loaded. Optionally initiates the loading of the images. |
| checkID(int) | Returns TRUE if all images associated with the identifier have been loaded. |
| checkID(int, boolean) | Returns TRUE if all images associated with the identifier have been loaded. Optionally initiates the loading of the images. |
| getErrorsAny() | Returns an array of objects associated with the MediaTracker instance that have encountered an error. |
| getErrorsID() | Returns an array of objects associated with the identifier that have encountered an error. |
| isErrorAny() | Returns TRUE if there are any objects associated with the MediaTracker instance that encountered an error. |
| isErrorID(int) | Returns TRUE if there are any objects associated with the identifier that encountered an error. |
| statusID(int, boolean) | Returns a combined status of all of the objects associated with the identifier. Optionally initiates the loading of the images. |
| statusAll(boolean) | Returns a status of the load process for all objects associated with the identifier. |
| waitForAll() | Blocking method that waits for all images associated with the MediaTracker instance to be loaded. |
| waitForAll(long) | Blocking method that waits for all images associated with the MediaTracker instance to be loaded. Times out after the specified number of milliseconds. |
| waitForID(int) | Blocking method that waits for all images associated with the identifier to be loaded. |
| waitForID(int, long) | Blocking method that waits for all images associated with the identifier to be loaded. Times out after the specified number of milliseconds. |
As an example of using the MediaTracker class, consider displaying several images. Listing 15.1 contains the Applet code used for example EX15A.
Listing 15.1. The Applet class used for example EX15A.
import java.applet.*;
import java.awt.*;
public class EX15A extends Applet implements Runnable
{
protected ImageCanvas Images[];
protected MediaTracker ImageTracker;
protected Thread ImageThread;
public void init()
{
// If you use a ResourceWizard-generated "control creator" class to
// arrange controls in your applet, you may want to call its
// CreateControls() method from within this method. Remove the following
// call to resize() before adding the call to CreateControls();
// CreateControls() does its own resizing.
//----------------------------------------------------------------------
resize(500, 300);
// create a tracker to monitor image loading
ImageTracker = new MediaTracker(this);
// create a place to put the image controls
Images = new ImageCanvas[3];
// add the images to be tracked
for (int index = 0; index < 3; index++) {
Images[index] = new ImageCanvas(ImageTracker,
getImage(getCodeBase(),
"images\\Christopher" +
Integer.toString(index + 1) + ".jpg"));
add(Images[index]);
}
}
public void start()
{
if (ImageThread == null) {
ImageThread = new Thread(this);
ImageThread.start();
}
}
public void stop()
{
if (ImageThread != null) {
ImageThread.stop();
ImageThread = null;
}
}
public void run()
{
// wait for all of the images to be loaded
for (int index = 0; index < 3; index++)
Images[index].WaitForImage();
}
}
The first step performed in the init method is to create the MediaTracker instance. Because Applet is derived from Component, this is passed to the constructor. The next task performed is to add the controls that will be used to display the images. A new class, ImageCanvas, is defined in this file, which is used to do the actual displaying of the image. The following code fragment does several things to instantiate an ImageCanvas for each image to be displayed:
Images[index] = new ImageCanvas(ImageTracker, getImage(getCodeBase(), "images\\Christopher" + Integer.toString(index + 1) + ".jpg"));
This code uses getImage to retrieve the image. The image's location can be found by using the code's URL and the string created, with the knowledge that the image is in an image subdirectory and is a JPEG file named christopherx.jpg, in which x indicates the image number. The image is then passed to the ImageCanvas constructor, along with the MediaTracker used to monitor the loading of the image.
The remaining code in the Applet class is concerned primarily with instantiating a thread that will be used with the MediaTracker instance to monitor the loading of the images. For more information on using threads, see Chapter 12, "Moving Up to Multithreading." Because monitoring the loading of images usually involves sleeping, this should always be in a user-allocated thread. This allows threads that are allocating and controlling the Applet classes to continue to do their jobs.
ImageCanvas was added to this example to show that you do not always have to handle displaying information in the Applet.paint method. Listing 15.2 shows the code for the ImageCanvas class.
Listing 15.2. The ImageCanvas class of example EX15A.
class ImageCanvas extends Canvas
{
// NextImageId is used to give each image a unique id
protected static int NextImageId = 1;
protected MediaTracker ImageTracker;
protected Image TheImage;
protected int LocalImageId;
protected boolean ImageIsLoaded = false;
protected boolean LoadError = false;
ImageCanvas(MediaTracker ImageTracker, Image TheImage)
{
// use the next available image id and increment the next id
LocalImageId = NextImageId;
NextImageId++;
// save the passed in info
this.ImageTracker = ImageTracker;
this.TheImage = TheImage;
// resize the canvas to a known size
resize(200, 140);
// add the image to the media tracker
ImageTracker.addImage(TheImage, LocalImageId,
size().width, size().height);
}
public void paint(Graphics g)
{
if (ImageIsLoaded) {
if (LoadError)
g.drawString("Error loading image.", 10, 10);
else
g.drawImage(TheImage, 0, 0, this);
}
else {
g.drawString("Loading", 10, 10);
}
}
public void WaitForImage()
{
try {
ImageTracker.waitForID(LocalImageId);
}
catch (InterruptedException e) {}
// set state flags
ImageIsLoaded = true;
LoadError = ImageTracker.isErrorID(LocalImageId);
// force the component to be updated
repaint();
}
}
This class has several protected members used to hold information about the image it is displaying (ImageTracker, TheImage, LocalImageId, ImageIsLoaded, and LoadError).
The first step in the class constructor is to assign a unique
ID for the current image, using the value of the static NextImageId
and then incrementing the value for the next instance. Additionally,
information is saved in the class, and the component is resized
to a reasonable size. Finally, the image is added to the MediaTracker
with the local identifier and the size of the Canvas
on which the image will be displayed.
| NOTE |
The implementation of example EX15A uses unique identifiers for each image being monitored. This was just to show the use of unique identifiers; the example could easily be modified to use a single identifier for all instances of the ImageCanvas class |
The paint method is straightforward, displaying a string if the image is being loaded or if there was an error loading the image, or displaying the image itself if it was successfully loaded.
Use WaitForImage if you want to wait until the image is loaded and ready to be displayed. MediaTracker.WaitForID is used to have the MediaTracker instance wait until the image is loaded. After the image is loaded, the state members are set to tell paint the appropriate action to take. Finally, repaint is called for the component to update the look of the component.
The result of running example EX15A is shown in Figure 15.1.
Note that the third image was intentionally named incorrectly on the disc to show the error condition being displayed.
In the preceding section, it was shown how MediaTracker could be used to monitor the loading of multiple images. This mechanism can be used in creating animation. Animation can be performed by displaying an image, pausing, and then displaying the next image. If the images are closely related, movement can be successfully simulated.
A banner is the displaying of a message such that the image scrolls by a "window." It appears to be a continuous image. A banner is a good way to explore animation, because the implementation is easy to follow, and the same basic techniques can be applied to more complicated animation.
One type of animation is exemplified by the use of a banner. A banner gives the impression that a wide image is being shown to the user in a scrolling fashion. Because the banner repeats, it gives the impression that the image makes a big loop and one end is tied to the other. Figure 15.2 shows example EX15B displaying a banner.
Figure 15.2 : A banner being displayed with example EX15B.
The implementation of animation is straightforward. The process is to display an image, pause for a short amount of time (typical computer animation is 10 to 20 frames per second), and then display another image that is only slightly different from the preceding image. The quality of animation is determined by the quality of the images being displayed and the amount of delay between each image. That is, the smaller the differential between images, the faster they need to be displayed. Of course, you pay a price for having a large number of images. Download time must be considered, because all the images need to be downloaded, which can be quite costly when you are accessing a slow server or using a slower machine. A happy medium must be achieved for successful animation.
Twenty images are loaded with example EX15B. Six of those images are displayed at a time. The init method is very similar to the preceding example. Because the sole purpose of the applet is to display the banner, however, the images are loaded and monitored directly in the Applet derived class. Similarly, the monitoring of the loading of the images is done in a user-declared thread. The start and stop methods are used to allocate and stop the thread appropriately.
The heart of the applet is in the paint and run methods. Listing 15.3 shows the paint method.
Listing 15.3. The paint method of example EX15B.
public void paint(Graphics g)
{
// if all the images haven't been loaded, don't do anything
if (LoadedCount >= ImageCount) {
int currentId = CurrentImageId;
for (int index = 0; index < 6; index++) {
// assume images have a width of 50
g.drawImage(Images[currentId - ImageIdBase],
index * 50, 0, this);
currentId = GetNextId(currentId);
}
}
}
The first thing to notice is that the animation is not even started unless all images have been loaded. Because the amount of time required to download the images is potentially so much greater than the amount of time required to cycle through the animation, it is always a good idea to not start the animation until all the images have been loaded.
If the images have been loaded, paint simply draws six images using the Graphics instance, g. Using the assumption that all the images are the same width, the location for each image is calculated, based on the index of the image being drawn and its width. Because the banner will continue indefinitely, GetNextId is used to get the ID of the next image to display.
GetNextId is a simple, protected function used to cycle through the IDs of the images used for the banner:
protected int GetNextId(int currentId)
{
int nextId = currentId + 1;
if (nextId >= ImageCount + ImageIdBase)
nextId = ImageIdBase;
return nextId;
}
Listing 15.4 shows the next major player in this animation game.
Listing 15.4. The run method of example EX15B.
public void run()
{
int id = ImageIdBase;
while (id <= ImageIdBase + ImageCount - 1) {
if ((ImageTracker.statusID(id, true) & MediaTracker.COMPLETE)
== MediaTracker.COMPLETE) {
IncrementImageCount();
id++;
}
else {
try {
Thread.sleep(250);
} catch (InterruptedException e) {}
}
}
ProgressMessage.hide(); // don't show progress any more
while (true) {
repaint();
try {
Thread.sleep(750);
} catch (InterruptedException e) {}
CurrentImageId = GetNextId(CurrentImageId);
}
}
The thread used to monitor the loading of the images was passed
this as the parameter to the constructor. Therefore,
the run method of the applet is called when the thread
is started. The first step in the thread is to cycle through the
IDs of the images being loaded, checking for a completion status.
If the image has completed the loading process, IncrementImageCount
is called to increment LoadedCount and update the message
being displayed. Listing 15.5 shows the implementation of the
IncrementImageCount method. If the loading of the image
has not completed, the thread sleeps for a quarter of a second.
| NOTE |
Remember that the identifier associated with the images of a MediaTracker instance also indicates the priority of the image to be loaded. Therefore, your application can successfully track the loading of the images, as seen by the while loop in the run method of example EX15B, which is used to wait for all the images to be loaded |
After all the images have been loaded, the label that is used to show the progress is hidden so that the banner can be displayed. The second while loop in this method is the typical endless loop found in user-defined threads. The loop consists of a call to the repaint method to paint the first, and subsequent, blocks of the banner. Because that was so hard, a little snooze is now in order. Finally, the current image ID is cyclically incremented using GetNextId.
Listing 15.5. The IncrementImageCount method of example EX15B.
protected synchronized void IncrementImageCount()
{
LoadedCount++;
// inform the user what is happening
ProgressMessage.setText("Loaded " +
Integer.toString(LoadedCount) + " of the " +
Integer.toString(ImageCount) + " images...");
ProgressMessage.resize(ProgressMessage.preferredSize());
// tell the layout manager to do its job
validate();
}
That covers the basics of animation. The techniques covered here are all that are needed to start investigating simple to complex animations. The following sections cover techniques that can be used to improve the look and performance of animations in general.
Due to the frequency with which animations are drawn, one of the biggest enemies of animations is flicker. A couple of things can be done to reduce the amount of flicker that occurs in the drawing of animations.
paint has to be a very generic method to handle the variety of tasks it might have to do. It has to be able to handle everything from drawing text to drawing like Gauguin. When repaint is called to update the screen, you are not really calling a method that calls paint, but telling the applet to paint the screen when it has time. Additionally, the function that really calls the paint method is update. The default implementation for update is something like:
public void update(Graphics g) {
g.setColor(getBackground());
g.fillRect(0, 0, width, height);
g.setColor(getForeground());
paint(g);
}
The first thing that happens is that the background color is selected into the Graphics and the background is filled. Guess where the main cause of the flicker is generated from. Because animations typically redraw the entire area that is being animated, a simple enhancement would be to override the update function and simply call the paint method. Here's an example:
public void update(Graphics g) {
paint(g);
}
Consider the previous banner example. Because the same area is always rendered in the paint method, this simple update goes a long way to reduce the flicker. This new update method was added to example EX15B and is on the accompanying CD as example EX15C.
The second method to improve the painting of animations is to actually reduce the amount of what is being painted. This can be easily accomplished by letting the update method do a little more work and setting the clipping region in the Graphics instance that is passed to paint.
Due to the fact that the simplified update was so effective in the banner example, the clipping region does not have any effect for that example. Instead, consider a classic example of animation: the bouncing ball in front of a complicated background. Figure 15.3 shows a sketch of two sequential positions of the ball.
Figure 15.3 : A sketch of two sequential positions of a bouncing ball.
Assume that the circle labeled P1 in the figure was the original location of the ball and the circle labeled P2 in the figure is the current location of the ball. Realize that this is a very exaggerated diagram of two sequential positions of the ball, to illustrate what the clipping region is.
Up to this point, the approach would be to draw the entire background and then add the ball at the new position. Depending on the size of the graphics boundary, this might or might not be an acceptable approach. A larger area, however, would take quite a while to draw, and our friend flicker would be back. When the image is being updated, a large portion of the image will not change from one position to the next. What will change is mainly the area between the two positions of the ball. Therefore, if we restrict the painting to only the region defined by the perimeter of the balls, also known as the clipping region, the painting task becomes much simpler, based only on the amount of painting being done.
To determine the clipping region, the following function could be used:
Rectangle GetClippingRegion(int x1, int y1, int x2, int y2, int width, int height)
{
int clipX, clipY, clipWidth, clipHeight;
// determine the leftmost x coordinates and subsequently the
// clipping region width
clipX = min(x1, x2);
clipWidth = abs(x2 - x1) + width;
// determine the uppermost y coordinates and subsequently the
// clipping region height
clipY = min(y1, y2);
clipHeight = abs(y2 - y1) + height;
return new Rectangle(clipX, clipY, clipWidth, clipHeight);
}
This function assumes that the point (x1, y1) defines the origin of the rectangular region that completely encloses the ball at position P1 and that (x2, y2) defines the origin of the rectangular region that completely encloses the ball at position P2. Additionally, because the ball is theoretically the same ball, it will be the same size at P1 and P2.
The preceding clipping region could now be utilized in a new update method:
public void update(Graphics g) {
Rectangle rect = GetClippingRegion(previousX, previousY,
currentX, currentY, ballWidth, ballHeight);
g.clipRec(rect.x, rect.y, rect.width, rect.height);
paint(g);
previousX = currentX;
previousY = currentY;
}
This method determines the region that needs to be updated, based on the previous position and the current position, and then sets the clipping rectangle of the Graphics that is passed to the paint method. Finally, the current position is saved as the previous position.
You might wonder how we now utilize that clipping region in the paint function. Well, we don't have to worry about that. Because the paint method makes calls to the Graphics to do any type of output, the Graphics worries about the clipping regions and simply ignores any updates to the regions outside the clipping rectangle.
These methods will reduce the amount of flickering associated with animation to a somewhat reasonable amount. Eliminating the background redraw stops unnecessary updates while defining a clipping region localized to what is being redrawn.
Perhaps one of the best general enhancements that can be done to animations and graphics renderings in general is to use a second buffer to draw the image. After the image is complete, it is copied to the main image in one fell swoop. This is commonly referred to as double buffering.
If the preceding clipping region example just showed how advantageous it is to reduce the area that is being rendered, how can double buffering help us, if we just have to copy the entire image at the end of the process? The answer is that the complicated images often are composed of several operations (that is, drawing the background images, drawing foreground images that were obscured by a main image, and then drawing a new version of the main image). By performing these drawing operations to an "in memory" buffer instead of the program's screen, you prevent the user from seeing the multiple operations. Additionally, when Java does perform the copy, the virtual machine can typically take advantage of block copy routines implemented in the native language that copies larger portions of images.
Let's revisit the banner example and add double buffering to help out the animation. The only difference between example EX15C and example EX15D is the update method found in Listing 15.6.
Listing 15.6. The double-buffering update method of example EX15D.
public void update(Graphics g)
{
// create an off-screen image if there isn't one
if (OffScreenImage == null)
OffScreenImage = createImage(size().width, size().height);
// get a graphics that can be used with the off-screen image
Graphics offScreenGraphics = OffScreenImage.getGraphics();
// use the paint method to draw to the off-screen image
paint(offScreenGraphics);
// splat!! the new image over the current image
g.drawImage(OffScreenImage, 0, 0, this);
// release the resources associated with the off-screen image
offScreenGraphics.dispose();
}
This method first allocates a new image, if one has not been allocated, making it the size of the applet. Next, a Graphics instance is retrieved from the image to be used to draw to the image. Now comes the tricky part. Because you have likely developed the paint method to perform any drawing that needs to be done, simply call the paint method to draw to the off-screen image. When the painting is completed, do a mass copy to the existing Graphics and the image will be updated. Finally, release the graphics context, because most operating systems have limited resources for drawing among all of their processes.
As an alternative to displaying multiple images, you might want to consider the manipulation of a single image. This cannot, however, be applied to all animations. For example, consider the default animated program generated by the Visual J++ Applet Wizard. For more information on using the AppletWizard, see Chapter 2 "Creating Your First Applet with Applet Wizard."
The default example shows the Earth spinning on its normal North Pole to South Pole axis. This could not be replicated using a single image unless you could come up with a very odd perspective. If, however, you want to totally change the entire ecosystem, you could take a single view of the Earth and experiment with rotating it on an axis that runs through the equator. In terms of animation, this would mean taking a single image and spinning the image.
Another example using a single image is the banner example. Figure 15.4 shows the entire banner used in the previous examples.
Figure 15.4 : The full banner contents.
This image is used to display the banner in the applet by taking advantage of the clipping region and being able to specify a starting location of the image being drawn that is outside the area of the image. Listing 15.7 shows the relevant methods of example EX15E.
Listing 15.7. The scrolling-banner class EX15E methods using a single image.
public void paint(Graphics g)
{
// if the image hasn't been loaded, don't do anything
if (ImageLoaded) {
g.clipRect(0, 0, ImageDisplayWidth, ImageDisplayHeight);
// draw the first round of the image
g.drawImage(SingleImage, CurrentDisplayXValue, 0, this);
// calculate where the wrapping image starts
int wrapXLocation = CurrentDisplayXValue + ImageWidth;
// if the wrap image has started into the displayable
// range, draw the image again
if (wrapXLocation < ImageDisplayWidth)
g.drawImage(SingleImage, wrapXLocation, 0, this);
}
}
public void run()
{
try {
ImageTracker.waitForID(ImageId);
}
catch (InterruptedException e) {}
ImageLoaded = true;
ProgressMessage.hide(); // don't show progress any more
while (true) {
repaint();
try {
Thread.sleep(ImageDelay);
} catch (InterruptedException e) {}
CurrentDisplayXValue = GetNextXValue(CurrentDisplayXValue);
}
}
protected int GetNextXValue(int currentXValue)
{
int nextXValue = currentXValue - ImageXDecrement;
if (nextXValue < -ImageWidth)
nextXValue = -ImageXDecrement;
return nextXValue;
}
The paint method first clips the drawing region to the area that is being displayed. The first image is drawn in the graphics using CurrentDisplayXValue. This value is the key to the scrollability of the image, and it ranges from 0 to the negative width of the image (that is, -1000). Because the clipping rectangle starts at (0, 0), nothing is displayed to the left of the applet display area, as expected. The next operation determines where the second drawing of the image needs to take place to perform the wrapping function. This calculation is essentially determining whether the end of the first image is within the display area. If it is, the image is drawn for the second time within the display area.
The run method is not atypical and should be pretty familiar. GetNextXValue is used to determine the next starting location for the first image. Notice that if the first image is being drawn so that the entire image is outside the displayable area, the image is reset to "take over" the scrolling of the second image found in paint.
As you can see, this method is not applicable to every animation situation. If it can be used, however, the savings in not having to load multiple images will add up.
Sounds can easily be added to your program through the use of the AudioClip class. The definition of AudioClip is simple:
public interface java.applet.AudioClip
{
public abstract void loop();
public abstract void play();
public abstract void stop();
}
As you can see from the code, AudioClip is an interface and not a class. The reason for this is that an instance of this class cannot be instantiated. Instead, the user must call a function that will allocate a system-defined class that implements audio suitable for the device being used by the user.
To obtain an AudioClip, the getAudioClip methods of Applet can be called:
public AudioClip getAudioClip(URL); public AudioClip getAudioClip(URL, String);
Both functions take URLs that indicate the location of the audio clip. For the first function, the audio clip has to be completely defined in the URL. The second function allows the additional specification of where the audio clip is located, relative to the URL.
Additionally, Applet provides "shortcuts" to playing audio with the following methods:
public void play(URL) public void play(URL, String)
The parameters to these methods are the same as those for getAudioClip, mentioned previously.
The audio clips currently supported by Java are rather limited.
The only supported format is 8-bit, mlaw, 8000 Hz, mono channel,
Sun .au files. With a careful search on the Internet,
however, you can find conversion utilities to convert from various
types of audio files to .au files.
TIP |
To add personalized audio to your programs when using Microsoft Windows, you can use the Sound Recorder and your multimedia hardware to record just about anything. This recording produces a standard Windows .wav file. From here, you can use a conversion utility to convert this file from a .wav file to an .au file. A good shareware utility I found on the Internet is GoldWave, which can be found a http://web.cs.mun.ca/-chris3/goldwave This utility not only allows you to convert the file types, but also supports various audio manipulations, including echoing, creating distortion, and adjusting the volume, to name a few. |
Figure 15.5 shows the interface for example EX15F. This interface allows the playing of three short audio clips and has a background clip that continually loops after it has been loaded.
The background audio clip is rather lengthy, so it is loaded in a user-allocated thread. The run method of example EX15F is shown in Listing 15.8.
Listing 15.8. The run method of example EX15F.
public void run()
{
HiHoo = getAudioClip(getCodeBase(), "audio\\Heighho.au");
HiHoo.loop();
Work.setLabel("No Work");
}
HiHoo is an AudioClip instance of the EX15F
class; it gets assigned using Applet.getAudioClip, using
the relative location of the method. Then, loop is used
to play the clip continuously.
| NOTE |
If audio is being used to play longer selections, more than just warning beeps and whistles, the program should allow the user to disable the audio. Even the best sounds found by the programmer can be interpreted as annoying by others. Additionally, some user might be trapped with slow access connections and might not want to wait for the Internet traffic to download the audio |
Listing 15.9 shows how the action method of the EX15F class does double duty in starting and stopping the background clip, and it uses Applet.play to play the short clips.
Listing 15.9. The action method of example EX15F.
public boolean action(Event evt, Object obj)
{
boolean retval = false;
// check to see if the audio should be turned off or on
if ("Work".equals(obj)) {
HiHoo.loop(); // start the music
Work.setLabel("No Work");
retval = true;
}
else if ("No Work".equals(obj)) {
HiHoo.stop(); // stop the music
Work.setLabel("Work");
retval = true;
}
// check to see if one of the simple sounds should be played
else if ("Play".equals(obj)) {
Checkbox current = SimplerSounds.getCurrent();
play(getCodeBase(), "audio\\" + current.getLabel() + ".au");
retval = true;
}
return retval;
}
In this chapter, you learned that going from graphics to animation is a straightforward process. The MediaTracker class makes it easy to load multiple images without having to add ImageObserver overhead to every program. After images have been loaded, a thread can handle showing an image, pausing, and then showing another image to produce simple animation. A few techniques were introduced to prevent animation flicker, including reducing background draws, using clipping rectangles to reduce the overall amount of what is being drawn, and finally using an in-memory image buffer to implement image double buffering. As an alternative to using multiple images, a single image could also be used that contains all the images that contribute to the animation. Calculating drawing of this single image can produce effective animations in certain circumstances. Finally, Java's sound support was introduced, which rounds out the multimedia topics.