The ImageObserver
interface is useful for monitoring a single image, but it starts to
fall apart when faced with multiple images. The
java.awt.MediaTracker
class can track the loading
status of many different images and organize them into logical
groups. In future versions of Java, it may even be able to monitor
the loading of audio clips and other kinds of multimedia content.
However, Sun’s been promising this since Java 1.0 and still
hasn’t delivered, so I’m not holding my breath.
To use java.awt.MediaTracker
, simply create an
instance of MediaTracker
and then use the
MediaTracker
’s addImage( )
method to place each Image
you care
about under the MediaTracker
’s control. When
you add an Image
to a
MediaTracker
, you give it a numeric ID. This ID
does not have to be unique; it is really a group ID that is used to
organize different images into groups. Before using the image, you
call a method such as checkID( )
to see whether
the image is ready. Other methods let you force loading to start,
discover which images in a group have failed to load successfully,
wait for images to finish loading, and so on.
One difference between a MediaTracker
and an
ImageObserver
is that the
MediaTracker
is called before the
Image
is used, while an
ImageObserver
is called after the
Image
is used.
Example 9.7 is an applet that loads an image from
the Net. As usual, the image is loaded from the document base, and
the name of the image file is read from a
<PARAM>
tag; the name of the parameter is
imagefile
. The applet uses a
MediaTracker
to see whether the image has loaded.
The applet displays the words “Loading Picture. Please hang
on” until the image has finished
loading.
Example 9-7. Load an Image
import java.awt.*; import java.applet.*; public class TrackedImage extends Applet implements Runnable { private Image picture; private MediaTracker tracker; public void init( ) { this.picture = this.getImage(this.getCodeBase( ), this.getParameter("imagefile")); this.tracker = new MediaTracker(this); this.tracker.addImage(this.picture, 1); Thread play = new Thread(this); play.start( ); } public void run( ) { try { this.tracker.waitForID(1); this.repaint( ); } catch (InterruptedException ie) { } } public void paint(Graphics g) { if (this.tracker.checkID(1, true)) { g.drawImage(this.picture, 0, 0, this); } else { g.drawString("Loading Picture. Please hang on", 25, 50); } } }
The init( )
method reads the name of the image to
be loaded; prepares an Image
object,
picture
, to hold it by calling getImage( )
; constructs a new MediaTracker
called
tracker
; and then adds picture
to tracker
with an ID of 1. Next it constructs a
new Thread
and starts it.
The Thread
that’s spawned just makes sure
that the applet is repainted as soon as the image has finished
loading. It calls tracker.waitForID(1)
, which
blocks until all media with ID number 1 have finished loading. When
that’s true, the method returns and repaint( )
is called. I used a separate thread so the call to
waitForID( )
won’t block the rest of the
applet.
The paint( )
method calls
tracker.checkID(1, true
) to see whether the media
with ID 1 is available. If this method returns
true
, the image is available, and the applet calls
drawImage( )
to render the image on the screen.
Otherwise, the picture is not available, so the applet displays the
string “Loading Picture. Please hang on”. A more
sophisticated applet could put up a progress bar that showed the
percentage of the Image
that had been loaded and
the approximate time remaining.
The MediaTracker
class has one constructor:
public MediaTracker(Component comp)
This constructor creates a MediaTracker
object
that tracks images for a specified Component
.
It’s usually invoked like this:
MediaTracker tracker = new MediaTracker(this);
The constructor’s argument is the component on which the images
you want to track will be displayed, generally a
Panel
or an Applet
or a
Canvas
. You often call the constructor within the
subclass defining the component that will be rendering the images;
therefore, the argument to MediaTracker( )
is
often this
.
The two overloaded addImage( )
methods each add
one Image
to the list of images being tracked by
this MediaTracker
object. When an image is added,
it is assigned an ID number by which it can later be referred to.
Images are loaded in order of their IDs; that is, image 1 is loaded
before image 2, and so on. If multiple images have the same ID,
there’s no guarantee which will be loaded first. Adding an
Image
to a MediaTracker
does
not start loading the image data. You have to call
checkID(ID,
true)
,
checkAll(true)
, or one of the four wait methods
first.
This addImage( )
method adds
image
to the list of images being tracked by this
MediaTracker
object and assigns it the ID number
id
. The image will eventually be displayed at its
normal (unscaled) size. For example, the following code fragment from
an applet sets up a tracker for an image called
logo.gif
that’s in the same directory as
the web page—it’s given the ID number 1:
Image picture = this.getImage(this.getDocumentBase( ), "logo.gif"); MediaTracker tracker = new MediaTracker(this); tracker.addImage(picture, 1);
If you are going to scale the image, you must use the next version of
addImage( )
.
This version of addImage( )
adds
image
to the list of Image
objects being tracked by this MediaTracker
with
the ID number id
. The image will eventually be
displayed scaled to the width width
and the height
height
. The following code fragment sets up a
tracker for an image called logo.gif
that’s in the same directory as the web page. This image will
be scaled into a 30-pixel × 30-pixel square when it’s
displayed:
Image picture = this.getImage(this.getDocumentBase( ), "logo.gif"); MediaTracker tracker = new MediaTracker(this); tracker.addImage(picture, 1, 30, 30);
There are four check methods that tell you whether particular images
tracked by a MediaTracker
have loaded. These are:
public boolean checkID(int id) public boolean checkID(int id, boolean load) public boolean checkAll( ) public boolean checkAll(boolean load)
This method checks whether all
the images with the indicated ID that are tracked by this
MediaTracker
have finished loading. If they have,
it returns true; otherwise, it returns false. Multiple
Image
objects may have the same ID. Merely
checking the status of an image with this method will not make it
start loading if it is not already loading. For example, the
following paint( )
method draws
thePicture
only if all the
Image
objects with ID 1 have finished loading:
public void paint(Graphics g) { if (theTracker.checkID(1)) { g.drawImage(thePicture, 0, 0, this); } else { g.drawString("Loading Picture. Please hang on", 25, 50); } }
There’s something a little funny about how this operates. The
Image
with ID 1 is not necessarily the same
Image
as thePicture
. There may
be zero or more Image
objects with ID 1, and none
of them are necessarily thePicture
. It’s up
to the programmer to make sure the ID you check is the ID of the
Image
you want to load. Although there are
occasions when you want to check one picture and display another,
this is rare. There probably should be a
MediaTracker
method with the signature
check(Image img)
, but there isn’t.
This method checks whether all the Image
objects
with the indicated ID that are tracked by this
MediaTracker
have finished loading. If they have,
it returns true; otherwise, it returns false. Multiple
Image
objects may have the same ID. If the
boolean
argument load
is true,
all images with that ID that are not yet being loaded will begin
loading.
The following paint( )
method checks whether the
Image
objects with ID 1 have finished loading. If
they have, then thePicture
is drawn. Otherwise,
the Image
objects with ID 1 start loading (if they
aren’t already), and the message “Loading Picture. Please
hang on” is displayed:
public void paint(Graphics g) { if (theTracker.checkID(1, true)) { g.drawImage(thePicture, 0, 0, this); } else { g.drawString("Loading Picture. Please hang on", 25, 50); } }
This method checks whether all
the Image
objects that are tracked by this object
have finished loading. ID numbers are not considered. If all images
are loaded, it returns true; otherwise, it returns false. It does not
start loading images that are not already loading.
This version of checkAll( )
checks whether all the
Image
objects that are tracked by this
MediaTracker
have finished loading. If they have,
it returns true; otherwise, it returns false. If the
boolean
argument load
is true,
then it also starts loading any Image
objects that
are not already being loaded.
MediaTracker
is often used in conjunction with the
getImage( )
methods of Applet
or
java.awt.Toolkit
. The getImage( )
method is called as many times as necessary to load
images. Then the MediaTracker
makes sure the
images have finished loading before the program continues. You could
do this yourself by loading images in separate threads, then waiting
on the threads loading the images. However, the
MediaTracker
class shields you from the details
and is substantially easier to use correctly.
The next four methods start loading images tracked by the
MediaTracker
and then block while waiting for the
images to load. For performance reasons, each method loads four
images at a time in four separate threads. If more than four images
need to be loaded, then some of them will have to wait for others to
finish first. After invoking one of these methods, the calling thread
will not continue until all the requested images have finished
loading. If you don’t want your program to block while you wait
for images to download, you can call these methods inside a separate
thread.
This method forces the images
with the ID number id
that are tracked by this
MediaTracker
to start loading and then waits until
each one has either finished loading, aborted, or received an error.
An InterruptedException
is thrown if another
thread interrupts this thread. For example, to begin loading
Image
objects with ID 1 and then wait for them to
finish loading, you would write:
try { tracker.waitForID(1); } catch (InterruptedException e) { }
Let’s look at a longer example. Netscape may be infamous for
foisting the <BLINK>
tag on the world, but
it made some solid contributions to HTML as well. Two of the best are
the HEIGHT
and WIDTH
attributes
of the <IMG>
tag. To the user, it seems much
faster to load a page that includes HEIGHT
and
WIDTH
attributes for all its images than one that
doesn’t. Actually, the time it takes to load the page is the
same either way; but browsers can start displaying text and partial
images as soon as they know how much space to reserve for each image.
Consequently, if the width and height of each image is specified in
the HTML, the browser can lay out the page and display the text
immediately, without forcing the user to wait for everything to load.
However, it takes a lot of time to measure every image on a page
manually and rewrite the HTML, especially for pages that include many
different size images.
Example 9.8 is a program that gets a URL from the
user, reads the requested page using an
HTMLEditorKit.Parser
, and outputs the HTML with
HEIGHT
and WIDTH
attributes
added to all the <IMG>
tags that
didn’t have them. It uses the default
Toolkit
object’s getImage( )
method to retrieve the Image
objects,
the Image
’s getWidth( )
and getHeight( )
methods to measure them, and a
MediaTracker
to make sure that the height and
width are available. Before the height and the width of a particular
image are read, the MediaTracker
’s
waitForID( )
method is invoked to let the image
finish loading first.
Example 9-8. A Program That Adds Height and Width Tags to the IMGs on a Web Page
import javax.swing.text.*; import javax.swing.text.html.*; import javax.swing.text.html.parser.*; import java.io.*; import java.net.*; import java.util.*; import java.awt.*; import java.awt.image.*; public class ImageSizer extends HTMLEditorKit.ParserCallback { private Writer out; private URL base; public ImageSizer(Writer out, URL base) { this.out = out; this.base = base; } public void handleStartTag(HTML.Tag tag, MutableAttributeSet attributes, int position) { try { out.write("<" + tag); this.writeAttributes(tag, attributes); out.write(">"); out.flush( ); } catch (IOException e) { System.err.println(e); e.printStackTrace( ); } } public void handleEndTag(HTML.Tag tag, int position) { try { out.write("</" + tag + ">"); if (tag.breaksFlow( )) out.write(" "); out.flush( ); } catch (IOException e) { System.err.println(e); } } private void writeAttributes(HTML.Tag tag, AttributeSet attributes) throws IOException { Enumeration e = attributes.getAttributeNames( ); while (e.hasMoreElements( )) { Object name = e.nextElement( ); String value = (String) attributes.getAttribute(name); out.write(" " + name + "="" + value + """); } // for the IMG tag we may have to add HEIGHT and WIDTH attributes if (tag == HTML.Tag.IMG) { try { if (attributes.getAttribute(HTML.Attribute.HEIGHT) == null || attributes.getAttribute(HTML.Attribute.WIDTH) == null) { URL u = new URL(base, (String) attributes.getAttribute(HTML.Attribute.SRC)); Image img = Toolkit.getDefaultToolkit( ).getImage(u); Component temp = new Label( ); MediaTracker tracker = new MediaTracker(temp); tracker.addImage(img, 1); try { tracker.waitForID(1); if (attributes.getAttribute(HTML.Attribute.WIDTH) == null) { out.write(" WIDTH="" + img.getWidth(temp) + """); } if (attributes.getAttribute(HTML.Attribute.HEIGHT) == null) { out.write(" HEIGHT="" + img.getHeight(temp) + """); } } catch (InterruptedException ex) { } } } catch (MalformedURLException ex) { // SRC attribute is malformed } } } public void handleComment(char[] text, int position) { try { out.write("<!-- "); out.write(text); out.write(" -->"); out.flush( ); } catch (IOException e) { System.err.println(e); } } public void handleText(char[] text, int position) { try { out.write(text); out.flush( ); } catch (IOException e) { System.err.println(e); e.printStackTrace( ); } } public void handleSimpleTag(HTML.Tag tag, MutableAttributeSet attributes, int position) { try { out.write("<" + tag); this.writeAttributes(tag, attributes); out.write(">"); } catch (IOException e) { System.err.println(e); e.printStackTrace( ); } } public static void main(String[] args) { for (int i = 0; i < args.length; i++) { // The ParserGetter class is from Chapter 8 ParserGetter kit = new ParserGetter( ); HTMLEditorKit.Parser parser = kit.getParser( ); try { URL u = new URL(args[0]); InputStream in = u.openStream( ); InputStreamReader r = new InputStreamReader(in); HTMLEditorKit.ParserCallback callback = new ImageSizer(new OutputStreamWriter(System.out), u); parser.parse(r, callback, false); } catch (IOException e) { System.err.println(e); e.printStackTrace( ); } } } }
This is a standalone application that extends
HTMLEditorKit.ParserCallback
. It’s very
similar to Example 8.10,
PageSaver
, in the last chapter. The only real
difference is that it adds HEIGHT
and
WIDTH
attributes to the IMG
tags rather than rewriting attribute values. Consequently, the only
significantly different part of this example is the
writeAttributes( )
method. If this method sees
that it’s dealing with an IMG
tag, it checks
to see whether that tag has both HEIGHT
and
WIDTH
attributes. If it does, then the tag is
simply copied from the input onto the output. However, if either is
missing, the method then constructs the full URL for the image and
retrieves it with Toolkit.getDefaultToolkit().getImage( )
. Next, a MediaTracker
is created. The
MediaTracker( )
constructor requires an
ImageObserver
as an argument, so a blank
java.awt.Label
is constructed to fill that role.
This will also be used later as the ImageObserver
for the getWidth( )
and getHeight( )
methods. The image is added to the tracker, and the
tracker’s waitForID( )
method is used to
pause execution until the image has actually finished loading. At
this point, the image’s getWidth( )
and
getHeight( )
methods are invoked, and their
results are written in the output as the value of the
HEIGHT
and WIDTH
attributes.
This version of waitForID( )
begins loading all the images
that are tracked by this MediaTracker
with the ID
number id
and then waits until each one has either
finished loading, aborted, or received an error or until the
specified number of milliseconds has passed. An
InterruptedException
is thrown if another thread
interrupts this thread. For example, to begin loading all
Image
objects with ID 1 and wait no longer than 2
minutes (120,000 milliseconds) for them to finish loading, you would
write:
try { tracker.waitForID(1, 120000); } catch (InterruptedException e) { }
The waitForAll( )
method starts loading all the
images that are tracked by this MediaTracker
in up
to four separate threads while pausing the thread that invoked it
until each tracked image has either finished loading, aborted, or
received an error. An InterruptedException
is
thrown if another thread interrupts the waiting thread.
This method might be used by an animation applet that wants to load
all frames before it begins playing. The applet would add each frame
to the MediaTracker
and then call
waitForAll( )
before starting the animation. Example 9.9 is a simple applet that does exactly this. To
play an animation, an applet must download a series of images, each
of which will be one cell of the animation. Downloading the images is
easy using the URL
class: just create
URL
objects that point to each image, and call
getImage( )
with each URL
. Use
a MediaTracker
to force the images to load; then
use waitForAll( )
to make sure all frames have
loaded. Finally, call drawImage( )
to display the
images in sequence.
Example 9.9 reads a list of filenames from
<PARAM>
tags in the applet; the parameter
names are Cell1
, Cell2
,
Cell3
, and so on. The value of the parameter
Cell
n should be the name of
the n
th file in the
animation sequence. This makes it easy to retrieve an indefinite
number of images; the init( )
method continues
reading parameters until it finds a parameter name that doesn’t
exist. The applet builds URLs from the filenames and the document
base and uses getImage( )
to retrieve each image.
The Image
objects are stored in a
Vector
since it’s not known in advance how
many there will be. The applet assumes that the files reside in the
same directory as the HTML page; therefore, one applet on a site can
play many different animations just by changing the
<PARAM>
tags and the image files. When an
image is retrieved, it’s added to the
MediaTracker
tracker
, and
tracker
’s checkID( )
method starts loading the image. Then the start( )
method spawns a new thread to display the images in sequence.
tracker
’s waitForAll( )
method pauses the animation thread until the images have finished
loading.
Example 9-9. A Very Simple Animator Applet
import java.awt.*; import java.awt.image.*; import java.awt.event.*; import java.applet.*; import java.util.*; public class Animator extends Applet implements Runnable, MouseListener { private boolean running = false; private int currentCell = 0; private Vector cells = new Vector( ); private MediaTracker tracker; public void init( ) { this.addMouseListener(this); String nextCell; this.tracker = new MediaTracker(this); for (int i = 0; (nextCell = this.getParameter("Cell" + i)) != null; i++) { Image img = this.getImage(this.getDocumentBase( ), nextCell); cells.addElement(img); tracker.addImage(img, i); // start loading the image in a separate thread tracker.checkID(i, true); } } public void run( ) { // wait for all images to finish loading try { this.tracker.waitForAll( ); } catch (InterruptedException e) { } for (currentCell=0; currentCell < cells.size( ); currentCell++) { if (!running) return; // paint the cell this.repaint( ); // sleep for a tenth of a second // i.e. play ten frames a second try { Thread.sleep(100); } catch (InterruptedException ie) { } } } public void stop( ) { this.running = false; } public void start( ) { this.running = true; Thread play = new Thread(this); play.start( ); } public void paint(Graphics g) { g.drawImage((Image) cells.elementAt(currentCell), 0, 0, this); } // The convention is that a mouseClick starts // a stopped applet and stops a running applet. public void mouseClicked(MouseEvent e) { if (running) { this.stop( ); } else { this.start( ); } } // do-nothing methods required to implement the MouseListener interface public void mousePressed(MouseEvent e) {} public void mouseReleased(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} }
It’s easy to imagine ways to enhance this applet. One possibility would be sprites that follow a path across a constant background. But whatever extensions you add, they won’t require any changes to the applet’s networking code.
This method is similar to the
previous waitForAll( )
method. It too starts
loading all the images that are tracked by this
MediaTracker
and waits until each one has either
finished loading, aborted, or received an error. However, this method
times out if the specified number of milliseconds has passed before
all images are complete. It throws an
InterruptedException
if another thread interrupts
this thread. The following code fragment begins loading all
Image
objects tracked by the
MediaTracker
tracker
and waits
not more than 2 minutes (120,000 milliseconds) for them to finish
loading:
try { tracker.waitForAll(120000); } catch (InterruptedException e) { }
As with any time-consuming operation, you should display some sort of progress bar so that the user has an idea how long the operation is likely to take and give the user an option to cancel the download.
If there is an error as an Image
is loaded or
scaled, then that Image
is considered
“complete”; no further loading of that image’s data
takes place. The following methods let you check whether an error has
occurred and find out which image is at fault. However, there are no
methods that tell you what sort of error has occurred.
This method checks whether an error occurred while loading any image tracked by this object:
if (tracker.isErrorAny( )) { System.err.println("There was an error while loading media"); }
This method does not tell you which image failed or why. If there was
an error, you can use getErrorsAny( )
to find out
which objects returned errors. Do not assume that a single
Image
caused the error; it is common for an entire
group of images to fail to load as a result of a single error, such
as a broken network connection. Consequently, if one image failed to
load, chances are others did too.
This method returns an array containing all the objects tracked by
this MediaTracker
that encountered an error while
loading. If there were no errors, it returns null
:
if (tracker.isErrorAny( )) { System.err.println("The following media failed to load:"); Object[] failedMedia = tracker.getErrorsAny( ); for (int i = 0; i < failedMedia.length; i++) { System.err.println(failedMedia[i]); } }
This method returns true if any of the media with the specified ID
encountered an error while loading; otherwise, it returns false. If
there was an error, use getErrorsID( )
to find out
which objects returned errors. Remember, a single ID may refer to
several Image
objects, any of which may have
encountered errors:
if (tracker.isErrorID(2)) { System.err.println("An error occurred loading media with ID 2"); }
This method returns an array containing all the objects with this ID
that encountered an error while loading. If there were no errors, it
returns null
:
if (tracker.isErrorID(2)) { System.err.println("The following media failed to load:"); Object[] failedMedia = tracker.getErrorsID(2); for (int i = 0; i < failedMedia.length; i++) { System.err.println(failedMedia[i]); } }
MediaTracker
has a number of methods that report
the status of image groups. Unfortunately,
MediaTracker
only lets you check the status of
image groups, not individual images—which is a good argument
for keeping image groups small. To report the status, the
MediaTracker
class defines four flag constants
that are combined to tell you whether the images in question are
loading, aborted, errored out, or completed. These constants are
shown in Table 9.2.
Table 9-2. Media Status Constants
Constant |
Value |
Meaning |
---|---|---|
|
1 |
At least one image in the group is still being downloaded. |
|
2 |
The loading process aborted for at least one image in the group. |
|
4 |
An error occurred during loading for at least one image in the group. |
|
8 |
At least one image in the group was downloaded successfully. |
These constants are compared to the values returned by the status methods to determine whether particular conditions or combinations of conditions are true. Since each constant is an integral power of two, each has exactly one set bit, and thus the different constants can be easily combined with the bitwise operators to test combinations of conditions.
This method returns the status of all the media tracked by this
MediaTracker
object. Thus to test whether any of
the media tracked by MediaTracker
m
have finished loading, you would write:
if ((m.statusAll(false) & MediaTracker.COMPLETED) == MediaTracker.COMPLETED) {
If the argument load
is true, any media that have
not yet begun loading will start loading. If it is false, they will
not.
This method returns the status of the media sharing the ID
id
that are tracked by this
MediaTracker
. If the load
argument is true, the media with the given id
will
begin loading if they haven’t already started. If
load
is false, they won’t. Thus, to test
whether any of the media tracked by MediaTracker
m
with ID 2 have finished loading, you would
write:
if ((m.statusAll(2, false) & MediaTracker.COMPLETED) == MediaTracker.COMPLETED) {...}
Because of the nature of the flags, if any of the images with ID 2 have finished loading, this will be true. Similarly, if any of the images with ID 2 have errored out, then the following expression will also be true:
(m.statusAll(2, false) & MediaTracker.ABORTED) == MediaTracker.ABORTED)
The same is true for the MediaTracker.LOADING
and
MediaTracker.ERRORED
flags. Because there
isn’t any way to check a single image (other than putting the
image into a group by itself), statusID( )
and
statusAll( )
can return apparently contradictory
results. For example, if there are four images in the group,
it’s entirely possible that statusID( )
will
return the value:
MediaTracker.LOADING | MediaTracker.ABORTED | MediaTracker.ERRORED | MediaTracker.COMPLETE
This means that, of the four images, one is still loading, one
aborted, an error occurred in another, and one loaded successfully. A
single group of images can appear to simultaneously have completed
loading, still be loading, have aborted, and have encountered an
error. There is no way to test the status of a single
Image
if it shares an ID with another
Image
. For this reason, it’s probably not a
good idea to let too many images share the same ID.
Starting in Java 1.1, it is possible to remove an image from a
MediaTracker
using one of the three overloaded
removeImage( )
methods:
public void removeImage(Image image) public void removeImage(Image image, int id) public void removeImage(Image image, int id, int width, int height)
The first method removes all instances of the
Image
argument from the
MediaTracker
. Most of the time, there’ll be
only one of these. The second variant removes the specified image if
it has the given ID. The third removes the specified image if it has
the given ID and width and height.
3.142.98.108