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.