ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Java Swing, 2nd Edition

JFC Swing: The SpringLayout Class

by Marc Loy, coauthor of Java Swing, 2nd Edition
09/11/2002

With SDK 1.4, a new -- but not really new -- layout manager was added. The SpringLayout manager uses the notion of springs and struts to keep everything in place. A version of SpringLayout existed in the early alpha and betas of the Swing package, but it was not included because the Swing team felt it still needed too much work. While it still needs a bit of work, it has come a long way; its inclusion in SDK 1.4 is a testament to that progress. The class diagram for SpringLayout and its helpers is shown in Figure 1.

Before you dive too deeply into this layout manager, you should know that its purpose in life is to aid GUI builders and other code-generating tools. It can certainly be hand-coded -- and we have the examples to prove it -- but you'll often leave this layout manager to the aforementioned tools. If you want a flexible replacement for the GridBagLayout, you might want to take a look at the RelativeLayout manager written by Jim Elliott. The complete package, with docs, tutorial, and source code, can be found on the Java Swing, 2nd Edition book Web page.


Figure 1. The SpringLayout manager classes.

Source Files

The source files for this article, FractionSpring.java and CompassButtons.java are available in this zip file.

Springs and Struts

Now that you're here for the long haul, let's look at the core of the SpringLayout manager's approach to component layout: springs and struts.

With SpringLayout at the helm, you use springs and struts to specify the bounds (x, y, width, height) of all of your components. (You could mimic the null layout manager by using only struts.)

The not-so-obvious big win in this layout manager is that springs can be anchored between the edges of components and will maintain their relationship even when the container is resized. This makes it possible to create layouts that would be difficult in other managers. While you could probably use a grand GridBagLayout to do the trick, SpringLayout should provide better performance once it's all fixed up and finalized.

Figure 2 shows a simple application that uses SpringLayout. We position directional buttons over a large picture for navigation. Notice how the North button stays horizontally centered and anchored to the top edge of the application after we resize the frame. The other buttons behave similarly. Just to reiterate, you could certainly accomplish this with nested containers or a properly constructed GridBagLayout; the SpringLayout should simply prove to be the most maintainable over the long haul. We'll look at the source code for this example after we examine the API in more detail.


Figure 2. A SpringLayout managed container at two different sizes.

Constants

The SpringLayout class thinks of components in terms of their edges. Several constants have been defined for the edges, as shown in Table 1.

Table 1. SpringLayout Constants.

Constant Type Description
NORTH String The top edge of the component. Corresponds to the y value of the component's bounding box.
SOUTH String The bottom edge of the component. Corresponds to the y value of the bounding box plus the height of the component.
WEST String The left edge of the component. Corresponds to the x value of the component's bounding box.
EAST String The right edge of the component. Corresponds to the x value of the bounding box plus the width of the component.

Constructors

The only constructor for SpringLayout is the default constructor. Similar to the way one uses GridBagLayout and CardLayout, you'll want to keep a reference to your SpringLayout manager handy.

Create a new SpringLayout manager:

public SpringLayout()

Constraint Methods

As with other layout managers, a majority of the methods in SpringLayout are devoted to meeting the contract of the LayoutManager and LayoutManager2 interfaces. The methods that make this manager interesting, however, are the methods dealing with components' constraints.

The following method returns the entire set of constraints (the springs on all four edges) for the given component. We discuss the Constraints inner class in the next section.

public SpringLayout.Constraints getConstraints(Component c)

The following method returns a particular spring for the specified edge (edgeName) of the given component (c).

public Spring getConstraint(String edgeName, Component c)

These methods place a constraint (spring) between two edges. The first method is just a convenience method that uses pad to create a strut. e1 and c1 are associated with the dependent component, while e2 and c2 refer to the anchor.

public void putConstraint(String e1, Component c1, int pad, String e2, Component c2)
public void putConstraint(String e1, Component c1, Spring s, String e2, Component c2)

No method exists for setting all of the constraints at one time, but you can use the object returned by the getConstraints() method to manipulate all of the edges on a component.

Here's the source code for the example application shown in Figure 2. Notice the three primary means of positioning components in a SpringLayout. We add the North and South buttons to the container with prebuilt constraints. The East and West buttons are positioned by retrieving their existing constraints and setting up the bounding box for the component. For example, the North and East buttons are set up like this:

// Add north button
c.add(nb, new SpringLayout.Constraints(northX, offsetS, widthS, heightS));

// Add east button
c.add(eb);
sl.getConstraints(eb).setX(eastX);
sl.getConstraints(eb).setY(eastY);
sl.getConstraints(eb).setWidth(widthS);
sl.getConstraints(eb).setHeight(heightS);

As an example of the third mechanism for positioning components, the viewport for the graphics image is laid out using several putConstraint() calls:

c.add(viewport);

