FlexBox Layout mit JavaFX (1)

In der Webentwicklung haben sich Grid und FlexBox Layout inzwischen als die beliebtesten Algorihthmen für flexible, responsive Layouts durchgesetzt. In JavaFX gibt es ebenfalls eine mächtige GridView, ein FlexBox Layout gab es jedoch bislang leider noch nicht. Hier stellen wir unsere eigene FlexBoxPane vor, mit dem sich nun auch in JavaFX leichter responsive Anwendungen umsetzen lassen.

Im ersten Teil dieser Serie geht es vor allem um die Constraints, die auf der FlexBoxPane selbst gesetzt werden können. Im nächsten Teil wird es dann um die Constraints für die Children (oder FlexItems) gehen. Im dritten Teil sehen wir uns dann einige Beispiellayouts an, die mit der FlexBoxPane umgesetzt werden können.

Mit dem Aufruf des Videos erklärst Du Dich einverstanden, dass Deine Daten an YouTube übermittelt werden und das Du die Datenschutzerklärung gelesen hast.

In unserem Layouts Repository findet Ihr die im Video verwendete Demoanwendung mit der Ihr die Auswirkungen der einzelnen Constraints ausprobieren könnt.

Vor einiger Zeit war ich auf der Suche nach einer FlexBox Implementierung in Java für unser Cross-Plattform-Framework DukeScript. Leider habe ich nur eine fehlgeschlagene Kickstarter Kampagne für ein JavaFX basiertes FlexBox Layout gefunden.

Daher haben wir das Layout inzwischen selbst umgesetzt. Die Implementierung ist unabhängig vom UI Toolkit und wird in Komponenten für iOS und JavaFX verwedet. Um das Layout in JavaFX zu Verwenden, benötigt Ihr folgende Maven Dependency:

<!-- https://mvnrepository.com/artifact/com.dukescript.amaronui.layouts/jfxflexbox -->
<dependency>
    <groupId>com.dukescript.amaronui.layouts</groupId>
    <artifactId>jfxflexbox</artifactId>
    <version>0.6</version>
</dependency>

Der Layout-Container heisst "FlexBoxPane" und wird wie jedes andere JavaFX-Layout verwendet:

import com.dukescript.layouts.jfxflexbox.FlexBoxPane;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.stage.Stage;

public class MainApp extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        FlexBoxPane flex = new FlexBoxPane();
        Label label = new Label("Hello Flex Item!");
        flex.getChildren().add(label);
        stage.setTitle("JavaFX FlexBox");
        Scene scene = new Scene(flex);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

Hello World Anwendung mit FlexBoxPane

Die Implementierung des Algorithmus orientiert sich möglichst genau an der Spezifikation. Für die aus der HTML-Implementierung bekannten CSS-Properties gibt es entsprechende Methoden in der FlexBoxPane. Parent-Properties werden direkt gesetzt; so wird aus "align-content: center":

FlexBoxPane flex = new FlexBoxPane();
flex.setAlignContent(FlexboxLayout.AlignContent.CENTER);
Label label = new Label("Hello Flex Item!");  
flex.getChildren().add(label);

Parent Constraints in der FlexBoxPane

Bei den Child-Properties haben wir uns an die JavaFX-Konventionen gehalten, Constraints über statische Methoden des Parent-Containers zu setzen. Im folgenden Beispiel setzen wir so den Margin und die "FlexGrow" Property, um festzulegen, wie der überschüssige Raum verteilt werden soll.

    @Override
    public void start(Stage stage) throws Exception {
        FlexBoxPane flex = new FlexBoxPane();
        flex.setAlignContent(FlexboxLayout.AlignContent.CENTER);

        flex.getChildren().add(createLabel(1));
        flex.getChildren().add(createLabel(2));
        flex.getChildren().add(createLabel(1));

        stage.setTitle("JavaFX FlexBox");
        Scene scene = new Scene(flex);
        scene.getStylesheets().add("/styles/Styles.css");
        stage.setScene(scene);
        stage.show();
    }

    private Label createLabel(int flexGrow) {
        Label label1 = new Label("Hello Flex Item!");
        label1.setMinWidth(20);
        label1.setMaxWidth(Double.MAX_VALUE);
        label1.setAlignment(Pos.CENTER);
        FlexBoxPane.setGrow(label1, flexGrow);
        FlexBoxPane.setMargin(label1, new Insets(5));
        return label1;
    }

