How to achieved collision detection in Translate Transition (in JavaFX)? - java

I have written the below JavaFX program in which two rectangle nodes are in translate transition:
public class Test extends Application{
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane borderPane = new BorderPane();
borderPane.setStyle("-fx-background-color: green;");
Rectangle rect1 = new Rectangle(20,20,50, 50);
rect1.setArcHeight(15);
rect1.setArcWidth(15);
rect1.setFill(Color.RED);
Rectangle rect2 = new Rectangle(20,20,30, 30);
rect2.setArcHeight(15);
rect2.setArcWidth(15);
rect2.setFill(Color.RED);
TranslateTransition translateTransition1 = new TranslateTransition(Duration.millis(2000), rect1);
translateTransition1.setFromX(0);
translateTransition1.setToX(300);
translateTransition1.setToY(300);
translateTransition1.setCycleCount(Timeline.INDEFINITE);
translateTransition1.setAutoReverse(true);
translateTransition1.play();
TranslateTransition translateTransition2 = new TranslateTransition(Duration.millis(2000), rect2);
translateTransition2.setFromX(300);
translateTransition2.setToX(0);
translateTransition2.setToY(300);
translateTransition2.setCycleCount(Timeline.INDEFINITE);
translateTransition2.setAutoReverse(true);
translateTransition2.play();
borderPane.getChildren().add(rect1);
borderPane.getChildren().add(rect2);
primaryStage.setScene(new Scene(borderPane, 500, 500));
primaryStage.show();
}
}
How can I implement collision detection of the two rectangle nodes which are in Translate Transition?

With rectangles it's pretty easy; just get their bounds in the parent and see if they intersect. The only drawback with this is it doesn't take into account the curved corners: you may need to compute that by hand if you want that level of accuracy. For non-rectangular shapes you can also just observe the bounds in parent properties, but you'd need to do the computation by hand to see if the shapes intersect.
ObservableBooleanValue colliding = Bindings.createBooleanBinding(new Callable<Boolean>() {
#Override
public Boolean call() throws Exception {
return rect1.getBoundsInParent().intersects(rect2.getBoundsInParent());
}
}, rect1.boundsInParentProperty(), rect2.boundsInParentProperty());
colliding.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> obs,
Boolean oldValue, Boolean newValue) {
if (newValue) {
System.out.println("Colliding");
} else {
System.out.println("Not colliding");
}
}
});

TranslateTransition isn't meant to support Collision Detection. It simply moves A to B without any regards to the state of anything but its node.
You would need a Transition mechanism that is aware of the other objects on the board.
The good news is that creating a Transition isn't too hard. You can create a class that inherits Transition and simply implement the interpolate() method.
From the JavaDoc:
Below is a simple example. It creates a small animation that updates
the text property of a Text node. It starts with an empty String and
adds gradually letter by letter until the full String was set when the
animation finishes.
final String content = "Lorem ipsum";
final Text text = new Text(10, 20, "");
final Animation animation = new Transition() {
{
setCycleDuration(Duration.millis(2000));
}
protected void interpolate(double frac) {
final int length = content.length();
final int n = Math.round(length * (float) frac);
text.setText(content.substring(0, n));
}
};
The bad news is that having a successful collision detection mechanism is a bit harder. I'm really no expert on the subject, but I would probably have a ObservableList of Nodes that have collision, pass it to the Transition and on the interpolate method I would do a intersection check of the node that's moving against all the other nodes and leave it still if he cannot move.
If you want anything better than that, you'll probably want to look into a 2D Game Framework like Slick2D.

