Book Home Java Enterprise in a Nutshell Search this book

4.5. Stroking Lines

One of the new graphic attributes defined by Java 2D is the java.awt.Stroke; it is set with the setStroke() method of a Graphics2D object. The Stroke attribute is used by Java 2D whenever it draws a line. Conceptually, the Stroke describes the pen or brush that is used to draw the line: it controls all line-drawing attributes, such as line width and dash pattern. Java 2D defines a single implementation of the Stroke interface, java.awt.BasicStroke, that is suitable for almost all line drawing needs.

4.5.1. BasicStroke

A BasicStroke object encapsulates several different line drawing attributes: the line width, the dash pattern, the end cap style for the line, and the join style for the line. You specify values for these attributes when you call the BasicStroke() constructor. BasicStroke objects are immutable, so that they can be safely cached and shared. This means, however, that they don't have set() methods that allow you to change the attribute values.

The line-width attribute specifies (obviously) the width of the line. This line width is measured in units of user space. If you are using the default coordinate system, then user space equals device space, and line widths are measured in pixels. For backward compatibility, the default line width is 1.0. Suppose you want to draw the outline of a circle of radius 100, using a line that is 10 units wide. You can code it like this:

Graphics2D g;                                       // Initialized elsewhere
Shape circle = new Ellipse2D.Float(100.0f, 100.0f,  // Upper-left corner
                                   300.0f, 300.0f); // Width and height
g.setStroke(new BasicStroke(10.0f));                // Set line width
g.draw(circle);                                     // Now draw it

The end-cap attribute specifies how the ends of lines are drawn, or, more specifically, what type of end caps are placed at the end of lines. There is no analogous line attribute in AWT prior to Java 2D, as end caps are necessary only for lines that are more than one-pixel wide. If you are not familiar with end caps, look at Figure 4-2, as they are best explained visually. This figure shows what lines look like when drawn with each of the three possible end cap styles.


Figure 4-2. BasicStroke end-cap styles

The BasicStroke.CAP_BUTT constant specifies that the line should have no end cap. The CAP_SQUARE constant specifies a rectangular end cap that projects beyond the end point of the line by a distance equal to half of the line width; this is the default value for the end-cap attribute. CAP_ROUND specifies a semicircular end cap, with a radius equal to half of the line width.

The join-style attribute is similar to the end-cap attribute, except that it applies to the vertex where two lines join, rather than to the end of a line. Like the end-cap attribute, the join-style attribute is necessary only with wide lines and is best understood visually. Figure 4-3 illustrates this BasicStroke attribute. Note that the join style attribute is used only when drawing a shape that includes multiple line segments, not when two intersecting lines are drawn as separate shapes.


Figure 4-3. BasicStroke join styles

The default join style is a mitered join, represented by the BasicStroke.JOIN_MITER constant. This value specifies that lines are joined by extending their outer edges until they meet. The JOIN_BEVEL constant specifies that lines are joined by drawing a straight line between the outside corners of the two lines, while JOIN_ROUND specifies that the vertex formed by the two lines should be rounded, with a radius of half the line width. To use cap style and join style, you can use code like this:

g.setStroke(new BasicStroke(5.0f,                     // Line width
                            BasicStroke.CAP_ROUND,    // End-cap style
                            BasicStroke.JOIN_ROUND)); // Vertex join style

When you use the JOIN_MITER style to join two lines that have a small angle between them, the miter can become quite long. To avoid this situation, BasicStroke includes another attribute known as the miter limit. If the miter would be longer than this value times half of the line width, it is truncated. The default miter limit is 10.0.

The dash pattern of a line is actually controlled by two attributes: the dash array and the dash phase. The dash array is a float[] that specifies the number of units to be drawn followed by the number of units to be skipped. For example, to draw a dashed line in which both the dashes and spaces are 25 units long, you use an array like:

new float[] { 25.0f, 25.0f }

To draw a dot-dash pattern consisting of 21 on, 9 off, 3 on, and 9 off, you use this array:

new float[] { 21.0f, 9.0f, 3.0f, 9.0f }

Figure 4-4 illustrates these dashed-line examples. The end-cap style you specify is applied to each dash that is drawn.


Figure 4-4. BasicStroke dash patterns

If, for some reason, you want to draw a dashed line but do not want your line to begin at the beginning of the dash pattern, you can specify the dash-phase attribute. The value of this attribute specifies how far into the dash pattern the line should begin. Note, however, that this value is not an integer index into the dash pattern array. Instead, it is a floating-point value that specifies a linear distance.

To draw a dashed line, you must use the most complicated BasicStroke() constructor and specify values for all attributes. For example:

Stroke s = new BasicStroke(4.0f,                      // Width
                           BasicStroke.CAP_SQUARE,    // End cap
                           BasicStroke.JOIN_MITER,    // Join style
                           10.0f,                     // Miter limit
                           new float[] {16.0f,20.0f}, // Dash pattern
                           0.0f);                     // Dash phase

4.5.2. How a Stroke Works

The BasicStroke class is sufficient for most drawing needs, so it is unlikely that you will ever need to implement the Stroke interface yourself. Nevertheless, the Stroke interface defines only a single createStrokedShape() method, and it is instructive to understand what this method does.

In Java 2D, filling an area is a more fundamental operation than drawing (or stroking) the outline of a shape. The Stroke object is the link between the two operations; it makes it possible to implement the draw() method using the fill() method.

Recall the code that we just used to draw the outline of a circle. The draw() method has to draw the outline of the circle using only the fill() method. If it simply calls fill() on the circle, it ends up creating a solid disk, not the outline of a circle. So instead, draw() first passes the circle to the createStrokedShape() method of the BasicStroke object we've specified. createStrokedShape() returns a new shape: a circle of radius 105, minus a concentric circle of radius 95. The interior of this shape is the area between the two circles, a region that always has a width of 10 units. Now draw() can call fill() on this stroked shape to draw the 10-unit-wide outline of the original circle. (We'll discuss the fill() operation and the graphics attributes that it uses in the next section.)

Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.