Skip to main content

JavaFX1.2: Layout

Posted by aim on September 10, 2009 at 1:31 PM PDT

Two Worlds Collide

On the one hand, JavaFX's powerful scene-graph and animation engine enables gamer types to
rapidly create dynamic visual scenes that are functionally expressed through binding and triggers and timelines.
On the other, it's growing controls and charts libraries clearly stake out a more traditional GUI turf. As interfaces
finally graduate to the 21st century, the lines between these two worlds is blurring in exciting ways.
Our challenge is to evolve the FX platform to support this convergence, which speaks precisely to why
layout in JavaFX is complicated enough that it requires a blog series to explain.

So let's get down to business. With JavaFX1.2 there are two approaches to laying out your scene:

  • app-managed: application directly sets the position and size of nodes and uses FX binding to express
    dynamic relationships.
  • container-managed: application places nodes inside containers which manage their size/position
    according to preferences set on the containers and nodes.

Typical JavaFX applications merrily blend both approaches, but before we get into the nitty-gritty, let's review
a couple of core concepts that have arisen from our collision of purpose.


Resizable vs. Not

In JavaFX, every single visual element is represented by a stateful node in the scene graph. This means
that the base javafx.scene.Node class needs to be very very very small and we must use all the
restraint we can muster to resist the api creep that tends to invade the base class of even the best-intentioned
toolkits. This is one reason we introduced the javafx.scene.layout.Resizable mixin; another reason is that not all node types want
to be resizable.

The Resizable mixin provides the machinery necessary to allow a node to be externally resized:

    public var width:Number;
    public var height:Number;
    public function getMinWidth();Number;
    public function getMinHeight():Number;
    public function getPrefWidth(height:Number):Number;
    public function getPrefheight(width:Number):Number
    public function getMaxWidth():Number;
    public function getMaxHeight():Number;

Resizable Node Classes:

  • Containers: HBox, VBox, Flow, Tile, Stack, Panel, ClipView
  • Controls: Button, TextBox, Label, ListView, Charts, etc..

Non-Resizable Node Classes:

  • Group, CustomNode, Text, Shapes, ImageView

It's important to understand that the non-Resizable nodes do in fact change size as their variables are manipulated,
it's just that they have no consistent api for explicitly setting their size. Even Rectangle, which teases you
with its width/height variables, isn't resizable, as it's width and height vars define it's shape geometry and
do not include its stroke (which is centered on the geometry) so its actual bounds fall outside its width/height boundary,
as shown in the image below:

A perfect seque ...

Layout Bounds

The layout bounds of a node is the rectangular area (e.g. bounding box) that is used for layout-related calculations.
It's defined as a read-only variable on the Node class:

    public-read protected layoutBounds:Bounds

It's type, javafx.geometry.Bounds, has the classical bounding box variables:

     public-init package minX:Number
     public-init package minY:Number
     public-init package width:Number
     public-init package height:Number
     public-read package maxX:Number
     public-read package maxY:Number

It's perfectly normal for the min and max coordinates to be negative, so don't assume the upper-left
bounds of a node are necessarily anchored at 0,0.

For non-Resizable leaf node classes (Shapes, Text, etc) the layout bounds will be equal to the node's
geometric bounds (shape geometry plus stroke) and it does NOT include the effect, clip, or any transforms.
This means you can add effects and transforms (shadows, rotation, scale, etc) and the layout bounds of
shapes will remain unchanged.

For example, here's a circle (centered at 0,0 by default) with a drop shadow:



Now, for Group nodes, the layout bounds will be equal to the union of all the boundsInParent of all of
its visible content nodes. This means that any effects, clipping, or transforms set on the child
nodes will be factored into the group's layout bounds, however any effects, clip, or transforms
set directly on the group will not be included in its layout bounds.

This is demonstrated in the example below which shows that setting the reflection directly on the group has
a different result than setting the reflection on the group's chlidren individually:

