ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Inside RelativeLayout
Pages: 1, 2, 3, 4

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.

Pages: 1, 2, 3, 4

Next Pagearrow