Posted by aim
on July 9, 2009 at 8:15 PM PDT
Taking the first step in updating my JavaFX Layout Primer for 1.2. What could be complicated about understanding the rectangular bounds of a scene graph node? ha!
This is the first in a series of articles to cover layout for JavaFX 1.2. We changed the api a bit (for the better of course), but that hasn't made it any easier to explain. However, if you're an impatient reader and want to cut to the chase, I recommend jumping to the tailing section Bounds in Practice.
The visuals displayed within a JavaFX scene are fully represented by a 2D scene graph where each visual element (line, path, image, etc) is represented by a distinct node with variables that can be easily manipulated dynamically. The node's size and position (otherwise known as its "bounds") becomes complicated when considering these many variables which contribute to its bounds, such as shape geometry (startX/startY, width, radius, etc), transformations (scale, rotate, etc), effects (shadows, glow, etc), and clipping. Understanding how each of these variables affects the bounds calculations of a node is crucial to getting the scene layout you want.
In JavaFX1.2, a node's rectangular bounds are represented by the Bounds class which provides init-only
minX, minY, maxX, maxY, width, height variables. Keep in mind that since the bounding box can be anywhere in the 2D coordinate space, the X/Y values may often be negative.
Note: The Bounds class was introduced in JavaFX1.2 in anticipation of adding Z in the future. BoundingBox (concrete 2D implementation of Bounds) has replaced Rectangle2D for bounds values, however Rectangle2D still exists as a general purpose geom class for other uses.
Node Bounds Variables
Bounds object is always relative to a particular coordinate system, and for each node it is useful to look at bounds in both the node's local (untransformed) coordinate space, as well as in its parent's coordinate space once all transforms have been applied. The Node api provides 3 variables for these bounds values:
|| physical bounds in the node's local, untransformed coordinate space, including shape geometry, space required for a non-zero strokeWidth that may fall outside the shape's position/size geometry, the effect and the clip.
|| physical bounds of the node after ALL transforms have been applied to the node's coordinate space, including transforms, scaleX/scaleY, rotate, translateX/translateY, and layoutX/layoutY.
||logical bounds used as basis for layout calculations; by default only includes a node's shape geometry, however its definition may be overridden in subclasses. It does not necessarily correspond to the node's physical bounds.
It's worth pointing out that a node's visibility has no affect on its bounds; these bounds can be queried whether its visible or not.
This might be easier to visualize in the diagram below, which shows the sequence in which the bounds-affecting variables are applied; variables are applied left-to-right where each is applied to the result of the one preceding it (geometry first, layoutX/Y last):
The reason we need a more malleable definition for layout bounds (vs. just using boundsInParent) is because a dynamic, animating interface often needs to control which aspects of a node should be factored into layout vs. not (more on this in a forthcoming article on 1.2 layout).
Note: In JavaFX1.0, layoutBounds was originally defined to be boundsInLocal plus the transforms sequence; we changed this in 1.2 because we found that often layout did not want to factor in the effect or transforms and it feels cleaner to have no transforms included (vs. 'some' transforms). It also greatly simplifies implementations of Resizable nodes.
Stepping Through a Node Bounds Example
Example 1: Simple Rounded Rectangle
Note that x and y are variables specific to Rectangle and that they position the rectangle within its own coordinate space rather than moving the entire coordinate space of the node. I throw this out as the first example because it is often the first thing that trips up developers coming from traditional toolkits such as Swing, where changing x,y effectively performs a translation of the component's coordinate space.
All of the javafx.scene.shape classes have variables for specifying appropriate shape geometry within their local coordinate space (e.g. Rectangle has x, y, width, height, Circle has centerX, centerY, radius, etc) and such position variables should not be confused with a translation on the coordinate space, as we'll look at in our next example.
Example 2: Translation
Now boundsInParent has changed to reflect the translated rectangle, however boundsInLocal and layoutBounds remain unchanged because they are relative to the rectangle's coordinate space, which was what was shifted. Although layoutBounds is relative to the node's local coordinate space, I'm showing it in the parent's to emphasize how a parent container interprets its value when laying out the node.
inside design note: we debated endlessly on whether to rename translateX,translateY to "x","y" (as it's less typing and more familiar to traditional toolkit programming), however we decided that "translate" was more descriptive in the 2D sense and keeping it avoided renaming the x,y position variables in some shape classes.
Example 3: Effect
The drop shadow is included in the physical bounds (boundsInLocal and boundsInParent), however it is not included in layoutBounds, which is often desirable for layout where the drop shadow is considered a subtle decoration rather than part of the object itself.
Example 4: Rotation & Scale
layoutBounds remains unchanged (from the perspective of the parent), even as the node's local coordinate system is scaled and rotated.
The bounds of a Group node have a slight twist:
layoutBounds is calculated by taking the union of the
boundsInParent on all visible children (invisible children are not factored into its bounds).
boundsInLocal will take the layoutBounds and add any effect or clip set on the group. Finally, the group's
boundsInParent is calculated by taking
boundsInLocal and applying any transforms set on the group.
Example 5: Group Bounds
The group's layoutBounds are tightly wrapped around the union of it's children boundsInParent and do not include the drop shadow.
Bounds in Practice
Now that you understand (in excruciating detail) the difference between these bounds variables, you might be wondering why you really need to know or care (as did J.Giles in his blog ). Aside from the personal glory of mastering the complexity of 2D geometry, it comes mostly into play when laying out your scene. Here are a handful of practical tips:
Remember that Bounds minX/Y and maxX/Y can be negative; don't assume nodes are anchored at their origin. (e.g. Circle is centered on 0,0).
Remember that layoutBounds is a logical concept -- layoutBounds needn't be tied to the physical bounds of a node and often you don't want it to be (ex. you want to spin an icon without disturbing the layout of its neighbors).
Always use layoutBounds when measuring the current size and position of nodes for the purpose of layout (either when using binding for layout or authoring Container classes).
Be aware of the layoutBounds on nodes you throw in Containers as all containers in javafx.scene.layout reference the node's layoutBounds.
If you want layoutBounds to match a node's physical bounds (including effects, clip, and transforms) then wrap it in a Group (ex. if you want a node to scale up on mouse-over and you want its neighbors to scoot over to make room for the magnified node).
You should rarely need to use boundsInParent.
You almost never need to use boundsInLocal.
Finale: Boundisizer Demo
During our JavaFX Controls session at JavaOne, I flew through an hour's worth of layout material in the meager 15 minutes that Rich and Jasper left me (the perils of speaking last!). In the session I showed a little demo that lets you tweak the variables and watch how the various bounds are affected. Action speaks way louder than words or color-coded diagrams:
Run the demo and browse the source at JFXtras .