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


Java Swing, 2nd Edition

Inside RelativeLayout

by James Elliott, coauthor of Java Swing, 2nd Edition
11/27/2002

As promised in my first article, "RelativeLayout: A Constraint-Based Layout Manager," here's a look inside the RelativeLayout package. This article explains how the layout manager works, and discusses how to extend it to support new kinds of constraints. Readers should be familiar with the original article, which introduces RelativeLayout and explains how to use it as a tool.

What's Inside?

Once you download and expand the source archive, you'll find the following items inside of it (Figure 1 shows everything it will contain once you're ready to build and run RelativeLayout):


Figure 1: RelativeLayout Source

The File build.xml

This is a build file for the Ant tool from Apache's Jakarta project. It is used to compile and test RelativeLayout. Once you have installed Ant on your system (which you have likely done already, since it has rapidly and deservedly become the build tool of choice for Java projects) you can compile RelativeLayout simply by moving to the top-level source directory and typing ant compile (after you've set up the lib directory as described below).

Other interesting build targets you can run include:

The Files example2.xml and example3.xml

These files are used by the XML-based examples in the first article. They contain the layout constraints used by the second and third example programs.

The Directory lib

Contains libraries used by RelativeLayout. It's empty when you first download and expand the source archive, because these libraries are available from separate organizations. In order to compile and use RelativeLayout, you'll need the JDOM library and (if you're using a Java SDK earlier than version 1.4) an XML parser such as Apache Xerces, as discussed in the first article. Once you've downloaded any libraries you need (which you likely did in order to run the examples when reading Part 1), copy their library jars (e.g. jdom.jar and xerces.jar) into the lib directory, and RelativeLayout will compile and run properly.

The File test.xml

I used this file along with a test program while I was developing RelativeLayout. It's not too useful now, unless you want to study and play with that test program. Note that the current configuration of the program (invoked through ant test) and this file are inconsistent and cause an over-constraint error to be reported. If you're into that sort of thing, debugging and fixing the problem could be an interesting exercise.

The Directory src

The rest of the source is organized under the src directory, so let's move in there and see what we find.

Related Reading

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

The Source

The source code for the classes that make up RelativeLayout includes extensive JavaDoc and internal comments that explain how they work. Rather than trying to reproduce that information, this article provides an overview of the relationships between the classes and how they work together. The goal is to provide you with a framework for understanding the architecture, and a starting point for delving into the documentation and source itself. As noted above, you'll find all of this source code in the directory src/com/brunchboy/util/swing/relativelayout and you should refer to it as you read this overview.

Here is a class diagram, in the style we use in Java Swing, that should help visualize the high-level relationships between the classes and interfaces I'm about to introduce:

Enumerations

There are two type-safe enumerations used throughout RelativeLayout to represent information about constraint and component attributes. The type-safe enumeration pattern is a great way of representing a fixed list of values in Java. It is covered in depth in Joshua Bloch's Effective Java Programming Language Guide (Addison Wesley Professional), and Sun has also made this section, "Replace Enums with Classes," available online.

In RelativeLayout, the AttributeAxis class represents the two axes on which component attributes can be defined (HORIZONTAL and VERTICAL), while the AttributeType class defines the eight attributes that position a component within a layout (LEFT, RIGHT, WIDTH, HORIZONTAL_CENTER, TOP, BOTTOM, HEIGHT, and VERTICAL_CENTER).

Both enumerations provide toString() methods so that their members can be printed out to show descriptive information, and static getInstance() factory methods that look up the instance corresponding to a name (which comes in very handy when building constraints from XML specifications).

AttributeType also has several other features, illustrating how an enumeration can grow into a functional class. Its members know, through the deriveValue() method, how to "fill in" missing values using other component attributes -- for example, WIDTH can be calculated if you have the LEFT and RIGHT attributes. Each attribute type also knows the AttributeAxis on which it applies, and AttributeType provides lists of all attributes defined for each axis. All of this makes it easier for ComponentSpecifications and DependencyManager to do their bookkeeping.

Interfaces

Constraints between components in RelativeLayout are expressed using the Constraint interface. The idea, as you'd expect, is to nail down exactly what is needed out of a constraint, so that multiple implementations with different details can evolve. As originally posted, RelativeLayout provides two different kinds of constraint, but you're free to come up with new ones if you have an interesting idea, as described below.

There are only two methods a Constraint implementation needs to support: getDependencies() returns a List containing any attributes whose values must be determined before this constraint's value can be calculated, and getValue() performs that calculation, assuming the dependencies have already been resolved. The discussion of the two built-in Constraint implementations (below) provides examples of how these methods are implemented.

In order to keep Constraint from needing to know any details of how the rest of the dependency manager and layout manager are implemented, getValue() relies on another interface, AttributeSource, to provide it with the attribute values it needs when it is calculating its value. In other words, it's up to the object calling getValue() on a constraint to know how to feed it the values of any attributes used in the calculation. RelativeLayout keeps track of all known constraints and attributes, and thus is easily able to act as the AttributeSource when it needs to ask one of those constraints to calculate its value.

Attribute

Armed with these enumerations and interfaces, we're ready to build the Attribute class. Attribute is a simple class that aggregates some pieces of information to uniquely identify an attribute being managed by RelativeLayout: an AttributeType and the name of the Component to which the attribute applies. It also has some methods that ensure instances can properly be compared to see if they're equal (represent the same attribute type of the same component), and can be stored efficiently and correctly in hash tables.

Although not very interesting in and of itself, Attribute makes the code of the remaining classes simpler to write and easier to understand.

The Key Bookkeeping Classes

There are two classes that do most of the "heavy lifting" in RelativeLayout. ComponentSpecifications keeps track of all of the important details associated with an individual component, and DependencyManager figures out the order in which all constraints need to be calculated so that each one can be sure that any attributes it relies on will be ready for it. These classes work closely with each other.

ComponentSpecifications is in charge of keeping track of all of the Constraints that have been set up for a particular component, and asking those constraints to resolve themselves when the DependencyManager determines that the time is right. It also helps the DependencyManager identify the attributes a given component will need to have resolved before its own constraints can be calculated. Each component being laid out within RelativeLayout will have its own ComponentSpecifications instance managing its constraints.

During layout, as constraints are resolved, their computed values are also tracked. ComponentSpecifications interact closely with their associated components to actually size and position them once all of the attributes have been resolved, and to figure out the size the components would take up if "left to their own devices" (for example, if a component has been assigned a constraint that centers it in the window without affecting its size). Also, when trying to estimate a minimum possible size for the layout (such as when the user is trying to shrink a window), the component will be asked for its minimum size.

There is one extra instance of ComponentSpecifications created to represent the container being laid out. This acts as the "root" set of attributes used by all constraints, since the height and width of the container are known when layout occurs, and used to calculate the positions and sizes of all of the other components.

Finally, ComponentSpecifications makes sure that a reasonable set of constraints have been supplied for its component. It throws an exception if an attempt is made to over-constrain its component on either axis, or if layout is attempted before there are sufficient constraints on both axes to uniquely determine the layout of the component. The introduction to Attributes and Constraints in the first article detailed these requirements.

DependencyManager, on the other hand, is in charge of the "big picture." Trying to figure out the value for a constraint that can depend on other constraints (and so on), in a potentially lengthy chain, might seem dauntingly complex. In the end, though, all constraints must end up depending on some known value, or layout would be impossible. So there must be some set of attributes that can be calculated immediately -- the ones that are defined only in terms of the container itself. Once these are resolved, there is another set of attributes, which depend only on the ones we just calculated, that we can now calculate. And so on, until all attributes have been resolved.

If only we knew the right order in which to calculate constraints, we'd never be faced with a request for a value that we didn't already know. And it's the role of DependencyManager to figure out that order, making the process of resolving the constraints a straightforward one. Even this is easier than you might expect. As each Constraint is added to RelativeLayout, it is queried for its dependencies, and these are all registered with the DependencyManager. Using an inner class called Node (the name gives away the computer-science-geeky nature of this part of the system), the dependencies are organized into a set of dependency trees.

Each Node instance represents the dependencies on a single Attribute. It stores a list of every "dependent" attribute that needs to wait for this "anchor" attribute to be figured out before it can itself be calculated. Here's a sketch of part of the dependency trees that get built for Example 1. In this sketch, attributes are represented using the shorthand "componentName.attributeType". Recall that the attributes of the container in which layout is being performed use a special component name of "_container", accessible through the constant DependencyManager.ROOT_NAME.

The arrows coming out of a node represent the dependents of that node. In this case, the HORIZONTAL_CENTER attributes of both the title and ok components depend on the HORIZONTAL_CENTER of the container, while the TOP of version depends on the BOTTOM of title, which in turn depends on the TOP of title and finally, the TOP of the container. (If you're really familiar with the example, you may wonder where the dependency from the bottom to the top of title came from -- it wasn't a constraint we supplied. This is an example of a "derived dependency": given any two attributes for the same component on a particular axis, it is possible to calculate the others. In this case, from the title's TOP, which we've constrained, and its HEIGHT, which is determined by the component itself, we can calculate the BOTTOM. ComponentSpecifications knows about these derived dependencies, and reports them to the DependencyManager, so they show up in the trees.

Given the complete set of dependency trees represented as Node graphs, DependencyManager can check that it has a valid and usable set of dependencies, and sort them in the order in which they need to be resolved. It starts by figuring out which nodes are roots, which means they have no arrows pointing at them. Each node has a "reference count" to help in this process. To begin with, each is set to zero. Then all of the nodes are examined. If a node has any dependents, the reference count of each dependent is incremented (the more arrows that point to a node, the higher its reference count). When this is done, any nodes whose reference counts are still zero are the roots. Any node that turns out to be a root had better belong to the special "_container" component, the attributes of which are known in advance, or it will be impossible to ever resolve. If any such rogue roots are detected, DependencyManager throws an exception, reporting that the dependency graph is under-constrained. If any node is found that depends on itself (points to itself), an exception is thrown as well, because circular dependencies can't be resolved.

The next step is a more thorough test for circular dependencies. Starting with each of the roots, the trails of arrows are followed, counting the number of hops that have been taken. If, during this process, the length of the path from a root ever gets longer than the total number of nodes, this means that there must be a loop in the tree. If this happens, an exception is thrown to report the circular dependency.

If we've survived this far, it means that each individual component is happy about its constraints, and the DependencyManager has found a valid set of dependency trees, so layout can proceed. All that remains is to sort the nodes (which correspond to attributes) in the order in which they can be calculated. It turns out that the reference count we came up with for finding roots helps here, too -- we interpret it as the number of dependencies that need to be resolved before each node can be calculated. Starting with the roots again, we look at each dependent. Since we know the root is already resolved (doesn't require any calculation), we can decrement its dependents' reference counts. If any reach zero, that attribute is now ready to be calculated, so we add it to the sorted list, and perform the same operation recursively on its own dependents, which might now be ready for calculation themselves.

If you look at the source code for the sort() method and think about the trees in the example (or perhaps some slightly more complicated, but valid ones), you should be able to convince yourself that at the end of this process we end up with a list of attributes in an order that guarantees that we can calculate each one, and never run into a situation where one depends on another attribute whose value isn't yet available. This is the magic that makes RelativeLayout work.

RelativeLayout Itself

The RelativeLayout class pulls everything together, and is the main point of contact for your own programs as well as for Swing. It implements the LayoutManager2 interface so you can assign it as the layout manager of a Swing container. Its addLayoutComponent() methods create the ComponentSpecifications objects to help with the dependency-managed layout process, and keep track of all components being managed. If you look at the version that accepts a constraints object, you'll see I was thinking of building a way to let you pass in a full set of Constraint definitions at the time you added an object to the container, but it turned out that the XML-based approach was more convenient than this idea anyway, so it was never implemented. Instead, you either use the addConstraint() method to manually register your constraints, or, more likely, let XmlConstraintBuilder do it for you.

When it comes time for the container to be laid out according to your constraints, Swing calls layoutContainer(). This figures out the available size of the container, calls the resolveComponents() method to perform all of the bookkeeping and calculation described in the previous section (although most of it can be skipped if no constraints have been changed since the last time layout was performed), then positions and sizes the components based on the results.

There are a couple of other methods used to estimate the preferred and minimum sizes for the layout (which are based on the constraints and the preferred or minimum sizes of all of the components). Swing uses these when it's trying to figure out how much space to give the container if it's nested in another container that cares, and (in some Look-and-Feels) to prevent you from shrinking a window so much that it crushes components into smaller spaces than they want to fit. The way RelativeLayout estimates these sizes is to pretend the window is as small as possible (zero by zero), perform layout based on the minimum (or preferred) sizes of the components, then measure how badly they didn't fit.

Constraint Implementations

We've already looked at the Constraint interface, which sets up the general behavior that is required of a constraint within the layout process, but the actual details are dependent on classes that implement the interface. RelativeLayout ships with two such implementations, and by creating new ones it's possible to add new capabilities to the layout manager.

AttributeConstraint gets its value from an attribute of an "anchor" component, plus some fixed offset (which may be zero). It also can work with a list of several anchor components, in which case the attribute is calculated from the smallest bounding box that encloses the components. The implementation is pretty straightforward: it keeps track of the component(s) on which it bases its value, as well as the AttributeType to use and the offset. The constructors set these all up, and build a cached list of dependencies on the specified anchor components to speed things up later on. Supporting the mandatory getDependencies() method is therefore trivial -- it just returns this list. For the other required method, getValue(), it loops over the anchors, tallying up the values of the desired attribute in the appropriate way, and returns the result.

Related Reading

Java and XML
Solutions to Real-World Problems
By Brett McLaughlin

AxisConstraint lets you specify a fractional point along an axis of an anchor component as the source of its value. The constructor records all of the necessary information: the component of interest, the axis you're using, and the position along that axis. It also creates a cached list of its (two) dependencies. If you're using a horizontal axis, it needs to know the left edge and width of the anchor; for a vertical axis, it uses the top and height. As before, getDependencies() can simply return this list, while getValue() looks up these two attributes and does the simple math required to return its result.

As you can see, the code required to implement the Constraint interface, even for some pretty useful constraints, is quite straightforward.

XML Support

All of the above classes are enough to implement the dependency-managed relative layout manager. In fact, they were written first, and RelativeLayout was useful with just them. I wanted to be able to use a more compact way to express dependencies and constraints, though, and XML configuration files were an appealing solution.

To take best advantage of a validating XML parser (so the Java code need not worry about whether the XML file has the right elements and structure), the first step is to come up with a specification for the format of the configuration file; one that can be understood by the parser. This takes the form of an XML Document Type Definition (DTD).

The XML DTD

Creating a DTD is something of an arcane art (or at least, I still find it so). I'd previously created a couple of simple ones, but I definitely learned a lot creating this one. I wouldn't have been able to tackle it without help from both Brett McLaughlin's Java & XML (2nd Edition, O'Reilly & Associates) and Bob DuCharme's XML: The Annotated Specification (Prentice Hall). Although the latter is a little dry, it's the only place where I've found detailed explanations and examples of all of the pieces that can go into a DTD, and how they constrain the parser.