Child Constraints in der FlexBoxPane

Parent Properties

Im folgenden Teil wird gezeigt, wie die FlexBoxPane angepasst werden kann um die Anordnung der Flex Lines und der Items darin zu steuern.

FlexDirection

Dadurch wird die Hauptachse festgelegt und damit die Richtung definiert, in der die Flex-Items im Flex-Container platziert werden. Flexbox ist ein eindimensionales Layoutkonzept. Flex-Items werden immer in horizontalen Zeilen (FlexDirection.ROW) oder vertikalen Spalten (FlexDirection.COLUMN) angeordnet.

       @Override
    public void start(Stage stage) throws Exception {
        FlexBoxPane flex = new FlexBoxPane();
        flex.setFlexDirection(FlexboxLayout.FlexDirection.COLUMN);

        flex.getChildren().add(createLabel(1, 1));
        flex.getChildren().add(createLabel(2, 2));
        flex.getChildren().add(createLabel(3, 1));

        stage.setTitle("JavaFX FlexBox");
        Scene scene = new Scene(flex);
        scene.getStylesheets().add("/styles/Styles.css");
        stage.setScene(scene);
        stage.show();
    }

    private Label createLabel(int idx, int flexGrow) {
        Label label1 = new Label("Flex Item "+idx);
        label1.setMinWidth(20);
        label1.setMaxWidth(Double.MAX_VALUE);
        label1.setMinHeight(20);
        label1.setMaxHeight(Double.MAX_VALUE);
        label1.setAlignment(Pos.CENTER);
        FlexBoxPane.setGrow(label1, flexGrow);
        FlexBoxPane.setMargin(label1, new Insets(5));
        return label1;
    }

Auswirkung von FlexDirection

Neben der Hauptrichtung der FlexBox Linien kann auch die Anordnung der Flex-Items festgelegt werden. Mit FlexDirection.COLUMN_REVERSE und FlexDirection.ROW_REVERSE werden die Flex-Items in umgekehrter Reihenfolge angeordnet:

Das gleiche Beispiel mit FlexDirection.COLUMN_REVERSE

FlexWrap

Das FlexBox Layout ist "elastisch" und versucht den verfügbaren Raum unter den Komponenten bestmöglich aufzuteilen. In unserem Beispiel haben wir für die Labels eine Minimalgröße für Breite und Höhe festgelegt. Reicht der verfügbare Raum nicht aus, wird die Linie normalerweise umbrochen.

Automatischer Umbruch der Flex Line

Mit der FlexWrap.NOWRAP lässt sich das Umbrechen verhindern.

flex.setFlexWrap(FlexboxLayout.FlexWrap.NOWRAP);

Umbruch der Flex Line wird durch FexWrap.NONE verhindert

Wie bei der FlexDirection, gibt es auch hier die Möglichkeit die Hauptrichtung der Lines umzukehren.

flex.setFlexWrap(FlexboxLayout.FlexWrap.WRAP_REVERSE);

Hier das Ergebnis im Bild. Die Hauptrichtung wurde wieder auf FlexDirection.ROW gesetzt: Mit WRAP wird festgelegt, dass die FlexLine umbrochen werden soll Mit WRAP_REVERSE wird die Richtung des Umbruchs umgekehrt

JustifyContent

