Search |
|||
Amy Fowler's blogJavaFX1.2: LayoutPosted by aim on September 10, 2009 at 1:31 PM PDT
Two Worlds CollideOn 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:
Resizable vs. NotIn 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:
Resizable Node Classes:
Non-Resizable Node Classes:
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 BoundsThe 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:
It's type, javafx.geometry.Bounds, has the classical bounding box variables:
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:
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:
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 NodesNode has two sets of translation variables, which often perplexes newcomers:
The final translation on a node is computed by adding these together:
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:
Or, in the case of object literals, don't forget the bind:
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 NodesFor 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 BehaviorI 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
Ensuring the toplevel container resizes to fill the scene when the user resizes the stage:
Attaching a background to something
Assembling the layout of private content within a CustomNode subclass:
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.
Containersjavafx.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:
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 ContainersJavaFX1.2 provides a handful of concrete container classes in javafx.scene.layout to cover common layout idioms.
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:
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.
The Container classes provide variables to control the overall aiignment of the content within the
container's own width/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:
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.
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:
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.
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.
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:
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:
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:
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):
Insert a background Rectangle into a Container:
Customize alignment:
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.
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:
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.
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 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 ContainersAs 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 GuidelinesI'll wrap this up with a list of key points worth remembering as you venture into layout:
Epilogue: A Word on Binding vs. Procedural LayoutIt'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. »
Comments
Comments are listed in date ascending order (oldest first)
|
CategoriesArchives |
||
|
|