// The order here is important...need to have a valid width and height
// in place before binding the (x,y) location
sl.putConstraint(SpringLayout.SOUTH, viewport, Spring.minus(borderS), 
SpringLayout.SOUTH, c);

sl.putConstraint(SpringLayout.EAST, viewport, Spring.minus(borderS), 
SpringLayout.EAST, c);

sl.putConstraint(SpringLayout.NORTH, viewport, topBorder,
SpringLayout.NORTH, c);

sl.putConstraint(SpringLayout.WEST, viewport, leftBorder,
SpringLayout.WEST, c);

You might notice some funky springs in this example. We'll explain the centering spring and the sum() and minus() methods in the section on the Spring class itself.

The Constraints Inner Class

SpringLayout.Constraints embodies the spring constraints placed on a single component in a SpringLayout-managed container. It holds the bounding box for a component, but it uses Spring references rather than ints for the x, y, width, and height properties.

Properties

The Constraints inner class consists entirely of the properties shown in Table 2. With the exception of the constraint property, these properties mimic the Rectangle class often used to describe the bounds of components. The constraint property is indexed by edge name (a String). See Table 1, SpringLayout Constants, for a list of valid edge names and their respective relationships to the x , y , width, and height properties.

Table 2. SpringLayout.Constraints Properties.

Property Data Type get is set Default Value
constraint i Spring *   * null
height Spring *   * null
width Spring *   * null
x Spring *   * null
y Spring *   * null

i is indexed (by String values, see Table 1).

Constructors

There are several constructors for building a Constraints object. You might want to do this if you intend to build the constraints before adding the component to its container. (You can use the Container.add(Component, Object) method to accomplish this.)

public SpringLayout.Constraints()
public SpringLayout.Constraints(Spring x, Spring y)
public SpringLayout.Constraints(Spring x, Spring y, Spring width, Spring height)

These constructors all build Constraints objects. Any unspecified property is left as a null value. The first constructor creates a completely empty Constraints object, while the second one leaves the width and height properties null.

The Spring Class

So what are these Spring objects we keep seeing everywhere? As stated earlier, they are essentially a collection of three values: a minimum, a maximum, and a preferred value. You could use a spring to describe the height property of a text area, for example. Its minimum is 25 pixels; its maximum is the height of the screen (say, 1024), and its preferred height is 8 rows, or 200 pixels. By the same token, you can create a strut by specifying a spring with identical values for all three properties; for example, a text field with 25 for its minimum, maximum, and preferred heights.

Beyond the basic expandability of a spring, you can do some fun things with them. Springs can be manipulated using mathematical concepts (and achieve a semantically-correct result). For example, you can add two springs together. The new spring consists of the sum of the minimums, the sum of the preferred values, and the sum of the preferred maximums. You can also negate a spring and effectively multiply each of its values by -1. Summing with a negated spring, then, becomes a difference operation.

Now here's where things get really interesting. When you sum two springs, the result is not calculated immediately. The resulting spring stores a reference to the two operand springs. When needed, the summed spring queries its sub-springs. This has the practical upshot of making springs dynamic. If you change one spring in a sum, the sum changes, too. This turns out to be very useful in layout managers. Attach a spring to the bottom of the container, and it stretches whenever the container stretches.

Consider the y property for the South button in the application shown in Figure 2. We can use subtraction to keep the button about 50 pixels above the bottom of the frame, even after the frame has been resized.

Spring offset = Spring.sum(buttonHeight, Spring.constant(50));
Spring southY = Spring.sum(bottom, Spring.minus(offset));

Figure 3 shows the details of each spring behind this button. (You can refer back to the code for the syntax on creating all of the springs you see in the diagram.)


Figure 3. Constructing the x, y, width, and height springs (and struts) for the South button.

Constants

Only one constant is defined for the Spring class, as shown in Table 3.

Table 3. Spring Constants.