Mit JustifyContent definiert man die Ausrichtung entlang der Hauptachse. JustifyContent hilft dabei, verfügbaren Freiraum zu verteilen. Dazu müssen wir das Beispiel etwas anpassen, da unsere FlexItems durch Ihre MaxWidth allen Freiraum beanspruchen. Wir entfernen also die Größenconstraints beim erzeugen der Labels:

    @Override
    public void start(Stage stage) throws Exception {
        FlexBoxPane flex = new FlexBoxPane();
        flex.setJustifyContent(FlexboxLayout.JustifyContent.FLEX_START);   
        flex.getChildren().add(createLabel(1, 1));
        flex.getChildren().add(createLabel(2, 2));
        flex.getChildren().add(createLabel(3, 1));
        stage.setTitle("JavaFX FlexBox");
        Scene scene = new Scene(flex, 200, 100);
        scene.getStylesheets().add("/styles/Styles.css");
        stage.setScene(scene);
        stage.show();
    }

    private Label createLabel(int idx, int flexGrow) {
        Label label1 = new Label("Flex Item "+idx);
        label1.setAlignment(Pos.CENTER);
        FlexBoxPane.setGrow(label1, flexGrow);
        FlexBoxPane.setMargin(label1, new Insets(5));
        return label1;
    }

JustifyContent.FLEX_START : Items werden am Anfang Linie platziert

Mit JustifyContent.FLEX_START werden Items am Anfang platziert

JustifyContent.FLEX_END : Items werden am Ende Linie platziert

Mit JustifyContent.FLEX_END werden Items am Ende der Zeile platziert

JustifyContent.CENTER : Items werden zentral auf der Linie platziert

Mit JustifyContent.CENTER werden Items in der Linie zentriert

JustifyContent.SPACE_AROUND : Verfügbarer Platz wird um die Elemente herum verteilt, auch zum Rand hin.

Mit JustifyContent.SPACE_AROUND wird der Freiraum um die Items verteilt

JustifyContent.SPACE_BETWEEN : Verfügbarer Platz wird zwischen den Elemente verteilt, erstes und letztes Element sind jeweils ganz am Rand.

Mit JustifyContent.SPACE_BETWEEN wird der Freiraum im Zwischenraum der Items verteilt

AlignItems

Hiermit wird das Standardverhalten definiert, wie Flex-Items entlang der Querachse auf der aktuellen Linie angeordnet sind. Es ist also das "JustifyContent" für die Querachse (senkrecht zur Hauptachse). Um die Auswirkung sichtbar zu machen legen wir für unsere Labels unterschiedliche Höhen fest:

    @Override
    public void start(Stage stage) throws Exception {
        FlexBoxPane flex = new FlexBoxPane();
        flex.setJustifyContent(FlexboxLayout.JustifyContent.SPACE_BETWEEN);
        flex.setAlignItems(FlexboxLayout.AlignItems.FLEX_START);
        flex.getChildren().add(createLabel(1, 1));
        flex.getChildren().add(createLabel(2, 2));
        flex.getChildren().add(createLabel(3, 1));
        stage.setTitle("JavaFX FlexBox");
        Scene scene = new Scene(flex, 500, 100);
        scene.getStylesheets().add("/styles/Styles.css");
        stage.setScene(scene);
        stage.show();
    }

    private Label createLabel(int idx, int flexGrow) {
        Label label1 = new Label("Flex Item " + idx);
        label1.setPrefHeight(20 + (idx * 10));
        label1.setAlignment(Pos.CENTER);
        FlexBoxPane.setGrow(label1, flexGrow);
        FlexBoxPane.setMargin(label1, new Insets(5));
        return label1;
    }

AlignItems.FLEX_START : Items werden am vorderen Rand der Querachse platziert

Auswirkung von AlignItems.FLEX_START

AlignItems.FLEX_END : Items werden am Ende der Querachs-Linie platziert

Auswirkung von AlignItems.FLEX_END

AlignItems.CENTER : Items werden zentral auf der Querachs-Linie platziert

Auswirkung von AlignItems.CENTER

AlignItems.STRETCH : Elemente werden auf die Größe der Querachs-Linie gestreckt.

Auswirkung von AlignItems.STRETCH

