Thursday Feb 13, 2014

Dynamical layouts in FX

Since I took over JavaFX layout code, I had several bug reports that were related to dynamical layouts and noticed that many people struggle with the implementation. By dynamical layout I mean a layout Pane that not only resizes and positions it's children, but also manipulates with it's child list depending on the acquired size.

In a standard layout Pane, the typical workflow is this:

  1. Parent calls prefWidth/prefHeight; the preferred size is computed from the current set of children and Pane properties/constraints
  2. Parent assigns the actual width/height
  3. The layout is computed from the width/height and the same set of children used in step #1

A dynamical Pane would work like this:

  1. The preferred size is either fixed or depends just on Pane properties or some items (based on which the children are created)
  2. Parent assigns the actual width/height
  3. The layout is computed from the width/height and children are created accordingly

A simple dynamical Pane with fixed-size Rectangles may be implemented like this:


      private class GrowingPane extends Pane {

        private static final int PREF_NUMBER = 2;
        private static final int RECTANGLE_SIZE = 50;
        private static final int PADDING = 10;

        @Override
        protected double computePrefWidth(double height) {
            return PREF_NUMBER * RECTANGLE_SIZE + (2 + PREF_NUMBER - 1) * PADDING;
        }

        @Override
        protected double computePrefHeight(double width) {
            return RECTANGLE_SIZE + 2 * PADDING;
        }


        @Override
        protected void layoutChildren() {
            final double w = getWidth();

            getChildren().clear();
            // count the number of Rectangles that will fit
            final int num = (int) (w - PADDING) / (RECTANGLE_SIZE + PADDING);

            int curX = PADDING;
            // Do the layout
            for (int i = 0; i < num; ++i) {
                Rectangle rec = new Rectangle(RECTANGLE_SIZE, RECTANGLE_SIZE, Color.LIGHTGREEN);
                getChildren().add(rec);
                rec.relocate(curX, PADDING);
                curX += RECTANGLE_SIZE + PADDING;
            }

        }
    }
     

Now we look at a more sophisticated version with Buttons that are not-fixed size. For our sample, we generate the Button text - each subsequent button text is double the size. Notice the difference in computePrefWidth/computePrefHeight:


      private class GrowingPane2 extends Pane {

        private static final int PREF_NUMBER = 2;
        private static final String BASE_STRING = "A";
        private double prefWidth = -1;
        private double prefHeight = -1;
        private static final int PADDING = 10;

        @Override
        protected double computePrefWidth(double height) {
            if (prefWidth == -1) {
                computePrefSize();
            }
            return prefWidth;
        }

        private void computePrefSize() {
            String str = BASE_STRING;

            prefHeight = snapSpace(2 * PADDING + computeHeight());

            prefWidth = PADDING;
            for (int i = 0; i < PREF_NUMBER; ++i) {
                prefWidth += computeWidthForString(str) + PADDING;
                str += str;
            }
            prefWidth = snapSpace(prefWidth);

        }

        private double computeHeight() {
            Button b = new Button(BASE_STRING);
            getChildren().add(b);
            b.applyCss();
            double result = b.prefHeight(-1);
            getChildren().remove(b);
            return result;
        }

        private double computeWidthForString(String str) {
            Button b = new Button(str);
            getChildren().add(b);
            b.applyCss();
            double result = b.prefWidth(-1);
            getChildren().remove(b);
            return result;
        }

        @Override
        protected double computePrefHeight(double width) {
            if (prefHeight == -1) {
                computePrefSize();
            }
            return prefHeight;
        }


        @Override
        protected void layoutChildren() {
            final double w = getWidth();

            getChildren().clear();

            double widthLeft = w - PADDING;
            double curX = PADDING;
            String s = BASE_STRING;

            while(widthLeft >= PADDING) {
                double bw = computeWidthForString(s);
                if (bw + PADDING > widthLeft) {
                    break;
                }
                Button b = new Button(s);
                getChildren().add(b);
                b.applyCss();
                s += s;

                b.autosize();
                b.relocate(curX, PADDING);
                curX += bw + PADDING;
                widthLeft -= bw + PADDING;
            }

        }
    }
      

In real-life example, where you generate Buttons (or whatnot) based on some item list or other model, you'd invalidate the computed size on this model change.

Lets look at the computation. You may notice that we added and removed the child and also called applyCss() before computing it's preferred size. This is because the CSS pass is done after the layout pass, so modifying the children in layout pass require explicit execution of CSS pass for every new Node. The CSS depends on context (position in the scenegraph), so we need to add the child first. Of course, to be absolutely accurate, we should add the child to it's correct position and clear the list at the end of the computation. 

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
« February 2014
SunMonTueWedThuFriSat
      
1
2
3
4
5
6
7
8
9
10
11
12
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 
       
Today