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

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.