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);
}
}
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);
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;
}
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;
}
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:
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.
Mit der FlexWrap.NOWRAP
lässt sich das Umbrechen verhindern.
flex.setFlexWrap(FlexboxLayout.FlexWrap.NOWRAP);
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:
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
JustifyContent.FLEX_END
: Items werden am Ende Linie platziert
JustifyContent.CENTER
: Items werden zentral auf der Linie platziert
JustifyContent.SPACE_AROUND
: Verfügbarer Platz wird um die Elemente herum verteilt, auch zum Rand hin.
JustifyContent.SPACE_BETWEEN
: Verfügbarer Platz wird zwischen den Elemente verteilt, erstes und letztes Element sind jeweils ganz am Rand.
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
AlignItems.FLEX_END
: Items werden am Ende der Querachs-Linie platziert
AlignItems.CENTER
: Items werden zentral auf der Querachs-Linie platziert
AlignItems.STRETCH
: Elemente werden auf die Größe der Querachs-Linie gestreckt.
AlignItems.BASELINE
: Elemente werden zentriert unter Beachtung der Text-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
AlignContent.FLEX_END
: Lines werden am Ende des Containers platziert
AlignContent.CENTER
: Lines werden im Container zentriert
AlignContent.SPACE_AROUND
: Verfügbarer Platz wird um die Lines herum verteilt, auch zum Rand hin.
AlignContent.SPACE_BETWEEN
: Verfügbarer Platz wird zwischen den Elemente verteilt, erstes und letztes Element sind jeweils ganz am Rand.
AlignContent.STRETCH
: Linien werden entlang der Querachse gestreckt.
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.