For Resizable nodes classes (Containers & Controls), the layout bounds will be wired to 0,0 width x height,
regardless of what the actual physical bounds of the node is.

in fact, if you create a class which mixes in Resizable, you should ensure that this is wired properly by adding
the following line to your class:

override var layoutBounds = bind lazy BoundingBox{ minX:0 minY:0 width:width height:height }

insiders note: we may add a ResizableCustomNode base class in a future release to handle this book-keeping,
although we're searching for a shorter class name, maybe CustomResizable.

It may seem odd that layout bounds isn't necessarily tied to the physical bounds of a node, but in
this highly dynamic world of animating graphical objects, it's often desirable to separate the notion of
layout (which often needs to be stable) from the physical bounds of a node which are changing in ways
that often need not be factored into the layout -- like a bouncing icon in a task bar or drop shadows and glow effects.
But if you're still scratching your head, you can read more detail on this in my Understanding Bounds blog.



App-Managed Layout

One approach to laying out your scene is to explicitly set the size and position of your nodes and establish
dynamic behavior by using binding. In early versions of JavaFX this was the only option and it remains
a perfectly valid approach.


Positioning Nodes

Node has two sets of translation variables, which often perplexes newcomers:

    public var layoutX:Number
    public var layoutY:Number

    public var translateX:Number
    public var translateY:Number

The final translation on a node is computed by adding these together:

       tx= layoutX+translateX   ty= layoutY+translateY

This separation allows layoutX/layoutY to be used for controlling a node's stable layout position
and leaves translateX/translateY for use in dynamic adjustments to that position.
In fact, the javafx.animation.transition.TranslateTransition class does exactly that -- modifies translateX/translateY to
animate the position of the node without disturbing the surrounding layout (e.g. great for flying, bouncing, or jiggling animations).

Therefore, to establish a node's stable layout position, set layoutX/layoutY. Be aware that these variables define
a translation on the node's coordinate space to adjust it from its current layoutBounds.minX/minY location
and are not final position values (in hindsight we probably should have named them "layoutTX"/"layoutTY"
to emphasize this subtlety). This means you should use the following formula for positioning a node at x,y:

    node.layoutX = x - node.layoutBounds.minX
    node.layoutY = y - node.layoutBounds.minY

Or, in the case of object literals, don't forget the bind:
    def p = Polygon {
        layoutX: bind x - p.layoutBounds.minX
        layoutY: bind y - p.layoutBounds.minY
        ...
    }

I'll illustrate this simply with a Circle, which is centered on the origin by default, putting its minX and minY in
the negative coordinate space:

The blue circle does not end up at 50,50 because that 50,50 translation is added to its current minX, minY,
which is -25,-25, resulting in a final position of 25,25.



Sizing Nodes

For non-Resizable Shape classes, you can just set the various geometric variables to control their size
(e.g. width/height/strokeWidth for Rectangle, radius for Circle, etc). You might be tempted to use a
scale operation as an easy means for adjusting a shape's size, however be aware that this scales
the entire coordinate space of the node, including it's stroke, fill, etc. so be cautious not to confuse
resizing with scaling
.

For javafx.scene.text.Text nodes (also non-Resizable), their size is defined by their content string, except in the case of
multiline, where you can set the Text's wrappingWidth variable. Note that due to a bug,
the layoutBounds of a Text node will not account for any spaces that pad the beginning or end of the
string, so the layout bounds always shrink to fit visible character shapes (e.g. " hello " would be treated
as "hello") ; we'll fix this in a future release, as this once sent me on a 6 hour wild goose chase.

Group nodes cannot be explicitly sized -- they just take on the collective bounds of their content nodes,
expanding or shrinking to fit.