The DTD is short enough to walk through the entire document here, in case it helps anyone else have some of the "a-ha" experiences I did when assembling it. If this doesn't interest you, or becomes hard going as we dig into the weeds, feel free to jump ahead to the discussion of XmlConstraintBuilder.

The beginning of the file is just a comment:

<!--
  constraint-set.dtd, created Monday, May 20, 2002.
  Defines the syntax of a constraint-set specification XML document.
  $Id: constraint-set.dtd,v 1.3 2002/08/16 05:13:04 jim Exp $
-->

The stuff on the last line enclosed between $ signs is just a CVS ID, information automatically provided by the source-control system I use, identifying the version of the file and the time it was last committed. You'll see them in all of the source files. The real meat of the file begins on the next line:

<!ELEMENT constraint-set (constrain*)>

This states that the constraint-set document contains zero or more elements of type constrain (which has yet to be defined). Because there is no corresponding ATTLIST directive for constraint-set, this element has no attributes, only the nested elements.

Before we go on to pin down what exactly goes into a constrain element, we pause to declare an entity that will make the file more readable. You can think of an entity as a macro; wherever we use it later in the file, it gets expanded into the value defined for it. Our entity lists the valid attribute names you can use with a constraint:

<!ENTITY % attributeName "(left | horizontalCenter | right | width |
                           top | verticalCenter | bottom | height)">