EDIT: Made a few simple alterations and went with a State based approach, code has been updated.
Well my approach is different that all the above ...
NOTE: I'm using 1.8 source
I created a Collidable interface:
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.shape.Shape;
public interface Collidable{
public enum CollisionState{
WAITING,
TOUCHING;
}
ObjectProperty<CollisionState> state = new SimpleObjectProperty<>(CollisionState.WAITING);
public default ReadOnlyObjectProperty<CollisionState> collisionStateProperty(){return state;}
public default CollisionState getCollisionState(){return state.get();}
BooleanProperty collided = new SimpleBooleanProperty(false){{
addListener((ObservableValue<? extends Boolean> observable1, Boolean oldValue, Boolean touching) -> {
if(touching){
state.set(CollisionState.TOUCHING);
}else{
state.set(CollisionState.WAITING);
}
});
}};
public default boolean hasCollided(){return collided.get();}
public default BooleanProperty collidedProperty(){return collided;}
public default void checkCollision(Shape src, Shape other){
if(Shape.intersect(src, other).getBoundsInLocal().getWidth() > -1 && !getCollisionState().equals(CollisionState.TOUCHING)){
collided.set(true);
handleCollision(other);
}else if(Shape.intersect(src, other).getBoundsInLocal().getWidth() <= 0){
collided.set(false);
}
}
public void handleCollision(Shape other);
}
And a simple implementation:
import javafx.animation.Animation;
import javafx.animation.ParallelTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
*
* #author Dub-Laptop
*/
public class CollisionTesting extends Application {
private TranslateTransition cAnim;
#Override
public void start(Stage primaryStage) {
Group root = new Group();
Scene scene = new Scene(root);
primaryStage.setTitle("Collision Testing");
primaryStage.setScene(scene);
primaryStage.show();
Rectangle r = new Rectangle(100,50, Color.AQUA);
r.setLayoutX(10);
r.setLayoutY(200);
CollidableCircle c = new CollidableCircle(50, Color.GREEN);
c.setLayoutX(800);
c.setLayoutY(200);
/* can change this to anything you like
I used translateXProperty for simplicity
*/
c.translateXProperty().addListener((Observable observable) -> {
c.checkCollision(c, r);
});
root.getChildren().addAll(r, c);
TranslateTransition rAnim = new TranslateTransition();
rAnim.setToX(600);
rAnim.setAutoReverse(true);
rAnim.setCycleCount(Animation.INDEFINITE);
rAnim.setDuration(Duration.seconds(5));
rAnim.setNode(r);
cAnim = new TranslateTransition();
cAnim.setToX(-590);
cAnim.setAutoReverse(true);
cAnim.setCycleCount(Animation.INDEFINITE);
cAnim.setDuration(Duration.seconds(5));
cAnim.setNode(c);
rAnim.play();
cAnim.play();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
private class CollidableCircle extends Circle implements Collidable{
public CollidableCircle(double radius, Paint fill) {
super(radius, fill);
new AnimationTimer(){
#Override
public void handle(long now) {
root.getChildren().filtered((Node n)->{
return !n.equals(CollidableCircle.this) && n instanceof Shape;
}).forEach(other ->{
checkCollision(CollidableCircle.this, (Shape)other);
});
}
}.start();
// I added this for local property changes to this node
collisionStateProperty().addListener((ObservableValue<? extends CollisionState> observable, CollisionState oldValue, CollisionState newValue) -> {
if(newValue.equals(CollisionState.TOUCHING)){
setScaleX(1.25);
setScaleY(1.25);
setFill(Color.GREENYELLOW);
cAnim.pause();
}else if(newValue.equals(CollisionState.WAITING)){
setScaleX(1.0);
setScaleY(1.0);
setFill(Color.GREEN);
cAnim.play();
}
});
}
#Override
public void handleCollision(Shape other) {
// handle updates that affect other node here
System.out.println("Collided with : " + other.getClass().getSimpleName());
}
}
}
IMHO rather than using Bounds for checking Shape collisions, use the boolean :
(Shape.intersect(s1,s2).getBoundsInLocal().getWidth() > -1)
This approach is more accurate for Shapes as it will check for non-null pixels within the Shape Bounds, rather than the normal rectangular Bounds.
Though if you really want to use Bounds, this should work also:
if(sourceShape.getBoundsInLocal().intersects(otherShape.getBoundsInParent()){
Shape intersect = Shape.intersect(sourceShape, otherShape);
if(intersect.getBoundsInLocal().getWidth > -1){
// handle code here
}
}
though, as you can see it's more verbose and virtually the same as my other method.
Hope this helps.

Related

JavaFX How can I use one instance of scene in 2 windows? (Mirroring)

I made a chess game that is supposed to run in 2 windows (stages). When a player moves a piece, in the second window the same piece moves accordingly. But the second window is rotated by 180 degrees to simulate a 2 player experience.
To realize this I thought it would be the easiest to use the 1 scene in 2 Windows. Basically a mirror of the first scene.
Problem: Figure doesn't move in the second window but the game knows that it has been moved in the first window because the player can't move the same figures again but the other color.
There is also a Main Menu, which has a play button, that starts the 2 windows.
#FXML
public void play_game(ActionEvent event) throws IOException {
Parent root = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("game_board.fxml")));
Parent second_screen = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("game_board.fxml")));
stage = (Stage)((Node)event.getSource()).getScene().getWindow();
scene = new Scene(root);
stage.setScene(scene);
stage.show();
Stage second_stage = new Stage();
second_stage.setScene(new Scene(second_screen));
second_screen.rotateProperty().set(180);
second_stage.show();
}
The **controller class ** for the first window:
public class board_controller {
#FXML
GridPane chess_board;
#FXML
GridPane second_board;
#FXML
public void initialize(){
game_logic game_logic = new game_logic(chess_board);
}
The controller class for the second window:
public class second_board_controller extends board_controller {
#FXML
public void initialize(){
game_logic new_round = new game_logic(chess_board);
}
}
My question is: How can I use the same exact instance of the scene but only rotated in the second window (Basically a Mirror of the first window)?
Game Example
Thank you!
I tried making the gridpane in the first_board controller static using it in the second board controller with the hopes of them updating automatically but with no results. Setting the main scene in first and second stages but my IDE said it's not allowed. I'm out of ideas...
Share the Model
Since you want to mirror between two windows in the same process, the general idea is to create two instances of your view but share only one instance of the model. The model should be observable in some way so that the view/controller can react to changes in the model by updating the view. With the model being shared and observed, updating it from one window will be seen by the other window.
Models
Note a model should not know about the view. In your code, you do:
game_logic game_logic = new game_logic(chess_board);
From the name of the class, this indicates you're passing a GridPane (the view) to your model. It would be better if your model only modelled a chess game. The controller/view is responsible for translating that state into a visual representation.
Rotating the Second View
The simplest approach to this would be to add state/a method to your controller, and then only on the second instance of the controller configure it to rotate the view. It is at least somewhat justifiable to put this logic in the controller/view because it is only a view thing (it does not affect the game state).
Though instead of rotating the board, you might want to consider "inverting" the location of the pieces (vertically). In other words, for the second view, have it so that a white piece in the bottom-left corner of the board is actually displayed in the top-left corner (and the opposite for black pieces). That way the chess piece images are not rotated along with the rest of the board.
Example
Here's a proof-of-concept for mirroring a draggable rectangle (much simpler than a chess game). Note it only demonstrates the mirroring, it does not show how to e.g., rotate the view in one window but not the other.
RectangleModel.java:
package sample;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
public class RectangleModel {
private final List<Consumer<? super Dimensions>> listeners = new CopyOnWriteArrayList<>();
private Dimensions dimensions;
public RectangleModel(Dimensions dimensions) {
this.dimensions = Objects.requireNonNull(dimensions);
}
public RectangleModel(double x, double y, double width, double height) {
this(new Dimensions(x, y, width, height));
}
public void move(double deltaX, double deltaY) {
if (deltaX != 0.0 || deltaY != 0.0) {
double x = dimensions.x() + deltaX;
double y = dimensions.y() + deltaY;
double w = dimensions.width();
double h = dimensions.height();
dimensions = new Dimensions(x, y, w, h);
notifyListeners();
}
}
public Dimensions getDimensions() {
return dimensions;
}
public void addDimensionsListener(Consumer<? super Dimensions> listener) {
listeners.add(Objects.requireNonNull(listener));
}
public void removeDimensionsListener(Consumer<? super Dimensions> listener) {
listeners.remove(Objects.requireNonNull(listener));
}
private void notifyListeners() {
for (var listener : listeners) {
listener.accept(dimensions);
}
}
public record Dimensions(double x, double y, double width, double height) {}
}
RectangleController.java:
package sample;
import java.util.function.Consumer;
import javafx.fxml.FXML;
import javafx.geometry.Point2D;
import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Rectangle;
public class RectangleController {
private final Consumer<RectangleModel.Dimensions> listener = this::updateRectangle;
#FXML
private Rectangle rectangle;
private Point2D offset;
private RectangleModel model;
public void setModel(RectangleModel model) {
if (this.model != null) {
this.model.removeDimensionsListener(listener);
}
this.model = model;
if (model != null) {
model.addDimensionsListener(listener);
updateRectangle(model.getDimensions());
} else {
updateRectangle(null);
}
}
private void updateRectangle(RectangleModel.Dimensions dims) {
if (dims != null) {
rectangle.setX(dims.x());
rectangle.setY(dims.y());
rectangle.setWidth(dims.width());
rectangle.setHeight(dims.height());
} else {
rectangle.setX(0);
rectangle.setY(0);
rectangle.setWidth(0);
rectangle.setHeight(0);
}
}
#FXML
private void handleMousePressed(MouseEvent event) {
event.consume();
offset = new Point2D(event.getX(), event.getY());
}
#FXML
private void handleMouseDragged(MouseEvent event) {
event.consume();
double deltaX = event.getX() - offset.getX();
double deltaY = event.getY() - offset.getY();
model.move(deltaX, deltaY);
offset = new Point2D(event.getX(), event.getY());
}
#FXML
private void handleMouseReleased(MouseEvent event) {
event.consume();
offset = null;
}
}
RectangleView.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.shape.Rectangle?>
<Pane xmlns="http://javafx.com/javafx/" xmlns:fx="http://javafx.com/fxml/"
fx:controller="sample.RectangleController">
<Rectangle fx:id="rectangle" onMousePressed="#handleMousePressed" onMouseDragged="#handleMouseDragged"
onMouseReleased="#handleMouseReleased"/>
</Pane>
Main.java:
package sample;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
var model = new RectangleModel(0, 0, 100, 50);
primaryStage.setScene(createScene(model));
primaryStage.setTitle("Primary Stage");
primaryStage.show();
var secondStage = new Stage();
secondStage.setScene(createScene(model));
secondStage.setTitle("Second Stage");
secondStage.show();
primaryStage.setX(primaryStage.getX() - primaryStage.getWidth() / 2);
secondStage.setX(primaryStage.getX() + primaryStage.getWidth());
secondStage.setY(primaryStage.getY());
primaryStage.setOnCloseRequest(e -> secondStage.close());
secondStage.setOnCloseRequest(e -> primaryStage.close());
}
private Scene createScene(RectangleModel model) throws IOException {
var loader = new FXMLLoader(Main.class.getResource("RectangleView.fxml"));
var root = loader.<Parent>load();
var controller = loader.<RectangleController>getController();
var scene = new Scene(root, 600, 400);
controller.setModel(model);
return scene;
}
}
Naming Conventions
You should follow the standard naming conventions of Java (or whatever language you're using) when posting on a public forum.
Classes and interfaces use PascalCase.
Methods, fields, parameters, and local variables use camelCase.
Static constants use UPPER_SNAKE_CASE.

How to create a continuous TranslateTransition in JavaFX

I'm using JavaFX to create a Java application which is able to apply a TranslateTransition to a generic node and recall it continuously.
I retrieved a simple right arrow from this url https://www.google.it/search?q=arrow.png&espv=2&source=lnms&tbm=isch&sa=X&ved=0ahUKEwiGheeJvYrTAhWMB5oKHU3-DxgQ_AUIBigB&biw=1600&bih=764#imgrc=rH0TbMkQY2kUaM:
and used it to create the node to translate.
This is my AnimatedNode class:
package application.model.utils.addon;
import javafx.animation.TranslateTransition;
import javafx.scene.Node;
import javafx.util.Duration;
public class AnimatedNode {
private Node node;
private double positionY;
private TranslateTransition translateTransition;
private boolean animated;
private int reverse = 1;
public AnimatedNode(Node node, double animationTime) {
setPositionY(0.0);
setNode(node);
setTranslateTransition(animationTime);
}
public void play() {
if(translateTransition != null && !isAnimated()) {
setAnimated(true);
new Thread() {
#Override
public void run() {
while(isAnimated()) {
translateTransition.setToY(positionY + 50 * reverse);
translateTransition.play();
reverse = -reverse;
setPositionY(translateTransition.getToY());
}
}
}.start();
}
}
public void stop() {
setAnimated(false);
}
public Node getNode() {
return node;
}
private void setNode(Node node) {
this.node = node;
}
public TranslateTransition getTranslateTransition() {
return translateTransition;
}
private void setTranslateTransition(double animationTime) {
translateTransition = new TranslateTransition();
if(node != null) {
translateTransition.setDuration(Duration.seconds(animationTime));
translateTransition.setNode(node);
}
}
public double getPositionY() {
return positionY;
}
private void setPositionY(double positionY) {
this.positionY = positionY;
}
public boolean isAnimated() {
return animated;
}
private void setAnimated(boolean animated) {
this.animated = animated;
}
}
and this is the Application class
package test;
import application.model.utils.addon.AnimatedNode;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class Test extends Application {
private final String TITLE = "Test application";
private final double WIDTH = 600;
private final double HEIGHT = 400;
private final String ARROW_PATH = "file:resources/png/arrow.png";
private BorderPane rootPane;
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle(TITLE);
rootPane = new BorderPane();
rootPane.setPrefSize(WIDTH, HEIGHT);
Image image = new Image(ARROW_PATH);
ImageView imageView = new ImageView(image);
imageView.setFitWidth(WIDTH);
imageView.setFitHeight(HEIGHT);
imageView.setPreserveRatio(true);
AnimatedNode animatedNode = new AnimatedNode(imageView, 0.7);
Pane pane = new Pane();
pane.getChildren().add(animatedNode.getNode());
pane.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent arg0) {
if(arg0.getButton().equals(MouseButton.PRIMARY))
animatedNode.play();
if(arg0.getButton().equals(MouseButton.SECONDARY))
animatedNode.stop();
}
});
rootPane.setCenter(pane);
Scene scene = new Scene(rootPane, WIDTH, HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The node is added to a generic pane; the pane has a MouseListener. I can start the TranslateTransition by using the primary button of the mouse and stop it with the secondary one.
I used a Thread in the play() method of AnimatedNode but I still have a continuous delay in the transition.
Is this the best way to perform the transition? Can I improve my code?
Thanks a lot for your support.
Sample
This is a simplified example which demonstrates a continuous animation started and stopped by left and right mouse clicks.
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.*;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class BouncingCat extends Application {
private static final double WIDTH = 100;
private static final double HEIGHT = 100;
private final String ARROW_PATH =
"http://icons.iconarchive.com/icons/iconka/meow-2/64/cat-rascal-icon.png";
// image source: http://www.iconka.com
#Override
public void start(Stage stage) {
Image image = new Image(ARROW_PATH);
ImageView imageView = new ImageView(image);
TranslateTransition animation = new TranslateTransition(
Duration.seconds(0.7), imageView
);
animation.setCycleCount(Animation.INDEFINITE);
animation.setFromY(0);
animation.setToY(50);
animation.setAutoReverse(true);
Pane pane = new Pane(imageView);
Scene scene = new Scene(pane, WIDTH, HEIGHT);
scene.setOnMouseClicked(e -> {
switch (e.getButton()) {
case PRIMARY:
animation.play();
break;
case SECONDARY:
animation.pause();
break;
}
});
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Advice
You don't need a Thread when you have a Transition. JavaFX will render updated transition frames automatically each pulse.
I don't advise keeping track of properties in a class, when those same values are already represented in the underlying tools you use.
For example:
replace int reverse = 1; with transition.setAutoReverse(true) or transition.setRate(1) (or -1).
replace animated with transition.getStatus().
instead of double positionY, set the toY of the transition.
I wouldn't advise calling your class AnimatedNode unless it extended node, otherwise it is confusing, instead call it something like AnimationControl.
import javafx.animation.TranslateTransition;
import javafx.scene.Node;
import javafx.util.Duration;
public class AnimationControl {
private final TranslateTransition translateTransition;
public AnimationControl(Duration duration, Node node) {
translateTransition = new TranslateTransition(duration, node);
}
public TranslateTransition getTranslateTransition() {
return translateTransition;
}
}
You only need to encapsulate the node and the transition in the AnimationControl and not other fields unless you need further functionality not apparent in your question and not already provided by Node or Transition. If you have that extra functionality then you can enhance the AnimationControl class above to add it.
Exposing the node and the translate transition is enough, as if the user wants to manage the animation, such as starting and stopping it, then the user can just get it from the AnimationControl class. Depending on your use case, the entire AnimationControl class might be unnecessary as you might not need the encapsulation it provides and might instead prefer to just work directly with the node and the transition (as demoed in the sample).

How to know if the resizing of a window is done from the left or from the right?

I am using JavaFX to build a GUI and I'm having problems on knowing the mouse location when resized. The idea is that if a mouse gets on the very edge of the GUI it changes to a double sided arrow indicating that you can now press the mouse and resize the window.
I need the location of the mouse pointer on this edge, but I don't know how to do that. I need to know in which direction the window is resized.
Updated Added new options and discussed pros and cons.
This is tricky. The issue is that the window keeps track of its top, left, width, and height. When it is resized from the right or bottom, things are easy enough: the width or height change. But when it is resized from the left, both x and width must change. These two changes do not happen atomically, as x and width are stored as two independent properties.
The first approach to this is to keep track of the mouse coordinates, and just see if it's in the left half or the right half. (Obviously you can do the same with the height.) This approach is independent of any implementation details, but the user can cause it to fail by being extremely careful with the mouse. If you move the mouse to the right edge of the window to the exact pixel of the window boundary, then resize, you can see incorrect output.
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
Scene scene = new Scene(root,400,400);
class MouseLocation {
double x,y ;
}
MouseLocation mouseLocation = new MouseLocation();
scene.setOnMouseMoved(event -> {
mouseLocation.x = event.getX();
mouseLocation.y = event.getY();
});
primaryStage.widthProperty().addListener((obs, oldWidth, newWidth) -> {
if (mouseLocation.x < primaryStage.getWidth() / 2) {
System.out.println("Resized from left");
} else {
System.out.println("Resized from right");
}
});
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The second approach is to keep track of the last known horizontal range of the window (minX and maxX), and update that range when the width changes. Then you can check to see whether the minX or maxX has changed. The problem with this approach is that it's dependent on undocumented implementation details. It appears (on my system, using the current version, etc) that when the window is resized from the left, x is changed first, then the width is changed. If that were to change in a subsequent release, the following would break:
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
Scene scene = new Scene(root,400,400);
class MutableDouble {
double value ;
}
MutableDouble windowLeftEdge = new MutableDouble();
primaryStage.widthProperty().addListener((obs, oldWidth, newWidth) -> {
if (primaryStage.getX() == windowLeftEdge.value) {
System.out.println("Resized from right");
} else {
System.out.println("Resized from left");
}
windowLeftEdge.value = primaryStage.getX() ;
});
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The third approach (that I can think of) is to coalesce changes that happen quickly into a single change. This is a bit tricky to program correctly, so instead of doing it from scratch, I used the third party ReactFX framework which models "event streams" and has a built-in mechanism for combining events that happen in quick succession. This is probably the most robust of the three solutions presented here, but at the cost of either a degree of complexity, or the inclusion of an external framework.
import java.time.Duration;
import org.reactfx.Change;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ReactFXVersion extends Application {
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
Scene scene = new Scene(root,400,400);
ObservableValue<Bounds> windowBounds = Bindings.createObjectBinding(() ->
new BoundingBox(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight()),
primaryStage.xProperty(), primaryStage.yProperty(), primaryStage.widthProperty(), primaryStage.heightProperty());
EventStream<Change<Bounds>> bounds = EventStreams.changesOf(windowBounds)
.reduceSuccessions((previousChange, nextChange) ->
new Change<>(previousChange.getOldValue(), nextChange.getNewValue()),
Duration.ofMillis(10));
bounds.subscribe(boundsChange -> {
Bounds newBounds = boundsChange.getNewValue();
Bounds oldBounds = boundsChange.getOldValue();
if (newBounds.getWidth() != oldBounds.getWidth()) {
if (newBounds.getMinX() != oldBounds.getMinX()) {
System.out.println("Resized from left");
} else if (newBounds.getMaxX() != oldBounds.getMaxX()) {
System.out.println("Resized from right");
}
}
if (newBounds.getHeight() != oldBounds.getHeight()) {
if (newBounds.getMinY() != oldBounds.getMinY()) {
System.out.println("Resized from top");
} else if (newBounds.getMaxY() != oldBounds.getMaxY()) {
System.out.println("Resized from bottom");
}
}
});
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

dragging nodes when their parent has a transformation results in nodes disappearing

Very weird problem, I finally managed to distill it into a small piece of code which demonstrates the problem. I have a pane, which contains 1 group, that groups contains a group which contains some ellipses. The top group has a rotate transform applied to it. The ellipses are made draggable.
Try the below example, drag some ellipses downwards (outside the group's bounds), you'll see them disappearing. If you maximize the window, they appear again but you can't drag them anymore, they don't receive any events anymore.
Now for the really strange part, there are three ways I can make the problem go away:
don't apply the transform
remove one ellipse (!?) (I experimented to get to this number, 11)
start ScenicView alongside and select the group containing the ellipses so you can see the bounds of the group
I'm at a total loss here, completely stupefied. Please, does anyone have any idea why this problem is occuring and how to solve it?
Code (JavaFX 2.2.3 and java 1.7.0_09 64bit Windows 7):
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.GroupBuilder;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.EllipseBuilder;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.RotateBuilder;
import javafx.stage.Stage;
public class DragProblem extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
DrawingPane drawingPane = new DrawingPane();
drawingPane.setStyle("-fx-background-color: darkgrey;");
Scene scene = SceneBuilder.create().root(drawingPane).width(1280d).height(1024d).build();
primaryStage.setScene(scene);
primaryStage.show();
}
public class DrawingPane extends Pane {
private Group transformedGroup;
private Group splinePoints;
public DrawingPane() {
transformedGroup = GroupBuilder.create().id("transformedGroup").build();
getChildren().add(transformedGroup);
addPoints();
makePointsDraggable();
}
public void addPoints() {
double[] coords = new double[] {
// comment any one the below x,y coordinates and the problem doesn't occur..
239.28353881835938, 488.2192687988281,
245.04466247558594, 505.30169677734375,
258.56671142578125, 539.49462890625,
267.2294006347656, 563.618408203125,
282.89141845703125, 587.84033203125,
309.6925048828125, 602.2174072265625,
327.4945068359375, 616.4683227539062,
345.25445556640625, 633.718994140625,
371.0416259765625, 649.0819702148438,
393.78704833984375, 667.402587890625,
442.67010498046875, 676.0886840820312 };
splinePoints = GroupBuilder.create().build();
for (int i = 0; i < coords.length; i += 2) {
Ellipse ellipse = EllipseBuilder.create().radiusX(3).radiusY(3).centerX(coords[i]).centerY(coords[i + 1]).build();
splinePoints.getChildren().add(ellipse);
}
transformedGroup.getChildren().add(splinePoints);
Rotate rotateTransform = RotateBuilder.create().build();
rotateTransform.setPivotX(224);
rotateTransform.setPivotY(437);
rotateTransform.setAngle(15);
// ..or comment this line to prevent the problem occuring
transformedGroup.getTransforms().add(rotateTransform);
}
public void makePointsDraggable() {
for (final Node n : splinePoints.getChildren()) {
Ellipse e = (Ellipse) n;
final NodeDragHandler ellipseDragHandler = new NodeDragHandler(e, transformedGroup);
e.setOnMousePressed(ellipseDragHandler);
e.setOnMouseDragged(ellipseDragHandler);
}
}
}
public class NodeDragHandler implements EventHandler<MouseEvent> {
protected final Ellipse node;
private final Node transformedGroup;
private double initialX;
private double initialY;
private Point2D initial;
private boolean dragStarted = false;
public NodeDragHandler(Ellipse node, Group transformedGroup) {
this.node = node;
this.transformedGroup = transformedGroup;
}
#Override
public void handle(MouseEvent event) {
if (!dragStarted) {
initialX = event.getScreenX();
initialY = event.getScreenY();
initial = transformedGroup.localToParent(new Point2D(node.getCenterX(), node.getCenterY()));
dragStarted = true;
} else {
double xDragged = event.getScreenX() - initialX;
double yDragged = event.getScreenY() - initialY;
Point2D newPos = new Point2D(initial.getX() + xDragged, initial.getY() + yDragged);
Point2D p = transformedGroup.parentToLocal(newPos.getX(), newPos.getY());
node.setCenterX(p.getX());
node.setCenterY(p.getY());
}
}
}
}
It's been acknowledged as a bug in JavaFX and will be solved in 2.2.6, see here. I've tested it with the early access release and I can confirm it has been solved.

javafx animation: displaying circles

I want to display 5 randomly positioned and colored circles. It was easy part. Now I want to adjust this code to be an animation. This application should generate random circles endlessly but the condition is that it should keep only last five circles on the screen. This is where I got stuck. JavaFx provides ListChangeListener. I think it is what I should use. But how?
The following is my unfinished code:
import java.util.Random;
import javafx.application.Application;
import javafx.collections.ListChangeListener;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class RandomColorTest extends Application {
int radius = 20;
int sceneWidth = 300;
int sceneHeight = 300;
private void init(Stage primaryStage) {
Group root = new Group();
primaryStage.setResizable(false);
primaryStage.setScene(new Scene(root, sceneWidth,sceneHeight));
for (int i = root.getChildren().size(); i < 5; i++) {
root.getChildren().add(createCircle());
// the following should convey the idea:
// if the collection holds 5 elements then keep least recently generated element for 1 second and then delete it
// add one new element
// if the collection holds 5 elements then keep least recently generated element for 1 second and then delete it
// add one new element
// and so on
root.getChildren().addListener(new ListChangeListener<E>() {
#Override
public void onChanged(
javafx.collections.ListChangeListener.Change<? extends E> arg0) {
// TODO Auto-generated method stub
}
});
}
}
// Create randomly positioned and colored circle
private Circle createCircle() {
final Circle circle = new Circle();
circle.setRadius(radius);
Random r = new Random();
int rCol1 = r.nextInt(256);
int rCol2 = r.nextInt(256);
int rCol3 = r.nextInt(256);
int rX = radius+r.nextInt(sceneWidth);
if (rX>sceneWidth-radius) {
rX=rX-2*radius;
}
int rY = radius+r.nextInt(sceneHeight);
if (rY>sceneHeight-radius) {
rY=rY-2*radius;
}
circle.setLayoutX(rX);
circle.setLayoutY(rY);
circle.setStroke(Color.BLACK);
circle.setFill(Color.rgb(rCol1,rCol2,rCol3));
System.out.println(rCol1+"-"+rCol2+"-"+rCol3+"-"+rX+"-"+rY);
return circle;
}
#Override public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
}
public static void main(String[] args) { launch(args); }
}
After having managed to make ListChangeListener compile without errors it doesn't still work the way expected. Changes made to for loop:
for (int i = root.getChildren().size();;i++) {
final ObservableList<Node> ol = root.getChildren();
// the following should convey the idea:
// if the collection holds 5 elements then keep least recently generated element for 1 second and then delete it
// add one new element
// if the collection holds 5 elements then keep least recently generated element for 1 second and then delete it
// add one new element
// and so on
ol.add(createCircle());
ol.addListener( new ListChangeListener<Node>(){
#Override
public void onChanged(
javafx.collections.ListChangeListener.Change<? extends Node> arg0) {
// TODO Auto-generated method stub
System.out.println("one new element added, size:"+ol.size());
if (ol.size()==5) {
ol.remove(0);
}
}
});
}
For loop is defined to loop infinitely (probably not the right way to solve this problem also) and I can see from console that circles are removed and added during the program run. Alas, I can't see GUI anymore.
A similar question was also asked on the on the Oracle forums last year.
Here is sample solution using Timeline, which I prefer to a solution relying on worker threading. Though both can get the job done, I find using the JavaFX animation APIs more elegant and less error prone.
import javafx.animation.*;
import javafx.application.Application;
import javafx.event.*;
import javafx.scene.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.Random;
public class FiveAutoCircleExample extends Application {
private static final Random r = new Random();
public static final int SCENE_SIZE = 800;
public static void main(String[] args) throws Exception { launch(args); }
public void start(final Stage stage) throws Exception {
final Group circles = new Group();
final Timeline animation = new Timeline(
new KeyFrame(Duration.seconds(.5),
new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent actionEvent) {
while (circles.getChildren().size() >= 5) circles.getChildren().remove(0);
int radius = 10 * r.nextInt(20);
circles.getChildren().add(
new Circle(
r.nextInt(SCENE_SIZE - radius * 2) + radius, r.nextInt(SCENE_SIZE - radius * 2) + radius,
radius,
new Color(r.nextDouble(), r.nextDouble(), r.nextDouble(), r.nextDouble())
)
);
}
})
);
animation.setCycleCount(Animation.INDEFINITE);
animation.play();
// display the scene.
stage.setScene(new Scene(circles, SCENE_SIZE, SCENE_SIZE, Color.CORNSILK));
stage.show();
}
}
In your code, you have some mistakes:
the GUI is not shown because, the execution flow never reaches the primaryStage.show(); due to infinite loop in the init(primaryStage);.
new ListChangeListener is added again and again in a loop. However you should add it only once in normal situations.
You are manipulating the ol (ol.remove(0);) in its own listener which triggers the new change event recursively.
As a solution: periodic tasks, long-time background executions can be separated to a different thread.
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
primaryStage.setResizable(false);
primaryStage.setScene(new Scene(root, sceneWidth, sceneHeight));
final ObservableList<Node> ol = root.getChildren();
new Thread(new Runnable() {
#Override public void run() {
while (true) {
try {
// Wait for 2 seconds.
Thread.sleep(2000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
Platform.runLater(new Runnable() {
#Override public void run() {
System.out.println("ol size:" + ol.size());
if (ol.size() == 5) {
ol.remove(0);
}
ol.add(createCircle());
}
});
}
}
}).start();
primaryStage.show();
}
I have only changed the content of start(Stage primaryStage). There is no need to add a listener. This solution is very quick but not elegant way. You must manage the thread yourself. For more elegant and modern approach refer to Worker Threading in JavaFX 2.0.
In addition, if you really want a real animation then see the example Colorful Circles Application.

Categories