by Donald Doherty
Colorful graphics and animation can change a dull, static, and gray Web page into an exciting and interesting place to visit. It's no wonder, then, that programming applets that display graphics and animation is one of the most popular uses of Java.
Java provides a wide range of tools for creating and displaying graphics. The majority of Java's graphics methods are contained in the Graphics class.
Java's Graphics class provides methods for manipulating a number of graphical features including the following:
In the following sections you'll learn about all of the above graphical features and how to implement them in Java applets. Along the way, you'll acquire a complete understanding of the Graphics class and its thirty-three methods.
You'll find Java's Graphics class in the awt (Abstract Window Toolkit) package. Be sure to properly import the Graphics class when you use it in your code. Include the following line at the beginning of your file:
import java.awt.Graphics;
You display the various graphics you produce-lines, rectangles, images, and so on, at specific locations in an applet window. To do this, you pass window coordinates to the Graphics class methods that you're using. A simple Cartesian (x, y) coordinate system defines each location within a Java applet window. The upper-left corner of a window is its origin (0, 0). x increases by the number of screen pixels that you move to the right of the left-hand edge of an applet's window. The number of pixels you move down from the top of a window is y.
Java's Graphics class provides you with methods that make it easy to draw 2-D graphics primitives. You can draw any 2-D graphics primitive including
You'll learn how to draw these graphics primitives in the following sections.
Perhaps the simplest graphics primitive is a line. In Java's Graphics class, drawLine provides a single method for drawing lines. The complete definition of the drawLine method is
public abstract void drawLine(int x1, int y1, int x2, int y2);
The drawLine method takes two pairs of coordinates-x1, y1 and x2, y2-and draws a line between them. The applet in Listing 44.1 uses the drawLine method to draw some lines. The output from this applet is shown in Figure 44.1.
Figure 44.1 : This applet displays two lines drawn using the drawLine method.
Listing 44.1 Source Code for DrawLines.java
import java.awt.Graphics;
// This applet draws a pair of lines using the Graphics class
public class DrawLines extends java.applet.Applet
{
public void paint(Graphics g)
{
// Draw a line from the upper-left corner to the point at (400, 200)
g.drawLine(0, 0, 400, 200);
// Draw a line from (20, 170) to (450, 270)
g.drawLine(20, 170, 450, 270);
}
}
Java's Graphics class provides six methods for drawing rectangles: drawRect, fillRect, drawRoundRect, fillRoundRect, draw3DRect, and fill3DRect. Use these methods to
To draw a simple rectangle using the drawRect method, use the following definition:
public void drawRect(int x, int y, int width, int height);
Pass the x and y applet window coordinates of the rectangle's upper-left corner along with the rectangle's width and height to the drawRect method. For instance, let's say that you want to draw a rectangle that is 300 pixels wide (width = 300) and 170 pixels high (height = 170). You also want to place the rectangle with its upper-left corner 150 pixels to the right of the left edge of the applet's window (x = 150) and 100 down from the window's top edge (y = 100). To do this, fill in the drawRect method's arguments as follows:
g.drawRect(150, 100, 300, 170);
| NOTE |
The drawRect method call above assumes that you've created an object from the Graphics class named g as in Listing 44.2. |
The code for an applet that uses the example rectangle coordinates is in Listing 44.2, and its output is shown in Figure 44.2.
Figure 44.2 : The rectangle displayed by this applet was created with the drawRect method.
Listing 44.2 Source Code for OneRectangle.java
import java.awt.Graphics;
public class OneRectangle extends java.applet.Applet
{
public void paint(Graphics g)
{
g.drawRect(150, 100, 300, 170);
}
}
Use the fillRect method if you want to draw a solid rectangle. The following is the complete definition of fillRect:
public abstract void fillRect(int x, int y, int width, int height);
As you can see, fillRect takes the same parameters as drawRect. The result of using drawRect and fillRect from the output of the TwoRectangles applet is shown in Figure 44.3. The rectangle at the left of the figure is drawn with drawRect and the one at the right is drawn with fillRect. You'll find TwoRectangles applet's code in Listing 44.3.
Listing 44.3 Source Code for TwoRectangles.java
import java.awt.Graphics;
public class TwoRectangles extends java.applet.Applet
{
public void paint(Graphics g)
{
g.drawRect(20, 20, 200, 100);
g.fillRect(240, 20, 200, 100);
}
}
Java's Graphics class also provides two methods for drawing rectangles with rounded corners. The drawRoundRect and fillRoundRect methods are similar to drawRect and fillRect except that they take two extra parameters: arcWidth and arcHeight. Their complete definitions are
public abstract void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight); public abstract void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight);
The arcWidth and arcHeight parameters determine how the corners will be rounded. For instance, using an arcWidth of 10 results in including the left-most five pixels and the right-most five pixels of each horizontal side of a rectangle in the rectangle's rounded corners. Similarly, using an arcHeight of 8 includes the top-most four pixels and the bottom-most four pixels of each vertical side of a rectangle in the rectangle's rounded corners. Figure 44.4 shows rectangles with rounded corners constructed using the parameter values above. The code for the applet is in Listing 44.4.
Figure 44.4 : Rectangles with rounded corners were drawn using drawRoundRect and fillRoundRect.
Listing 44.4 Source Code for Rounded.java
import java.awt.Graphics;
public class Rounded extends java.applet.Applet
{
public void paint(Graphics g)
{
g.drawRoundRect(20, 20, 200, 100, 40, 20);
g.fillRoundRect(240, 20, 200, 100, 40, 20);
}
}
In addition to regular rectangles and those with rounded corners, Java's Graphics class provides two methods for drawing 3-D rectangles: draw3DRect and fill3DRect. The complete definitions of the 3-D rectangle methods are
public void draw3DRect(int x, int y, int width, int height, boolean raised); public void fill3DRect(int x, int y, int width, int height, boolean raised);
The syntax for the draw3DRect and fill3DRect
are similar to drawRect and fillRect except
that they have an extra parameter added to the end of their parameter
lists. It's a Boolean parameter that results in a raised rectangle
effect when set to True. If it's set to false, the face of the
rectangle shows a sunken effect. The applet in Listing 44.5 draws
raised and lowered, filled and unfilled rectangles.
| NOTE |
The 3-D rectangles discussed here do not actually exist as 3-D objects. A shadow effect is used to create the illusion that they're three-dimensional. Generally, this effect consists of a relatively dark color along two adjacent sides of a rectangle and a light light color along the opposite two sides. You see a square with shading along its top edge and light along its bottom edge as sunken. In contrast, you see a square with a light strip along its top edge and shade along its bottom edge as raised. This is because your visual system expects the light source to be from above, from the sun. |
Listing 44.5 Source Code for Rect3D.java
import java.awt.Graphics;
// This applet draws four varieties of 3-D rectangles.
// It sets the drawing color to the same color as the
// background.
public class Rect3D extends java.applet.Applet
{
public void paint(Graphics g)
{
// Make the drawing color the same as the background
g.setColor(getBackground());
// Draw a raised 3-D rectangle in the upper-left
g.draw3DRect(20, 20, 200, 100, true);
// Draw a lowered 3-D rectangle in the upper-right
g.draw3DRect(240, 20, 200, 100, false);
// Fill a raised 3-D rectangle in the lower-left
g.fill3DRect(20, 140, 200, 100, true);
// Fill a lowered 3-D rectangle in the lower-right
g.fill3DRect(240, 140, 200, 100, false);
}
}
Figure 44.5 shows the output from the Rect3D applet. Notice that the raised rectangles appear the same for the filled and unfilled. This is because the drawing color is the same color as the background. If a different drawing color were used, the filled rectangle would be filled with the drawing color, whereas the unfilled rectangle would still show the background color.
Figure 44.5 : The draw3DRect and fill3DRect methods use shading to produce a 3-D effect.
| TIP |
3-D rectangles typically look best when their color matches the background color. Three-dimensional effects depend on several aspects of your system including your computer's graphics and color capabilities and the browser that you use. If your 3-D effects are less than satisfactory, try using different colors. Color manipulation using Java is covered in detail later in this chapter in the section "Displaying Colors." |
Java's Graphics class provides two methods for drawing ovals or circles: drawOval and fillOval. The full definitions of these methods are
public abstract void drawOval(int x, int y, int width, int height); public abstract void fillOval(int x, int y, int width, int height);
| NOTE |
A circle is an oval with its width equal to its height. |
To draw an oval, imagine surrounding the oval with a rectangle that just touches the oval at its widest and highest points as illustrated in Figure 44.6. The code is shown in Listing 44.6.
Figure 44.6 : The same oval is inside its bounding rectangle on the left and by itself on the right.
Listing 44.6 Source Code for OvalDemo.java
import java.awt.Graphics;
import java.awt.Color;
public class OvalDemo extends java.applet.Applet
{
public void paint(Graphics g)
{
g.setColor(Color.gray);
g.drawRect(20, 20, 200, 100);
g.setColor(Color.black);
g.drawOval(20, 20, 200, 100);
g.drawOval(240, 20, 200, 100);
}
}
You pass drawOval or fillOval the coordinates of the upper-left corner of the imaginary surrounding rectangle and the width and height of the oval. The width and height is equal to the width and height of the imaginary surrounding rectangle.
The applet in Listing 44.7 draws a circle and a filled oval. The output from this applet is shown in Figure 44.7.
Listing 44.7 Source Code for Ovals.java
import java.awt.Graphics;
// This applet draws an unfilled circle and a filled oval
public class Ovals extends java.applet.Applet
{
public void paint(Graphics g)
{
// Draw a circle with a diameter of 150 (width=150, height=150)
// With the enclosing rectangle's upper-left corner at (20, 20)
g.drawOval(20, 20, 150, 150);
// Fill an oval with a width of 150 and a height of 80
// The upper-left corner of the enclosing rectangle is at (200, 20)
g.fillOval(200, 20, 150, 80);
}
}
You'll use what you learned about drawing ovals to draw arcs in Java. An arc is segment of the line that forms the perimeter of an oval as demonstrated in Figure 44.8. The applet's code is in Listing 44.8.
Figure 44.8 : At the left is the arc and its associated oval and at the right is the arc alone.
Listing 44.8 Source Code for ArcDemo.java
import java.awt.Graphics;
import java.awt.Color;
public class ArcDemo extends java.applet.Applet
{
public void paint(Graphics g)
{
g.setColor(Color.gray);
g.drawOval(20, 20, 200, 100);
g.setColor(Color.black);
g.drawArc(20, 20, 200, 100, 0, 90);
g.drawArc(240, 20, 200, 100, 0, 90);
}
}
Two Graphics class methods are provided for drawing arcs: drawArc and fillArc. Their complete definitions are
public abstract void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle); public abstract void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle);
Use the first four parameters just as you did with the oval methods. In fact, you're drawing an invisible oval and the arc is a segment of the oval's perimeter defined by startAngle and arcAngle, the last two parameters.
The startAngle parameter defines where your arc starts along the invisible oval's perimeter. In Java, angles are set around a 360° circle as follows:
The arcAngle parameter defines the distance, in degrees, that your arc traverses along the invisible oval's perimeter. Angles are positive in the counter clockwise direction and negative in the clockwise direction.
The arc you saw in Figure 44.8 began at 0°, or at 3 o'clock, and traversed the invisible oval 90° in the positive, or counter clockwise, direction. The relevant line in Listing 44.8 is reproduced below.
g.drawArc(20, 20, 200, 100, 0, 90);
Notice that the last parameter is given in the angle traversed and not the angle at which the arc ends, so if you want an arc that starts at 45° and ends at 135°, you must provide a startAngle of 45° and an arcAngle of 90° as shown in Figure 44.9 and Listing 44.9.
Figure 44.9 : This arc starts at 45° and ends at 135°.
Listing 44.9 Source Code for ArcDemo2.java
import java.awt.Graphics;
import java.awt.Color;
public class ArcDemo2 extends java.applet.Applet
{
public void paint(Graphics g)
{
g.setColor(Color.gray);
g.drawOval(20, 20, 200, 100);
g.setColor(Color.black);
g.drawArc(20, 20, 200, 100, 45, 90);
g.drawArc(240, 20, 200, 100, 45, 90);
}
}
When you use a negative arcAngle, the arc sweeps clockwise along the invisible oval's perimeter. For instance, if you start an arc at 0° (like in Figure 44.8), but now give an arcAngle of -90° (rather than 90°), you'll get an arc that looks something like the one in Figure 44.10.
You'll find the code in Listing 44.10.
Listing 44.10 Source Code for ArcDemo3.java
import java.awt.Graphics;
import java.awt.Color;
public class ArcDemo3 extends java.applet.Applet
{
public void paint(Graphics g)
{
g.setColor(Color.gray);
g.drawOval(20, 20, 200, 100);
g.setColor(Color.black);
g.drawArc(20, 20, 200, 100, 0, -90);
g.drawArc(240, 20, 200, 100, 0, -90);
}
}
Using the fillArc method results in a filled pie-shaped wedge defined by the center of the invisible oval and the perimeter segment transversed by the arc. For instance, the applet shown in Figure 44.11 uses the same parameters as the previous example in Listing 44.10; but, instead of drawArc, it employs the fillArc method as you can see in Listing 44.11.
Figure 44.11 : This arc is drawn with the fillArc method.
Listing 44.11 Source Code for ArcDemo4.java
import java.awt.Graphics;
import java.awt.Color;
public class ArcDemo4 extends java.applet.Applet
{
public void paint(Graphics g)
{
g.setColor(Color.gray);
g.drawOval(20, 20, 200, 100);
g.setColor(Color.black);
g.fillArc(20, 20, 200, 100, 0, -90);
g.fillArc(240, 20, 200, 100, 0, -90);
}
}
Java's Graphics class provides four methods for building polygons: two versions of drawPolygon and two versions of fillPolygon. There are two methods each so you can either pass two arrays containing the x and y coordinates of the points in the polygon or you can pass an instance of a Polygon class.
The Polygon class is defined in Java's awt package. Be sure to properly import the Polygon class when you use it in your code. Include the following line at the beginning of your file:
import java.awt.Polygon;
First look at how to create a polygon using two arrays. Two methods draw polygons using arrays: drawPolygon and fillPolygon. Their full definitions are
public abstract void drawPolygon(int xPoints[], int yPoints[], int nPoints); public abstract void fillPolygon(int xPoints[], int yPoints[], int nPoints);
The applet in Listing 44.12 draws a polygon using an array of x coordinates (xCoords) and an array of y coordinates (yCoords). Each x and y pair, the first x (50) and the first y (100) pair, for instance, defines a point on a plane (50, 100). You use drawPolygon to connect each point to the following point in the list. The first pair is (50, 100) and connects by a line to the second pair (200, 0), and so on. The drawPolygon method's third parameter, nPoints, is the number of points in the polygon and should equal the number of pairs in the x and y arrays. The applet's output is shown in Figure 44.12.
Figure 44.12 : Draw polygons using x and y arrays.
Listing 44.12 Source Code for DrawPoly.java
import java.awt.Graphics;
public class DrawPoly extends java.applet.Applet
{
int xCoords[] = { 50, 200, 300, 150, 50 };
int yCoords[] = { 100, 0, 50, 300, 200 };
int xFillCoords[] = { 450, 600, 700, 550, 450 };
public void paint(Graphics g)
{
g.drawPolygon(xCoords, yCoords, 5);
g.fillPolygon(xFillCoords, yCoords, 5);
}
}
| TIP |
The applets in this chapter assume that you have a graphics resolution of at least 800 pixels across by 600 pixels top to bottom (800´600). If your monitor displays a smaller number of pixels (640´480, for example) you can either use your browser's scroll bars to see the rest of the applet window or you can change some of the coordinates in the example so that they're no larger than your largest screen coordinates. |
Notice that in this example the polygon is not closed-there is no line between the last point in the polygon and the first one. If you want to close the polygon, you must repeat the first point at the end of the array. You can create a closed version of the polygon in Listing 44.12 by adding the first x and y pair to the end of the x and y arrays and by increasing the nPoints parameter by one as shown in Listing 44.13. The result is shown in Figure 44.13.
Figure 44.13 : Draw closed polygons by adding the first point to the end of the x and y arrays.
Listing 44.13 Source Code for DrawClosedPoly.java
import java.awt.Graphics;
public class DrawClosedPoly extends java.applet.Applet
{
int xCoords[] = { 50, 200, 300, 150, 50, 50 };
int yCoords[] = { 100, 0, 50, 300, 200, 100 };
int xFillCoords[] = { 450, 600, 700, 550, 450, 450 };
public void paint(Graphics g)
{
g.drawPolygon(xCoords, yCoords, 6);
g.fillPolygon(xFillCoords, yCoords, 6);
}
}
Using Java's Polygon Class Java's Polygon class provides features that often make it the most convenient way to define polygons. The Polygon class provides the two constructors, defined below.
public Polygon(); public Polygon(int xpoints[], int ypoints[], int npoints);
These constructors let you either create an empty polygon or create a polygon by initially passing an array of x and an array of y numbers and the number of points made up of the x and y pairs. If you do the latter, the parameters are saved in these Polygon class's fields:
public int xpoints[]; public int ypoints[]; public int npoints;
Whether you started with an empty polygon or not, you can add points to it dynamically using Polygon class's addPoint method defined below.
public void addPoint(int x, int y);
The addPoint method automatically increments Polygon class's number of points field, npoints.
The Polygon class includes two other methods, getBoundingBox and inside:
public Rectangle getBoundingBox(); public boolean inside(int x, int y);
You can use the getBoundingBox method to determine the minimum size box that can completely surround the polygon in screen coordinates. The Rectangle class returned by getBoundingBox contains variables indicating the x and y coordinates of the rectangle along with the rectangle's width and height.
You determine whether or not a point is contained within the polygon or is outside it by calling the inside methods with the x and y coordinates of the point.
Use the Polygon class in place of the x and y arrays for either the drawPolygon or fillPolygon method as indicated in their definitions:
public void drawPolygon(Polygon p); public void fillPolygon(Polygon p);
The Polygon class is used for both the drawPolygon and the fillPolygon methods in Listing 44.14, and the applet's output is shown in Figure 44.14.
Listing 44.14 Source Code for Polygons.java
import java.awt.Graphics;
import java.awt.Polygon;
public class Polygons extends java.applet.Applet
{
int xCoords[] = { 50, 200, 300, 150, 50, 50 };
int yCoords[] = { 100, 0, 50, 300, 200, 100 };
int xFillCoords[] = { 450, 600, 700, 550, 450, 450 };
public void paint(Graphics g)
{
Polygon myPolygon = new Polygon(xCoords, yCoords, 6);
Polygon myFilledPolygon = new Polygon(xFillCoords, yCoords, 6);
g.drawPolygon(myPolygon);
g.fillPolygon(myFilledPolygon);
}
}
You're directly manipulating the wavelength of the light that is transmitted to your eyes when you manipulate colors on a computer screen. This is different than manipulating colors using crayons or other pigments.
The primary colors of pigments are red, yellow, and blue. Orange is the result if you mix red and yellow pigments, and green is the result when you mix yellow and blue. Mixing blue and red results in purple, black is formed from mixing all the pigments together, and white indicates the absence of pigment.
In contrast, the primary colors of directly transmitted light
are red, green, and blue. Some common combinations are red and
green which results in brown, green and blue resulting in cyan,
and red and blue resulting in magenta. Black is formed by the
absence of all light while white is formed by the combination
of all the primary colors. In other words, red, blue, and green
transmitted in equal amounts results white.
| NOTE |
The color effects of pigments and directly transmitted light are closely related. Each color pigment absorbs light but not all of it. The color of a pigment is due to the wavelength of the light that the pigment does not absorb, and, therefore, the light that the pigment reflects. Because the absence of pigments results in all wavelengths of light being reflected, the result is white. This is the same as the transmission of all the primary colors of light. In contrast, all the different colored pigments mixed together absorb all light. This is equivalent to the color black, resulting from the absence of light. |
Java uses the RGB (Red, Green, and Blue) color model. You
define the colors you want by indicating the amount of red, green,
and blue light that you want to transmit to the viewer. You can
do this either by using integers between 0 and 255 or by using
floating point numbers between 0.0 and 1.0. Table 44.1 indicates
the red, green, and blue amounts for some common colors.
| Color Name | |||
| Black | |||
| Blue | |||
| Cyan | |||
| Dark Gray | |||
| Gray | |||
| Green | |||
| Light Gray | |||
| Magenta | |||
| Orange | |||
| Pink | |||
| Red | |||
| White | |||
| Yellow |
Java's Graphics class provides two methods for manipulating colors: getColor and setColor. Their full definitions are
public abstract Color getColor(); public abstract void setColor(Color c);
The getColor method returns the Graphics object's current color as a Color object while the setColor method sets the Graphics objects color by passing it a Color object.
The Color class is defined in Java's awt package. Be sure to properly import the Color class when you use it in your code. Include the following line at the beginning of your file:
import java.awt.Color;
The Color class provides three constructors. The first constructor allows you to create a Color object using red (r), green (g), and blue (b) integers between 0 and 255:
public Color(int r, int g, int b);
The second constructor is very similar to the first. Instead of integer values, it uses floating point values between 0.0 and 1.0 for red (r), green (g), and blue (b):
public Color(float r, float g, float b);
The third constructor allows you to create a color using red, green, and blue integers between 0 and 255, but you combine the three numbers into a single, typically hexadecimal, value (rgb):
public Color(int rgb);
In the 32-bit rgb integer, bits 16 through 23 (8 bits) hold the red value, bits 8 through 15 (8 bits) hold the green value, and bits 0 through 7 (8 bits) hold the blue value. The highest 8 bits, bits 24 through 32, are not manipulated. You usually write RGB values in hexadecimal notation so it's easy to see the color values. A number prefaced with 0x is read as hexadecimal. For instance, 0xFFA978 would give a red value of 0xFF (255 decimal), a green value of 0xA9 (52 decimal), and a blue value of 0x78 (169 decimal).
Once you create a color, set a Graphics object's drawing color using its setColor method. Graphics objects have the default drawing color as black. The applet given in Listing 44.15 gets the graphics context's default color and assigns it to the Color object named defaultColor. A new Color object creates and is named newColor using the hexadecimal values discussed above. A filled circle creates on the left with newColor and on the right with defaultColor. The resulting output is shown in Figure 44.15.
Listing 44.15 Source Code for ColorPlay.java
import java.awt.Graphics;
import java.awt.Color;
public class ColorPlay extends java.applet.Applet
{
public void paint(Graphics g)
{
Color newColor = new Color(0xFFA978);
Color defaultColor = g.getColor();
g.setColor(newColor);
g.fillOval(50, 50, 200, 200);
g.setColor(defaultColor);
g.fillOval(300, 50, 200, 200);
}
}
You needn't always create colors manually. The Color
class provides class constants of the colors with RGB values listed
in Table 44.1. The Color class constants are listed in
Table 44.2.
| Color.black | Color.green | Color.red |
| Color.blue | Color.lightGray | Color.white |
| Color.cyan | Color.magenta | Color.yellow |
| Color.darkGray | Color.orange | |
| Color.gray | Color.pink |
You must import the Color class to use a Color class constant, but you don't need to create a color object. Simply type the class name, followed by the dot operator, followed by the color, as shown in Table 44.2 and in Listing 44.16. The output of Listing 44.16 isn't shown because the figures are all in black and white.
Listing 44.16 Source Code for DefColors.java
import java.awt.Graphics;
import java.awt.Color;
public class DefColors extends java.applet.Applet
{
public void paint(Graphics g)
{
g.setColor(Color.pink);
g.fillOval(50, 50, 200, 200);
}
}
Java's Graphics class provides seven methods related to displaying text. However, before plunging into the various aspects of drawing text, you should be familiar with some common terms for fonts and text.
Baseline is the imaginary line that the text rests on.
Descent is the distance below baseline that a particular character extends. For instance, the letters g and j extend below baseline.
Ascent is the distance above baseline that a particular character extends. For instance, the letter d has a higher ascent than the letter x.
Leading is the space between a line of text's lowest descent and the following line of text's highest ascent. Without leading, the letters g and j would touch the letters M and H on the next line.
| CAUTION |
The term "ascent" in Java is slightly different from the same term in the publishing world. The publishing term "ascent" refers to the distance from the top of a letter, such as x, to the top of a character, such as d. In contrast, the Java term "ascent" refers to the distance from baseline to the top of a character, such as d. |
| NOTE |
You may hear the terms proportional and fixed associated with fonts. Characters in a proportional font only take up as much space as they need. In a fixed font, every character takes up the same amount of space. For example, most of the text in this book is in a proportional font. Look at some of the words and notice how the letters only take up as much space as necessary. (Compare the letters i and m, for instance.) In contrast, the code examples in this book are written in a fixed font. Notice how each letter takes up exactly the same amount of space. |
Perhaps the most rudimentary (some might say primitive) way to display text in Java is to draw from an array of bytes representing characters or simply an array of characters. You can use an array of ASCI codes when you use the drawBytes method, or you can use an array of characters when you use the drawChars method. Each of these two methods is available in Java's Graphics class and defined as:
public void drawBytes(byte data[], int offset, int length, int x, int y); public void drawChars(char data[], int offset, int length, int x, int y);
The offset parameter refers to the position of the first character or byte in the array to draw. This will most often be zero because you will usually want to draw from the beginning of the array. The Length parameter is the total number of bytes or characters in the array. The x coordinate is the integer value that represents the beginning position of the text, in number of pixels, from the left edge of the applet's window. The y coordinate is distance, in pixels, from the top of the applet's window to the text's baseline. The applet in Listing 44.17 displays text from an array of ASCI codes in blue and text from an array of characters in red. Figure 44.16 shows its output.
Listing 44.17 Source Code for DrawChars.java
import java.awt.Graphics;
import java.awt.Color;
public class DrawChars extends java.applet.Applet
{
byte[] bytesToDraw = { 72, 101, 108, 108, 111, 32,
87, 111, 114, 108, 100, 33 };
char[] charsToDraw = { 'H', 'e', 'l', 'l', 'o', ' ',
'W', 'o', 'r', 'l', 'd', '!' };
public void paint(Graphics g)
{
g.setColor(Color.blue);
g.drawBytes(bytesToDraw, 0, bytesToDraw.length, 10, 20);
g.setColor(Color.red);
g.drawChars(charsToDraw, 0, charsToDraw.length, 10, 50);
}
}
| NOTE |
The numbers used in the byte array, bytesToDraw, are base ten ASCI codes. They're the numbers that the computer uses to represent letters. You could use any base for these numbers, including the popular hexadecimal. |
Arrays are objects in Java and we created two array objects when we built the two arrays, bytesToDraw and charsToDraw, in Listing 44.17. That's why we were able to use the array method, length, to get the lengths of the arrays in number of bytes or characters.
Java provides another object type, the String object, that is similar to the array objects we just created, but it is more convenient for manipulating text.
Java's Graphics class provides a method for displaying text by drawing a string of characters. The method, shown below, takes a String object as a parameter.
public abstract void drawString(String str, int x, int y);
If you put double quotes around a string, a String object is automatically created. You then pass an x coordinate, the integer value that represents the beginning position of the text in number of pixels from the left edge of the applet's window, and pass a y coordinate, the distance in pixels from the top of the applet's window to the text's baseline.
The applet in Listing 44.18 demonstrates two ways of passing a String object to drawString. A String object is automatically created in the argument list of the first drawString and is displayed in blue. The second drawString method is passed a String object, myString, created earlier in the listing, which then displays in red. The applet's output is shown in Figure 44.17.
Figure 44.17 : You can either make a String object automatically or by using new.
Listing 44.18 Source Code for StringObjects.java
import java.awt.Graphics;
import java.awt.Color;
public class StringObjects extends java.applet.Applet
{
String myString = new String("Hello World!");
public void paint(Graphics g)
{
g.setColor(Color.blue);
g.drawString("Hello World!", 10, 20);
g.setColor(Color.red);
g.drawString(myString, 10, 50);
}
}
Notice that the output is the same as the previous listing, Listing 44.17, that used the drawBytes and drawChars methods. Also notice that the creation of the String object named myString in Listing 44.18 is a trivial example. The quoted "Hello World!" text is passed to the String constructor but "Hello World!" is already a String object! Remember that strings surrounded by double quotes automatically turn into String objects so myString was initiated with another String object through the following String class constructor:
public String(String value);
Java's String class provides seven constructors, including the one above, so you have several options for building a String object. In fact, you can create String classes from the byte arrays or character arrays you used for the drawBytes and drawChars methods above.
You can use one of the following two constructors when you create String objects from byte arrays:
public String(byte ascii[], int hibyte); public String(byte ascii[], int hibyte, int offset, int count);
The first parameter, ascii[], is the byte array. The
second parameter, hibyte, is included so that you can
use Unicode in your Java applets. Set hibyte to 0 when
you use ASCI codes. Otherwise, when you use Unicode, set hibyte
to the top 8 bits of each 16-bit Unicode character. Finally, in
the second constructor, the offset parameter refers to
the position of the first byte in the array to draw. The count
parameter is the total number of bytes to include in the output.
| NOTE |
Unicode is a character code standard that uses 16 bits rather than ASCII's 8 bits. This allows Unicode to include almost every known character from all of the languages in recorded history. You can create international programs when you use Unicode. |
Both String constructors that deal with byte arrays are demonstrated
in Listing 44.19. The String object named bytesString is simply
passed an array of ASCII codes and 0 (because we're using ASCII
codes and not Unicode). The String object named bytesAnotherString
is passed, in addition, 6 for the offset and 6 for the count.
| TIP |
To count your offset, start with 0. An offset of 6 ends up on "W" in "Hello World!" Don't forget to count spaces as characters. |
Listing 44.19 Source Code for StringsOne.java
import java.awt.Graphics;
import java.awt.Color;
public class StringsOne extends java.applet.Applet
{
byte[] bytesToDraw = { 72, 101, 108, 108, 111, 32,
87, 111, 114, 108, 100, 33 };
String bytesString = new String(bytesToDraw, 0);
String bytesAnotherString = new String(bytesToDraw, 0, 6, 6);
public void paint(Graphics g)
{
g.setColor(Color.blue);
g.drawString(bytesString, 10, 20);
g.setColor(Color.red);
g.drawString(bytesAnotherString, 10, 50);
}
}
The output of Listing 44.19 is shown in Figure 44.18.
You use one of the following two constructors when you create String objects from character arrays:
public String(char value[]); public String(char value[], int offset, int count);
Listing 44.20 demonstrates the use of both String constructors that deal with character arrays.
Listing 44.20 Source Code for StringsTwo.java
import java.awt.Graphics;
import java.awt.Color;
public class StringsTwo extends java.applet.Applet
{
char[] charsToDraw = { 'H', 'e', 'l', 'l', 'o', ' ',
'W', 'o', 'r', 'l', 'd', '!' };
String charsString = new String(charsToDraw);
String charsAnotherString = new String(charsToDraw, 6, 6);
public void paint(Graphics g)
{
g.setColor(Color.blue);
g.drawString(charsString, 10, 20);
g.setColor(Color.red);
g.drawString(charsAnotherString, 10, 50);
}
}
The output of Listing 44.20 is shown in Figure 44.19.
If you pass nothing to String, use the default constructor:
public String();
Passing a String nothing results in an empty String object. You can then use the object's methods to dynamically create content. In fact, it's the methods provided by String class that makes it so handy. It's beyond the scope of this chapter to go into each of over forty methods but you've already seen one of the most useful ones, the length method. The length method simply returns the length of the string contained in the String object.
You may find that the default font that you've been working with so far isn't very interesting. Fortunately, you can select from a number of different fonts. Java's Graphics class provides the setFont method, so that you can change your text's font characteristics:
public abstract void setFont(Font font);
The setFont method takes a Font object as its argument. Java provides a Font class that gives you a lot of text formatting flexibility. The Font class provides a single constructor:
public Font(String name, int style, int size);
The name of the font, surrounded by double quotes, passes to the name parameter. The availability of fonts varies from system to system, so it's a good idea to make sure that the user has the font you want. You can check the availability of a font by using Toolkit class's getFontList method:
public abstract String[] getFontList();
You don't typically import the Toolkit class. Instead, you use Applet class's getToolkit method, which is inherited from the Component class:
public Toolkit getToolkit();
You use the style parameter to set the font style. Bold, italic, plain, or any combination is available. The Font class provides three class constants, Font.BOLD, Font.ITALIC, and Font.PLAIN, that you can use in any combination to set the font style. For instance, to set a font to bold, simply pass Font.BOLD to the style parameter. If you want to create a bold italic font, you pass Font.BOLD + Font.ITALIC to Font class's style parameter.
Finally, you set the point size of the font by passing an integer to Font class's size parameter. The point size is a printing term. There are 100 points to an inch when printing on a printer, but this does not necessarily apply to screen fonts. A typical point size value for printed text is either 12 or 14. The point size does not indicate the number of pixels high or wide, it is simply a relative term. A point size of 24 is twice as big as a point size of 12.
All of this information is pulled together in the applet coded in Listing 44.21. Figure 44.20 shows the applet's output.
Figure 44.20 : Java provides a number of different fonts and font styles.
Listing 44.21 Source Code for ShowFonts.java
import java.awt.Graphics;
import java.awt.Font;
public class ShowFonts extends java.applet.Applet
{
public void paint(Graphics g)
{
String fontList[];
int startY = 15;
fontList = getToolkit().getFontList();
for (int i=0; i < fontList.length; i++)
{
g.setFont(new Font(fontList[i], Font.PLAIN, 12));
g.drawString("This is the " + fontList[i] + " font.",
5, startY);
startY += 15;
g.setFont(new Font(fontList[i], Font.BOLD, 12));
g.drawString("This is the bold "+ fontList[i] + " font.",
5, startY);
startY += 15;
g.setFont(new Font(fontList[i], Font.ITALIC, 12));
g.drawString("This is the italic " + fontList[i] + " font.",
5, startY);
startY += 20;
}
}
}
There are two steps to displaying images with Java. You must get the image and then you must draw it. Java's Applets class provides methods for getting images and Java's Graphics class provides methods for drawing them.
Java's Applets class provides the following two getImage methods:
public Image getImage(URL url); public Image getImage(URL url, String name);
In the first method listed, you provide an URL class. You can simply type the URL surrounded by double quotes or you can create an URL object and then pass it to getImage. If you do the latter, be sure to import the URL class. The URL class is part of Java's net package. Include the following line at the beginning of your file:
import java.net.URL;
Whichever way you pass the URL, the first method takes the whole path including the file name of the image itself. Because images are usually aggregated into a single directory or folder, it's usually handier to keep the path and file name separate.
The second method takes the URL path to the image as the url parameter and it takes the file name, or even part of the path and the file name, as a string enclosed by double quotes and passed to the name parameter.
The Applet class provides the following two methods that are particularly useful: the getDocumentBase and getCodeBase methods:
public URL getDocumentBase(); public URL getCodeBase();
The getDocumentBase method returns an URL object containing the path to where the HTML document resides that displays the Java applet. Similarly, the getCodeBase method returns an URL object that contains the path to where the applet is running the code. Using these methods makes your applet flexible. You and others can use your applet on different servers, directories, or folders.
Java's Graphics class provides drawImage methods to use for displaying images. The most basic method is
public abstract boolean drawImage(Image img, int x, int y, ImageObserver observer);
The first parameter, img, takes an Image object. Often, you'll just get an Image object using getImage, as discussed above. The second, x, and third, y, parameters set the position of the upper-left corner of the image in the applet's window. The last parameter, observer, takes an object that implements the ImageObserver interface. You can watch the progress of an image as it loads through objects that implement the ImageObserver interface. This enables your code to make decisions based on an image's loading status. It's usually enough to know that the Applet class inherits the ImageObserver interface from the Components class. Simply passing the applet to the observer parameter is usually sufficient. You might use an alternate object with an ImageObserver interface if you're tracking the asynchronous loading of many images.
Basic image display is easier than it sounds. The applet in Listing 44.22 displays an image residing in the same directory or folder as the applet itself.
Listing 44.22 Source Code for DrawImage.java
import java.awt.Graphics;
public class DrawImage extends java.applet.Applet
{
public void paint(Graphics g)
{
g.drawImage(getImage(getCodeBase(), "teddy0.jpg"), 0, 0, this);
}
}
The code in Listing 44.22 is simple but not recommended. It's poor coding practice to put actions that are not directly related to putting something on the screen inside your applet's paint method. Every time your applet's window needs updating, the paint method is called. It is inefficient to reload an image every time your applet's window is refreshed. You typically override Applet class's init method to do things, like loading images, that are only done once at the beginning of the applet's life. The init method takes no arguments and returns void. Listing 44.23 shows code for a well-behaved applet that results in the same output as the applet in Listing 44.22.
Listing 44.23 Source Code for DrawImageTwo.java
import java.awt.Graphics;
import java.awt.Image;
public class DrawImageTwo extends java.applet.Applet
{
private Image imageTeddy;
public void init()
{
imageTeddy = getImage(getCodeBase(), "teddy0.jpg");
}
public void paint(Graphics g)
{
g.drawImage(imageTeddy, 0, 0, this);
}
}
Notice that in Listing 44.23 the image from the Teddy0.jpg file loads only once in the init method where the imageTeddy object is created. Whenever the applet's window is refreshed, imageTeddy is passed to drawImage without reloading the image from Teddy0.jpg.
Another version of drawImage is similar to the one you've already seen and used, but it includes two additional parameters. These allow you to determine the size of the image displayed in an applet's window. This version of drawImage is
public abstract boolean drawImage(Image img, int x, int y,
int width, int height, ImageObserver observer);
The width and height parameters take the width and height, in pixels, of the display area for the image regardless of the image's native size. You can stretch and shrink an image using these parameters. Just remember that doing this can degrade the quality of your image.
Displaying animation is probably the most popular use of Java applets on the Web. You can use HTML to display static images, but you need Java to make them come alive.
There are three general steps that should usually be followed when creating animation:
You'll create an animation of a dancing teddy bear using three images. You've already seen the code used to produce the first image, TEDDY0.JPG. It's a teddy bear standing upright on both of its hind limbs. The second image, TEDDY1.JPG, shows the teddy bear standing on its left hind limb. Finally, the third image, TEDDY2.JPG, shows the teddy bear standing on its right hind limb.
Loading the dancing teddy bear images is nearly identical to the way you've loaded images before. The only difference is that you need to load more than one image. You need to create an instance variable that holds an array of Image objects. There are three dancing teddy bear images so you will create an array for three Image objects:
Image imagesTeddy[] = new Image[3];
Now load the images in an overloaded init method. You can conveniently use a "for" statement to load the images because each image has an identical name followed by a number. The following is the dancing teddy bear init method:
public void init()
{
for (int i=0; i < imagesTeddy.length; i++)
imagesTeddy[i] = getImage(getCodeBase(), "teddy" + i + ".jpg");
}
You should start at least one thread for every animation you create. In fact, you should start a thread anytime you create code that may require a lot of time and attention from the user's computer.
Individual processes, known as programs, are typically run sequentially, so if your word processor starts printing, you must wait for the printing to finish before continuing work with your document. However, in systems that support threads, individual processes are broken up into several executable subprocesses, or threads. In systems that support threads, such as Windows 95, Windows NT, and the Java Virtual Machine (Java VM), printing can run in a separate thread from document editing features. That way, as you print your document, you can get right back to work on your document. The tasks run parallel in separate threads.
To use threads in your applet, you must first change the signature of your applet class so that it implements the Runnable interface. For the teddy bear animation, you'll include the following signature:
public class DancingTeddy extends java.applet.Applet implements Runnable
The Runnable interface defines default thread behaviors.
Create a thread by defining an instance variable of the Thread class. Because you've used the Runnable interface for the dancing teddy bear applet, you'll pass the applet itself to the Thread object constructor. You'll use the following line to create a thread for your dancing teddy bear:
Thread threadTeddyAnimation = new Thread(this);
After you've created a thread, you need to start it when someone views your applet and stop it while they're not viewing your applet. In this way you conserve computer resources. You can override the Java applet start and stop methods with the following listing:
public void start(); public void stop();
The start method is called every time the applet is started. An applet starts after it's initialized by the init method. Unlike initialization, an applet can restart an indefinite number of times. An applet's stop method is called every time a reader leaves the page containing the applet. When the user returns to the page, the applet's start method is called. This can happen any number of times.
To start the dancing bear thread named threadTeddyAnimation running, override the start method and add Thread class's start method:
public void start()
{
if (threadTeddyAnimation != null)
threadTeddyAnimation.start();
}
First the thread is tested to be sure that it was successfully created.
Similarly, to stop the thread, override the stop method and add Thread class's stop method:
public void stop()
{
if (threadTeddyAnimation != null)
threadTeddyAnimation.stop();
}
Your applet is now threaded!
Finally, to run the animation you must override the run method and put in your animation code. The dancing teddy bear will start standing straight up on both hind limbs (Teddy0.jpg), then onto its left limb (Teddy1.jpg), back to both hind limbs (Teddy0.jpg), then onto its right limb (Teddy2.jpg), and then back to both hind limbs (Teddy0.jpg). The bear will do this for as long as the applet is running. The picture number pattern is {0, 1, 0, 2} and repeats over and over. As you can see in the run method, listed below, the array named iPictureNumber contains the pattern and a for loop that causes an infinite loop. The code loops through the picture number list and sets the imageCurrent variable to the appropriate teddy image. Repaint is called and the thread sleeps for 100 milliseconds (one tenth of a second).
public void run()
{
int iPictureNumber[] = {0, 1, 0, 2};
while (true)
{
for (int i = 0; i < iPictureNumber.length; i++)
{
imageCurrent = imagesTeddy[iPictureNumber[i]];
repaint();
try { Thread.sleep( 100 ); }
catch (InterruptedException e) { }
}
}
}
The call to the repaint method also calls the update method. The update method clears the screen and then calls the paint method. You must override the paint method to draw the current image, imageCurrent.
public void paint(Graphics g)
{
g.drawImage( imageCurrent, 0, 0, this);
}
You've now gone through a complete animation of a dancing teddy bear. You can find the complete listing in Listing 44.24. Don't forget to put the teddy images in the correct directory or folder with the class file.
Listing 44.24 Source Code for DancingTeddy.java
import java.awt.Graphics;
import java.awt.Image;
public class DancingTeddy extends java.applet.Applet
implements Runnable
{
Image imagesTeddy[] = new Image[3];
Image imageCurrent;
Thread threadTeddyAnimation = new Thread(this);
public void init()
{
for (int i=0; i < imagesTeddy.length; i++)
imagesTeddy[i] = getImage(getCodeBase(),
"teddy" + i + ".jpg");
}
public void start()
{
if (threadTeddyAnimation != null)
threadTeddyAnimation.start();
}
public void stop()
{
if (threadTeddyAnimation != null)
threadTeddyAnimation.stop();
}
public void run()
{
int iPictureNumber[] = {0, 1, 0, 2};
while (true)
{
for (int i = 0; i < iPictureNumber.length; i++)
{
imageCurrent = imagesTeddy[iPictureNumber[i]];
repaint();
try { Thread.sleep( 100 ); }
catch (InterruptedException e) { }
}
}
}
public void paint(Graphics g)
{
g.drawImage( imageCurrent, 0, 0, this);
}
}
If you ran the DancingTeddy applet from the previous section, you probably saw a lot of flicker. Each time an applet's window is updated (with a call to the update method), the entire applet window clears and fills in with the current background color. The graphics method is then called and the new image is drawn. This happened every tenth of a second in the DancingTeddy applet.
One relatively easy way to fight flicker is to override the update method so that the applet's window isn't cleared. Add the following to the DancingTeddy code in Listing 44.24:
public void update(Graphics g)
{
paint(g);
}
Notice that the update method takes a single argument, a Graphics object.
You should notice a dramatic reduction in flicker when you recompile DancingTeddy with the overridden update method. It works well because the same area of the screen is drawn to each time the paint method is called. However, if you're only updating portions of the applet's window, you'll run into trouble using this method.
Soon you'll use more advanced anti-flicker techniques, but first you need to create a more advanced animation. Using the same three teddy bear images, you'll now create an animated bear with more behaviors. The teddy bear will now move left, right, dance in place, or just stand.
The new applet, AnimatedTeddy, is similar to the DancingTeddy applet found in the last section except that the code in the overridden run method is completely changed, and three new methods are added: hop, dance, and sleep.
The hop method, listed below, supplies the teddy bear's hopping behavior. The hop method takes three arguments. The first two are the place where the bear will move from, iStart, and the place where it'll move to, iStop, all in x coordinates. The final argument, stringDirection, can either be "right" or "left" and tells the method the direction, relative to the bear, that the bear will hop:
void hop(int iStart, int iStop, String stringDirection)
{
boolean bGround = true;
if (stringDirection == "left")
imageCurrent = imagesTeddy[1];
else
{
imageCurrent = imagesTeddy[2];
iStart = -iStart;
iStop = -iStop;
}
for (int i = iStart; i < iStop; i += 10)
{
xPosition = Math.abs(i);
if (bGround)
{
yPosition = 50;
bGround = false;
}
else
{
yPosition = 40;
bGround = true;
}
repaint();
sleep( 100 );
}
}
The first line of hop method's body contains a boolean variable, bGround. When bGround is true, (its default value), it signifies that the teddy bear stands on the ground. Ground level is defined as a y value, yPosition, of 50.
The hop method tests the stringDirection variable to see if it equals left. If it does, then the imageCurrent is set equal to Teddy1.jpg, the image with the bear standing on its left hind limb. Otherwise, the bear is assumed to be moving right, so imageCurrent is set equal to Teddy2.jpg, the image with the bear standing on its right hind limb. The iStart and iStop variables are negated also. This is a convenient way to change the bear's hopping direction.
The bear's movement is controlled by the for loop. The index variable, i, is set equal to the value of iStart. The loop continues as long as i is less than the value of iStop. The index is incremented by 10 each time.
When the teddy bear is moving to the left, iStart is less than iStop and the numbers, as they increase, match the increasing number of pixels from left to right in Java's coordinate system. However, when the bear is moving to the right, iStart is greater than iStop. You could use a decrementing for loop for moving right, but it's easier to take the negative of iStart and iStop. This way the same for loop can be used because the value of iStart is now less than the value of iStop. The only problem is that the index represents the bear's x coordinate, in pixels, in the applet's window. This is solved by taking the absolute value of i, Math.abs(i), before setting it equal to xPosition.
Next, if bGround is true, the bear is standing on ground level and yPosition is equal to 50. bGround is set to false so that next time around the loop, the bear will hop up. If bGround is false, the bear has jumped up above the ground so the yPosition variable is set to 40. bGround is set to true so that next time around the loop, the bear will drop back to the ground.
Each time around the for loop, the repaint and then the sleep methods are called. The repaint method is the standard method that repaints the applet's window. The sleep method is new:
void sleep(int iTime)
{
try { Thread.sleep( iTime ); }
catch (InterruptedException e) { }
}
AnimatedTeddy's sleep method is a method that wraps Thread object's own sleep method. Pass the amount of time, in milliseconds, that you want the thread to sleep and it effectively pauses the animation for that amount of time.
The new dance method behaves just the same as the for loop in AnimatedTeddy except now there are two for loops. You pass the number of repetitions of the dance, iJigs, to the dance method. The first for loop repeats the basic dance the number of times defined by iJig:
void dance(int iJigs)
{
int iPictureNumber[] = {0, 1, 0, 2};
for (int i = 0; i < iJigs; i++)
{
for (int j = 0; j < iPictureNumber.length; j++)
{
imageCurrent = imagesTeddy[iPictureNumber[j]];
repaint();
sleep( 100 );
}
}
}
You now pull all of these new behaviors together in AnimatedTeddy's run method:
public void run()
{
while (true)
{
hop( 0, 300, "left");
dance( 5 );
imageCurrent = imagesTeddy[0];
repaint();
sleep( 1000 );
hop( 300, 600, "left");
hop( 600, 300, "right");
dance( 5 );
imageCurrent = imagesTeddy[0];
repaint();
sleep( 1000 );
hop( 300, 0, "right");
}
}
AnimatedTeddy's entire run method consists of an infinite loop created using the while statement. Inside the infinite loop, the teddy bear's various behaviors are laid out. The teddy starts from the left side of the applet's window and hops left to its approximate center. It then dances five jigs and then stands on both hind limbs for 1000 milliseconds. The bear then continues hopping left until it reaches the right (your right) side of the applet's window, then reverses direction and hops right back to the center of the window where it dances five jigs again, stands for 1000 milliseconds, and finally continues hopping right until it reaches the left (your left) side of the applet's window. The teddy bear repeats this dance indefinitely.
All of the code for the AnimatedTeddy applet is in Listing 44.25.
Listing 44.25 Source Code for AnimatedTeddy.java
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Color;
public class AnimatedTeddy extends java.applet.Applet
implements Runnable
{
Image imagesTeddy[] = new Image[3];
Image imageCurrent;
Thread threadTeddyAnimation = new Thread(this);
int xPosition = 0;
int yPosition = 50;
public void init()
{
setBackground(Color.white);
for (int i=0; i < imagesTeddy.length; i++)
imagesTeddy[i] = getImage(getCodeBase(),
"teddy" + i + ".jpg");
}
public void start()
{
if (threadTeddyAnimation != null)
threadTeddyAnimation.start();
}
public void stop()
{
if (threadTeddyAnimation != null)
threadTeddyAnimation.stop();
}
public void run()
{
while (true)
{
hop( 0, 300, "left");
dance( 5 );
imageCurrent = imagesTeddy[0];
repaint();
sleep( 1000 );
hop( 300, 600, "left");
hop( 600, 300, "right");
dance( 5 );
imageCurrent = imagesTeddy[0];
repaint();
sleep( 1000 );
hop( 300, 0, "right");
}
}
void hop(int iStart, int iStop, String stringDirection)
{
boolean bGround = true;
if (stringDirection == "left")
imageCurrent = imagesTeddy[1];
else
{
imageCurrent = imagesTeddy[2];
iStart = -iStart;
iStop = -iStop;
}
for (int i = iStart; i < iStop; i += 10)
{
xPosition = Math.abs(i);
if (bGround)
{
yPosition = 50;
bGround = false;
}
else
{
yPosition = 40;
bGround = true;
}
repaint();
sleep( 100 );
}
}
void dance(int iJigs)
{
int iPictureNumber[] = {0, 1, 0, 2};
for (int i = 0; i < iJigs; i++)
{
for (int j = 0; j < iPictureNumber.length; j++)
{
imageCurrent = imagesTeddy[iPictureNumber[j]];
repaint();
sleep( 100 );
}
}
}
void sleep(int iTime)
{
try { Thread.sleep( iTime ); }
catch (InterruptedException e) { }
}
public void paint(Graphics g)
{
g.drawImage( imageCurrent, xPosition, yPosition, this);
}
}
You'll notice the return of the dreaded flicker when you run AnimatedTeddy. You didn't override the update method used before because it doesn't work. Try it! You'll find bits of the old images that haven't been erased because you're moving the paint region around. There are two techniques to help solve this problem: clipping and double-buffering.
By default, the update method clears the whole applet window. Why clear everything when you only need to erase the teddy bear from the old position and redraw it in the new position? The two areas combined form only a small portion (generally 130×130 pixels) of the whole applet window (750×200 pixels). Clipping techniques give you the ability to update only that part of the applet window that needs updating.
You might think that you'll use the clipRect method from Graphics class to define a clipping area in the teddy bear applet. However, when you're drawing images to the screen, you must use the clearRect method from Graphics class which is
public abstract void clearRect(int x, int y, int width, int height);
The x and y parameters take the applet's window coordinates of the top left corner of the clipping rectangle. The width parameter takes the width, and the height parameter takes the height of the clipping rectangle. Everything inside this rectangle clears and everything outside it is left alone.
You must use clearRect rather than clipRect to display images because drawImage already restricts (or clips) its output to the size of the image itself. When you use the clipRect method, only the intersection between the rectangle defined in clipRect and the drawing rectangle updates. The result is just like overriding the update method as mentioned at the end of the last section; you're left with bits of the old images that haven't been erased!
To properly update the teddy bear image, you must repaint a rectangle that includes the union between the areas covered by the old the new images. You can do this with the clearRect method.
Start modifying the AnimatedTeddy applet by adding instance variables that hold the width (120 pixels) and height (120 pixels) of the three images, like the following
int imageWidth = 120; int imageHeight = 120;
You'll need to keep track of the x and y coordinates of the previously displayed image in addition to the current coordinates that you already track. You can save these old coordinates in the following instance variables:
int xOldPosition = 0; int yOldPosition = 50;
All other changes are made in the newly overridden update method and in the paint method.
Override the default behavior of the update method and define the clipping area in it so that, rather than clearing the whole screen every time update is called, only the clipped area clears. This is, unfortunately, a little complicated because our teddy bear moves in four directions. The clipping rectangle must always define the union of the areas of both the old and the new image position.
When the current image position along the x axis (xPosition) is greater than the previous image position (xOldPosition), the clipping rectangle's position starts at the old x coordinate and the rectangle's width is the new position minus the old position plus the image's width. This makes sense because the x coordinate always defines the left edge of a rectangle. Therefore, the x coordinate must always be at the left-most edge of the area that needs clearing (assuming that you use a positive width for your rectangle). Also, the width must always equal the distance between the left-most edge of the old image position and the left-most edge of the new image position plus the width of the image. You can see this in code in the first if statement in the update method:
public void update(Graphics g)
{
int xClip;
int yClip;
if (xPosition >= xOldPosition)
{
clipWidth = (xPosition - xOldPosition) + imageWidth;
xClip = xOldPosition;
}
else
{
clipWidth = (xOldPosition - xPosition) + imageWidth;
xClip = xPosition;
}
if (yPosition >= yOldPosition)
{
clipHeight = (yPosition - yOldPosition) + imageHeight;
yClip = yOldPosition;
}
else
{
clipHeight = (yOldPosition - yPosition) + imageHeight;
yClip = yPosition;
}
g.clearRect( xClip, yClip, clipWidth, clipHeight );
paint(g);
}
When the current image's x coordinate is less than the old image's x coordinate, the clipping rectangle must have an x coordinate equal to the current image's x coordinate. Again, this is because the current image's x coordinate is the left-most position. The same logic is behind the code for the y coordinates.
After all the clipping coordinates are figured out and placed into method variables xClip, yClip, clipWidth, and clipHeight, clearRect from Graphics class is called and the area defined by the clipping rectangle clears. Finally, the last line of code in the update method is a call to the paint method.
Only two lines are added to the paint method. Set the xOldPosition and yOldPosition instance variables equal to the recently current x and y positions:
public void paint(Graphics g)
{
g.drawImage( imageCurrent, xPosition, yPosition, this);
xOldPosition = xPosition;
yOldPosition = yPosition;
}
The complete listing of the updated applet is in Listing 44.26.
Listing 44.26 Source Code for AnimatedTeddyTwo.java
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Color;
public class AnimatedTeddyTwo extends java.applet.Applet
implements Runnable
{
Image imagesTeddy[] = new Image[3];
Image imageCurrent;
int imageWidth = 120;
int imageHeight = 120;
Thread threadTeddyAnimation = new Thread(this);
int xPosition = 0;
int yPosition = 50;
int xOldPosition = 0;
int yOldPosition = 50;
public void init()
{
setBackground(Color.white);
for (int i=0; i < imagesTeddy.length; i++)
imagesTeddy[i] = getImage(getCodeBase(),
"teddy" + i + ".jpg");
}
public void start()
{
if (threadTeddyAnimation != null)
threadTeddyAnimation.start();
}
public void stop()
{
if (threadTeddyAnimation != null)
threadTeddyAnimation.stop();
}
public void run()
{
while (true)
{
hop( 0, 300, "left");
dance( 5 );
imageCurrent = imagesTeddy[0];
repaint();
sleep( 1000 );
hop( 300, 600, "left");
hop( 600, 300, "right");
dance( 5 );
imageCurrent = imagesTeddy[0];
repaint();
sleep( 1000 );
hop( 300, 0, "right");
}
}
void hop(int iStart, int iStop, String stringDirection)
{
boolean bGround = true;
if (stringDirection == "left")
imageCurrent = imagesTeddy[1];
else
{
imageCurrent = imagesTeddy[2];
iStart = -iStart;
iStop = -iStop;
}
for (int i = iStart; i < iStop; i += 10)
{
xPosition = Math.abs(i);
if (bGround)
{
yPosition = 50;
bGround = false;
}
else
{
yPosition = 40;
bGround = true;
}
repaint();
sleep( 100 );
}
}
void dance(int iJigs)
{
int iPictureNumber[] = {0, 1, 0, 2};
for (int i = 0; i < iJigs; i++)
{
for (int j = 0; j < iPictureNumber.length; j++)
{
imageCurrent = imagesTeddy[iPictureNumber[j]];
repaint();
sleep( 100 );
}
}
}
void sleep(int iTime)
{
try { Thread.sleep( iTime ); }
catch (InterruptedException e) { }
}
public void update(Graphics g)
{
int xClip;
int yClip;
int clipWidth;
int clipHeight;
if (xPosition >= xOldPosition)
{
clipWidth = (xPosition - xOldPosition) + imageWidth;
xClip = xOldPosition;
}
else
{
clipWidth = (xOldPosition - xPosition) + imageWidth;
xClip = xPosition;
}
if (yPosition >= yOldPosition)
{
clipHeight = (yPosition - yOldPosition) + imageHeight;
yClip = yOldPosition;
}
else
{
clipHeight = (yOldPosition - yPosition) + imageHeight;
yClip = yPosition;
}
g.clearRect( xClip, yClip, clipWidth, clipHeight );
paint(g);
}
public void paint(Graphics g)
{
g.drawImage( imageCurrent, xPosition, yPosition, this);
xOldPosition = xPosition;
yOldPosition = yPosition;
}
}
You may be disappointed with the amount of flicker still detectable in the latest version of the teddy bear animation. You'll always need to use some sort of clipping method but it still isn't enough for advanced animation. You need clipping techniques, in conjunction with double-buffering, to really do the job.
For advanced animation, the best solution for fighting flicker is usually the technique called double-buffering. With double-buffering, you create an off-screen image and do all of your drawing to that off-screen image. Once you're finished drawing, you copy the off-screen image to the applet's window in one quick call.
Modify the AnimatedTeddyTwo applet to support double-buffering and eliminate flickering. Add the following instance variables for an off-screen image and its graphics context at the top of the AnimatedTeddyTwo class:
private Image imageOffScreen; private Graphics graphicsOffScreen;
Next, add the following line to the init method to create an off-screen Image object
imageOffScreen = createImage(size().width, size().height);
Follow the preceding line with the following line to create an off-screen graphics context for the off-screen image:
graphicsOffScreen = imageOffScreen.getGraphics();
Finally, follow the preceding line with the following two lines that set the image background to white:
graphicsOffScreen.setColor(Color.white); graphicsOffScreen.fillRect(0, 0, size().width, size().height);
You can delete the setBackground method from the init method. The setBackground method doesn't work when you use double-buffering.
Next, replace the g.clearRect method found at the end of AnimatedTeddyTwo applet's update method with the following three lines of code:
graphicsOffScreen.setColor(Color.white); graphicsOffScreen.clearRect( xClip, yClip, clipWidth, clipHeight ); graphicsOffScreen.fillRect(xClip, yClip, clipWidth, clipHeight);
You now call the off-screen graphics context's clearRect method. The two new lines appear because the background color must be specified and filled explicitly when using an off-screen graphics context.
The final changes are made to the paint method. Draw the current image, using the drawImage method, to the off-screen graphics context by adding the following line to the beginning of the paint method:
graphicsOffScreen.drawImage( imageCurrent, xPosition, yPosition, this);
Then display the off-screen image to the applet's window by modifying the g.drawImage method:
g.drawImage( imageOffScreen, 0, 0, this);
You've created a double-buffered version of the teddy bear animation. You'll find the code for the entire applet in Listing 44.27.
Listing 44.27 Source Code for AnimatedTeddyThree.java
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Color;
public class AnimatedTeddyThree extends java.applet.Applet
implements Runnable
{
Image imagesTeddy[] = new Image[3];
Image imageCurrent;
Image imageOffScreen;
Graphics graphicsOffScreen;
int imageWidth = 120;
int imageHeight = 120;
Thread threadTeddyAnimation = new Thread(this);
int xPosition = 0;
int yPosition = 50;
int xOldPosition = 0;
int yOldPosition = 50;
public void init()
{
imageOffScreen = createImage(size().width, size().height);
graphicsOffScreen = imageOffScreen.getGraphics();
graphicsOffScreen.setColor(Color.white);
graphicsOffScreen.fillRect(0, 0, size().width, size().height);
for (int i=0; i < imagesTeddy.length; i++)
imagesTeddy[i] = getImage(getCodeBase(),
"teddy" + i + ".jpg");
}
public void start()
{
if (threadTeddyAnimation != null)
threadTeddyAnimation.start();
}
public void stop()
{
if (threadTeddyAnimation != null)
threadTeddyAnimation.stop();
}
public void run()
{
while (true)
{
hop( 0, 300, "left");
dance( 5 );
imageCurrent = imagesTeddy[0];
repaint();
sleep( 1000 );
hop( 300, 600, "left");
hop( 600, 300, "right");
dance( 5 );
imageCurrent = imagesTeddy[0];
repaint();
sleep( 1000 );
hop( 300, 0, "right");
}
}
void hop(int iStart, int iStop, String stringDirection)
{
boolean bGround = true;
if (stringDirection == "left")
imageCurrent = imagesTeddy[1];
else
{
imageCurrent = imagesTeddy[2];
iStart = -iStart;
iStop = -iStop;
}
for (int i = iStart; i < iStop; i += 10)
{
xPosition = Math.abs(i);
if (bGround)
{
yPosition = 50;
bGround = false;
}
else
{
yPosition = 40;
bGround = true;
}
repaint();
sleep( 100 );
}
}
void dance(int iJigs)
{
int iPictureNumber[] = {0, 1, 0, 2};
for (int i = 0; i < iJigs; i++)
{
for (int j = 0; j < iPictureNumber.length; j++)
{
imageCurrent = imagesTeddy[iPictureNumber[j]];
repaint();
sleep( 100 );
}
}
}
void sleep(int iTime)
{
try { Thread.sleep( iTime ); }
catch (InterruptedException e) { }
}
public void update(Graphics g)
{
int xClip;
int yClip;
int clipWidth;
int clipHeight;
if (xPosition >= xOldPosition)
{
clipWidth = (xPosition - xOldPosition) + imageWidth;
xClip = xOldPosition;
}
else
{
clipWidth = (xOldPosition - xPosition) + imageWidth;
xClip = xPosition;
}
if (yPosition >= yOldPosition)
{
clipHeight = (yPosition - yOldPosition) + imageHeight;
yClip = yOldPosition;
}
else
{
clipHeight = (yOldPosition - yPosition) + imageHeight;
yClip = yPosition;
}
graphicsOffScreen.setColor(Color.white);
graphicsOffScreen.clearRect( xClip, yClip,
clipWidth, clipHeight );
graphicsOffScreen.fillRect(xClip, yClip,
clipWidth, clipHeight);
paint(g);
}
public void paint(Graphics g)
{
graphicsOffScreen.drawImage( imageCurrent,
xPosition, yPosition, this);
g.drawImage( imageOffScreen, 0, 0, this);
xOldPosition = xPosition;
yOldPosition = yPosition;
}
}
Loading images over the Web presents special problems. When you begin drawing, you may find that the image you want to draw hasn't completely arrived yet, due to slow network links. You can use the MediaTracker class to determine whether an image is ready for display. Using MediaTracker gives you the ability to do other things in the applet's window, such as display text, until the image is ready for display.
The MediaTracker class is defined in Java's awt package. Be sure to properly import the MediaTracker class when you use it in your code. Include the following line at the beginning of your file:
import java.awt.MediaTracker;
To use the MediaTracker class, you must create a MediaTracker object in your applet:
MediaTracker myTracker = new MediaTracker(this);
Next, begin retrieving the image that you want to display:
Image myImage = getImage("teddy0.jpg");
Now tell the MediaTracker object to monitor, or track, the image's progress. You give the image an ID number when you pass it to the MediaTracker object. For instance, you might track myImage and give it an ID of 0:
myTracker.addImage(myImage, 0);
Once you start tracking an image, you can load it and wait for it until it's ready by using the waitForID method:
myTracker.waitForID(0);
You can also wait for all images using the waitForAll method:
myTracker.waitForAll();
You may not want to take the time to load an image before starting your applet. You can use the statusID method to initiate a load asynchronously. When you call statusID, you pass an image ID and a boolean flag that indicates if the image should be loading. If you pass true, the image begins to load:
myTracker.statusID(0, true);
A companion to statusID is statusAll, which checks the status of all images tracked by the MediaTracker object:
myTracker.statusAll(true);
The statusID and statusAll methods return an integer that is made up of the following flags:
You can also use checkID and checkAll to see if an image was successfully loaded. All the variations of checkAll and checkID return a boolean value that is true if all the images checked were loaded:
boolean checkID(int id);
This returns true if all images with a specific ID have been loaded. It does not start loading the images if they are not loading already:
boolean checkID(int id, boolean startLoading);
This returns true if all images with a specific ID have been loaded. If startLoading is true, it initiates the loading of any images that are not already loading:
boolean checkAll();
This returns true if all images being tracked by this MediaTracker have been loaded, but does not initiate loading if an image is not being loaded:
boolean checkAll(boolean startLoading);
This returns true if all images being tracked by this MediaTracker have been loaded. If startLoading is true, it initiates the loading of any images that have not yet started loading.
The applet in Listing 44.28 uses the MediaTracker to watch for an image to complete loading. It draws text in place of the image until the image is complete, and then it draws the image.
Listing 44.28 Source Code for ImageTracker.java
import java.awt.MediaTracker;
import java.awt.Image;
import java.awt.Graphics;
public class ImageTracker extends java.applet.Applet
implements Runnable
{
Thread threadAnimation;
int waitCount;
MediaTracker myTracker;
Image myImage;
public void init()
{
myImage = getImage(getDocumentBase(), "teddy0.jpg");
myTracker = new MediaTracker(this);
myTracker.addImage(myImage, 0);
}
public void run()
{
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
while (true)
{
waitCount++;
if (waitCount == 10)
{
myTracker.checkID(0, true);
}
repaint();
try {Thread.sleep(1000);}
catch (Exception sleepProblem) { }
}
}
public void paint(Graphics g)
{
if (myTracker.checkID(0))
{
g.drawImage(myImage, 0, 0, this);
}
else
{
g.drawString("Image goes here", 0, 30);
}
}
public void start()
{
threadAnimation = new Thread(this);
threadAnimation.start();
}
public void stop()
{
threadAnimation.stop();
threadAnimation = null;