I am trying to create a custom pane in javafx that holds a group of buttons.
The pane auto lays out the buttons so that they take up the maximum allowable width.
The pane works great, the problem I am having is that when the scene is set on the stage, the buttons lay out, but are off the screen to the bottom.
Any action I make on the stage (including clicking another windows and de-focusing the stage) causes them to lay out correctly.
I can't figure out what is going on.
I want to post a screenshot of what is happening, the problem is as mentioned when I go to take the screenshot, the window de-focuses and the bar then lays out correctly.
public class ButtonBar extends Pane {
//Add to auto generated buttons
protected String buttonPrefix = "F";
protected String buttonSuffix = "";
//Container sizing
protected DoubleProperty prefHeight = new SimpleDoubleProperty();
//Button Sizing
protected DoubleProperty buttonWidth = new SimpleDoubleProperty();
protected DoubleProperty buttonHeight = new SimpleDoubleProperty();
//Button Positioning
protected ArrayList<DoubleProperty> buttonXLocation;
//Static Button sizing
protected double interButtonMargin = 1;
protected double endButtonMargin = 10;
//Store whether initialized
protected boolean initialized = false;
//Listen for change to relayout
InvalidationListener layoutListener;
public int getNumberButtons() {
return numberButtons;
}
public void setNumberButtons(int numberButtons) {
this.numberButtons = numberButtons;
}
//Number of buttons
protected int numberButtons = 12;
protected boolean respectApsectRatio = true;
public ButtonBar() {
this(12);
}
public ButtonBar(List<Button> buttons) {
this.numberButtons = buttons.size();
getChildren().addAll(buttons);
}
public ButtonBar(int numButtons) {
this.numberButtons = numButtons;
for (int i = 0; i < numButtons; i++) {
Button button = new Button(String.valueOf(i + 1));
button.setStyle("-fx-background-radius: 7; -fx-font-size: 30px; -fx-background-color: #ffb366");
getChildren().add(button);
}
}
protected void init() {
buttonXLocation = new ArrayList<DoubleProperty>(numberButtons);
List<Node> children = getChildren();
//Calculate button width minus paddings
buttonWidth.bind(
widthProperty()
.subtract(interButtonMargin * (numberButtons - 1))
.subtract(endButtonMargin * 2)
.divide(numberButtons)
);
if (!respectApsectRatio) {
buttonHeight.bind(heightProperty());
prefHeight.bind(heightProperty());
} else {
buttonHeight.bind(buttonWidth);
prefHeight.bind(buttonHeight);
}
int i = 0;
for (Node n : getChildren()) {
Button b = (Button) n;
DoubleProperty buttonX = new SimpleDoubleProperty();
buttonX.bind(buttonWidth
.multiply(i)
.add(endButtonMargin)
.add(interButtonMargin * i)
);
buttonXLocation.add(i, buttonX);
//Set button text
b.setText(buttonPrefix + b.getText() + buttonSuffix);
i++;
}
setNeedsLayout(true);
this.initialized = true;
layoutListener = new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
setNeedsLayout(true);
layout();
layoutChildren();
}
};
widthProperty().addListener(layoutListener);
heightProperty().addListener(layoutListener);
}
#Override
protected void layoutChildren() {
//Only initialize once
if( !initialized ) init();
super.layoutChildren();
setPrefHeight(prefHeight.doubleValue());
List<Node> buttons = getChildren();
for (int i = 0; i < buttons.size(); i++) {
Button button = (Button) buttons.get(i);
button.setPrefWidth(buttonWidth.doubleValue());
button.setPrefHeight(buttonHeight.doubleValue());
button.setLayoutX(buttonXLocation.get(i).doubleValue());
button.setLayoutY(0);
}
super.layoutChildren();
}`
Related
You maybe have seen some apps that when the String in a tab can't be fully shown then it is animated and is going back and front so the user can see what String the tab contains.Android is doing that is settings when your phone display is not enough to show the the whole label.
Below is a code to achieve it in JavaFX using Service,but it is not a good way.
The Question is:
Here is how i can do it using Animation or another build in JavaFX class?
Code:
import javafx.application.Platform;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.scene.control.Label;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import tools.InfoTool;
public class MoveTitleService extends Service<Void>{
private String title;
volatile boolean doAnimation;
private int counter;
public Label movingText = new Label("A reallyyy big teeeeexxxxxxxxxxxxxxxxxxxxxxxt");
/**
*Constructor
*/
public MoveTitleService() {
movingText.setFont(Font.font("null",FontWeight.BOLD,14));
movingText.setTextFill(Color.WHITE);
setOnSucceeded( s ->{
movingText.setText("");
});
}
//Start the Service
public void startTheService(String title) {
this.title = title;
doAnimation = true;
restart();
}
//Stop the Service
public void stopService(){
doAnimation=false;
}
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
while (doAnimation) {
//System.out.println("MoveTitleService is Running...");
// One letter at a time
for (int m = 0; m <= title.length(); m++) {
counter=m;
Platform.runLater( () ->{
movingText.setText(title.substring(0, counter) + addSpaces(title.length() - counter));
});
if(!doAnimation) break;
Thread.sleep(150);
}
// Disappearing to back
for (int m = 0; m < title.length(); m++) {
counter=m;
Platform.runLater( () ->{
movingText.setText(title.substring(counter));
});
if(!doAnimation) break;
Thread.sleep(150);
}
// Appearing to front
for (int m = 1; m <= title.length(); m++) {
counter=m;
Platform.runLater( () ->{
movingText.setText(title.substring(title.length() - counter));
});
if(!doAnimation) break;
Thread.sleep(150);
}
if(!doAnimation) break;
for(int i=0; i<3000/150; i++)
Thread.sleep(150);
Thread.sleep(3000);
}
return null;
}
private String addSpaces(int spaces) {
String z = "";
for (int i = 0; i <= spaces; i++)
z += " ";
return z;
}
};
}
}
You could use a Timeline for this:
// min distance to Pane bounds
private static final double OFFSET = 25;
#Override
public void start(Stage primaryStage) {
Text text = new Text("A reallyyy big teeeeexxxxxxxxxxxxxxxxxxxxxxxt!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
text.setLayoutY(25);
text.setManaged(false);
text.setLayoutX(OFFSET);
Pane pane = new Pane(text);
pane.setMinHeight(50);
Timeline timeline = new Timeline();
KeyFrame updateFrame = new KeyFrame(Duration.seconds(1 / 60d), new EventHandler<ActionEvent>() {
private boolean rightMovement;
#Override
public void handle(ActionEvent event) {
double tW = text.getLayoutBounds().getWidth();
double pW = pane.getWidth();
double layoutX = text.getLayoutX();
if (2 * OFFSET + tW <= pW && layoutX >= OFFSET) {
// stop, if the pane is large enough and the position is correct
text.setLayoutX(OFFSET);
timeline.stop();
} else {
if ((rightMovement && layoutX >= OFFSET) || (!rightMovement && layoutX + tW + OFFSET <= pW)) {
// invert movement, if bounds are reached
rightMovement = !rightMovement;
}
// update position
if (rightMovement) {
layoutX += 1;
} else {
layoutX -= 1;
}
text.setLayoutX(layoutX);
}
}
});
timeline.getKeyFrames().add(updateFrame);
timeline.setCycleCount(Animation.INDEFINITE);
// listen to bound changes of the elements to start/stop the animation
InvalidationListener listener = o -> {
double textWidth = text.getLayoutBounds().getWidth();
double paneWidth = pane.getWidth();
if (textWidth + 2 * OFFSET > paneWidth
&& timeline.getStatus() != Animation.Status.RUNNING) {
timeline.play();
}
};
text.layoutBoundsProperty().addListener(listener);
pane.widthProperty().addListener(listener);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
Note that repeadedly updating the position yourself needs to be done, since Animations cannot be adjusted while running, so for any resizing to take effect during the animation, you need repeated updates...
I'm making a memory game for school and I'm having a bit of trouble hiding the tiles in my gridpane.
I've got a seperate class where I use my eventhandlers.
I used an ObservableList to fill in my gridpane with tiles but I got no idea how to make them disappear. Thanks for taking a look , I tried to use a FadeTransition in the view class.
View
public class GameView extends GridPane {
private static final int PAREN = 10;
private Label scoreSpeler1;
private Label countDown;
private Label title;
private final static Color backgroundColor = Color.rgb(225, 93, 68);
int waarde = 0;
ObservableList<Button> vakken;
public GameView() {
this.initialiseNodes();
this.layoutNodes();
this.verdwijnWaarde();
this.toonWaarde();
}
private void initialiseNodes() {
scoreSpeler1 = new Label("Score: 0");
countDown = new Label("Tijd: 60");
title = new Label("Memory");
/*knoppen met cijfers*/
vakken = FXCollections.observableArrayList(new ArrayList<>());
for (int i = 0; i < PAREN; i++) {
vakken.add(new Button(String.valueOf(waarde)));
vakken.add(new Button(String.valueOf(waarde)));
waarde++;
}
FXCollections.shuffle(vakken);
}
private void layoutNodes() {
//score en tijd plaatsen
this.add(scoreSpeler1, 0, 0);
this.add(title, 2, 0);
this.add(countDown, 4, 0);
//memory knoppen plaatsen
int k = 0;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 4; j++) {
this.add(vakken.get(k), i, j + 2);
k++;
}
}
//label uiterlijk
GridPane.setHalignment(countDown, HPos.RIGHT);
GridPane.setHalignment(title, HPos.CENTER);
title.setId("titelGame");
scoreSpeler1.setId("scoreSpeler1");
countDown.setId("countDown");
//button/memory uiterlijk
for (Button button : vakken) {
button.setPrefHeight(120);
button.setPrefWidth(90);
}
//gridpane uiterlijk
this.setGridLinesVisible(false);
this.setBackground(new Background(new BackgroundFill(backgroundColor, null, null)));
this.setHgap(15);
this.setVgap(15);
this.setPadding(new Insets(10, 10, 10, 10));
this.autosize();
this.setAlignment(Pos.CENTER);
}
public void verdwijnWaarde() {
FadeTransition verstopWaardes = new FadeTransition();
verstopWaardes.durationProperty(Duration.seconds(2), this.vakken.get(1));
verstopWaardes.setToValue(0);
verstopWaardes.play();
}
public void toonWaarde() {
FadeTransition toonWaardes = new FadeTransition();
toonWaardes.durationProperty(Duration.seconds(0.5), vakken);
toonWaardes.setToValue(1);
toonWaardes.play();
}
}
presenter
public class GamePresenter {
private MemoryModel model;
private GameView gameView;
public GamePresenter(MemoryModel model, GameView gameView){
this.model = model;
this.gameView = gameView;
addEventHandlers();
}
private void addEventHandlers(){
gameView.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
gameView.toonWaarde();
}
});
}
private void updateView(){
}
public void addWindowEventHandlers(){
}
}
I initialized rowSize (number of rows) and colSize (number of columns) inside the MapTest class. I created 3 methods: startStage1(), startStage2() and startStage3(); each is assigned to a button. Each method assigns the rowSize and colSize to a new integer.
I want to be able to the re-size of my GridPane whenever I click a button but it does not works that way.
What could I have done differently?
import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class MapTest extends Application implements EventHandler<KeyEvent> {
static Stage theStage;
static Scene scene1, scene2;
// top box
HBox topBox = new HBox();
// bottom box
HBox bottomBox = new HBox();
// grid dimensions (I'm trying to manipulate these variables)
int rowSize;
int colSize;
int tileSize;
GridPane gridPane;
GridMapT gridMap;
#Override
public void start(Stage firstStage) throws Exception {
theStage = firstStage;
// scene 2 //////////////
Label label2 = new Label("scene 2: choose a stage");
Button stage1_btn = new Button("Room 5x5");
Button stage2_btn = new Button("Room 7x7");
Button stage3_btn = new Button("Room 10x10");
Button button5 = new Button("Exit");
stage1_btn.setOnAction(e -> {
startStage1();
});
stage2_btn.setOnAction(e -> {
startStage2();
});
stage3_btn.setOnAction(e -> {
startStage3();
});
button5.setOnAction(e -> System.exit(0));
// Layout1
VBox layout = new VBox(20);
layout.setAlignment(Pos.CENTER);
layout.getChildren().addAll(label2, stage1_btn, stage2_btn, stage3_btn, button5);
scene1 = new Scene(layout, 800, 600);
// Scene 3 ////////////////////
// top box
Label title = new Label("Map test");
topBox.getChildren().add(title);
// bottom box
Label instruction = new Label("");
bottomBox.getChildren().add(instruction);
// scene 3
BorderPane gameScreen = new BorderPane();
scene2 = new Scene(gameScreen);
// set up gridPane
gridPane = new GridPane();
gridMap = new GridMapT(rowSize, colSize);
for (int x = 0; x < rowSize; x++) {
for (int y = 0; y < colSize; y++) {
String grid = gridMap.getMap()[x][y];
// floor labels
if (grid == "floor") {
Label table = new Label("F");
table.setMinWidth(tileSize);
table.setMinHeight(tileSize);
gridPane.add(table, x, y);
}
// wall labels
if (grid == "wall") {
Label table = new Label("W");
table.setMinWidth(tileSize);
table.setMinHeight(tileSize);
gridPane.add(table, x, y);
}
}
}
////// ////////////////////////////////////////////////////////////////////////
// add a clickable reset and Debug Mode to bottom box
Button resetBtn = new Button();
resetBtn.setText("Quit game");
bottomBox.getChildren().add(resetBtn);
resetBtn.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<Event>() {
#Override
public void handle(Event event) {
try {
restart(firstStage);
} catch (Exception e) {
e.printStackTrace();
}
}
});
// keyboard input
scene2.setOnKeyPressed(this);
// setting up the whole borderPane
gameScreen.setTop(topBox);
gameScreen.setBottom(bottomBox);
gameScreen.setCenter(gridPane);
// set scene1 as start up screen
firstStage.setScene(scene1);
firstStage.show();
}
// restart method
public void restart(Stage stage) throws Exception {
topBox.getChildren().clear();
bottomBox.getChildren().clear();
gridPane.getChildren().clear();
gridMap = new GridMapT(rowSize, colSize);
start(stage);
}
///////////////////////////////////////////////////////////////////////////////////////
// stage setting methods
public void startStage1() {
rowSize = 21;
colSize = 21;
tileSize = 40;
theStage.setScene(scene2);
}
public void startStage2() {
rowSize = 29;
colSize = 29;
tileSize = 30;
theStage.setScene(scene2);
}
public void startStage3() {
rowSize = 41;
colSize = 41;
tileSize = 20;
theStage.setScene(scene2);
}
#Override
public void handle(KeyEvent event) {
// TODO Auto-generated method stub
}
public static void main(String[] args) {
launch(args);
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// GridMap Class
public class GridMapT {
private String[][] map;
public GridMapT(int rowSize, int colSize) {
this.map = new String[rowSize][colSize];
// set up wall and fog
for (int i = 0; i < rowSize; i++) {
for (int j = 0; j < colSize; j++) {
if (i % 4 == 0 || j % 4 == 0) {
map[i][j] = "wall";
} else {
map[i][j] = "floor";
}
}
}
}
public String[][] getMap() {
return map;
}
}
}
I made a couple of changes to your code and made it work.
Firstly Application#start method should not be called by anybody but system. It is your app's entry point and calling it manually breaks app's lifecycle.
Secondly there's no need to create GridMapT instance in start method just because you don't know by the time it runs how many columns and rows there should be. And that's why there's no need to put map into your GridPane here too.
Thirdly you need to create new GridMapT instance or change existing any time you need to update your map. So after you created new map or updated existing (added or removed items) you need to put it into your GridPane. Both pieces of code that creates map and updates grid seems to be ok, but they are called at the wrong time and in the wrong place.
Lastly int type is a primitive in Java. Changing any int value affects only it and you still need to recreate and/or update anything that depends on it manually. This means that you need manually create/update map with new colSize and rowSize values each time you change them. If you create new map you need manually pass it to your GridPane instance.
In general your app's logic should look like that:
Create stage, scenes, components.
Show first scene
Wait for user's input (click, size, etc.)
Update second scene
Show second scene
Wait for user's input (click Quit)
Close second scene
Repeat 3-7 according to user's actions
There are still a couple of things that can be done better here and I wish you luck to find and improve them :)
Improved code:
public class Test extends Application implements EventHandler<KeyEvent>{
static Stage theStage;
static Scene scene1, scene2;
// top box
HBox topBox = new HBox();
// bottom box
HBox bottomBox = new HBox();
// grid dimensions (I'm trying to manipulate these variables)
int rowSize;
int colSize;
int tileSize;
GridPane gridPane;
GridMapT gridMap;
#Override
public void start(Stage firstStage) throws Exception{
theStage = firstStage;
// scene 2 //////////////
Label label2 = new Label("scene 2: choose a stage");
Button stage1_btn = new Button("Room 5x5");
Button stage2_btn = new Button("Room 7x7");
Button stage3_btn = new Button("Room 10x10");
Button button5 = new Button("Exit");
stage1_btn.setOnAction(e -> {
startStage1();
});
stage2_btn.setOnAction(e -> {
startStage2();
});
stage3_btn.setOnAction(e -> {
startStage3();
});
button5.setOnAction(e -> System.exit(0));
// Layout1
VBox layout = new VBox(20);
layout.setAlignment(Pos.CENTER);
layout.getChildren().addAll(label2, stage1_btn, stage2_btn, stage3_btn, button5);
scene1 = new Scene(layout, 800, 600);
// Scene 3 ////////////////////
// top box
Label title = new Label("Map test");
topBox.getChildren().add(title);
// bottom box
Label instruction = new Label("");
bottomBox.getChildren().add(instruction);
// scene 3
BorderPane gameScreen = new BorderPane();
scene2 = new Scene(gameScreen);
// set up gridPane
gridPane = new GridPane();
////// ////////////////////////////////////////////////////////////////////////
// add a clickable reset and Debug Mode to bottom box
Button resetBtn = new Button();
resetBtn.setText("Quit game");
bottomBox.getChildren().add(resetBtn);
resetBtn.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<Event>(){
#Override
public void handle(Event event){
try {
//There's no need to call start method anymore.
//Think of the stage like it's your app's window
//and of a scene like it is window's content.
//restart(firstStage);
firstStage.setScene(scene1);
} catch (Exception e) {
e.printStackTrace();
}
}
});
// keyboard input
scene2.setOnKeyPressed(this);
// setting up the whole borderPane
gameScreen.setTop(topBox);
gameScreen.setBottom(bottomBox);
gameScreen.setCenter(gridPane);
// set scene1 as start up screen
firstStage.setScene(scene1);
firstStage.show();
}
//TODO pass rowSize and colSize here
private void createMap(){
gridMap = new GridMapT(rowSize, colSize);
}
//TODO pass gridMap
private void redrawMap(){
gridPane.getChildren().clear();
for (int x = 0; x < rowSize; x++) {
for (int y = 0; y < colSize; y++) {
String grid = gridMap.getMap()[x][y];
// floor labels
if (grid == "floor") {
Label table = new Label("F");
table.setMinWidth(tileSize);
table.setMinHeight(tileSize);
gridPane.add(table, x, y);
}
// wall labels
if (grid == "wall") {
Label table = new Label("W");
table.setMinWidth(tileSize);
table.setMinHeight(tileSize);
gridPane.add(table, x, y);
}
}
}
}
// restart method
public void restart(Stage stage) throws Exception{
topBox.getChildren().clear();
bottomBox.getChildren().clear();
gridPane.getChildren().clear();
gridMap = new GridMapT(rowSize, colSize);
start(stage);
}
///////////////////////////////////////////////////////////////////////////////////////
// stage setting methods
public void startStage1(){
rowSize = 21;
colSize = 21;
tileSize = 40;
createMap();
redrawMap();
theStage.setScene(scene2);
}
public void startStage2(){
rowSize = 29;
colSize = 29;
tileSize = 30;
createMap();
redrawMap();
theStage.setScene(scene2);
}
public void startStage3(){
rowSize = 41;
colSize = 41;
tileSize = 20;
createMap();
redrawMap();
theStage.setScene(scene2);
}
#Override
public void handle(KeyEvent event){
// TODO Auto-generated method stub
}
public static void main(String[] args){
launch(args);
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// GridMap Class
public class GridMapT{
private String[][] map;
public GridMapT(int rowSize, int colSize){
this.map = new String[rowSize][colSize];
// set up wall and fog
for (int i = 0; i < rowSize; i++) {
for (int j = 0; j < colSize; j++) {
if (i % 4 == 0 || j % 4 == 0) {
map[i][j] = "wall";
} else {
map[i][j] = "floor";
}
}
}
}
public String[][] getMap(){
return map;
}
}
}
You maybe have seen some apps that when the String in a tab can't be fully shown then it is animated and is going back and front so the user can see what String the tab contains.Android is doing that is settings when your phone display is not enough to show the the whole label.
Below is a code to achieve it in JavaFX using Service,but it is not a good way.
The Question is:
Here is how i can do it using Animation or another build in JavaFX class?
Code:
import javafx.application.Platform;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.scene.control.Label;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import tools.InfoTool;
public class MoveTitleService extends Service<Void>{
private String title;
volatile boolean doAnimation;
private int counter;
public Label movingText = new Label("A reallyyy big teeeeexxxxxxxxxxxxxxxxxxxxxxxt");
/**
*Constructor
*/
public MoveTitleService() {
movingText.setFont(Font.font("null",FontWeight.BOLD,14));
movingText.setTextFill(Color.WHITE);
setOnSucceeded( s ->{
movingText.setText("");
});
}
//Start the Service
public void startTheService(String title) {
this.title = title;
doAnimation = true;
restart();
}
//Stop the Service
public void stopService(){
doAnimation=false;
}
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
while (doAnimation) {
//System.out.println("MoveTitleService is Running...");
// One letter at a time
for (int m = 0; m <= title.length(); m++) {
counter=m;
Platform.runLater( () ->{
movingText.setText(title.substring(0, counter) + addSpaces(title.length() - counter));
});
if(!doAnimation) break;
Thread.sleep(150);
}
// Disappearing to back
for (int m = 0; m < title.length(); m++) {
counter=m;
Platform.runLater( () ->{
movingText.setText(title.substring(counter));
});
if(!doAnimation) break;
Thread.sleep(150);
}
// Appearing to front
for (int m = 1; m <= title.length(); m++) {
counter=m;
Platform.runLater( () ->{
movingText.setText(title.substring(title.length() - counter));
});
if(!doAnimation) break;
Thread.sleep(150);
}
if(!doAnimation) break;
for(int i=0; i<3000/150; i++)
Thread.sleep(150);
Thread.sleep(3000);
}
return null;
}
private String addSpaces(int spaces) {
String z = "";
for (int i = 0; i <= spaces; i++)
z += " ";
return z;
}
};
}
}
You could use a Timeline for this:
// min distance to Pane bounds
private static final double OFFSET = 25;
#Override
public void start(Stage primaryStage) {
Text text = new Text("A reallyyy big teeeeexxxxxxxxxxxxxxxxxxxxxxxt!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
text.setLayoutY(25);
text.setManaged(false);
text.setLayoutX(OFFSET);
Pane pane = new Pane(text);
pane.setMinHeight(50);
Timeline timeline = new Timeline();
KeyFrame updateFrame = new KeyFrame(Duration.seconds(1 / 60d), new EventHandler<ActionEvent>() {
private boolean rightMovement;
#Override
public void handle(ActionEvent event) {
double tW = text.getLayoutBounds().getWidth();
double pW = pane.getWidth();
double layoutX = text.getLayoutX();
if (2 * OFFSET + tW <= pW && layoutX >= OFFSET) {
// stop, if the pane is large enough and the position is correct
text.setLayoutX(OFFSET);
timeline.stop();
} else {
if ((rightMovement && layoutX >= OFFSET) || (!rightMovement && layoutX + tW + OFFSET <= pW)) {
// invert movement, if bounds are reached
rightMovement = !rightMovement;
}
// update position
if (rightMovement) {
layoutX += 1;
} else {
layoutX -= 1;
}
text.setLayoutX(layoutX);
}
}
});
timeline.getKeyFrames().add(updateFrame);
timeline.setCycleCount(Animation.INDEFINITE);
// listen to bound changes of the elements to start/stop the animation
InvalidationListener listener = o -> {
double textWidth = text.getLayoutBounds().getWidth();
double paneWidth = pane.getWidth();
if (textWidth + 2 * OFFSET > paneWidth
&& timeline.getStatus() != Animation.Status.RUNNING) {
timeline.play();
}
};
text.layoutBoundsProperty().addListener(listener);
pane.widthProperty().addListener(listener);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
Note that repeadedly updating the position yourself needs to be done, since Animations cannot be adjusted while running, so for any resizing to take effect during the animation, you need repeated updates...
I am trying to prove out an expandable widget with JavaFX that differs from a TitledPane in that the border surrounding the label grows with the widget as it expands. I have found a way of doing it.
However I have a weird glitch that shows up after expanding/collapsing once or twice.
This is a link to a short video where the problem appears at the third and fourth expansion:
Short Youtube Concept / Glitch Video
I've run out of things I could try to get it to behave.
The code is as follows, apologies for the wackiness of it, wanted to get it working before I refactor it.
class ExpansionManager {
enum LayoutState {
INITIALIZE,
ANIMATING,
IDLE,
REQUEST_ANIMATION
}
LayoutState layoutState = LayoutState.INITIALIZE;
Double fromWidth = 0.0;
Double fromHeight = 0.0;
Double stepWidth = 0.0;
Double stepHeight = 0.0;
Double toWidth = 0.0;
Double toHeight = 0.0;
}
public class ExpandableTitledList extends VBox {
private Label title = new Label();
private ListProperty<String> listItem = new SimpleListProperty<>();
private ListView listView = new ListView<>(listItem);
Timeline timeline;
WritableValue<Double> writableHeight = new WritableValue<Double>() {
#Override
public Double getValue() {
return expansionManager.stepHeight;
}
#Override
public void setValue(Double value) {
expansionManager.stepHeight = value;
requestLayout();
}
};
WritableValue<Double> writableWidth = new WritableValue<Double>() {
#Override
public Double getValue() {
return expansionManager.stepWidth;
}
#Override
public void setValue(Double value) {
expansionManager.stepWidth = value;
requestLayout();
}
};
private boolean expanded = false;
ExpansionManager expansionManager = new ExpansionManager();
// private Dimension2D contractedDimension;
// private Dimension2D expandedDimension;
public ExpandableTitledList() {
setTitle("boom");
// title.layout();
// System.out.println(title.getLayoutBounds().getWidth());
// set down right caret
listItem.setValue(FXCollections.observableArrayList("one", "two"));
Insets theInsets = new Insets(-3, -5, -3, -5);
Border theBorder = new Border(
new BorderStroke(
Color.BLACK,
BorderStrokeStyle.SOLID,
new CornerRadii(4),
new BorderWidths(2),
theInsets
)
);
// expandedDimension = new Dimension2D(200,200);
setBorder(theBorder);
getChildren().addAll(title);
title.setOnMouseClicked((event) -> {
System.out.println("mouse clicked");
if (this.expanded) contract();
else expand();
});
}
#Override
protected void layoutChildren() {
System.out.println(expansionManager.layoutState);
if (expansionManager.layoutState == ExpansionManager.LayoutState.INITIALIZE) {
super.layoutChildren();
expansionManager.layoutState = ExpansionManager.LayoutState.IDLE;
} else if (expansionManager.layoutState == ExpansionManager.LayoutState.ANIMATING) {
super.layoutChildren();
} else if (expansionManager.layoutState == ExpansionManager.LayoutState.REQUEST_ANIMATION) {
setCache(false);
listView.setCache(false);
expansionManager.layoutState = ExpansionManager.LayoutState.ANIMATING;
System.out.println("from : " + expansionManager.fromWidth + ", "+ expansionManager.fromHeight);
System.out.println("to : " + expansionManager.toWidth + ", "+ expansionManager.toHeight);
timeline = new Timeline();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO,
new KeyValue(writableHeight, expansionManager.fromHeight),
new KeyValue(writableWidth, expansionManager.fromWidth)),
new KeyFrame(Duration.millis(100),
new KeyValue(writableHeight, expansionManager.toHeight),
new KeyValue(writableWidth, expansionManager.toWidth))
);
timeline.play();
timeline.setOnFinished((done) -> {
System.out.println("done");
expansionManager.layoutState = ExpansionManager.LayoutState.IDLE;
timeline = null;
});
} else {
System.out.println("idle");
super.layoutChildren();
}
}
#Override
protected double computePrefHeight(double width) {
if (expansionManager.layoutState == ExpansionManager.LayoutState.INITIALIZE) {
expansionManager.fromHeight = super.computePrefHeight(width);
return expansionManager.fromHeight;
} else if (expansionManager.layoutState == ExpansionManager.LayoutState.ANIMATING) {
return expansionManager.stepHeight;
} else if (expansionManager.layoutState == ExpansionManager.LayoutState.REQUEST_ANIMATION) {
expansionManager.fromHeight = getHeight();
expansionManager.stepHeight = expansionManager.fromHeight;
expansionManager.toHeight = super.computePrefHeight(width);
return expansionManager.fromHeight;
} else {
return expansionManager.toHeight;
}
}
#Override
protected double computePrefWidth(double height) {
if (expansionManager.layoutState == ExpansionManager.LayoutState.INITIALIZE) {
expansionManager.fromWidth = super.computePrefWidth(height);
return expansionManager.fromWidth;
} else if (expansionManager.layoutState == ExpansionManager.LayoutState.ANIMATING) {
return expansionManager.stepWidth;
} else if (expansionManager.layoutState == ExpansionManager.LayoutState.REQUEST_ANIMATION) {
expansionManager.fromWidth = getWidth();
expansionManager.stepWidth = expansionManager.fromWidth;
expansionManager.toWidth = super.computePrefWidth(height);
return expansionManager.fromWidth;
} else {
System.out.println("BANG BANG BANG");
return expansionManager.toWidth;
}
}
// #Override
// protected double computeMinWidth(double height) {
// return computePrefWidth(height);
// }
//
// #Override
// protected double computeMinHeight(double width) {
// return computePrefHeight(width);
// }
//
// #Override
// protected double computeMaxWidth(double height) {
// return computePrefWidth(height);
// }
//
// #Override
// protected double computeMaxHeight(double width) {
// return computePrefHeight(width);
// }
private void expand() {
System.out.println(expansionManager.layoutState);
// if(contractedDimension == null)
// contractedDimension = new Dimension2D(this.getWidth(), this.getHeight());
// setPrefSize(expandedDimension.getWidth(), expandedDimension.getHeight());
expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
this.getChildren().setAll(title, listView);
expanded = true;
}
private void contract() {
// this.setPrefSize(contractedDimension.getWidth(), contractedDimension.getHeight());
expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
this.getChildren().setAll(title);
expanded = false;
}
public String getTitle() {
return title.getText();
}
public void setTitle(String title) {
this.title.setText(title);
}
}
I could figure out that the ListView is the reason why there's a glitch.
Basically, except for the first time, everytime you add the list to the VBox you can see the list at its full size for a really brief instant, outside its container, and then when the timeline starts, it's properly resized.
In fact, you can add a delay (one second for instance) to the timeline:
timeline.setDelay(Duration.millis(1000));
and you'll see the problem for the whole second if you expand the box for the second time:
The list is visible outside the VBox, because it isn't resized to fit in it. When the animation starts, it's resized and the problem is gone.
I've tried several approaches to resize the list at that point, without success. Maybe you can solve it...
One ugly solution will be creating a new instance of the list every time you expand the box:
private void expand() {
expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
// this works...
listView = new ListView<>(listItem);
this.getChildren().setAll(title,listView);
expanded = true;
}
Looking for other altenatives, I've bound the box disableProperty() to the timeline, so you can't click while the titled list is being expanded or contracted.
So another solution is binding the list visibleProperty() to the timeline, but you won't see the nice growing effect.
And there's a third solution, that will also be in line with the animation: set the opacity to 0 right before adding the list to the box:
private void expand() {
expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
// this will avoid seeing the unresized listView
listView.setOpacity(0);
this.getChildren().setAll(title,listView);
expanded = true;
}
and add a new KeyValue increasing the list opacityProperty() from 0 to 1 to the timeline:
timeline.getKeyFrames().setAll(
new KeyFrame(Duration.ZERO,
new KeyValue(listView.opacityProperty(), 0),
new KeyValue(writableHeight, expansionManager.fromHeight.get()),
new KeyValue(writableWidth, expansionManager.fromWidth.get())),
new KeyFrame(Duration.millis(300),
new KeyValue(listView.opacityProperty(), 1),
new KeyValue(writableHeight, expansionManager.toHeight.get()),
new KeyValue(writableWidth, expansionManager.toWidth.get()))
);
Now you won't see the glitch, and the list will be showing up nicely while the box is resized. In fact I'll increase the duration of the second keyframe.
EDIT
I have another alternative to avoid the 'glitch'. Also it could improve the animation, since the list will be visible also when the titled list is contracted.
When you contract the titled list, first of all you remove the list, so super.computePrefHeight(width) and super.computePrefWidth(height) get the new size of the small box. This has the clear drawback that on the next expand, the list has to be added again, and the glitch happens.
In order to avoid this, we won't remove the list. First, we create two new fields on ExpansionManager:
Double minWidth = 0.0;
Double minHeight = 0.0;
Then we get the minimun size of the box (on the first expansion), and use it for every contraction:
#Override
protected double computePrefHeight(double width) {
...
if (expansionManager.layoutState == ExpansionManager.LayoutState.REQUEST_ANIMATION) {
if(expansionManager.minHeight==0d){
expansionManager.minHeight=getHeight();
}
expansionManager.fromHeight = getHeight();
expansionManager.stepHeight = expansionManager.fromHeight;
expansionManager.toHeight = expanded?super.computePrefHeight(width):
expansionManager.minHeight;
return expansionManager.fromHeight;
}
}
#Override
protected double computePrefWidth(double height) {
...
if (expansionManager.layoutState == ExpansionManager.LayoutState.REQUEST_ANIMATION) {
if(expansionManager.minWidth==0d){
expansionManager.minWidth=getWidth();
}
expansionManager.fromWidth = getWidth();
expansionManager.stepWidth = expansionManager.fromWidth;
expansionManager.toWidth = expanded?super.computePrefWidth(height):
expansionManager.minWidth;
return expansionManager.fromWidth;
}
}
Finally, we need to hide the list after any contraction, otherwise a small border will be seen, and change the expand() and contract() methods to call requestLayout(), given that the box children list is no longer modified (except on the first call):
#Override
protected void layoutChildren() {
timeline.setOnFinished((done) -> {
expansionManager.layoutState = ExpansionManager.LayoutState.IDLE;
listView.setVisible(expanded);
timeline = null;
});
}
private void expand() {
expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
expanded = true;
listView.setVisible(true);
if(this.getChildren().size()==1){
this.getChildren().add(listView);
}
requestLayout();
}
private void contract() {
expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
expanded = false;
requestLayout();
}