Constant Type Description
UNSET int An indicator that this property has not yet been calculated. (It's internal value is Integer.MIN_VALUE.) Can be used as a notification to recalculate any cached values.

Properties

The properties for Spring, shown in Table 4, are fairly straightforward. In addition to the minimum, maximum, and preferred properties, you can read and write the current value of the spring. Notice that the minimum, maximum, and preferred properties are read-only. You can extend Spring if you need to have a more dynamic spring.

Table 4. Spring Properties.

Property Data Type get is set Default Value
maximum int *     UNSET
minimum int *     UNSET
preferred int *     UNSET
value int *   * UNSET

Creating Springs

The default constructor for Spring is protected. It's obviously meant for subclasses. To create a spring, you use the factory-style constant() methods.

The following method creates a strut. The minimum, preferred, and maximum properties are all set to pref.

public static Spring constant(int pref)

The following method creates a spring. The minimum, preferred, and maximum properties are set from min, pref, and max, respectively.

public static Spring constant(int min, int pref, int max)

Manipulation Methods

Three manipulations are defined for springs. Recall that the calculated values are not actually hard-coded into the resulting springs. References to s1 and s2 are stored, so that if either spring changes, the resulting springs also change. (This is done with the help of a private inner class extension of Spring known as a "proxy spring.")

This method returns a new spring the properties of which represent the Math.max() of the respective properties of s1 and s2. In other words, the minimum property of the new spring is the max of s1.getMinimum() and s2.getMinimum().

public static Spring max(Spring s1, Spring s2)

This method returns a new spring, the properties of which represent the negative of the respective properties of s1. In other words, the minimum property of the new spring is -s1.getMinimum().

public static Spring minus(Spring s)

This method returns a new spring, the properties of which represent the sum of the respective properties of s1 and s2. In other words, the minimum property of the new spring is equal to s1.getMinimum() + s2.getMinimum().

public static Spring sum(Spring s1, Spring s2)

Other Operations

As mentioned earlier, you can combine the manipulation methods to create other operations. In particular, Spring.sum(s1, Spring.minus(s2)) returns the difference of the respective properties in s1 and s2. The Math.min() function can be mimicked using Spring.minus(Spring.max(Spring.minus(s1), Spring.minus(s2))). Try it on paper -- it really works!

Arranging Components

The combination of Spring math operations and constraints can make certain layouts easy to create (and easy to manipulate). That's "easy" in the "if you're designing a GUI builder" sense, of course. Figure 4 shows four buttons laid out in a vertical row with various Spring constraints holding them in place.


Figure 4. Vertically stacked buttons in a SpringLayout.

To show off more of the Spring constraint combinations, we varied the layout code for these buttons:

// We'll leave all buttons at their preferred widths and heights

// b1 gets placed at (10,10)
c.add(b1);
sl.getConstraints(b1).setX(offsetS);
sl.getConstraints(b1).setY(offsetS);

// b2 gets placed at (10, offset + b1.height + offset)
c.add(b2);
sl.getConstraints(b2).setX(offsetS);
sl.getConstraints(b2).setY(Spring.sum(Spring.sum(offsetS,
sl.getConstraints(b1).getHeight()), offsetS));

// b3 gets placed at (10, b2.south + offset)
c.add(b3);
sl.getConstraints(b3).setX(offsetS);
sl.getConstraints(b3).setY(Spring.sum(offsetS,
sl.getConstraint(SpringLayout.SOUTH, b2)));

// b4 gets placed at (b3.west, b3.south + offset)
c.add(b4);
sl.putConstraint(SpringLayout.WEST, b4, 0, SpringLayout.WEST, b3);
sl.putConstraint(SpringLayout.NORTH, b4, offsetS, SpringLayout.SOUTH, b3);

You could use any one of the techniques shown on all of the buttons, if you were so inclined. That said, there are a few consequences to the constraints we created in this example. For example, look at the y constraint of b2. It is simply the sum of two offsets and the height of b1. It is not dependent on the bottom edge of b1. It doesn't care where b1 is placed. The y constraints of b3 and b4, however, are dependent. If b2 moves down, so does b3 -- and if b3 moves down, so does b4.

One other fun constraint in this example is the left edge of b4. We tied it to the left edge of b3. If you change the x constraint of b3, b4 follows.

Custom Springs

Related Reading

Java Swing
By Marc Loy, Robert Eckstein, Dave Wood, James Elliott, Brian Cole

There are some things that cannot be duplicated using sum(), minus(), and max(). For those things, you can simply extend the Spring class. The compass navigation example in Figure 2 keeps the North and South buttons horizontally centered. (Of course, the East and West buttons are vertically centered.) To keep the buttons centered, even after the user resizes the frame, we need a new spring that returns the center of a parent spring. That can be built as we do in the FractionSpring class.

If you look a bit closer, this class can actually handle any multiplier value. The factory method half() produces the spring we need most often, but you can use the public constructor to supply an alternative. You could certainly write other factory methods for common values you find useful. Maybe a goldenMean() method is in your future?

One method you want to pay attention to is setValue(). In several derived springs (like our FractionSpring), the setValue() call does not make sense. Normally, we throw an UnsupportedOperationException to indicate that this required method from our abstract parent does not really apply; however, the special value UNSET can be used to help in a particular scenario: value caching.

If the current value of the spring comes from an expensive operation, you can cache that value. If your spring is bound to another spring, such as the border of your container, changing the size of the container causes a chain of UNSET values to be passed to dependent springs. You could watch for UNSET and recalculate your spring values only when you receive it. (Try uncommenting the println() in our setValue() method and rerun the example. You should see four UNSET calls each time you resize the frame -- one for each button.)

While it may take a few tries to get your brain around SpringLayout and spring math, you can accomplish some complex layouts with one container and a single, efficient layout manager.

Marc Loy is a trainer and media specialist in Cincinnati, OH. When he's not working with digital video and DVDs, he's programming in Java.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.