Each of these attribute names is an element that will also be defined later. (Remember that there are two different meanings of "attribute" that are relevant here: positional attributes of components used in laying them out, and the XML syntax notion of an attribute associated with an element in the document. This is talking about the layout kind.) First, here's where we use them:

<!ELEMENT constrain ((%attributeName;)+)>
<!ATTLIST constrain name CDATA #REQUIRED >

This is the promised definition of the constrain element. It states that this element contains at least one and possibly many nested elements, which are chosen from the set defined by the attributeName entity. It also has a mandatory name attribute (of the XML sort). This is getting a little dangerously abstract; let's look at an example of what this means, to bring it back to Earth (even though we've not yet defined the details of any of the attributeName elements themselves yet). From the XML used in Example 2, here's the constraint for the title component:

<constrain name="title">
   <top>
      <toAttribute reference="_container" attribute="top" offset="10"/>
   </top>
   <horizontalCenter>
      <toAttribute reference="_container" attribute="horizontalCenter"/>
   </horizontalCenter>
</constrain>

Ignoring for the moment the contents of the top and horizontalCenter elements, this reflects the structure mandated by the DTD: the constrain element has a name attribute with the value "title". It contains two nested elements, top and horizontalCenter, both of which are taken from the list defined by attributeName. The DTD lets the parser enforce this structure without us writing Java code to test for it.

