JavaFX | changing Line anchor? - javafx

I tried drawing a line and changing its end coordinate when dragging.
The problem is, it changes both of the lines' ends, with respect to the middle, supposedly the anchor.
Is there a way to move the anchor to the start of the line?
My code is:
Line path = new Line(30,30, 70 , 75);
path.setStrokeWidth(5);
Circle point = new Circle(3);
point.setCenterX(path.getEndX());
point.setCenterY(path.getEndY());
point.setFill(Paint.valueOf("red"));
point.setOnMouseDragged(e ->{
point.setCenterX(e.getX());
point.setCenterY(e.getY());
path.setEndX(point.getCenterX());
path.setEndY(point.getCenterY());
});
Group shapes = new Group();
shapes.getChildren().addAll(path, point);
BorderPane root = new BorderPane(shapes);
Scene scene = new Scene(root,600,400);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
the result:
The ideal is that the pivot point will be at the start of the line, and not at the middle.

What you're seeing is not the line growing or shrinking in both directions; instead, what's happening is, as the line changes length, the BorderPane repositions it in order to keep it centered. The same thing is happening with the "rotation". When you move an end in such a way as to change the angle of the line, the bounding box changes in a way that, when the BorderPane repositions the line, causes the other end to move in the opposite direction.
More specifically, the BorderPane is repositioning the Group—effectively the same thing since Group doesn't position its children. One fix for this is to make the Group unmanaged. This will stop the BorderPane from repositioning the Group as its bounds change. Note, however, that this will also stop the Group from contributing to the size and layout calculations of the BorderPane.
Here's an example:
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class Main extends Application {
private static void installDragHandlers(Circle circle) {
circle.setOnMousePressed(event -> {
Point2D offset = new Point2D(
event.getX() - circle.getCenterX(),
event.getY() - circle.getCenterY()
);
circle.setUserData(offset);
event.consume();
});
circle.setOnMouseDragged(event -> {
Point2D offset = (Point2D) circle.getUserData();
circle.setCenterX(event.getX() - offset.getX());
circle.setCenterY(event.getY() - offset.getY());
event.consume();
});
circle.setOnMouseReleased(event -> {
circle.setUserData(null);
event.consume();
});
}
#Override
public void start(Stage primaryStage) {
Line line = new Line(200, 200, 400, 200);
line.setStrokeWidth(2);
Circle start = new Circle(5, Color.GREEN);
start.centerXProperty().bindBidirectional(line.startXProperty());
start.centerYProperty().bindBidirectional(line.startYProperty());
installDragHandlers(start);
Circle end = new Circle(5, Color.RED);
end.centerXProperty().bindBidirectional(line.endXProperty());
end.centerYProperty().bindBidirectional(line.endYProperty());
installDragHandlers(end);
Group group = new Group(line, start, end);
group.setManaged(false);
primaryStage.setScene(new Scene(new BorderPane(group), 600, 400));
primaryStage.setTitle("SO-55196882");
primaryStage.show();
}
}
The Line is initialized with hard coded start and end points so that it is initially centered in the scene (whose initial dimensions are also hard coded).

Put shapes in group.
public void start(final Stage primaryStage) throws AWTException {
final Line path = new Line(30, 30, 70, 75);
path.setStrokeWidth(5);
final Circle point = new Circle(3);
point.setCenterX(path.getEndX());
point.setCenterY(path.getEndY());
point.setFill(Paint.valueOf("red"));
point.setOnMouseDragged(e -> {
point.setCenterX(e.getX());
point.setCenterY(e.getY());
path.setEndX(point.getCenterX());
path.setEndY(point.getCenterY());
});
final Group root = new Group(path, point);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}

Related

JavaFX custom chart class - how to bind a node's layoutX and layoutY properties to the display positions of a NumberAxis?

