Tuesday Jul 30, 2013

The peculiarities of JavaFX layout, pt.1

While working on JavaFX layouts, I have seen a number of bugs caused by a false assumption of the developer about the layouts. There are multiple types of Nodes that behave differently during the layout pass and use the layout information in a different way, which often leads to confusion as it might be quite hard to see the big picture just by reading the javadoc of the classes. That is why I decided to do a few blog posts that would describe the layout from a different point of view.

In this first part, we will look at the way layout works in detail, so I assume you already know the basics of JavaFX layout like types of layout classes, minimum/preferred/maximum sizes, content bias and resizability. If you don't, I can recommend you the JavaFX Class Tour tutorial from Amy Fowler. The subsequent part(s) will cover the common traps and pitfalls for custom layout/control developers and also for those who just want to do some simple adjustments of a layout in their applications. 

Note: there are still open issues that, once resolved, might change the behaviour described in this post. I will keep it up-to-date with the current 8.0 build available.  

How the particular classes use the layout

Some of the confusion originates in a different ways the layout classes are treated during the layout pass. Let's look at this table that describes the way layout bounds and size hints are computed. The last column shows the order in which the layout properties are used/set on the particular Node type. 

layout bounds computation minimum/preferred size resizeable layout pass order

Node
(leaf)

as "bounds in local"
without transformations, clip and effect
always equal to layout bounds no
  1. parent reads pref size to compute the area where the Node will be positioned
  2. the Node is positioned (node.relocate()) using it's current layoutBounds
Group
(autosize)

a sum of current bounds of it's children
(incl. transforms, clip and effect)
Note: querying layout bounds will trigger a layout
in order to compute the most up-to-date layout bounds 

always equal to layout bounds no
  1. parent reads pref size to compute the area where the Group will be positioned
    1. In order to compute the pref size, the Group must trigger the internal layout at this stage using Group.layout().
      Without this side-effect, the layout of a Group would require a second pass
  2. the Group is positioned using it's newly computed layoutBounds in step #1.1
Group
(not autosize)

a sum of current bounds of it's children
(incl. transforms, clip and effect)

always equal to layout bounds no
  1. parent reads pref size to compute the area where the Group will be positioned
  2. the Group is positioned using it's current layoutBounds
Region
Pane
Control
x,y : 0,0
width, height: set by the parent during the layout, by using resize() call
depends on the implementation
The parent of the Region uses the information to
compute the final size during the layout (which means
also the layout bounds)
yes
  1. parent reads pref size to compute the area where the Region will be positioned
    1. This is computed internally, without any side-effect
  2. the Region is resized to the newly computed size, changing it's layoutBounds
  3. the Region is positioned using it's new layoutBounds
  4. Region.layout() is triggered to compute the internal layout using the new size from step #2.


While this may seem to be very complicated, there's actually a good reason why the steps vary between the different types of Nodes. Let's look at how a generic layout pass of a layout Pane looks like:

  1. The layout is computed using the min./max./pref. size of the children. ( + content bias, baseline; we will cover that later)
  2. Resizeable child (means only Region/Pane/Control) is resized to the computed size.
  3. The child (any kind) is positioned according to it's layout bounds.

As you can see from the table, with the exception of Region, the min/pref size ( = layout bounds) is needed in order to correctly compute the layout . The Region however does not need to know it's layout bounds,  the layout bounds are being computed by the it's parent instead.

Moreover, autosize Group does need to layout itself before computing it's layout bounds, which means on the step #1 of it's parent's layout.  This again is absolutely different in Region, which does it's layout after step #3 (though theoretically it's possible after step #2), because the internal layout depends on the size available for the Region, which is known after the resize (at step #2). 

Here's a little diagram that shows the dependencies:


Layout Roots 

JavaFX has a concept of special Parents, called "layout roots". A layout root is a kind of a Parent that doesn't require it's own parent to be laid out when something changes in it's subtree.
Any Parent that satisfies one of these properties is a layout root:

  • managedProperty() is set to false
  • the Parent is a root of a Scene or a SubScene

You may think anything under a non-autosize Group should be also a layout root, but that is not true as a Group must be re-positioned when it's layout bounds change.

You can also use layout roots to validate the layout immediately by using layout() call on the layout root of the Node you want to validate. Very useful for instant measurements of the Node size. Example coming in the following blog post. :-)

The layout invalidation

Now you know how the layout pass works internally, but before anything can be laid-out, the current layout must be invalidated. This is done automatically for you when some child changes, but you may want to trigger a layout when some of your properties that define your layout changes. This is done by simply calling a requestLayout() method, like this:

    public final IntegerProperty helloProperty() {
        if (helloProperty == null) {
            helloProperty = new SimpleIntegerProperty(this, "hello") {
                @Override
                public void invalidated() {
                    requestLayout();
                }

            };
        }
        return helloProperty;
    }

The same method is often used for invalidating pre-computed data of the layout. It's very useful to pre-compute data for computePrefWidth/computePrefHeight and layoutChildren methods, so you don't have to compute the same 3-times. You can then invalidate this data in requestLayout, which is guaranteed to be called whenever a layout-affecting change is made in the subtree of a Parent.

Size bounds

The last source of confusion we will cover in this part are the bounds for width & height of a Region. The Region allows you to override computed size by calling setPrefWidth(double) / setMinWidth(double) / setMaxWidth(double) methods  ( + the equivalent for height). What many of you might find surprising is that the minimum size always takes precedence over maximum size, whether the maximum size is computed or overridden by the developer.

To get a Region/Control below it's (computed) minimum size, you have to override also the minimum size, not just maximum. As the minimum size should be really the smallest size when the Region/Control is usable, this logic serves to prevent layout from resizing the Region below it and keep it functional.

So much about the insides of layout processing. we will look more at some examples in the next post. 

About

JavaFX is a Java GUI toolkit, partially developed from Prague, Czech Republic. The Prague team uses this blog to post articles, code samples and insights about the range of topics the team members specialize in. This includes JavaFX Scenegraph (javafx.scene.*), JavaFX Core libraries & animations, iOS port & Android port.

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today