I briefly toyed with the idea of making the DTD enforce even more structure: because of the way RelativeLayout works, a constrained component needs to have between two and four constraints. It must have either one or two constraints on each of the two axes. If you constrain anything other than width or height, that's enough for the corresponding axis, because the width or height can come from the component's preferred size. I soon realized that this would make for an incredibly complicated DTD, and doubted I could get it right. So I left the DTD loose in this respect, and let the code already in DependencyManager and ComponentSpecifications take care of enforcing these complex semantics.

So, back to the DTD. The next section sets up another entity we'll use in several places to stand for the various types of constraints supported by RelativeLayout. If you ever create a new implementation of the Constrain interface, you'll need to add its name to this list, and then define the structure of its element later in the file:

<!-- These are the currently-known constraint types. They're used in all of the
   attribute definitions. -->
<!ENTITY % constraintType "(toAttribute | toAxis)">

As it stands, this supports the two kinds of constraints that are built in to RelativeLayout. Wherever a constraintType is found, it can either be toAttribute or toAxis.

The next section of the file is where the constraint types are used. It elaborates on all of the layout attributes that were introduced in our first entity, attributeName:

<!ELEMENT left (%constraintType;)>
<!ELEMENT top (%constraintType;)>
<!ELEMENT horizontalCenter (%constraintType;)>
<!ELEMENT verticalCenter (%constraintType;)>
<!ELEMENT right (%constraintType;)>
<!ELEMENT bottom (%constraintType;)>
<!ELEMENT width (%constraintType;)>
<!ELEMENT height (%constraintType;)>