I'm writing a rudimentary Candlestick chart class in which the candlesticks are created as Regions and are plotted by setting their layoutX and layoutY values to the getDisplayPosition() of the relevant axis.
For example, to plot a candlestick at value 3 on an X axis, I do this:
candlestick.setLayoutX(xAxis.getDisplayPosition(3));
When the stage resizes or when the axes are zoomed in or out, the candlesticks' layout values have to be reset so that the chart renders correctly. I'm currently handling this via ChangeListeners for resize events and Button.setOnAction()s for zooming.
However, I'd rather bind the candlesticks' layout properties to the axes' display positions than set/reset the layout values, but can't find a "displayPositionProperty" (or similar) for a NumberAxis.
Is it possible to do this? Which NumberAxis property would I bind to? ie.
candlestick.layoutXProperty().bind(xAxis.WHICH_PROPERTY?);
Also, would binding the properties be more efficient than resetting layout positions? Some of the charts could potentially have thousands of candlesticks but I can't test resource usage until I figure out how to code the bind.
I've experimented with scaling the candlesticks to the axes' scale but can't use that approach because scaling a Region affects its border width. For certain types of candlesticks, that can change its meaning.
I've also played with the Ensemble candlestick demo chart. It was useful in giving me a start but is too simplistic for my needs.
Here's a MVCE that demonstrates my approach. Any guidance re binding would be very much appreciated.
I'm using OpenJFX 17.
package test023;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Test023 extends Application {
#Override
public void start(Stage stage) {
NumberAxis xAxis = new NumberAxis(0D, 10D, 1D);
Pane pChart = new Pane();
Pane pAxis = new Pane();
VBox vb = new VBox();
BorderPane bp = new BorderPane();
pChart.setPrefHeight(100D);
pAxis.getChildren().add(xAxis);
xAxis.prefWidthProperty().bind(pAxis.widthProperty());
xAxis.setAnimated(false);
vb.setPadding(new Insets(10D));
vb.getChildren().addAll(pChart, pAxis);
Region point = new Region();
point.setPrefSize(5D, 5D);
point.setStyle("-fx-background-color: black;");
pChart.getChildren().add(point);
//Plot the point in its initial position (value 3 on the axis)
double pointXValue = 3D;
plotPoint(point, pointXValue, xAxis);
//*****************************************************************
//Can the listeners and button.setOnActions be replaced by binding
//the point's layout value to the axis display position?
//*****************************************************************
//Handle resize events
pChart.widthProperty().addListener((obs, oldVal, newVal) -> {
plotPoint(point, pointXValue, xAxis);
});
stage.maximizedProperty().addListener((obs, oldVal, newVal) -> {
plotPoint(point, pointXValue, xAxis);
});
//Handle zooming (hard-coded upper and lower bounds for the
//sake of simplicity)
Button btnZoomIn = new Button("Zoom in");
btnZoomIn.setOnAction((event) -> {
xAxis.setLowerBound(2D);
xAxis.setUpperBound(8D);
xAxis.layout();
plotPoint(point, pointXValue, xAxis);
});
Button btnZoomOut = new Button("Zoom out");
btnZoomOut.setOnAction((event) -> {
xAxis.setLowerBound(0D);
xAxis.setUpperBound(10D);
xAxis.layout();
plotPoint(point, pointXValue, xAxis);
});
bp.setCenter(vb);
bp.setTop(new HBox(btnZoomIn, btnZoomOut));
stage.setScene(new Scene(bp));
stage.setTitle("Test bind layoutX");
stage.setWidth(400D);
stage.setHeight(200D);
stage.show();
}
private void plotPoint(Region region, double axisPos, NumberAxis axis) {
Platform.runLater(() -> {
double posX = axis.getDisplayPosition(axisPos);
region.setLayoutX(posX);
region.setLayoutY(80D);
});
}
public static void main(String args[]){
launch(args);
}
}
+1 for #LukasOwen answer which answer you actual question related to bindings.
But as you are aware that every problem has more than one approach, I am suggesting mine, considering the scalability (adding many points) and too many bindings (for every point).
The key things in this approach are:
You add all your points numbers and its node to a map.
Every time the xAxis is rendered, you update the all the points position. So this will be implicitly done if you resize, change range, or maximize the window.
Below is the example of the approach:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.HashMap;
import java.util.Map;
public class Test023 extends Application {
Map<Double, Region> plotPoints = new HashMap<>();
double yOffset = 80D;
Pane pChart;
#Override
public void start(Stage stage) {
NumberAxis xAxis = new NumberAxis(0D, 10D, 1D);
xAxis.needsLayoutProperty().addListener((obs, old, needsLayout) -> {
if(!needsLayout) {
plotPoints.forEach((num, point) -> {
double posX = xAxis.getDisplayPosition(num);
point.setLayoutX(posX);
point.setLayoutY(yOffset);
});
}
});
pChart = new Pane();
Pane pAxis = new Pane();
VBox vb = new VBox();
BorderPane root = new BorderPane();
pChart.setPrefHeight(100D);
pAxis.getChildren().add(xAxis);
xAxis.prefWidthProperty().bind(pAxis.widthProperty());
xAxis.setAnimated(false);
vb.setPadding(new Insets(10D));
vb.getChildren().addAll(pChart, pAxis);
addPoint(3D, "black");
addPoint(4D, "red");
addPoint(5D, "blue");
//Handle zooming (hard-coded upper and lower bounds for the sake of simplicity)
Button btnZoomIn = new Button("Zoom in");
btnZoomIn.setOnAction((event) -> {
xAxis.setLowerBound(2D);
xAxis.setUpperBound(8D);
});
Button btnZoomOut = new Button("Zoom out");
btnZoomOut.setOnAction((event) -> {
xAxis.setLowerBound(0D);
xAxis.setUpperBound(10D);
});
root.setCenter(vb);
root.setTop(new HBox(btnZoomIn, btnZoomOut));
stage.setScene(new Scene(root));
stage.setTitle("Test bind layoutX");
stage.setWidth(400D);
stage.setHeight(200D);
stage.show();
}
private void addPoint(double num, String color) {
Region point = new Region();
point.setPrefSize(5D, 5D);
point.setStyle("-fx-background-color: " + color);
plotPoints.put(num, point);
pChart.getChildren().add(point);
}
public static void main(String args[]) {
launch(args);
}
}
Something like this would work
#Override
public void start(Stage stage) {
NumberAxis xAxis = new NumberAxis(0D, 10D, 1D);
Pane pChart = new Pane();
Pane pAxis = new Pane();
VBox vb = new VBox();
BorderPane bp = new BorderPane();
pChart.setPrefHeight(100D);
pAxis.getChildren().add(xAxis);
xAxis.prefWidthProperty().bind(pAxis.widthProperty());
xAxis.setAnimated(false);
vb.setPadding(new Insets(10D));
vb.getChildren().addAll(pChart, pAxis);
Region point = new Region();
point.setPrefSize(5D, 5D);
point.setStyle("-fx-background-color: black;");
pChart.getChildren().add(point);
//Plot the point in its initial position (value 3 on the axis)
double pointXValue = 3D;
point.setLayoutY(80D);
point.layoutXProperty().bind(Bindings.createDoubleBinding(()-> {
return xAxis.getDisplayPosition(pointXValue);
}, xAxis.lowerBoundProperty(), xAxis.upperBoundProperty(), pChart.widthProperty()));
//Handle zooming (hard-coded upper and lower bounds for the
//sake of simplicity)
Button btnZoomIn = new Button("Zoom in");
btnZoomIn.setOnAction((event) -> {
xAxis.setLowerBound(2D);
xAxis.setUpperBound(8D);
xAxis.layout();
});
Button btnZoomOut = new Button("Zoom out");
btnZoomOut.setOnAction((event) -> {
xAxis.setLowerBound(0D);
xAxis.setUpperBound(10D);
xAxis.layout();
});
bp.setCenter(vb);
bp.setTop(new HBox(btnZoomIn, btnZoomOut));
stage.setScene(new Scene(bp));
stage.setTitle("Test bind layoutX");
stage.setWidth(400D);
stage.setHeight(200D);
stage.show();
}
This creates a custom double binding with a function that calculates the value of the binding every time the dependencies are changed, see createDoubleBinding​ for more info.

