javafxgridpane

JavaFX GridPane spacing issue


I have some code in which I am trying to display seven unique "squares" of characters. There is a "Shuffle" button - upon clicking the characters should change sequence. There is also a "flying" effect added. The shuffle and flying effects are working, but the spacing of the squares in the GridPane gets messed up upon shuffling. Great if someone can help. Here is the code:

package com.example.javafxdemo.shuffle;

import javafx.animation.ParallelTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.*;

public class ShuffleGridPaneExample_Animation_BUGGY_BUT_CLOSE_1_DEBUG extends Application {

    private final int gridSize = 7; // Number of columns in the grid
    private final int squareSize = 80; // Size of each square

    private final GridPane gridPane = new GridPane();
    private final List<Square> squares = new ArrayList<>();

    @Override
    public void start(Stage primaryStage) {
        createGridPane();

        Button shuffleButton = new Button("Shuffle");
        shuffleButton.setOnAction(event -> shuffleSquares());

        GridPane root = new GridPane();
        root.setAlignment(Pos.CENTER);
        root.add(gridPane, 0, 0);
        root.add(shuffleButton, 0, 1);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Shuffle GridPane Example");
        primaryStage.show();
    }

    private void createGridPane() {
        Random random = new Random();
        for (int i = 0; i < gridSize; i++) {
            Square square = createSquare(generateRandomLetter(random), i);
            squares.add(square);
            gridPane.add(square, i, 0);
        }
    }

    static HashSet<Integer> generatedLetters = new LinkedHashSet<>();

    private char generateRandomLetter(Random random) {
        while (true) {
            int letterIndex = random.nextInt(26); // Generate a random index between 0 and 25
            if (generatedLetters.contains(letterIndex)) {
                continue;
            }
            return (char) ('A' + letterIndex); // Convert the index to the corresponding letter (A-Z)
        }
    }

    private Square createSquare(char letter, int columnIndex) {
        Square square = new Square(squareSize, squareSize, letter, columnIndex);
        GraphicsContext gc = square.getGraphicsContext2D();
        gc.setFill(generateRandomColor());
        gc.fillRect(0, 0, squareSize, squareSize);
        gc.setFill(Color.BLACK);
        gc.setFont(Font.font("Arial", FontWeight.BOLD, 40));
        gc.fillText(String.valueOf(letter), squareSize / 2 - 10, squareSize / 2 + 10);
        return square;
    }

    private void shuffleSquares() {
        Collections.shuffle(squares);
        gridPane.getChildren().clear();

        int columnIndex = 0;
        for (Square square : squares) {
            gridPane.add(square, columnIndex, 0);
            columnIndex++;
        }

        animateTileMovement();
    }

    private void animateTileMovement() {
        ParallelTransition parallelTransition = new ParallelTransition();

        for (int i = 0; i < gridSize; i++) {
            Square square = squares.get(i);
            int targetIndex = gridPane.getColumnIndex(square);

            if (square.getOriginalIndex() != targetIndex) {
                double startX = square.getLayoutX();
                double targetX = targetIndex * squareSize;

                TranslateTransition translateTransition = new TranslateTransition(Duration.seconds(3), square);
                translateTransition.setFromX(startX);
                translateTransition.setToX(targetX);

                // Add the flying effect
                double startY = square.getLayoutY() - squareSize;
                translateTransition.setFromY(startY);
                double targetY = square.getLayoutY();
                translateTransition.setToY(targetY);

                System.out.println(i + "'" + square.letter + "' (" + startX + ", " + startY + ") (" + targetX + ", " + targetY + ")");
                parallelTransition.getChildren().add(translateTransition);
            }
        }

        parallelTransition.play();
    }

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

    private static class Square extends Canvas {
        private final int originalIndex;
        private final char letter;

        public Square(double width, double height, char letter, int originalIndex) {
            super(width, height);
            this.originalIndex = originalIndex;
            this.letter = letter;
        }

        public int getOriginalIndex() {
            return originalIndex;
        }
    }