This is pretty repetitive, because each component attribute supports the same kinds of constraints, either toAttribute or toAxis. Using the constraintType entity at least saved us from listing both of them each time (and gives us a single place to add a new one if and when we extend RelativeLayout). If you look back at the constraint document snippet, you can see examples of the top and horizontalCenter elements being used (each chose toAttribute for its nested constraintType). Also note that none of these elements supports any (XML) attributes, only the single nested element.

Hang on, we're getting to the home stretch! The only thing left to define is the two elements that make up constraint types. The first is for AttributeConstraint:

<!ELEMENT toAttribute (reference*)>
<!ATTLIST toAttribute
   reference CDATA #IMPLIED
   attribute CDATA #REQUIRED
   offset CDATA #IMPLIED
>

The toAttribute element can have zero or more nested reference elements (the components whose attributes this constraint is anchored against; the structure of this element is defined at the end of the file), or this information can be supplied as the content of the optional reference XML attribute string. (The DTD doesn't enforce that one or the other must be present, it's up to XmlConstraintBuilder to do that in Java code.) There is also a mandatory XML attribute named (confusingly enough) attribute, which specifies the name of the component attribute being anchored against. Finally, there is an optional XML attribute offset, which specifies how much to add to or subtract from the anchor value. Hopefully, some concrete examples will clarify this dizzying abstraction again. These are all valid, and the last two are equivalent:

