I have an app with lots of draggable Nodes, which can get a bit slow. In particular when:
dragging a Node containing other nodes or
dragging Node inside another IF it results in widening of the parent's Bounds [almost as if it was caching some range-related data, common for double values inside, and needed to do extra processing if range changed] (EDIT: this has now been explained to be the parent's layout pass)
Q: Is there anything to gain by doing some of the operations (the few for which I can control types) on integers (converting to ints first) and then re-assigning to doubles?
(EDIT: this has now been answered)
Q1: How else could I speed it up?
==========UPDATE:
Thanks to advice so far I determined my main problem is panning a Region container inside a Region root, as it triggers a layout pass in the container Region, which in turn (I think) ping-pongs to do a layout pass in all the (ca. 40) children Nodes (which are complex in their own right - they contain textfields, buttons, etc.).
So far the fastest solution I found is panning a Group in a Group (already a lot faster), where the Groups are additionally modified (subclass, override compute... methods) to make sure the parent/root will not need to be resized during dragging (sadly, not so noticeable).
class FastGroup extends Group {
//'stop asking about size' functionality
double widthCache;
double heightCache;
protected double computePrefWidth(double height) {
return widthCache != 0 ? widthCache : super.computePrefWidth(height);
}
protected double computePrefHeight(double width) {
return heightCache != 0 ? heightCache : super.computePrefHeight(width);
}
public void initDragging(){
//if a child of this Group goes into drag
//add max margins to Group's size
double newW = getBoundsInLocal().getWidth() + TestFXApp03.scene.getWidth()*2;
double newH = getBoundsInLocal().getHeight() + TestFXApp03.scene.getHeight()*2;
//cache size
widthCache = newW;
heightCache = newH;
}
public void endDragging(){
widthCache = 0;
heightCache = 0;
}
}
However Group creates problems of its own.
Q3: why can't I achieve the same with Pane (tried that as a 3rd option)? According to the Docs, a Pane:
'does not perform layout beyond resizing resizable children to their
preferred size'
...while a Group:
'will "auto-size" its managed resizable children to their preferred
sizes during the layout pass'
'is not directly resizable'
...which sounds quite the same to me, and yet the Pane used as root results in very slow panning of the contained Group.
Ok... I spent some time in checking the performance of mouse-drags with many objects....
I did it on a Mac Pro (4 core, 8GB RAM).
A mouse drag using setTranslateX() and setTranslateY() in the setOnMouseDragged() handler is called at an interval of 17ms (60Hz).
If I add small circles to the drag-group, the drag keeps its pace up to 25.000 circle dragged along.
At 100.000 circles the drag interval is about 50ms (and shows some stops during drag)
At 1.000.000 circles the drag interval is about 500ms
If I implement the drag as image drag using the snapshot() function, the snapshot of 1.000.000 circles takes 600ms. Then the drag is smooth and fast.
Here the full code with and without using an image during drag:
public class DragMany extends Application {
Point2D lastXY = null;
Scene myScene;
long lastDrag;
ImageView dragImage;
void fill(Group box) {
Color colors[] = new Color[]{Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW, Color.PINK, Color.MAGENTA };
for (int i = 0; i < 100000; i++) {
Circle c = new Circle(2);
box.getChildren().add(c);
c.setFill(colors[(i/17) % colors.length]);
c.setLayoutX(i % 30 * 3);
c.setLayoutY((i/20) % 30*3);
}
box.setLayoutX(40);
box.setLayoutY(40);
}
void drag1(Pane pane, Group box) {
box.setOnMousePressed(event -> {
lastXY = new Point2D(event.getSceneX(), event.getSceneY());
lastDrag = System.currentTimeMillis();
});
box.setOnMouseDragged(event -> {
event.setDragDetect(false);
Node on = box;
double dx = event.getSceneX() - lastXY.getX();
double dy = event.getSceneY() - lastXY.getY();
on.setLayoutX(on.getLayoutX()+dx);
on.setLayoutY(on.getLayoutY()+dy);
lastXY = new Point2D(event.getSceneX(), event.getSceneY());
event.consume();
});
box.setOnMouseReleased(d -> lastXY = null);
}
void drag3(Pane pane, Group box) {
box.setOnMousePressed(event -> {
long now = System.currentTimeMillis();
lastXY = new Point2D(event.getSceneX(), event.getSceneY());
SnapshotParameters params = new SnapshotParameters();
params.setFill(Color.TRANSPARENT);
WritableImage image = box.snapshot(params, null);
dragImage = new ImageView(image);
dragImage.setLayoutX(box.getLayoutX());
dragImage.setLayoutY(box.getLayoutY());
dragImage.setTranslateX(box.getTranslateX());
dragImage.setTranslateY(box.getTranslateY());
pane.getChildren().add(dragImage);
dragImage.setOpacity(0.5);
box.setVisible(false);
System.out.println("Snap "+(System.currentTimeMillis()-now)+"ms");
pane.setOnMouseDragged(e -> {
if (dragImage == null) return;
Node on = dragImage;
double dx = e.getSceneX() - lastXY.getX();
double dy = e.getSceneY() - lastXY.getY();
on.setTranslateX(on.getTranslateX()+dx);
on.setTranslateY(on.getTranslateY()+dy);
lastXY = new Point2D(e.getSceneX(), e.getSceneY());
e.consume();
});
pane.setOnMouseReleased(e -> {
if (dragImage != null) {
box.setTranslateX(dragImage.getTranslateX());
box.setTranslateY(dragImage.getTranslateY());
pane.getChildren().remove(dragImage);
box.setVisible(true);
dragImage = null;
}
lastXY = null;
e.consume();
});
lastDrag = System.currentTimeMillis();
event.consume();
});
}
public void start(Stage primaryStage) {
Pane mainPane = new Pane();
myScene = new Scene(mainPane, 500, 500);
primaryStage.setScene(myScene);
primaryStage.show();
Group all = new Group();
fill(all);
mainPane.getChildren().add(all);
drag3(mainPane, all);
}
public static void main(String[] args) {
launch(args);
}
Using ints or floats instead of doubles won't give you much performance boost.
Instead, you should try optimizing when you change the position of nodes. For example, when you are dragging, instead of changing the position of all nodes, you could just render all the nodes that are being dragged at an offset and only change position of one node. Once you are done dragging, you could recalculate positions of all nodes you dragged.
Here is my guess of how you do it right now:
void drag(float x, float y) {
setPosition(x, y); // I'm gueessing this is where your bottle neck is
for (Node node : nodes) {
node.drag(x, y);
}
}
void render() {
screen.draw(this.x, this.y);
for (Node node : nodes) {
node.render();
}
}
Here is how I would optimize it by introducing render offset, so that you only set position of all the nodes when you are finished dragging:
float xo, yo; // render offsets
void drag(float x, float y) {
xo = x;
yo = y;
}
void dragEnd(float x, float y) {
setPosition(x, y);
for (Node node : nodes) {
node.dragEnd(x, y);
}
}
void render(float xo, float yo) {
xo += this.xo;
yo += this.yo;
screen.draw(this.x + xo, this.y + yo); // render with at offset
for (Node node : nodes) {
node.render(xo, yo);
}
}
Related
Im trying to make lists of rectangles in matrix like manner, I would want them to scale depending on amount of rectangles in row to always fit to the fixed size of window. End of row is little cut or couple of them are outside of border, depending on amount of rectangles.
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
final int size = 80;
stage.setWidth(1040);
stage.setHeight(920);
final double cellDimension = (stage.getWidth() / size) - 1;
stage.setScene(new Scene(render(20, size, cellDimension), Color.WHITE));
stage.show();
}
public static Region render(int generations, int size, double cellDimension) {
VBox results = new VBox(0);
Random random = new Random();
for (int y = 0; y < generations; y++) {
HBox hBox = new HBox(0);
hBox.getChildren().addAll(IntStream.range(0, size).
mapToObj(idx -> random.nextInt(2)).map(item -> createAppropriateRectangle(item == 1, cellDimension)).
collect(Collectors.toList()));
results.getChildren().add(hBox);
}
results.setSnapToPixel(true);
return results;
}
private static Rectangle createAppropriateRectangle(boolean state, double cellDimension) {
Rectangle rectangle = new Rectangle(cellDimension, cellDimension);
rectangle.setStroke(Color.GRAY);
if (state) {
rectangle.setFill(Color.WHITE);
}
return rectangle;
}
}
What am I doing wrong ? If something is unclear please let me know, thanks :)
for list of 90 values, up is original, bottom shows how much was outside of border
EDIT:
Made mwe using #DaveB stripped example
Building a MRE would probably have shown you the answer fairly quickly. It looks like using a GridPane was just an attempt to make the rectangles fit, but GridPane potentially adds complexity that may obscure the answer. So I went with a VBox holding a bunch of HBoxes. That handles the relative positioning of the elements without any fuss.
You didn't include the Generator class, meaning your code was un-runnable, so I've stripped it down to just the mechanics of putting some random black and white squares on the screen, and dumped everything else:
public class Squares extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
final int size = 80;
stage.setWidth(1040);
stage.setHeight(920);
final double cellDimension = (stage.getWidth() / size) - 1;
stage.setScene(new Scene(render(20, size, cellDimension), Color.WHITE));
stage.show();
}
public static Region render(int generations, int size, double cellDimension) {
VBox results = new VBox(0);
Random random = new Random();
for (int y = 0; y < generations; y++) {
HBox hBox = new HBox(0);
hBox.getChildren().addAll(IntStream.range(0, size).mapToObj(idx -> random.nextInt(2)).map(item -> createAppropriateRectangle(item == 1, cellDimension)).collect(Collectors.toList()));
results.getChildren().add(hBox);
}
return results;
}
private static Rectangle createAppropriateRectangle(boolean state, double cellDimension) {
Rectangle rectangle = new Rectangle(cellDimension, cellDimension);
rectangle.setStroke(Color.GRAY);
if (state) {
rectangle.setFill(Color.WHITE);
}
return rectangle;
}
}
It looks like you need to allow 1 extra pixel per rectangle to make the math work properly. Also, it seems to work only when the stage width divides nicely by the number of rectangles per row - there's probably some mechanics of partial pixels that comes into play here. You could probably find a rounding policy that would work when the row size is not an even divisor of the stage width.
You can bind it to stage or parent width, height property.
Binding is one of the most powerful concepts of JavaFX.
I have a matrix which consists of 0s and 1s. I want to visualize this matrix like a grid and put a mark to the cells which has 1 in it. I made my research in order to find a way of doing this in Java FX but I could not find something similar to what I said. Is there any way to do that in Java FX? Best Regards,
I visualized the matrix with using GridPane and Timeline in Java FX. I found this method thanks to c0der's comment above. In my case I needed to draw a path on the matrix, it is like finding a minimum path problem. Here is my source code:
public class VisualizationThread implements EventHandler<ActionEvent> {
private ArrayList<ArrayList<Integer>> matrix; // Matrix
private ArrayList<Integer> solution; // One individual
private ArrayList<ArrayList<Integer>> visited; // Visited coordinate list
private int currentX; // Current x coordinate
private int currentY; // Current y coordinate
private int index; // Current movement index
private int attempt; // unsuccessful attempt count (used while visualizing all generations)
private boolean closeAtTheAnd; // Close all stages except the last one which shows final situation, the correct path
private boolean isSuccessful; // Currently visualizing an unsuccessful attempt or correct path?
private Stage primaryStage; // primary stage (gui component)
public VisualizationThread(ArrayList<ArrayList<Integer>> matrix, ArrayList<Integer> solution, int currentX, int currentY,
boolean isSuccessful, int attempt, boolean closeAtTheAnd){
this.matrix = matrix;
this.solution = solution;
this.currentY = currentY;
this.currentX = currentX;
this.isSuccessful = isSuccessful;
this.attempt = attempt;
this.primaryStage = new Stage();
this.index = 0;
this.closeAtTheAnd = closeAtTheAnd;
this.visited = new ArrayList<ArrayList<Integer>>();
}
// Visualize a single individual
public void run() {
// GUI processes
BorderPane root = new BorderPane();
GridPane grid = new GridPane();
grid.setPadding(new Insets(10));
grid.setHgap(10);
grid.setVgap(10);
StackPane[][] screen_buttons = new StackPane[matrix.size()][matrix.size()];
visited.add(new ArrayList<Integer>(){{add(currentX); add(currentY);}});
for (int y=0;y<matrix.size();y++) {
for (int x=0;x<matrix.get(y).size();x++) {
screen_buttons[y][x] = new StackPane();
Rectangle rec = new Rectangle(60,60);
// Visualize visited points as green and others yellow and foods red
if(!(currentX>=matrix.size() || currentY>=matrix.size())){
if(isVisited(visited, x, y)){
rec.setFill(Color.GREEN);
}else {
rec.setFill(matrix.get(y).get(x) == 0 ? Color.YELLOW : Color.RED);
}
}else {
rec.setFill(matrix.get(y).get(x) == 0 ? Color.YELLOW : Color.RED);
}
rec.setStyle("-fx-arc-height: 20; -fx-arc-width: 20;");
screen_buttons[y][x].getChildren().addAll(rec);
grid.add(screen_buttons[y][x], x, y);
}
}
//container for controls
GridPane controls = new GridPane();
root.setCenter(grid);
root.setBottom(controls);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setTitle(isSuccessful ? "Correct Path" : "Unsuccessful Attempt: " + attempt);
primaryStage.show();
if(!(index >= solution.size())){
switch (solution.get(index)){
case 1:
setCurrentY(getCurrentY()-1);
break;
case 2:
setCurrentX(getCurrentX()-1);
break;
case 3:
setCurrentY(getCurrentY()+1);
break;
case 4:
setCurrentX(getCurrentX()+1);
break;
}
index++;
}
if(closeAtTheAnd && index == solution.size()){
primaryStage.close();
}
}
// This method finds if current coordinate is visited before or not
// #param visitedList: List of visited points
// #param x: x coordinate
// #param y: y coordinate
public boolean isVisited(ArrayList<ArrayList<Integer>> visitedList, int x, int y){
boolean result = false;
// Check if x and y coordinates are visited before
for (ArrayList<Integer> temp : visitedList){
if(temp.get(0) == x && temp.get(1) == y){
result = true;
break;
}
}
return result;
}
public int getCurrentX() {
return currentX;
}
public void setCurrentX(int currentX) {
this.currentX = currentX;
}
public int getCurrentY() {
return currentY;
}
public void setCurrentY(int currentY) {
this.currentY = currentY;
}
public void handle(ActionEvent event) {
run();
}
}
// In another part of the code which I need to trigger the event above.
ArrayList<Integer> bestOfLastPopulation = geneticAlgorithm.findMostAte(finalPopulation, matrix, centerX, centerY);
VisualizationThread visualizationThread = new VisualizationThread(matrix, bestOfLastPopulation, centerX, centerY, true, 0, false);
Timeline fiveSecondsWonder = new Timeline(new KeyFrame(Duration.millis(150), visualizationThread));
fiveSecondsWonder.setCycleCount(bestOfLastPopulation.size() + 1);
fiveSecondsWonder.play();
For someone who is trying to find the same thing, should look at the part which begins with a nested for loop and ends with primaryStage.show(). At this part of code, it is visualizing the matrix in different colors. For example, if a cell contains 1 then red, if it contains a 0 then yellow and if it is visited before then it will painted to green color. Hope that helps to someone else. Best Regards,
Many online maps have this feature where when one reaches the left/right end of an image they find themselves looking at the opposite end. How is this implementable in JavaFX and is it compatible with a scrollPane? In addition, when wrapped around will I be looking at the original image or a copy of the image(with the former preferable)? If there are any questions about what I am specifically trying to accomplish ask below.
You could show the same Image in multiple ImageViews. This way the image is stored only once in memory.
ScrollPane wouldn't be a good choice here, since you're trying to create a "infinite" pane which ScrollPane does not support.
The following example allows the user to move a 2 x 2 grid of Mona Lisas and adjusts the whole content area of the window is covered with images. Depending on the image size and the visible area you may need a larger grid. (Check how many images fit into the visible area in x / y direction starting at the top left and then add one to these numbers to determine the required grid size.)
#Override
public void start(Stage primaryStage) {
Image image = new Image("https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/687px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg");
GridPane images = new GridPane();
for (int x = 0; x < 2; x++) {
for (int y = 0; y < 2; y++) {
images.add(new ImageView(image), x, y);
}
}
Pane root = new Pane(images);
images.setManaged(false);
class DragHandler implements EventHandler<MouseEvent> {
double startX;
double startY;
boolean dragging = false;
#Override
public void handle(MouseEvent event) {
if (dragging) {
double newX = (event.getX() + startX) % image.getWidth();
double newY = (event.getY() + startY) % image.getHeight();
if (newX > 0) {
newX -= image.getWidth();
}
if (newY > 0) {
newY -= image.getHeight();
}
images.setLayoutX(newX);
images.setLayoutY(newY);
}
}
}
DragHandler handler = new DragHandler();
root.setOnMouseDragged(handler);
root.setOnDragDetected(evt -> {
images.setCursor(Cursor.MOVE);
handler.startX = images.getLayoutX() - evt.getX();
handler.startY = images.getLayoutY() - evt.getY();
handler.dragging = true;
});
root.setOnMouseReleased(evt -> {
handler.dragging = false;
images.setCursor(Cursor.DEFAULT);
});
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.setResizable(false);
primaryStage.show();
}
I want to know how I can make an interaction when rectangle collapses with a circle . Like some kind of action that happens when circle and rectangle collapses. There is also a problem that circle can go out of frame, which I don't know how to limit it's movement. I'm not even sure if it's possible to do what I want this way.This is supposed to be a game where I need to dodge all rectangles and this is best I could do. Thanks it advance :)
public class Java2 extends Application {
public static final int KRUG_WIDTH = 10;
public static final int PANEL_WIDTH = 600;
public static final int PANEL_HEIGHT = 600;
private int mX = (PANEL_WIDTH - KRUG_WIDTH) / 2;
private int mY = (PANEL_HEIGHT - KRUG_WIDTH) / 2;
Random ran = new Random();
#Override
public void start(Stage primaryStage) {
Rectangle rekt = new Rectangle(20, 20);
Rectangle rekt1 = new Rectangle(20, 20);
Circle r1 = new Circle(mX,mY,KRUG_WIDTH);
Pane root = new Pane(); //PANE
r1.setFill(Color.WHITE);
r1.setStroke(Color.BLACK);
root.getChildren().add(r1);
root.getChildren().add(rekt);
root.getChildren().add(rekt1);
Scene scene = new Scene(root, PANEL_WIDTH, PANEL_HEIGHT);
PathTransition pathTransition = new PathTransition();
Path path = new Path();
//REKT-PATH
pathTransition.setDuration(javafx.util.Duration.millis(600));
pathTransition.setPath(path);
pathTransition.setNode(rekt);
pathTransition.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
pathTransition.setCycleCount(2);
pathTransition.setAutoReverse(true);
pathTransition.setOnFinished(e -> {
pathTransition.setPath(createPath());
pathTransition.play();
});
pathTransition.play();
PathTransition pathTransition1 = new PathTransition();
Path path1 = new Path();
//REKT1-PATH
pathTransition1.setDuration(javafx.util.Duration.millis(550));
pathTransition1.setPath(path1);
pathTransition1.setNode(rekt1);
pathTransition1.setOrientation(
PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
pathTransition1.setCycleCount(Timeline.INDEFINITE);
pathTransition1.setAutoReverse(true);
pathTransition1.setOnFinished(e -> {
pathTransition1.setPath(createPath());
pathTransition1.play();
});
pathTransition1.play();
r1.setOnKeyPressed(e -> {
switch (e.getCode()) {
case DOWN: r1.setCenterY(r1.getCenterY()+ 10);
break;
case UP: r1.setCenterY(r1.getCenterY()- 10);
break;
case LEFT: r1.setCenterX(r1.getCenterX() - 10);
break;
case RIGHT: r1.setCenterX(r1.getCenterX() + 10);
break;
case SPACE:
break;
default:
}
});
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
r1.requestFocus();
}
private Path createPath() {
int loc2 = ran.nextInt(600 - 300 + 1) + 300;
int loc = ran.nextInt(600 - 20 + 1) + 20;
Path path = new Path();
path.getElements().add(new MoveTo(20, 20));
path.getElements().add(new LineTo(loc, loc2));
return path;
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Your approach isn't one I would take. Usually you have a timer as a game loop in which all the magic is happening, i. e. move sprites, check collision, update sprites in the UI. In JavaFX this would be an AnimationTimer.
In your code you miss several important things, among them the keyboard or mouse input. Or how you check the collision. There is e. g. an intersects method for the nodes, but that checks only the rectangular bounds of a node. You however have a circle, which makes matters more complex. However, if performance isn't an issue for your game, you could use the Shape's intersect method to create a new shape out of Rectangle and Circle and depending on the result decide whether you have a collision or not.
Some time ago I created example code about how to move sprites on the screen and check if they collide with others.
Regarding bouncing off the scene bounds in that code you simply check the scene bounds and change the movement delta to a bouncing value, i. e. if the sprite moves from left to right (dx is positive) and is at the right side, you set the delta dx to a negative value so that it moves in the opposite direction.
You can also take a look at a simple Pong game which uses a slightly modified version of that engine.
Example for shape intersection with your code:
Shape shape = Shape.intersect(rekt, r1);
boolean intersects = shape.getBoundsInLocal().getWidth() != -1;
if( intersects) {
System.out.println( "Collision");
}
That alone raises the question where you'd put the check. Normally you'd have to perform it when anything moves, i. e. in both path transitions like this
pathTransition.currentTimeProperty().addListener( e -> {
...
});
and in the circle transition. That would be 2 checks too many.
I'm doing a Klondike game. The logic is all working. I'm just having trouble with the UI in javafx.
I've been trying to move/drag the cards from the 'tableau pile' arround without the expected result.
My card is a ImageView with an Image inside. The cards are inside a Pane:
Pane tableau = new Pane();
for (int i = 0; i < n; i++) {
Image img = new Image("resources/images/" + (i + 1) + ".png");
ImageView imgView = new ImageView(img);
imgView.setY(i * 20);
//imgView Mouse Events here
tableau.getChildren().add(imgView);
}
I tried:
imgView.setOnMousePressed((MouseEvent mouseEvent) -> {
dragDelta.x = imgView.getLayoutX() - mouseEvent.getSceneX();
dragDelta.y = imgView.getLayoutY() - mouseEvent.getSceneY();
});
imgView.setOnMouseDragged((MouseEvent mouseEvent) -> {
imgView.setLayoutX(mouseEvent.getSceneX() + dragDelta.x);
imgView.setLayoutY(mouseEvent.getSceneY() + dragDelta.y);
});
This solution dont work because I'm setting the positions so when release the card wont return to where it was, and because the card is colliding with other UI objects.
Another attempt:
imgView.setOnDragDetected((MouseEvent event) -> {
ClipboardContent content = new ClipboardContent();
content.putImage(img);
Dragboard db = imgView.startDragAndDrop(TransferMode.ANY);
db.setDragView(img, 35, 50);
db.setContent(content);
event.consume();
});
In this solution the problems are: the card comes semitransparent, like moving a file, the cursor becames no/forbidden but other than that it works well: no collisions and the card goes to his original place if I release the mouse.
Another problem is that I dont know if I can move more than 1 card with this solution?
My final question is, How can I move a node (in this case an ImageView) or a group of nodes, from one pile to another, like in a Solitaire Card Game?
For knowing the original card position you should use the setTranslateX (and Y) instead of setLayoutX in your mouse handler. So when the user releases the card, you can simply invoke a Transition and let the card fly back to the layout position. If the user releases the card on a valid place, you set the translate coordinates to 0 and change the layout position or use relocate.
If you want to make the cards semitransparent, you could e. g. change the opacity or apply CSS.
By the way, I wouldn't use Clipboardcontent, it seems inappropriate for your needs.
You can move multiple objects with your mouse handling code. You simple have to apply the translation to multiple objects simultaneously. When you drag a pile, you determine the cards on top of the selected card and apply the transition to all of them.
Here's a quick example to show you how it could look like.
Card.java, you'll use an ImageView.
public class Card extends Rectangle {
static Random rand = new Random();
public Card() {
setWidth(100);
setHeight(200);
Color color = createRandomColor();
setStroke(color);
setFill( color.deriveColor(1, 1, 1, 0.4));
}
public static Color createRandomColor() {
int max = 200;
Color color = Color.rgb( (int) (rand.nextDouble() * max), (int) (rand.nextDouble() * max), (int) (rand.nextDouble() * max));
return color;
}
}
Game.java, the application.
public class Game extends Application {
static List<Card> cardList = new ArrayList<>();
#Override
public void start(Stage primaryStage) {
MouseGestures mg = new MouseGestures();
Group root = new Group();
for( int i=0; i < 10; i++) {
Card card = new Card();
card.relocate( i * 20, i * 10);
mg.makeDraggable(card);
cardList.add( card);
}
root.getChildren().addAll( cardList);
Scene scene = new Scene( root, 1600, 900);
primaryStage.setScene( scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
// TODO: don't use a static method, I only added for the example
public static List<Card> getSelectedCards( Card currentCard) {
List<Card> selectedCards = new ArrayList<>();
int i = cardList.indexOf(currentCard);
for( int j=i + 1; j < cardList.size(); j++) {
selectedCards.add( cardList.get( j));
}
return selectedCards;
}
}
MouseGestures.java, the mouse handling mechanism.
public class MouseGestures {
final DragContext dragContext = new DragContext();
public void makeDraggable(final Node node) {
node.setOnMousePressed(onMousePressedEventHandler);
node.setOnMouseDragged(onMouseDraggedEventHandler);
node.setOnMouseReleased(onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
dragContext.x = event.getSceneX();
dragContext.y = event.getSceneY();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
Node node = (Node) event.getSource();
double offsetX = event.getSceneX() - dragContext.x;
double offsetY = event.getSceneY() - dragContext.y;
node.setTranslateX(offsetX);
node.setTranslateY(offsetY);
// same for the other cards
List<Card> list = Game.getSelectedCards( (Card) node);
for( Card card: list) {
card.setTranslateX(offsetX);
card.setTranslateY(offsetY);
}
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
Node node = (Node) event.getSource();
moveToSource(node);
// same for the other cards
List<Card> list = Game.getSelectedCards( (Card) node);
for( Card card: list) {
moveToSource(card);
}
// if you find out that the cards are on a valid position, you need to fix it, ie invoke relocate and set the translation to 0
// fixPosition( node);
}
};
private void moveToSource( Node node) {
double sourceX = node.getLayoutX() + node.getTranslateX();
double sourceY = node.getLayoutY() + node.getTranslateY();
double targetX = node.getLayoutX();
double targetY = node.getLayoutY();
Path path = new Path();
path.getElements().add(new MoveToAbs( node, sourceX, sourceY));
path.getElements().add(new LineToAbs( node, targetX, targetY));
PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(1000));
pathTransition.setNode(node);
pathTransition.setPath(path);
pathTransition.setCycleCount(1);
pathTransition.setAutoReverse(true);
pathTransition.play();
}
/**
* Relocate card to current position and set translate to 0.
* #param node
*/
private void fixPosition( Node node) {
double x = node.getTranslateX();
double y = node.getTranslateY();
node.relocate(node.getLayoutX() + x, node.getLayoutY() + y);
node.setTranslateX(0);
node.setTranslateY(0);
}
class DragContext {
double x;
double y;
}
// pathtransition works with the center of the node => we need to consider that
public static class MoveToAbs extends MoveTo {
public MoveToAbs( Node node, double x, double y) {
super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
// pathtransition works with the center of the node => we need to consider that
public static class LineToAbs extends LineTo {
public LineToAbs( Node node, double x, double y) {
super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
}
When you pick a card or a multiple of cards, they'll transition back to their origin.
Here's a screenshot where I dragged the 3rd card from top.
When you check if a card is on a valid destination, you should invoke the fixPosition method. It simply relocates the card, i. e. calculates the position using the translation values, repositions the node and sets its translation to 0.
The tricky part was the PathTransition. It's working from the center of a node, not from the x/y coordinates. Here's a thread regarding that issue to which I replied in case you wish to know more about it.