JavaFX HBox layout binding as Circle

I'm having a problem positioning JavaFX's HBox in a similar manner to Circle.
If using a circle shape it is possible to manually position it such that it is bound to a different node. This is what I've done until now, by having a Pane as the point of reference:
Pane node; //can be dragged around/resized
//...
Circle terminal = new Circle(10);
terminal.setStroke(Color.GREEN);
terminal.setFill(Color.GREEN);
terminal.centerXProperty().bind( node.layoutXProperty() );
terminal.centerYProperty().bind( node.layoutYProperty() );
The pane (node) functions as a graph node and can be dragged around and resized. The circle functions as a port/terminal for edge connections in the graph. Seeing that the node should have more than one the idea is to put the circles into an HBox that is attached/bound to the pane like the circle has until now. This makes it so that manual layout calculations are unnecessary when adding or removing ports, resizing the node, etc. So the code then used was:
Pane node; //can be dragged around/resized
//...
HBox terminalContainer = new HBox();
terminalContainer.layoutXProperty().bind( node.layoutXProperty() );
terminalContainer.layoutYProperty().bind( node.layoutYProperty() );
//... adding circles into HBox as scenegraph children
The only difference is swapping out the HBox for the Circle and using the layoutXProperty() as there is no centerXProperty(). But of course this fails, and the ports appear glued on to the top part of the containing frame, acting strangely. Is there a fix for this? I tried changing the parenting Pane to an anchorPane, this allowed to manually anchor down the HBox in the correct place, but caused issues with the resizing/dragging code.
Minimal example:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main2 extends Application {
private AnchorPane component;
#Override
public void start(Stage primaryStage) {
component = new AnchorPane();
Scene scene = new Scene(component, 1024, 768);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
//This works, but is hard to maintain
Cell c1 = new Cell();
Cell c2 = new Cell();
Port p1 = new Port(c1);
Port p2 = new Port(c2);
component.getChildren().addAll(c1, c2, p1, p2);
c1.relocate(150, 150);
c2.relocate(550, 550);
//This does not work, even if unbinding circles, but is simpler
HBox pc1 = new HBox();
HBox pc2 = new HBox();
pc1.layoutXProperty().bind( c1.layoutXProperty() );
pc1.layoutYProperty().bind( c1.layoutYProperty() );
pc2.layoutXProperty().bind( c2.layoutXProperty() );
pc2.layoutYProperty().bind( c2.layoutYProperty() );
Port p3 = new Port(c1);
Port p4 = new Port(c2);
pc1.getChildren().add(p3);
pc2.getChildren().add(p4);
component.getChildren().addAll(pc1, pc2);
}
class Cell extends Pane {
public Cell() {
Rectangle view = new Rectangle(50,50);
view.setStroke(Color.DODGERBLUE);
view.setFill(Color.DODGERBLUE);
getChildren().add(view);
}
}
class Port extends Pane {
public Port(Cell owner) {
Circle view = new Circle(10);
view.setStroke(Color.GREEN);
view.setFill(Color.GREEN);
view.centerXProperty().bind( owner.layoutXProperty() );
view.centerYProperty().bind( owner.layoutYProperty() );
getChildren().add(view);
}
}
public static void main(String[] args) {
launch(args);
}
}
Got it to work, was a typo in the code binding the layoutXProperty twice instead of the layoutYProperty facepalm