<toAttribute reference="_container" attribute="top" offset="10"/>
<toAttribute reference="address,phone,email" attribute="left" offset="5"/>
<toAttribute attribute="left" offset="5">
<reference name="address"/>
<reference name="phone"/>
<reference name="email"/>
</toAttribute>

By using CDATA for the attribute values, I'm allowing any old string inside the quotation marks. Again, it's up to XmlConstraintBuilder to do some of the semantic validation here. The definition for the toAxis constraint type is similar but a little simpler:

<!ELEMENT toAxis EMPTY>
<!ATTLIST toAxis
   reference CDATA #REQUIRED
   axis (horizontal | vertical) #REQUIRED
   fraction CDATA #REQUIRED
>

The toAxis element contains no nested elements, and three mandatory XML attributes. The reference attribute is just like the one we used before, while axis is specified to always contain either "horizontal" or "vertical". Finally, fraction can contain any old string, but XmlConstraintBuilder will make sure it's a floating point number and interpret it as a position along the specified axis. An example of a valid element, as specified by this part of the DTD, is this one from Example 3:

<toAxis reference="sample" axis="horizontal" fraction="0.5"/>

Finally, we need to define the reference element that can be nested inside of toAttribute. This is even simpler.

<!ELEMENT reference EMPTY>
<!ATTLIST reference
   name CDATA #REQUIRED
>

This element has no nested elements and supports a single attribute, name, which contains a string. This is consistent with the example above.

So, was this all worth it? Having the DTD enforce this structure on the XML document certainly made XmlConstraintBuilder easier to write; I could be sure that if the XML parsed without error, it would be easy to walk through it, knowing what to expect at each level. It's time to look at how that works.

XmlConstraintBuilder

By taking advantage of the power of JDOM and letting the DTD enforce the structural details of a constraint-set specification, I was able to support XML-based configuration of RelativeLayout without writing much code. Working upwards from the bottom of the XmlConstraintBuilder source file, the public addConstraints() method uses JDOM to parse the supplied configuration file using our DTD, and then calls the protected addConstraints() method to walk through the parsed file, the structure of which we now trust.

