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 clearl ]]>y 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

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 notbe 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.TranslateTransitionclass 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.Containeris 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.LayoutInfoBaseis 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.

LayoutInfois 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.

TheChris 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.