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.
AttributeAxis class represents the two axes on which component attributes can be defined (
VERTICAL), while the
AttributeType class defines the eight attributes that position a component within a layout (
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
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
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 is a simple class that aggregates some pieces of information to uniquely identify an attribute being managed by
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
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.
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
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
it is queried for its dependencies, and these are all registered with the
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.
Node instance represents the dependencies on a single
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 "
Recall that the attributes of the container in which layout is being performed
use a special component name of "
through the constant
The arrows coming out of a node represent the dependents of that node. In this
HORIZONTAL_CENTER attributes of both the
ok components depend on the
of the container, while the
title, which in turn depends on the
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
which we've constrained, and its
HEIGHT, which is determined by
the component itself, we can calculate the
knows about these derived dependencies, and reports them to the
so they show up in the trees.
Given the complete set of dependency trees represented as
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
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