|
|
||
Amy Fowler's Blog
«responding to questions on Swing, RIA, and JavaFX |
Main
| Layout, Controls, and Hybrids at JavaOne »
Layout Primer for JavaFX1.0Posted by aim on January 09, 2009 at 11:21 PM | Comments (17)Don't believe anyone who tells you that you don't need layout management for rich internet applications. While it's true that this emerging class of interfaces are more graphical, fluid, organic, and animating than the traditional rectangular GUIs of the last 20 years, they, by their nature of being "applications" and not merely glitzy ads, must deal with dynamic content and user interaction. Perhaps some of the bad rap on layout management is due to our own coddling of AWT "Layout Managers", a mostly programmatic approach for achieving layout in Java clients. Anyone who's ever had to design and construct a significant user interface knows that the more natural process is to draw the thing, or at least lay it out visually. But even with the most exceptional tools, the initial layout is just the beginning. The sticky wicket is how to then integrate the dynamic layout behavior -- what should happen if the container is resized or the content changes. Tools like Interface Builder, Dreamweaver, and NetBeans/Mattisse have strived to support this with varying success. The fact is that it's a really hard problem and the graphic sophistication of RIAs only increases the complexity. Dynamic layout now has to also work with timeline based animation and visual effects never anticipated in the aforementioned tool set. The JavaFX api was designed with these needs in mind. Although the FX designer tool is not yet available to fully reveal the larger picture, developers dipping their toes into FX should understand the basic principles of scene graph layout, which are laid out (pun intended) in the following article. Ironic sidenote: you might be questioning how I could extoll the virtues of using tools for layout and then follow that with a detailed article on programmatic layout interfaces. The answer is that I'm a geek who likes to write for like-minded geeks. And fortunately the material translates to understanding 2D scene graph principles, whether programmatic or tool driven. Note: The APIs covered in the article have been slightly modified in JavaFX1.2, so this article should be read with caution until I have a chance (very soon!) to replace it with the 1.2 version. Layout Primer for JavaFX1.0 JavaFX1.0 provides a full-featured 2D scene graph API for creating dynamic and visually rich interfaces, however it does not yet include concrete layout container classes which automate the process of creating dynamic layout in a scene. A future version of JavaFX will more fully address automated layout, however this document will explain how to best achieve dynamic layout (including use with animated transitions) using the 1.0 API. This article assumes you have knowledge of basic 2D scene graph concepts and are somewhat familiar with the JavaFX scripting language. For more information on these prerequisites, see: What Is Dynamic Layout? Dynamic layout is the ability to dynamically position and size elements in the scene graph in response to internal and external changes to the scene which are often triggered by user interaction and involve unpredictable content. For example, the user resizes the stage (desktop profile) and expects the content to adjust to fill it, or on a mobile device the user loads a set of thumbnail images and expects them to flow horizontally and wrap at screen boundaries. Additionally critical is the ability to support animated transitions (zooming out, pulsing, sliding, etc) without disturbing the layout of other nodes in the scene. In JavaFX1.0, there are hooks in the geometry APIs designed for this purpose. Dynamic layout can be achieved either by using binding on individual nodes to respond to layout changes in the scene or by creating Container classes which have the ability to algorithmically layout their children. This article will cover the related apis in detail. Understanding Node Bounds Calculations To layout a scene effectively, one must be able to query the rectangular bounds of nodes in the graph. The question of what is the current bounds of a node becomes complicated when considering the many variables which contribute to its bounds, such as local shape geometry (startX/startY, width, radius, etc), transformations (scale, rotate, etc), effects (shadows, glow, etc), and clipping. It turns out that each of these variables is applied to a node in a specific sequence, and that being able to query the bounds of the node at specific points in this transformation sequence is crucial to achieving desired layouts in the scene graph. Below is a diagram which shows the transformation sequence for a javafx.scene.Node; transformations are applied left to right (effect first, translateX/Y last):
Rectangular bounds are represented by the javafx.geometry.Rectangle2D class which provides minX, minY, maxX, maxY, width, height variables. Since the bounding box can be anywhere in the 2D coordinate space, the X/Y values may often be negative, as we'll see in further examples. To query the bounds of a node at particular points in the transformation sequence, the JavaFX api supports the following variables on the Node class: boundsInLocal (read-only): The rectangular bounds in the node's untransformed coordinate space, including any space required for a non-zero stroke that may fall outside the shape's position/size geometry, the effect (if set) and the clip (if set). layoutBounds: By default, layoutBounds is defined as boundsInLocal plus transforms set in the transforms[] chain variable. Unlike the other read-only bounds variables, layoutBounds may be set or bound to other values. Its purpose is to define the bounding box for layout code to use when performing layout calculations and it can be wired to something other than the default. boundsInParent (read-only): The bounds of the node after ALL transforms have been applied to the boundInLocal, including those set by transforms[], scaleX/scaleY, rotate, translateX/translateY. This is easier to visualize graphically, as depicted in the diagram below:
Stepping Through a Node Bounds Example Example 1: Let's look further at these bounds variables using a concrete code example which creates a rectangle.
Note that x and y are variables specific to javafx.scene.shape.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: To translate the rectangle along with its coordinate space (rather than move the rectangle within it), we instead set translateX/translateY, which are variables on Node.
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. 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: And now let's scale the same rectangle by 150% by placing a Scale object in the the transforms sequence variable.
Note that the default pivot point for javafx.scene.transform.Scale is 0,0, therefore the rectangle (and its coordinate space) grows down and to the right from its origin. Example 4: And finally, if we instead scale the rectangle by using the scaleX/scaleY variables instead of a scale inside the transforms[] sequence, we'll see different results:
The most obvious difference is that the scaleX/scaleY variables on Node always scale about the node's center, which is different from the previous example which scaled from 0,0. Next, the layoutBounds does not account for the scale since it wasn't set in the transforms sequence, which means that code which lays out this rectangle will use its unscaled bounds (I'll explain why that's useful later). This distinction also applies to the rotation and translation transforms; if set in the transforms sequence, layoutBounds will account for them, however if set from Node's rotate, translateX/translateY variables, it will not. Transformation Order Matters Before moving on to layout, there is one more point to make about the order transformations are applied. This is easiest to explain if we rotate my Node chain diagram above vertically and show it in the context of a scene graph branch where a sequence of transforms has been set:
Transformations are applied bottom to top (again, effect first, translateX/translateY last) and by that I mean that a given node in the chain takes the one below it as input. However, "order" can be a misleading term. When it comes to understanding a series of transformations, it's often easier to visualize them by processing them top-down, which happens to be the order they are listed in the transforms sequence. For example, the transforms specified in the above diagram would be coded like this:
And the picture below shows how each transform effects the coordinate space and node:
Now, if we invert the order of the transforms so the Scale is listed first:
We'll see that the result is very different:
In fact, the reason the transforms[] sequence exists on the Node class is to give complete flexibility in both the transformations and the order in which they are applied. This is all basic 2D graphics (recall your matrix math from college), but I illustrate it here because it can confuse those who haven't made a career in it. Laying Out A Node When laying out a node in a scene, it's often necessary to determine its current size so it can be aligned within a certain space (e.g. centered) and you must know it's current location so you can perform the appropriate translation to adjust it to where you want. This is why translateX and translateY are NOT included in the layoutBounds. layoutBounds is used to query the node's current position and size and THEN translateX and translateY are set to perform the adjustment for layout (if translateX and translateY were included in layoutBounds, you wouldn't be able to use them for positioning since it would cause a circular definition -- every time you updated translateX it would cause layoutBounds to change, which would recompute translateX!). For example, if a rectangle is created:
And later some code needs to layout that rectangle at an x,y location:
Note that translateX and translateY are the deltas required to adjust the current position of the node (as defined by layoutBounds.minX, layoutBounds.minY) to get it to the desired location. translateX/translateY are NOT the final destination values.
another insider design note: we realize this is cumbersome and are considering providing variables where the final position could be set directly without this extra math. If node transformations need to be incorporated into layout calculations, then those should be set using the transforms[] variable. An example would be if you scaled a node larger and you wanted nodes laid out relative to it to adjust their positions to make room for the larger node. On the flip side, if a transform is to be applied without affecting its layout or those laid out relative to it, then it should be set using the scaleX, scaleY, and rotate variables, which will NOT be incorporated into layoutBounds. This is particularly useful for implementing animated transitions where you want to animate a node without affecting the layout of other nodes in the scene (e.g. a pulsating glow or zoom-fade). The transition classes in javafx.scene.transition are already built to work this way. For example, the following code shows how you could add a pulse effect to a circle using javafx.animation.transition.ScaleTransition without disturbing the layout of the circle or nodes relative to it:
Using Binding to Layout Nodes For cases where you need to configure dynamic layout for individual nodes, then you can use the powerful binding feature of JavaFX. For details on the language binding feature, see the current JavaFX Language specification. For example, if you want to create a background color on a group of nodes, you would create a Rectangle node and use binding to ensure its bounds track the bounds of the group:
Another common case where binding is useful for layout is in centering a node within an area. Here is an example of how to create a circle that remains centered in the scene even it the stage is resized:
A Future for Containers Where the binding approach becomes tedious is when you have a multitude of nodes that need to be placed relative to each other in some normalized scheme, such as a horizontal flow or a grid. In such cases it would be more convenient to use a Container class whose purpose is to layout its children using a particular algorithm. The scene graph api includes a javafx.scene.Group class, and although it acts as a parent to its child nodes, it is not a "container" in the layout sense. The Group class merely allows nodes to be treated as a collection for scene graph operations (e.g. fade this group of nodes by 50% or shift this group of nodes to the right by 100, etc) . It is not designed to constrain its children to fit into a particular geometric area (it has no settable width/height variables) and it merely takes on the collective bounds of its child nodes. For example, if a Circle (whose default centerX and centerY is at its origin) is placed inside a Group, the Group will take on the same bounds and be centered on its origin:
The 1.0 api provides two simple Group subclasses that will lay out nodes in either a row or a column, javafx.scene.layout.HBox and javafx.scene.layout.VBox. The javafx.scene.layout.Container is the base class for creating nodes which are capable of both laying out children and constraining those children to fit within a particular area. The layout algorithm is performed in a layout function (defined by the concrete Container subclass) which is called by the scene graph automatically when it detects a change that requires layout. Currently the hooks for creating Containers include private api (public variables and functions prefixed with "impl") that will change in the future, therefore be aware that Containers written for 1.0 will need to be modified in future FX versions. Ideally, for a container to perform an algorithm that positions and resizes nodes, it must be able to query the minimum, preferred, and maximum dimensions of its child nodes, as well as be able to set width and height of the nodes. Node subclasses that wish to be resized by containers need to extend the javafx.scene.layout.Resizable class so that Containers can properly size them. Note that Node and the concrete shape classes do NOT currently extend Resizable, therefore when placed in containers, they will be positioned, but not resized (a deficiency we'll address in a future release). Layout Beyond 1.0 Even the most graphical 3D games often need to present information to users in standard layouts and we've definitely found in our own FX experience that more support for layout management is crucial. And as a design tool emerges to enable visual-driven layout, the need for the scene graph to support the right apis is a top priority for our team (and me personally). In more concrete terms, we are planning on providing Container classes for common layout idoms -- flow and grid in particular, as well as defining a mechanism for animating changes in layout. And a primary goal of this effort will be to stablize the Container apis for external use. So watch this space and send feedback, ideas, and bugs my way. Bookmark blog post: CommentsComments are listed in date ascending order (oldest first) | Post Comment
| ||
|
|