JavaFX ScrollPane and Scaling of the Content

I would like to show a photo as an ImageView in a ScrollPane with an ZoomIn and ZoomOut Function. But if I reduce by means of scale the imageview, an undesirable empty edge is created in the ScrollPane. How can you make sure that the ScrollPane is always the size of the scaled ImageView?
See the following example. For simplicity, I replaced the ImageView with a rectangle.
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ScrollPaneDemo extends Application {
double scale;
Pane contPane = new Pane();
#Override
public void start(Stage primaryStage) {
BorderPane pane = new BorderPane();
ScrollPane sp = new ScrollPane();
sp.setContent(contPane);
sp.setVvalue(0.5);
sp.setHvalue(0.5);
Rectangle rec = new Rectangle(2820, 1240,Color.RED);
scale = 0.2;
contPane.setScaleX(scale);
contPane.setScaleY(scale);
contPane.getChildren().add(rec);
Button but1 = new Button("+");
but1.setOnAction((ActionEvent event) -> {
scale*=2;
contPane.setScaleX(scale);
contPane.setScaleY(scale);
});
Button but2 = new Button("-");
but2.setOnAction((ActionEvent event) -> {
scale/=2;
contPane.setScaleX(scale);
contPane.setScaleY(scale);
});
HBox buttons = new HBox(but1, but2);
pane.setTop(buttons);
pane.setCenter(sp);
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
contPane scaled by using transform don't change its layoutBounds automatically. If you want not to make empty space in contPane, you'd better wrap the node in Group.
See this post. Layout using the transformed bounds
sp.setContent(new Group(contPane));
In addition, if you don't want to make empty space in ScrollPane, limit minimum scale to rate which width or height of the content fits viewport's one.
Button but1 = new Button("+");
but1.setOnAction((ActionEvent event) -> {
updateScale(scale * 2.0d);
});
Button but2 = new Button("-");
but2.setOnAction((ActionEvent event) -> {
updateScale(scale / 2.0d);
});
HBox buttons = new HBox(but1, but2);
pane.setTop(buttons);
pane.setCenter(sp);
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
updateScale(0.2d);
private void updateScale(double newScale) {
scale = Math.max(newScale, Math.max(sp.getViewportBounds().getWidth() / rec.getWidth(), sp.getViewportBounds().getHeight() / rec.getHeight()));
contPane.setScaleX(scale);
contPane.setScaleY(scale);
}
Consider a case of the image is smaller than ScrollPane's viewport. Because for showing no empty space, this code will stretch contents when it doesn't have enough size.
In a case of huge images, TravisF's comment helps you.

What is the difference between a Pane and a Group?

In JavaFX, what is the difference between a Pane and a Group? I can't make out any difference.
A Group is not resizable (meaning that its size is not managed by its parent in the scene graph), and takes on the union of the bounds of its child nodes. (So, in other words, the local bounds of a Group will be the smallest rectangle containing the bounds of all the child nodes). If it is larger than the space it is allocated in its parent, it will be clipped.
By contrast, a Pane is resizable, so its size is set by its parent, which essentially determine its bounds.
Here is a quick demo. The Group is on top and the Pane below. Both contain a fixed blue square at (100,100) and a green square which is moved by pressing the left/right arrow keys. Note how at the beginning, the blue square appears in the top left corner of the group, because the local bounds of the group start at the top-leftmost point of all its child nodes (i.e. the local bounds of the group extend from (100, 100) right and down). As you move the green rectangles "off screen", the group adjusts its bounds to incorporate the changes, wherever possible, whereas the pane remains fixed.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class GroupVsPaneDemo extends Application {
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Group group = new Group();
VBox.setVgrow(group, Priority.NEVER);
VBox.setVgrow(pane, Priority.NEVER);
VBox vbox = new VBox(group, pane);
Rectangle rect1 = new Rectangle(100, 100, 100, 100);
Rectangle rect2 = new Rectangle(100, 100, 100, 100);
Rectangle rect3 = new Rectangle(200, 200, 100, 100);
Rectangle rect4 = new Rectangle(200, 200, 100, 100);
rect1.setFill(Color.BLUE);
rect2.setFill(Color.BLUE);
rect3.setFill(Color.GREEN);
rect4.setFill(Color.GREEN);
group.getChildren().addAll(rect1, rect3);
pane.getChildren().addAll(rect2, rect4);
Scene scene = new Scene(vbox, 800, 800);
scene.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
double deltaX ;
switch(e.getCode()) {
case LEFT:
deltaX = -10 ;
break ;
case RIGHT:
deltaX = 10 ;
break ;
default:
deltaX = 0 ;
}
rect3.setX(rect3.getX() + deltaX);
rect4.setX(rect4.getX() + deltaX);
});
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The few important difference between Pane and Group is that :
Pane can have its own size, where as a Group will take on the collective bounds of its children and is not directly resizable.
Pane can be used when you want to position its nodes at absolute position.
Also, note that Group was designed to be very lightweight and doesn't support a lot of styles. For example, you can't set border or background color for the group.
See this answer for more details.

JavaFX - using FillTransition and TranslateTransition misplaces the object

So I have a couple of Rectangle objects of different sizes, named r1 and r2, that initially appear in the dead center of the screen. They have different colors (say one is red and the other blue) and the smaller one is placed on top of the larger one, so even when they are stacked in the center, the two are still distinguishable. I wanted them to move around and return to their original positions, and thus used TranslateTransition as follows:
tt1 = new TranslateTransition(Duration.millis(20000), button1);
tt1.setByX(100); // moves 100 pixels to the right
tt1.setByY(-100); // moves 100 pixels down
tt1.setCycleCount(20); // oscillates 20 times
tt1.setAutoReverse(true);
tt2 = new TranslateTransition(Duration.millis(20000), button2);
tt2.setByX(-100); // moves 100 pixels to the left
tt2.setByY(100); // moves 100 pixels up
tt2.setCycleCount(20); // oscillates 20 times
tt2.setAutoReverse(true);
During their movement, however, if any of r1 and r2 is pressed by a MouseEvent, I would like them to disappear for a few seconds (say 5 seconds) and come back alive again. Using the fact the background color is completely black, I used FillTransition to achieve that effect:
FillTransition ft1 = new FillTransition(Duration.millis(5000), r1, Color.BLACK, Color.BLACK);
FillTransition ft2 = new FillTransition(Duration.millis(5000), r2, Color.BLACK, Color.BLACK);
By converting the Rectangles from Color.BLACK to Color.BLACK for 5 seconds, it gives an effect that the buttons have disappeared for 5 seconds. Also, I have the following setOnMouseClicked on r1 and r2 so they can disappear when a user input is made:
r1.setOnMouseClicked((MouseEvent t) -> {
ft1.play();
});
r2.setOnMouseClicked((MouseEvent t) -> {
ft2.play();
});
After the two objects have disappeared for 5 seconds, they must reappear in the center, as they did in the beginning, and repeat the same oscillating motion using tt1 and tt2, which I achieved with setOnFinished on ft1 and ft2:
ft1.setOnFinished((ActionEvent event) -> {
r1.setFill(color1); // restore the original color
tt1.play();
});
ft2.setOnFinished((ActionEvent event) -> {
r2.setFill(color2); // restore the original color
tt2.play();
});
The problem is, however, when r1 and r2 reappear, they are positioned not at the center, but rather at the location from which they last disappeared - in other words, the location of their rebirth is where they were at during the last TranslateTransition when a user's MouseEvent is detected. I have tried to modify this by using r1.setX(centerX) and r1.setY(centerY), where centerX and centerY are the original center coordinates used in the beginning, but it could not fix the problem. In fact, when I used r1.getX(), the returned value equaled the original centerX value even when it was conspicuous that r1 was not placed in the center. This gave me a suspicion that TranslateTransition performs its duty without altering the actual getX() values. I have also thought of using ParallelTransition somehow on TranslateTransition and FillTransition, so tt1 could finish while ft1 takes effect, but since then ft1 would start running when tt1 has already been running for some time, it would not provide a feasible solution.
So my question is, if an object's TranslateTransition is interrupted in the middle, how do I restore the object's "original" coordinate, not where the object was last left off when TranslateTransition was interrupted?
p.s. I want to avoid creating new Rectangle objects every time a MouseEvent is detected, because that means all TranslateTransition and FillTransition linked to r1 and r2 must be recreated as well.
Sample Solution
Run the program, the rectangle will start moving. Click on the rectangle and it will disappear momentarily. Shortly after it has disappeared, the rectangle will re-appear at its original start location and start moving along its original trajectory.
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Pauser extends Application {
#Override
public void start(Stage stage) throws Exception {
final Rectangle r1 = new Rectangle(
50, 150, 30, 30
);
final TranslateTransition tt1 = new TranslateTransition(
Duration.seconds(5),
r1
);
tt1.setFromX(0); // start at the layout origin for the node
tt1.setFromY(0); //
tt1.setByX(100); // moves 100 pixels to the right
tt1.setByY(-100); // moves 100 pixels down
tt1.setCycleCount(TranslateTransition.INDEFINITE);
tt1.setAutoReverse(true);
tt1.play();
final PauseTransition pt1 = new PauseTransition(
Duration.seconds(1)
);
pt1.setOnFinished(event -> {
tt1.playFromStart();
r1.setVisible(true);
});
r1.setOnMouseClicked(event -> {
r1.setVisible(false);
tt1.stop();
r1.setTranslateX(0);
r1.setTranslateY(0);
pt1.play();
});
stage.setScene(
new Scene(
new Group(r1),
200, 200
)
);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
This just solves your problem for one rectangle. You can stick the solution in a loop to handle multiple rectangles.
Observations
A PauseTransition is used rather than a FillTransition and the visibility of the node is set to false while the pause is running. With a FillTransition the user can still click on the node, even though a node filled with the background color cannot be visibly distinguished from the background. So a FillTransition is probably undesirable.
fromX/fromY properties are set for the translate transition otherwise if you stop and run it from the start it will just use the current translateX/translateY values of the node rather than the origin values of 0/0 which is what you want.
While the node is not visible, there is no need to keep running the TranslateTransition, so the transition is stopped for that duration.
When the translate transition is stopped, it leaves the translateX/translateY co-ordinates at wherever they are currently set for the last animation frame. So a manual call to set translateX/translateY to 0/0 is added.
After the pause transition is complete, a request is made to play the translate transition from the start rather than wherever it was previously paused or stopped.
Understanding Transformations
A node's position on the screen is based on transformations applied to its layout position. The layout position is maintained in the node's layoutX/layoutY properties. But if you apply a translate transformation to a node (as you are implicitly doing in a TranslateTransition), then the node's screen position will be layoutX+translateX / layoutY+translateY.
Background Study
Read the Node documentation, in particular the sections on transformations and bounding rectangles.
Try out this layout bounds demonstration to help understand the concepts.
So the key was using setFromX and setFromY to start from the original coordinate.
It was also key to set manually set the translateX/translateY values to 0/0. If this was not done, the node would flash at its last translated position before starting to move from the origin position. I think this is because there is a frame delay from when you request the animation to start and when it actually starts and uses the fromX/fromY co-ordinates of 0/0. Which is kind of a strange behavior.
This works fine for me. Hopefully it good approach. There could be new class to create extends rectangle with events and transitions in it. This is just sample for two rectangles.
import javafx.animation.FadeTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Rectangle rect1 = new Rectangle (100, 40, 120, 120);
rect1.setArcHeight(42);
rect1.setArcWidth(42);
rect1.setFill(Color.AQUA);
TranslateTransition tt1;
tt1 = new TranslateTransition(Duration.millis(3000), rect1);
tt1.setByX(200);
tt1.setByY(-200);
tt1.setCycleCount(2);
tt1.setAutoReverse(true);
tt1.play();
rect1.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent event) {
FadeTransition ft = new FadeTransition(Duration.millis(1000), rect1);
ft.setFromValue(1.0);
ft.setToValue(0.0);
ft.play();
event.consume();
}
});
tt1.setOnFinished(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
tt1.play();
FadeTransition ft = new FadeTransition(Duration.millis(1000), rect1);
ft.setToValue(1.0);
ft.play();
}
});
Rectangle rect2 = new Rectangle (100, 40, 80, 80);
rect2.setArcHeight(42);
rect2.setArcWidth(42);
rect2.setFill(Color.YELLOWGREEN);
TranslateTransition tt2;
tt2= new TranslateTransition(Duration.millis(6000), rect2);
tt2.setByX(-200);
tt2.setByY(200);
tt2.setCycleCount(2);
tt2.setAutoReverse(true);
tt2.play();
rect2.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent event) {
FadeTransition ft = new FadeTransition(Duration.millis(1000), rect2);
ft.setFromValue(1.0);
ft.setToValue(0.0);
ft.play();
event.consume();
}
});
tt2.setOnFinished(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
tt2.play();
FadeTransition ft = new FadeTransition(Duration.millis(1000), rect2);
ft.setToValue(1.0);
ft.play();
}
});
StackPane pane = new StackPane();
pane.setAlignment(Pos.CENTER);
pane.getChildren().addAll(rect1,rect2);
Scene scene = new Scene(pane, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args){
launch(args);
}
}

Resources