Статьи

JavaFX Совет 17: Анимированный макет рабочей среды с AnchorPane

Недавно мне пришлось реализовать макет для приложения, в котором область меню и область состояния могут быть скрыты или показаны с помощью анимации скольжения / выдвижения в зависимости от того, вошел ли пользователь в систему или нет. Следующее видео показывает макет в действии:

В прошлом я, вероятно, реализовывал такое поведение с помощью пользовательского элемента управления и пользовательского кода макета (как в «методе переопределения layoutChildren () в скине»). Но на этот раз мои настройки были другими, потому что я использовал afterburner.fx от Адама Бина, и теперь у меня был FXML и класс контроллера.

Так что же делать? Я решил попытать счастья с панелью привязки и обновить ограничения на панелях стека с помощью экземпляра временной шкалы. Ограничения хранятся в карте наблюдаемых свойств панелей стека. Всякий раз, когда эти ограничения изменяются, макет панели привязки запрашивается автоматически. Если это происходит без мерцания, мы получаем плавную анимацию. Кстати, от Swing я всегда ожидаю мерцания, но обычно это не происходит с JavaFX.

В итоге я написал следующий класс контроллера, управляющий панелью привязки и ее дочерними панелями стека. Обратите внимание на небольшую хитрость с промежуточными свойствами menuPaneLocation и bottomPaneLocation . Они необходимы, потому что временная шкала анимации работает со свойствами. Таким образом, он обновляет эти свойства и всякий раз, когда они меняются, применяются новые ограничения панели привязки.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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();
}
}

Ниже приведен FXML, который был необходим для этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
<?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>