|
Inside RelativeLayoutby James Elliott, coauthor of Java Swing, 2nd Edition11/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.
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
build.xmlThis 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:
ant ex1: runs
the first example program discussed in the first article. Similarly, the targets ex2 and ex3 run the second and third examples.ant doc: builds the JavaDoc for RelativeLayout. You may want to refer to this documentation from time to time as you read the overview of how the classes work, below.ant dist: builds the distribution file RelativeLayout.jar so you can easily use RelativeLayout with other projects.ant clean: cleans up any generated files and removes the doc and classes directories.example2.xml and example3.xmlThese 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.
libContains 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.
test.xmlI 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.
srcThe 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 |
src/Example1.java, src/Example2.java, and src/Example3.java
These are the three example programs discussed in Part 1.
src/Test.javaThis is the test program that works with test.xml as described above. It's no longer of much interest except for software archaeology, in that it provides a little insight into the development of the package.
src/overview.htmlThis package overview document is used by JavaDoc to provide introductory information on the starting page.
src/comThe Java source for RelativeLayout itself is grouped under this directory. To be precise, it's in the nested directory src/com/brunchboy/util/swing/relativelayout, corresponding to the package in which the classes themselves are organized, com.brunchboy.util.swing.relativelayout. The classes that make up RelativeLayout are explained in the next few sections. You'll best understand how everything works if you can examine the source itself while you read the descriptions below, perhaps by printing one or the other.
The relativelayout directory also contains the file package.html, used by JavaDoc to provide an introductory explanation for the classes in the directory, and constraint-set.dtd, the XML document type definition (described below), used by XmlConstraintBuilder to parse constraint specifications expressed as XML.
|
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:

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.
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.
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.
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 ItselfThe 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.
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 |
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.
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).
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.
XmlConstraintBuilderBy 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).
RelativeLayoutWhile 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.
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 AnchorsIf 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.
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.
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.