JavaFX Tip 17: Animated Workbench Layout with AnchorPane
I recently had to implement a layout for an application where the menu area and the status area could be hidden or shown with a slide-in / slide-out animation based on whether the user was logged in or not. The following video shows the the layout in action:
In the past I probably would have implemented this kind of behavior with a custom control and custom layout code (as in “override layoutChildren() method in skin”). But this time my setup was different because I was using afterburner.fx from Adam Bien and now I had FXML and a controller class.
So what do do? I decided to try my luck with an anchor pane and to update the constraints on the stack panes via a timeline instance. Constraints are stored in the observable properties map of the stack panes. Whenever these constraints change, a layout of the anchor pane is requested automatically. If this happens without any flickering then we end up with a nice smooth animation. By the way, coming from Swing, I always expect flickering, but it normally doesn’t happen with JavaFX.
I ended up writing the following controller class managing the anchor pane and its children stack panes. Please notice the little trick with the intermediate properties menuPaneLocation and bottomPaneLocation. They are required because the animation timeline works with properties. So it updates these properties and whenever they change new anchor pane constraints are applied.
import static javafx.scene.layout.AnchorPane.setBottomAnchor; import static javafx.scene.layout.AnchorPane.setLeftAnchor; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.fxml.FXML; import javafx.scene.layout.StackPane; import javafx.util.Duration;</code> /** * This presenter covers the top-level layout concepts of the workbench. */ public class WorkbenchPresenter { @FXML private StackPane topPane; @FXML private StackPane menuPane; @FXML private StackPane centerPane; @FXML private StackPane bottomPane; public WorkbenchPresenter() { } private final BooleanProperty showMenuPane = new SimpleBooleanProperty(this, "showMenuPane", true); public final boolean isShowMenuPane() { return showMenuPane.get(); } public final void setShowMenuPane(boolean showMenu) { showMenuPane.set(showMenu); } /** * Returns the property used to control the visibility of the menu panel. * When the value of this property changes to false then the menu panel will * slide out to the left). * * @return the property used to control the menu panel */ public final BooleanProperty showMenuPaneProperty() { return showMenuPane; } private final BooleanProperty showBottomPane = new SimpleBooleanProperty(this, "showBottomPane", true); public final boolean isShowBottomPane() { return showBottomPane.get(); } public final void setShowBottomPane(boolean showBottom) { showBottomPane.set(showBottom); } /** * Returns the property used to control the visibility of the bottom panel. * When the value of this property changes to false then the bottom panel * will slide out to the left). * * @return the property used to control the bottom panel */ public final BooleanProperty showBottomPaneProperty() { return showBottomPane; } public final void initialize() { menuPaneLocation.addListener(it -> updateMenuPaneAnchors()); bottomPaneLocation.addListener(it -> updateBottomPaneAnchors()); showMenuPaneProperty().addListener(it -> animateMenuPane()); showBottomPaneProperty().addListener(it -> animateBottomPane()); menuPane.setOnMouseClicked(evt -> setShowMenuPane(false)); centerPane.setOnMouseClicked(evt -> { setShowMenuPane(true); setShowBottomPane(true); }); bottomPane.setOnMouseClicked(evt -> setShowBottomPane(false)); } /* * The updateMenu/BottomPaneAnchors methods get called whenever the value of * menuPaneLocation or bottomPaneLocation changes. Setting anchor pane * constraints will automatically trigger a relayout of the anchor pane * children. */ private void updateMenuPaneAnchors() { setLeftAnchor(menuPane, getMenuPaneLocation()); setLeftAnchor(centerPane, getMenuPaneLocation() + menuPane.getWidth()); } private void updateBottomPaneAnchors() { setBottomAnchor(bottomPane, getBottomPaneLocation()); setBottomAnchor(centerPane, getBottomPaneLocation() + bottomPane.getHeight()); setBottomAnchor(menuPane, getBottomPaneLocation() + bottomPane.getHeight()); } /* * Starts the animation for the menu pane. */ private void animateMenuPane() { if (isShowMenuPane()) { slideMenuPane(0); } else { slideMenuPane(-menuPane.prefWidth(-1)); } } /* * Starts the animation for the bottom pane. */ private void animateBottomPane() { if (isShowBottomPane()) { slideBottomPane(0); } else { slideBottomPane(-bottomPane.prefHeight(-1)); } } /* * The animations are using the JavaFX timeline concept. The timeline updates * properties. In this case we have to introduce our own properties further * below (menuPaneLocation, bottomPaneLocation) because ultimately we need to * update layout constraints, which are not properties. So this is a little * work-around. */ private void slideMenuPane(double toX) { KeyValue keyValue = new KeyValue(menuPaneLocation, toX); KeyFrame keyFrame = new KeyFrame(Duration.millis(300), keyValue); Timeline timeline = new Timeline(keyFrame); timeline.play(); } private void slideBottomPane(double toY) { KeyValue keyValue = new KeyValue(bottomPaneLocation, toY); KeyFrame keyFrame = new KeyFrame(Duration.millis(300), keyValue); Timeline timeline = new Timeline(keyFrame); timeline.play(); } private DoubleProperty menuPaneLocation = new SimpleDoubleProperty(this, "menuPaneLocation"); private double getMenuPaneLocation() { return menuPaneLocation.get(); } private DoubleProperty bottomPaneLocation = new SimpleDoubleProperty(this, "bottomPaneLocation"); private double getBottomPaneLocation() { return bottomPaneLocation.get(); } }
The following is the FXML that was required for this to work:
<?xml version="1.0" encoding="UTF-8"?> <?import java.lang.*?> <?import javafx.scene.layout.*?> <AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.workbench.WorkbenchPresenter"> <children> <StackPane fx:id="bottomPane" layoutX="-4.0" layoutY="356.0" prefHeight="40.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" /> <StackPane fx:id="menuPane" layoutY="28.0" prefWidth="200.0" AnchorPane.bottomAnchor="40.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="40.0" /> <StackPane fx:id="topPane" prefHeight="40.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" /> <StackPane fx:id="centerPane" layoutX="72.0" layoutY="44.0" AnchorPane.bottomAnchor="40.0" AnchorPane.leftAnchor="200.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="40.0" /> </children> </AnchorPane>
Reference: | JavaFX Tip 17: Animated Workbench Layout with AnchorPane from our JCG partner Dirk Lemmermann at the Pixel Perfect blog. |