    private Color generateRandomColor() {
        Random random = new Random();
        double r = random.nextDouble();
        double g = random.nextDouble();
        double b = random.nextDouble();
        return new Color(r, g, b, 1.0);
    }
}

Solution

  • Issues

    One trick with animations and layouts is that translation and layout add together to position a node. After the animation, the translation should be zero, otherwise, the node is not positioned correctly. This is what is going wrong in your case.

    Also, you don't update your "original index" after you complete animating. So the next time you animate, the animation starts from the very first position that your grid was in, not the current position (which seems wrong).

    If the item does not change position it does not "fly in" (i.e. it is not animated at all), which seems a bit weird, but I guess it is what you wanted.

    Your animation calculation for the start position needs to consider that you have already rearranged the layout, so you need to translate from the original layout position to the current one (your calculations aren't currently correct for doing that).

    Changes

    In the square class make the originalIndex not final and add a setter for it:

    private int originalIndex;
    
    public void setOriginalIndex(int idx) {
        originalIndex = idx;
    }
    

    Size the scene so it is clear what the animation is doing and you can see what is happening. This is good for development and debugging purposes. Still, in your final implementation, you can remove the sizing so stuff "flies in" (or add a clip to the gridPane to allow for the "fly in" effect rather than relying on sizing and having stuff "off-screen").

    Scene scene = new Scene(root, squareSize * 7, squareSize * 4);
    

    Rewrite the animation function to take into account the points I mentioned earlier:

    private void animateTileMovement() {
        ParallelTransition parallelTransition = new ParallelTransition();
    
        for (int i = 0; i < gridSize; i++) {
            Square square = squares.get(i);
            int targetIndex = GridPane.getColumnIndex(square);
    
            if (square.getOriginalIndex() != targetIndex) {
                double startX = (targetIndex - square.getOriginalIndex()) * - squareSize;
                double targetX = 0;
    
                TranslateTransition translateTransition = new TranslateTransition(Duration.seconds(3), square);
                translateTransition.setFromX(startX);
                translateTransition.setToX(targetX);
    
                // Add the flying effect
                double startY = - squareSize;
                translateTransition.setFromY(startY);
                double targetY = 0;
                translateTransition.setToY(targetY);
    
                System.out.println(i + "'" + square.letter + "' (" + startX + ", " + startY + ") (" + targetX + ", " + targetY + ")");
                parallelTransition.getChildren().add(translateTransition);
            }
        }
    
        parallelTransition.setOnFinished(e -> {
            for (Square square : squares) {
                int targetIndex = GridPane.getColumnIndex(square);
                System.out.println(square + ": " + square.originalIndex + " -> " + targetIndex + " & " + square.getTranslateX() + "," + square.getTranslateY());
                square.setOriginalIndex(GridPane.getColumnIndex(square));
            }
        });
    
        parallelTransition.play();
    }
    

    I also added a bit more debugging output on completion of the animation so that you can clearly see where squares came from and went to and that their final translation value is zero (i.e. the layout, not the intermediate translation for animation, reflects their final position).

    Complete example

    All the other code is the same, but I added the full text of the updated code here for easy replication via copy/paste:

    import javafx.animation.ParallelTransition;
    import javafx.animation.TranslateTransition;
    import javafx.application.Application;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.canvas.Canvas;
    import javafx.scene.canvas.GraphicsContext;
    import javafx.scene.control.Button;
    import javafx.scene.layout.GridPane;
    import javafx.scene.paint.Color;
    import javafx.scene.text.Font;
    import javafx.scene.text.FontWeight;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    import java.util.*;
    
    public class ShuffleGridPaneExample_Animation extends Application {
    
        private final int gridSize = 7; // Number of columns in the grid
        private final int squareSize = 80; // Size of each square
    
        private final GridPane gridPane = new GridPane();
        private final List<Square> squares = new ArrayList<>();
    
        @Override
        public void start(Stage primaryStage) {
            createGridPane();
    
            Button shuffleButton = new Button("Shuffle");
            shuffleButton.setOnAction(event -> shuffleSquares());
    
            GridPane root = new GridPane();
            root.setAlignment(Pos.CENTER);
            root.add(gridPane, 0, 0);
            root.add(shuffleButton, 0, 1);
    
            Scene scene = new Scene(root, squareSize * 7, squareSize * 4);
            primaryStage.setScene(scene);
            primaryStage.setTitle("Shuffle GridPane Example");
            primaryStage.show();
        }
    
        private void createGridPane() {
            Random random = new Random();
            for (int i = 0; i < gridSize; i++) {
                Square square = createSquare(generateRandomLetter(random), i);
                squares.add(square);
                gridPane.add(square, i, 0);
            }
        }
    
        static HashSet<Integer> generatedLetters = new LinkedHashSet<>();
    
        private char generateRandomLetter(Random random) {
            while (true) {
                int letterIndex = random.nextInt(26); // Generate a random index between 0 and 25
                if (generatedLetters.contains(letterIndex)) {
                    continue;
                }
                return (char) ('A' + letterIndex); // Convert the index to the corresponding letter (A-Z)
            }
        }
    
        private Square createSquare(char letter, int columnIndex) {
            Square square = new Square(squareSize, squareSize, letter, columnIndex);
            GraphicsContext gc = square.getGraphicsContext2D();
            gc.setFill(generateRandomColor());
            gc.fillRect(0, 0, squareSize, squareSize);
            gc.setFill(Color.BLACK);
            gc.setFont(Font.font("Arial", FontWeight.BOLD, 40));
            gc.fillText(String.valueOf(letter), squareSize / 2 - 10, squareSize / 2 + 10);
            return square;
        }
    
        private void shuffleSquares() {
            Collections.shuffle(squares);
            gridPane.getChildren().clear();
    
            int columnIndex = 0;
            for (Square square : squares) {
                gridPane.add(square, columnIndex, 0);
                columnIndex++;
            }
    
            animateTileMovement();
        }
    
        private void animateTileMovement() {
            ParallelTransition parallelTransition = new ParallelTransition();
    
            for (int i = 0; i < gridSize; i++) {
                Square square = squares.get(i);
                int targetIndex = GridPane.getColumnIndex(square);
    
                if (square.getOriginalIndex() != targetIndex) {
                    double startX = (targetIndex - square.getOriginalIndex()) * - squareSize;
                    double targetX = 0;
    
                    TranslateTransition translateTransition = new TranslateTransition(Duration.seconds(3), square);
                    translateTransition.setFromX(startX);
                    translateTransition.setToX(targetX);
    
                    // Add the flying effect
                    double startY = - squareSize;
                    translateTransition.setFromY(startY);
                    double targetY = 0;
                    translateTransition.setToY(targetY);
    
                    System.out.println(i + "'" + square.letter + "' (" + startX + ", " + startY + ") (" + targetX + ", " + targetY + ")");
                    parallelTransition.getChildren().add(translateTransition);
                }
            }
    
            parallelTransition.setOnFinished(e -> {
                for (Square square : squares) {
                    int targetIndex = GridPane.getColumnIndex(square);
                    System.out.println(square + ": " + square.originalIndex + " -> " + targetIndex + " & " + square.getTranslateX() + "," + square.getTranslateY());
                    square.setOriginalIndex(GridPane.getColumnIndex(square));
                }
            });
    
            parallelTransition.play();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    
        private static class Square extends Canvas {
            private int originalIndex;
            private final char letter;
    
            public Square(double width, double height, char letter, int originalIndex) {
                super(width, height);
                this.originalIndex = originalIndex;
                this.letter = letter;
            }
    
            public int getOriginalIndex() {
                return originalIndex;
            }
    
            public void setOriginalIndex(int idx) {
                originalIndex = idx;
            }
        }
    
        private Color generateRandomColor() {
            Random random = new Random();
            double r = random.nextDouble();
            double g = random.nextDouble();
            double b = random.nextDouble();
            return new Color(r, g, b, 1.0);
        }
    }
    

    References