All the internal addConstraints() method needs to do is iterate over the constraint elements in the file (they're guaranteed to be the only children of the root element). For each constraint element, we pick off the component name and iterate over its children, each of which represents a constraint on that component, which we process by passing it to addComponentConstraint(). This method picks apart the constraint details, taking advantage of the factory methods in the type-safe enumerations to translate strings found in the XML to the AttributeType and AttributeAxis instances they represent.

This process could be made even more general by using a configuration file to map constraint types to class names and using reflection to create them, but this would be complicated by the fact that different types use different sets of parameters -- to really make it work cleanly, we'd want to move this responsibility to each Constraint implementation. A clean way to do this would be to require each Constraint implementation to provide a constructor that accepts the JDOM element containing its parsed XML parameters. With just two types, it wasn't worth it (actually, more to the point, I'd already written the constraint classes before I thought of supporting XML configuration). Although I'm getting a little ahead of myself, I will point out that this might be an interesting way to improve RelativeLayout.

The getReferences() method is a helper that collects the names of anchor components that were supplied for a toAttribute constraint, handling both nested reference elements and the (comma-delimited) value of a reference attribute, both of which are permitted by the DTD.

Although this pretty much covers the work of this class, there is one more detail worth explaining. The constructor sets up an EntityResolver for our parser. This is to work around a subtle issue that comes up when dealing with XML validation. All constraint set documents start out with a document-type directive that tells the parser how to validate them:

<?xml version="1.0"?>
<!DOCTYPE  constraint-set
   PUBLIC "-//Brunch Boy Design//RelativeLayout Constraint Set DTD 1.0//EN"
   "http://dtd.brunchboy.com/RelativeLayout/constraint-set.dtd">
<constraint-set>
...

The declaration provides an address at which the DTD can be downloaded over the Web (and I've actually made it available there, so people working with smart XML editors can take advantage of it, to enable code-completion and the like). But what if you're trying to use RelativeLayout on a system that isn't connected to the Internet? It would be annoying if the XML parser failed because it couldn't access the DTD. That's where the EntityResolver comes into play. It is a mechanism JDOM can use to locate documents needed during XML parsing. We've set it up so that when JDOM tries to load the DTD, our resolver recognizes the public identifier, and feeds JDOM a copy of the DTD that's built into the RelativeLayout distribution. Not only does this eliminate the need for an active Internet connection, it improves performance, since the parser doesn't need to wait for the document to come over a network interface.

Finally, an inner class, ParseException, is used to shield callers from the various detailed exceptions that can occur in working with the XML. Callers that actually care about such details can probe the underlying exception (this is structured to be forward-compatible with the exception-chaining mechanism built into Java SDK 1.4).

Extending RelativeLayout

While working on RelativeLayout (and on this article), I noticed some things that could work better or be fancier, but I somehow managed to resist adding such bells and whistles in order to get it finished enough to share with people. I'll point out some of the possibilities in case you happen to be looking for a small project with which to practice Java, learn more about how layout managers work, or just entertain yourself. If you do tackle any, please let me know! Maybe your improvements can be incorporated into a future release of RelativeLayout.

New Constraint Types

I've already mentioned one area of opportunity -- the modular nature of the Constraint interface makes it possible to come up with new ways to set up relationships between components. If you find a situation where AxisConstraint and AttributeConstraint aren't expressive enough to capture a design you're working on, see if you can fit your idea into the Constraint interface and build a new implementation. Once you get your constraint working, don't forget to update the DTD and XmlConstraintBuilder to support it. You'll need to come up with your own XML structure that represents your constraint in an intuitive way and then figure out how to parse it. What fun!

If you do this, I'd also encourage you to think about improving the encapsulation of the way individual constraint parameters are parsed, by moving this responsibility to the constraint implementations themselves, as I suggested earlier.

AxisConstraint over Multiple Anchors

If you don't want to come up with an entirely new kind of constraint, a smaller project would be to enhance AxisConstraint so that it supports bounding boxes the way AttributeConstraint does. In other words, it would allow you to specify more than one anchor component, figure out the smallest box that would enclose all of the anchors, and allow you to pick a position along the axis of that box. This change would affect AxisConstraint's part of the XML DTD and builder as well, making them even more similar to AttributeConstraint's.

Passing Constraint Lists to Containers

As things stand, there are two ways to supply constraints to RelativeLayout: create them manually and pass them individually to RelativeLayout's addConstraint() method, or create them through an XML file. In either case, this is a separate operation from adding the components themselves to their container -- all that's supplied to the container's add() method is the component itself and the logical name that will be used to bind it to constraints.

The layout manager interface allows you to pass in an arbitrary constraints object, which could encapsulate both the component's logical name and the entire list of constraints associated with it. In fact, if you look at the JavaDoc for RelativeLayout's addLayoutComponent(Component, Object) method, you'll see that I was starting along the path of implementing support for this approach, but I never got around to doing it because the XML file turned out to be more convenient for me.

Still, there might be some value in finishing this off. You'd need to define a class, perhaps called ComponentConstraints, that would encapsulate the logical name of a component, and a Collection of the actual Constraint implementations for that component. The constructor could set the component name and there could be a variant that accepted an initial Collection of Constraints, but you'd probably also want a method to add Constraints individually after construction. Once the constraints were all set up this way, you could add the component to the container and register its constraints in a single operation by calling the container's add(Component, Object) method.

The End (for Now?)

I hope you've found this dissection informative. It was certainly a fun exercise to put RelativeLayout together. Thanks are due to Marc Loy and Matthew Keene for providing helpful feedback on this article. And if you've not done so already, please take a look at the origins discussion in the first article to see where my inspiration came from. Finally, if you come up with interesting uses for this layout manager, or extend it in new directions (whether or not they correspond to the ideas I proposed above), I would love to hear about it.

James Elliott is a senior software engineer at Singlewire Software, with fifteen years' professional experience as a systems developer.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.