Any class which implements Resizable (again, Containers and Controls) can have width and height variables explicitly set,
although one should be aware that Resizable classes have a preferred size typically based on some internal state
(e.g. a Label's text or an HBox's content's preferred sizes).
Most Resizable classes will initialize their own width/height vars (if not explicitly set by the app) to their preferred size,
so you should only have to set the size if you need to deviate from the preferred.


Using Binding to Establish Dynamic Layout Behavior

I love using bind for establishing some of the simpler layout patterns in the scene. Following are some
common idioms:

Centering a node within a scene

def scene:Scene = Scene {
    var group:Group;
    width: 500  height: 400
    content: group = Group {
        layoutX: bind (scene.width - group.layoutBounds.width)/2 - group.layoutBounds.minX
        layoutY: bind (scene.height - group.layoutBounds.height)/2 - group.layoutBounds.minY
        content: [ ... ]
    }
}

Ensuring the toplevel container resizes to fill the scene when the user resizes the stage:

def scene:Scene = Scene {
    var hbox;HBox
    width: 400 height: 200
    content: hbox = HBox {
        width: bind scene.width
        height: bind scene.height
        content: [ ... ]
    }
}

Attaching a background to something

Group {
    var something:Group;
    content: [
        Rectangle {
            layoutX: bind something.layoutBounds.minX
            layoutY: bind something.layoutBounds.minY
            width: bind something.layoutBounds.width
            height: bind something.layoutBounds.height
            fill: LinearGradient {... }
        }
        something = Group {
            content: [ ...]
        }
    ]
}

Assembling the layout of private content within a CustomNode subclass:

class StatusField extends CustomNode {
    public var valid:Boolean = true;
    public-read protected var textbox:TextBox;
    var statusicon:Circle;
    override function create():Node {
        Group {
            content: [
                textbox = TextBox {
                    layoutY: bind statusicon.layoutBounds.height - 4 - textbox.layoutBounds.minY
                }
                statusicon = Circle {
                    fill: Color.RED
                    radius: 4
                    layoutX: bind textbox.layoutBounds.width - 4 - statusicon.layoutBounds.minX
                    layoutY: bind 0 - statusicon.layoutBounds.minY
                    visible: bind not valid
                    effect: DropShadow{ offsetX: 2 offsetY: 2 radius: 4}
              }
           ]
       }
   }
}




Container-Managed Layout

If you need to layout your nodes in classical ways (rows, columns, stacks, etc) then nothing beats just being
able to throw those nodes into a container class that efficiently performs all the aforementioned magic for you.


Containers

javafx.scene.layout.Container is a Resizable Parent class that performs a layout algorithm on its content nodes
as well as calculates reasonable values for its own minimum, preferred, and maximum sizes based on
those of its content. This means you can nest containers to your heart's content and they will just do
the right thing dynamically when layout conditions change (nodes are added/removed, state changes
that cause control's preferred sizes to change, etc).

You can put both Resizable and non-Resizable nodes inside a Container. Containers will control the position
of content by setting layoutX/layoutY and will typically strive to set the width/height of Resizables to their preferred size
and will treat non-Resizable nodes as rigid, which means non-Resizables (like Shapes, Groups) will be positioned but not resized.

So, once a Resizable is inside a Container, that Container gets to control its size/position,
obliterating any width/height values you may have hand-set on that Resizable. If you need
to explicitly control the size of a Resizable node inside a Container, you have two choices:

  1. bind the width/height of that Resizable -- the Container will treat it as a non-Resizable node (position it, but won't resize it).
  2. override the Resizable's preferred size using LayoutInfo (described later).

When state within a Container changes such that it would need to re-layout its contents, it automatically invokes
requestLayout(), marking itself and all ancestors as "dirty", such that that branch will be re-layed out on the next pulse.



Concrete Containers

JavaFX1.2 provides a handful of concrete container classes in javafx.scene.layout to cover common layout idioms.

  • HBox - horizontal row of nodes with configurable spacing and alignment
  • VBox - vertical column of nodes with configurable spacing and alignment
  • Flow - horizontal or vertically oriented flows that wrap at width/height boundaries
  • Stack - back-to-front stacking of nodes with configurable alignment
  • Tile - lays out nodes in grid of uniformly-sized tiles
  • Panel - allows layout customization using an object-literal
  • ClipView - provides a clipped, pan-able view of its content node

The basic usage of these classes is to simply set their content (as you would for a Group) and maybe customize a few variables.
For example:

HBox {
    spacing: 4
    content: [ Label { text: "Name:"}, TextBox {} ]
}

Flow {
    vertical: true
    content: for (img in images)
        ImageView { image: img }
}

and so on

The glaring hole in this list is a multi-purpose grid layout. Right now you can use either the Grid or MigLayout containers from the JFXtras Core extension package, but its addition to our runtime is inevitable.



Alignment

The Container classes provide variables to control the overall aiignment of the content within the
container's own width/height:

    public var hpos:HPos:  horizonal alignment of the content within the container's width
    public var vpos:VPos:  vertical alignment of the content within the container's height

Additionally, often a node cannot be resized to fill its allocated layout space, either because it isn't
Resizable or its maximum size prevents it, and so the Containers provide variables for controlling
the default node alignment within their own layout space:

    public var nodeHPos:HPos: default horizontal alignment of nodes within their layout space
    public var nodeVPos:VPos: default vertical alignment of nodes within their layout space

The HBox example below shows how these options can be used to control alignment:

One final alignment caveat is that in JavaFX1.2, VPos.BASELINE alignment is not supported inside Containers. This will be fixed in the next major release, making it much easier to properly align Labels with other Controls.


Clipping

Something that often surprises developers coming from traditional toolkits such as Swing is that
Containers do not automatically clip their content. It's quite possible for the physical bounds of a container's content to
extend outside of its layout bounds, either because effects and transforms have been applied to the content or because
they simply couldn't be resized to fit within the container's layout bounds.

If you want the old-fashioned clipping behavior, you can set the clip yourself:

def flow = Flow {
       clip: Rectangle {
            width: bind flow.layoutBounds.width
            height: bind flow.layoutBounds.height
       }
       content: [ ....]
}

But beware that by doing so, you'll be unable to have animation or effects that would extend beyond your container's
layout bounds, as it will all be clipped out -- just as you specified.


Node Visibility

The concrete Container classes will layout nodes regardless of their visibility, so if you don't want invisible nodes layed out,
then you should either remove them from the container or make them "unmanaged", which I'll show how to do in the next section
on LayoutInfo.


LayoutInfo

Often for layout its necessary to be able to specify layout constraints on a per-node basis, so in 1.2 we added just such
a hook on Node:

   public var layoutInfo:LayoutInfoBase

This layoutInfo variable is only referenced if the node's parent is a Container that honors it, otherwise it's ignored.
In other words, don't expect layoutInfo settings to have any affect on a node contained by a plain old Group --
Groups are agnostic to layout.

javafx.scene.layout.LayoutInfoBase is an abstract class which includes only a single variable:

       public var managed:Boolean

This variable (supported by all concrete LayoutInfo subclasses) enables a node in a container to be marked as unmanaged,
telling the container not to factor it into layout calculations. By default all nodes are managed unless a LayoutInfo where
managed equals false is set on it.

LayoutInfo instances may be shared across nodes, which is convenient and efficient when you want to apply a
set of layout constraints to multiple nodes. Just be aware that changing any of that shared layoutInfo's vars will
affect all nodes its attached to, a danger inherent in a stateful flyweight pattern.

LayoutInfo is a concrete extension of LayoutInfoBase to add common constraint variables:

     public var minWidth:Number
     public var minHeight:Number
     public var width:Number
     public var height:Number
     public var maxWidth:Number
     public var maxHeight:Number
     public var hpos:HPos;
     public var vpos:VPos;

Any values set on a node's LayoutInfo will reign supreme over the values normally computed by the Container,
essentially allowing min/preferred/max sizes and alignment to be customized on a per- object literal basis.
Note however that for most common layout scenarios using the Container classes, you should rarely have
to set a LayoutInfo on a node -- it exists as a trap door for customization.

Here are some common usage scenarios for LayoutInfo:

Override preferred size of a node (this is how to effectively set the size of a Resizable managed by a Container):

def buttonLAYOUT = LayoutInfo { width: 80 };  // override Button's preferred width
HBox {
    content: [
        Button {  text: "Apply"   layoutInfo: buttonLAYOUT }
        Button { text: "Cancel" layoutInfo: buttonLAYOUT }
   ]
}

Insert a background Rectangle into a Container:

def UNMANAGED = LayoutInfo { managed: false };
def tile = Tile {
    content: [
        bg = Rectangle {
            // note: this is simple because there is no stroke
            layoutInfo: UNMANAGED
            width: bind tile.layoutBounds.width
            height: bind tile.layoutBounds.height         
            fill: LinearGradient {...}
        }
        ...other content...
    ]
}

Customize alignment:

Stack {
    // stack's default nodeVPos is CENTER
    content: [
       ImageView { ...}
       Label {
           text: imageTitle
           layoutInfo: LayoutInfo { vpos: VPos.BOTTOM }
     ]
}

insider's note: the name of the LayoutInfo class was another infinite debate; initially we called it "LayoutConstraints", but
Richard and I hated how this exploded object-literals into the right margin, e.g. layoutConstraints: LayoutConstraints { ...}.
"LayoutInfo" seemed short and sweet.



Custom Containers

You can create your own re-usable Container subclasses by extending Container and overridding
getPrefWidth()/getPrefHeight(), and doLayout(). Container also provides a number of script-level
convenience functions that make the pursuit of Container-authoring easier:

      public function getManaged(content:Node[]):Node[]
      public function getNodeMinWidth(node:Node):Number
      public function getNodeMinHeight(node:Node):Number
      public function getNodePrefWidth(node:Node):Number
      public function getNodePrefHeight(node:Node):Number
      public function getNodeMaxWidth(node:Node):Number
      public function getNodeMaxHeight(node:Node):Number

      public function positionNode(node:Node, x:Number, y:Number)
      public function resizeNode(node:Node, width:Number, height:Number)
      public function layoutNode(node:Node, areaX:Number, areaY:Number, areaW:Number, areaH:Number
                                                         hpos:HPos, vpos:VPos):

These functions are smart -- they deal with Resizable vs. not, accessing LayoutInfo if it exists, adjusting
layoutX/Y based on minX/minY, catching exceptions thrown when width/height are bound, etc.

I'll save my Container authoring example for a future "advanced Layout" blog, as this one is already long-winded.


Panel

Sometimes you'll want to customize layout without the added burden of creating your own Container subclass,
which is something that comes up frequently when creating the hidden scenes inside of CustomNodes.

The
Panel class allows you to do just this by attaching container behaviors to an object literal using its
function vars:

      public var minWidth():function():Number
      public var minHeight:function():Number
      public var prefWidth:function(height:Number):Number
      public var prefHeight:function(width:Number):Number
      public var maxWidth:function():Number
      public var maxHeight:function():Number
      public var onLayout:function():Void

Panel also provides a resizeContent() function which will iterate through all managed Resizable children
and set their sizes to their preferred. In fact, this function serves as the default onLayout function,
effectively resizing but not positioning content, but it may also be called from the app's own onLayout function
if convenient.



When Resizables Are Not Managed by Containers

As previously mentioned, Resizable nodes should initialize their own sizes to their preferred upon
initialization so that they display properly regardless of the parent that contains them, be it a Group, CustomNode, or Container.
However, if some state changes within that Resizable that causes its preferred size to change (e.g. a Button's text
gets longer, another node is added to an HBox, etc) and that Resizable is not housed within a Container, then currently
there is nothing that will automatically cause that Resizable to adjust to its new preferred size (if housed in a Container
then that Container would resize it to its preferred on the next layout pass).

For JavaFX1.2, the best way to address this issue is to always place Resizable nodes (e.g. Controls and Containers)
inside a Container -- even if it's just a Panel which will by default resize all children to their preferred sizes during
the layout pass. However, since we've been inundated with bug reports that originate from this issue, we are mulling
over solutions for a future release (possibly to add an "autoSize" var to Resizable, detect the situation automatically,
or both).

This bites many people at the top of their Scene, because every Scene has a clandestine Group acting as the root for the Scene's
content nodes. And since that root Group is not a Container, it does not resize the scene's content to their new
preferred sizes when they change. For example, here we have a Label whose text changes when the mouse enters the
background rectangle:

Insider Note: We attempted to fix this behavior in 1.2 (essentially to change the hidden root to a Container),
however this change broke a number of apps that were relying on the ability to explicitly set the size of their Scene's
content nodes. We'll likely make this behavior configurable on Scene in the future.




Conclusion: Ten Layout Guidelines

I'll wrap this up with a list of key points worth remembering as you venture into layout:

  1. You can freely mix both app-managed and container-manage layout approaches

  2. If you create a Node with a Group as its parent, then you are responsible for setting its position and size, as Groups don't do layout.

  3. If you create a Node with a Container as its parent, you are handing control of that node's position (and size, if its Resizable) to the Container -- don't be surprised when your own width/height settings are overridden; see #9

  4. Use layoutBounds as the basis of all layout-related calculations (whether app or container-managed).

  5. If you want a node's effect, clip, or transforms to be factored into its layout, then wrap it in a Group.

  6. Set layoutX/layoutY to establish the stable layout position of nodes and translateX/translateY for animation or adjustments.

  7. Remember that layoutX/layoutY are offsets, not final location values, so use the position formula:

    layoutX = x - node.layoutBounds.minX, layoutY = y - node.layoutBounds.minY.

  8. LayoutInfo is only relevant when set on a Node that has a Container as its parent, otherwise it's ignored.

  9. If you need to control the size of a Resizable which is inside a Container,
    then either bind its width/height or override its preferred size using LayoutInfo.

  10. If you want to ensure that your Resizable node will automatically resize whenever its preferred size changes, then place it in a Container.



Epilogue: A Word on Binding vs. Procedural Layout


It's actually possible to use binding to establish fairly intricate layout behavior, as is evidenced
by the very clever code in Chris Oliver's blog.
This is the code of an FX-master and it puts me in awe. The downside it that it takes a high level of FX skill to both write and read recursive bound functions, so it's not an approach that will come easy to those new to FX.

The other problem is that extensive use of binding for the complex relationships needed to layout a reasonably large scene
could pose a performance issue in the current runtime. Binding is a seductive solution to many problems, but currently it comes
at a cost in both size and execution speed. The machinery behind bound variables takes up more memory and in the case of layout on
a deeply nested scene, re-evaluation of the dependency chain when any node changes size or position could be an
expensive operation. We encountered exactly this reality during development of 1.2, which is why we introduced the more
procedural layout pass that batches up requests to adjust layout and performs the layout algorithm once per pulse before rendering.

That said, the ultimate goal is to make binding inexpensive enough that it enables the expressive FX code which Chris advocates.
As we speak, our compiler team is working to reduce both the size and performance costs of binding; at the same time, we're
making improvements to the scene-graph that will ensure apps using the pure binding approach for dynamic layout don't pay any
of the procedural costs (which boil down to marking branches dirty and executing the layout pass before rendering).
Again, we aim to serve both masters.

Related Topics >>

Comments

Great article. Thanks.

Great article. Thanks.