AlignItems.BASELINE : Elemente werden zentriert unter Beachtung der Text-Baseline.

Auswirkung von AlignItems.BASELINE

AlignContent

Während sich AlignItems um die Anordnung der FlexItems auf der Querachse innerhalb der Linien kümmert, verteilt AlignContent, die Linien selbst entlang der Querachse. Um das zu demonstrieren, passen wir die Demo noch ein wenig an, um den Items eine bevorzugte Größe zu geben. Wir fügen auch noch ein paar zusätzliche Items hinzu:

    @Override
    public void start(Stage stage) throws Exception {
        FlexBoxPane flex = new FlexBoxPane();
        flex.setJustifyContent(FlexboxLayout.JustifyContent.SPACE_BETWEEN);
        flex.setAlignContent(FlexboxLayout.AlignContent.CENTER);
        flex.setAlignItems(FlexboxLayout.AlignItems.FLEX_START);
        flex.getChildren().add(createLabel(1, 1));
        flex.getChildren().add(createLabel(2, 2));
        flex.getChildren().add(createLabel(3, 1));
        flex.getChildren().add(createLabel(4, 1));
        flex.getChildren().add(createLabel(5, 1));
        flex.getChildren().add(createLabel(6, 1));
        stage.setTitle("JavaFX FlexBox");
        Scene scene = new Scene(flex, 500, 100);
        scene.getStylesheets().add("/styles/Styles.css");
        stage.setScene(scene);
        stage.show();
    }

    private Label createLabel(int idx, int flexGrow) {
        Label label1 = new Label("Flex Item " + idx);
        label1.setPrefWidth(175);
        label1.setAlignment(Pos.CENTER);
        FlexBoxPane.setGrow(label1, flexGrow);
        FlexBoxPane.setMargin(label1, new Insets(5));
        return label1;
    }

AlignContent.FLEX_START : Lines werden an den Anfang des Containers gepackt

Auswirkung von AlignContent.FLEX_START in der FlexBoxPane

AlignContent.FLEX_END : Lines werden am Ende des Containers platziert

Auswirkung von AlignContent.FLEX_END in der FlexBoxPane

AlignContent.CENTER : Lines werden im Container zentriert

Auswirkung von AlignContent.CENTER in der FlexBoxPane

AlignContent.SPACE_AROUND : Verfügbarer Platz wird um die Lines herum verteilt, auch zum Rand hin.

Auswirkung von AlignContent.SPACE_AROUND in der FlexBoxPane

AlignContent.SPACE_BETWEEN : Verfügbarer Platz wird zwischen den Elemente verteilt, erstes und letztes Element sind jeweils ganz am Rand.

Auswirkung von AlignContent.SPACE_BETWEEN in der FlexBoxPane

AlignContent.STRETCH : Linien werden entlang der Querachse gestreckt.

Auswirkung von AlignContent.STRETCH in der FlexBoxPane

Eigentlich sollte sich nach Spezifikation AlignContent nicht auswirken, wenn nur eine einzige Line existiert. Daher hatten wir das ursprünglich ebenfalls so implementiert. Nach einem Bugreport wegen dieses Verhaltens haben wir das jedoch geändert, da uns das Layout so sinnvoller erscheint.

Zusammenfassung

Mit der FlexBoxPane steht nun auch in JavaFX eine Implementierung des Flex Box Layout Modells zur Verfügung. Damit werden sehr einfach elastische "Mobile first" Layouts möglich. Im ersten Teil des Tutorials ging es um die Parent Constratints. Im nächsten Teil werden wir und um die Constraints der Children kümmern, bevor wir im dritten Teil einige Beispiellayouts umsetzen. Die FlexBoxPane ist kostenlos verwendbar unter der GPLv2 mit Classpath Exception.

Resourcen und thematisch passende Trainings

JFXFlexBox Projekt

FlexBox für RoboVM

FlexBoxPane Demo Anwendung

JavaFX Performance Tuning

JavaFX für Einsteiger

JavaFX für Business Applikationen