I have a list of animations and I want to be able to play them by clicking on a "next" button and playing them back by clicking a "previous" button. So I can play the first animation, then play the 2nd animation, then play the 2nd animation backwards and reach the position like after playing the first animation only.
My problem is that I can't reverse the animation after it's finished. I know that I can set autoReverse but then each animation will reverse immediately.
Here is an example for one animation:
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class AnimTest extends Application {
#Override
public void start(Stage stage) throws Exception {
Circle c = new Circle(5, Color.RED);
TranslateTransition move = new TranslateTransition(Duration.seconds(2), c);
move.setByX(10);
move.setByY(10);
Button next = new Button("Next");
Button previous = new Button("Previous");
next.setOnAction(e -> {
move.setRate(1);
move.play();
});
previous.setOnAction(e -> {
move.setRate(-1);
move.play();
});
Pane p = new Pane(c);
p.setPrefSize(50, 50);
HBox buttons = new HBox(next, previous);
VBox root = new VBox(p, buttons);
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
After pressing "next" I want "previous" to move the ball back to its original position (so effectively x by -10 and y by -10) and not playing the "following" animation in reverse.
In practice, my animations animate different objects in the scenegraph and they can be parallel/sequential transitions. For the list I keep a current location index i and doing:
next.setOnAction(e -> {
Animation move = list.get(i);
move.setRate(1);
move.play();
i++;
});
previous.setOnAction(e -> {
i--;
Animation move = list.get(i);
move.setRate(-1);
move.play();
});
in an attempt to reverse the previous animation.
How can I do this?
To clarify, my list is of Animation. The TranslateTransition was just an example.
The issue here is using "relative" movement instead of absolute movement.
If you set byX = 10 the animation moves the node 10 to the right when played forward which means the proper way of reversing the animation would be to place the node at the end position immediately and then moving the node back to the original location before starting the animation.
Since you don't want to use the same animation over and over again finding the correct way to invert different animations could be difficult for animations using "relative" values. If you instead use absolute ones this shouldn't simply playing the animations backwards shouldn't cause any issues.
Example
#Override
public void start(Stage stage) {
Circle c = new Circle(5, Color.RED);
// create alternating right/down movement animations with absolute movement
List<Animation> animations = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
TranslateTransition move = new TranslateTransition(Duration.seconds(1), c);
animations.add(move);
int step = i >> 1;
if ((i & 1) == 0) {
move.setFromX(step * 10);
move.setToX((step + 1) * 10);
} else {
move.setFromY(step * 10);
move.setToY((step + 1) * 10);
}
}
final ListIterator<Animation> iterator = animations.listIterator();
Button next = new Button("Next");
Button previous = new Button("Previous");
previous.setDisable(true);
next.setOnAction(e -> {
Animation move = iterator.next();
next.setDisable(!iterator.hasNext());
previous.setDisable(false);
move.setRate(1);
move.play();
});
previous.setOnAction(e -> {
Animation move = iterator.previous();
next.setDisable(false);
previous.setDisable(!iterator.hasPrevious());
move.setRate(-1);
move.play();
});
Pane p = new Pane(c);
p.setPrefSize(100, 100);
HBox buttons = new HBox(next, previous);
VBox root = new VBox(p, buttons);
stage.setScene(new Scene(root));
stage.show();
}
I managed to trick my way into "storing" the reverse cycle for later use using a PauseTransition that pauses the animation after the forward cycle. Then the animation can be played from the second cycle and it will reverse. Not the pretiest solution but it works (except for when you press the buttons too quickly. I tried to solve it with the comment code but it didn't quite get there so if anyone has a solution please tell)
import java.util.ArrayList;
import java.util.List;
import javafx.animation.Animation;
import javafx.animation.ParallelTransition;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class AnimTest extends Application {
int current = 0;
final int size = 5;
#Override
public void start(Stage stage) throws Exception {
Circle c = new Circle(10, Color.RED);
List<Animation> animations = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
TranslateTransition move = new TranslateTransition(Duration.seconds(1), c);
move.setByX(20);
move.setByY(20);
PauseTransition pauser = new PauseTransition(move.getCycleDuration());
ParallelTransition parallel = new ParallelTransition(move, pauser);
pauser.setOnFinished(e -> parallel.pause());
parallel.setCycleCount(2);
parallel.setAutoReverse(true);
animations.add(parallel);
}
Button next = new Button("Next");
Button previous = new Button("Previous");
previous.setDisable(true);
Label l = new Label(current + "");
next.setOnAction(e -> {
next.setDisable(current == size - 1);
previous.setDisable(false);
/* if (current > 0) {
Animation last = animations.get(current - 1);
last.jumpTo(last.getCycleDuration());
}*/
Animation cur = animations.get(current);
cur.playFromStart();
current++;
l.setText(current + "");
});
previous.setOnAction(e -> {
current--;
l.setText(current + "");
next.setDisable(false);
previous.setDisable(current == 0);
/* if (current < size - 1) {
Animation last = animations.get(current + 1);
last.stop();
}*/
Animation cur = animations.get(current);
cur.play();
});
Pane p = new Pane(c);
p.setPrefSize(200, 200);
HBox buttons = new HBox(5, next, previous, l);
VBox root = new VBox(p, buttons);
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I used the disable/enable buttons code from fabian (+1).
I found the quickest solution is to add a listener to the currentRateProperty, listen for it to change to -1 and pause the transition (after the first cycle completes).
There is no cycleCompleted listener or any similar listener, but listening for the cycle to complete can be achieved that way.
Note that the cycleCount must be set to 2, and autoReverse must be set to true.
Node node = ...;
double byX = ...;
TranslateTransition transition = new TranslateTransition();
transition.setNode(node);
transition.setCycleCount(2);
transition.setAutoReverse(true);
transition.setByX(byX);
transition.currentRateProperty().addListener((obs, old, now) -> {
if (now.intValue() == -1) {
transition.pause();
}
});
Button play = new Button("Play");
play.setOnAction(event -> transition.play());
Note that both the forward and backward translation are triggered by the same method call transition.play(), hence there is only one button for both motions, but this of course can be changed. But personally, i like it that way.
In your case, it would look like this:
public class AnimTest extends Application {
#Override
public void start(Stage stage) throws Exception {
Circle c = new Circle(5, Color.RED);
TranslateTransition move = new TranslateTransition(Duration.seconds(2), c);
move.setAutoReverse(true);
move.setCycleCount(2);
move.setByX(10);
move.setByY(10);
move.currentRateProperty().addListener((obs, old, now) -> {
if (now.intValue() == -1) {
move.pause();
}
});
Button next = new Button("Next/Previous");
next.setOnAction(e -> {
move.play();
});
Pane p = new Pane(c);
p.setPrefSize(50, 50);
HBox buttons = new HBox(next);
VBox root = new VBox(p, buttons);
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
To reverse an animation in java, first you have to set the animation's autoReverse property to true and also set the cycleCount to 2.
Below is a simple code snippet i wrote earlier that makes use of the things stated above.
ScaleTransition scaleTransition = new ScaleTransition(duration, btn);
scaleTransition.setByX(1.2);
scaleTransition.setByY(1.2);
scaleTransition.setAutoReverse(true);
scaleTransition.setCycleCount(2);
scaleTransition.play();
Related
I'm developing a simple image editing functionality as a part of a larger JavaFX application, but I'm having some trouble to work out the undo/zoom and draw requirements together.
My requirements are the following:
The user should be able to:
Draw freehand on the image
Zoom in and out the image
Undo the changes
If the canvas is bigger than the window, it should have scroll-bars.
How I implemented these requirements:
The Drawing is done by starting a line when the mouse is pressed on the canvas, stroking it when it is dragged and closing the path when the button is released.
The Zoom works by scaling the canvas to a higher or lower value.
The Undo method takes a snapshot of the current state of the canvas when the mouse is pressed (before any change is made) and push it to a Stack of Images. When I need to undo some change I pop the last image of the Stack and draw it on the canvas, replacing the current image by the last one.
To have scroll-bars I just place the Canvas inside a Group and a ScrollPane.
Everything works fine, except when I try to draw on a scaled canvas. Due to the way I implemented the Undo functionality, I have to scale it back to 1, take a snapshot of the Node then scale it back to the size it was before. When this happens and the user is dragging the mouse the image position changes below the mouse pointer, causing it to draw a line that shouldn't be there.
Normal (unscaled canvas):
Bug (scaled canvas)
I tried the following approaches to solve the problem:
Don't re-scale to take the snapshot - Doesn't cause the unwanted line, but I end up with different image sizes in the stack, if it's smaller (zoomed out) when the snapshot was taken I now have a lower resolution of the image that I can't scale up without losing quality.
Tweak the logic and put the pushUndo call to the mouseReleased event - It almost worked, but when the user scrolled to a place and it's drawing there, the re-scaling causes the image to scroll back to the top-left;
Tried to search an way to "clone" or serialize the canvas and store the object state in the Stack - Didn't found anything I was able to adapt, and JavaFX doesn't support serialization of its objects.
I think the problem can be solved either by reworking the undo functionality as it doesn't need to re-scale the canvas to copy its state or by changing the way I zoom the canvas without scaling it, but I'm out of ideas on how to implement either of those options.
Below is the functional code example to reproduce the problem:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Stack;
public class Main extends Application {
Stack<Image> undoStack;
Canvas canvas;
double canvasScale;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
canvasScale = 1.0;
undoStack = new Stack<>();
BorderPane borderPane = new BorderPane();
HBox hbox = new HBox(4);
Button btnUndo = new Button("Undo");
btnUndo.setOnAction(actionEvent -> undo());
Button btnIncreaseZoom = new Button("Increase Zoom");
btnIncreaseZoom.setOnAction(actionEvent -> increaseZoom());
Button btnDecreaseZoom = new Button("Decrease Zoom");
btnDecreaseZoom.setOnAction(actionEvent -> decreaseZoom());
hbox.getChildren().addAll(btnUndo, btnIncreaseZoom, btnDecreaseZoom);
ScrollPane scrollPane = new ScrollPane();
Group group = new Group();
canvas = new Canvas();
canvas.setWidth(400);
canvas.setHeight(300);
group.getChildren().add(canvas);
scrollPane.setContent(group);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setLineWidth(2.0);
gc.setStroke(Color.RED);
canvas.setOnMousePressed(mouseEvent -> {
pushUndo();
gc.beginPath();
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
});
canvas.setOnMouseDragged(mouseEvent -> {
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
gc.stroke();
});
canvas.setOnMouseReleased(mouseEvent -> {
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
gc.stroke();
gc.closePath();
});
borderPane.setTop(hbox);
borderPane.setCenter(scrollPane);
Scene scene = new Scene(borderPane, 800, 600);
stage.setScene(scene);
stage.show();
}
private void increaseZoom() {
canvasScale += 0.1;
canvas.setScaleX(canvasScale);
canvas.setScaleY(canvasScale);
}
private void decreaseZoom () {
canvasScale -= 0.1;
canvas.setScaleX(canvasScale);
canvas.setScaleY(canvasScale);
}
private void pushUndo() {
// Restore the canvas scale to 1 so I can get the original scale image
canvas.setScaleX(1);
canvas.setScaleY(1);
// Get the image with the snapshot method and store it on the undo stack
Image snapshot = canvas.snapshot(null, null);
undoStack.push(snapshot);
// Set the canvas scale to the value it was before the method
canvas.setScaleX(canvasScale);
canvas.setScaleY(canvasScale);
}
private void undo() {
if (!undoStack.empty()) {
Image undoImage = undoStack.pop();
canvas.getGraphicsContext2D().drawImage(undoImage, 0, 0);
}
}
}
Consider drawing Shape objects, in this case Path objects, and apply scale to them:
import java.util.Stack;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
public class Main extends Application {
private Path path;
private Stack<Path> undoStack;
private Group group;
private double scale = 1;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
undoStack = new Stack<>();
Button btnUndo = new Button("Undo");
btnUndo.setOnAction(actionEvent -> undo());
Button btnIncreaseZoom = new Button("Increase Zoom");
btnIncreaseZoom.setOnAction(actionEvent -> increaseZoom());
Button btnDecreaseZoom = new Button("Decrease Zoom");
btnDecreaseZoom.setOnAction(actionEvent -> decreaseZoom());
HBox hbox = new HBox(4, btnUndo, btnIncreaseZoom, btnDecreaseZoom);
group = new Group();
BorderPane root = new BorderPane(new Pane(group), hbox, null,null, null);
Scene scene = new Scene(root, 300, 400);
root.setOnMousePressed(mouseEvent -> newPath(mouseEvent.getX(), mouseEvent.getY()));
root.setOnMouseDragged(mouseEvent -> addToPath(mouseEvent.getX(), mouseEvent.getY()));
primaryStage.setScene(scene);
primaryStage.show();
}
private void newPath(double x, double y) {
path = new Path();
path.setStrokeWidth(1);
path.setStroke(Color.BLACK);
path.getElements().add(new MoveTo(x,y));
group.getChildren().add(path);
undoStack.add(path);
}
private void addToPath(double x, double y) {
path.getElements().add(new LineTo(x, y));
}
private void increaseZoom() {
scale += 0.1;
reScale();
}
private void decreaseZoom () {
scale -= 0.1;
reScale();
}
private void reScale(){
for(Path path : undoStack){
path.setScaleX(scale);
path.setScaleY(scale);
}
}
private void undo() {
if(! undoStack.isEmpty()){
Node node = undoStack.pop();
group.getChildren().remove(node);
}
}
}
I solved the problem by extending the Canvas component and adding a second canvas in the extended class to act as a copy of the main canvas.
Every time I made a change in the canvas I do the same change in this "carbon" canvas. When I need to re-scale the canvas to get the snapshot (the root of my problem) I just re-scale the "carbon" canvas back to 1 and get my snapshot from it. This doesn't cause the drag of the mouse in the main canvas, as it remains scaled during this process. Probably this isn't the optimal solution, but it works.
Below is the code for reference, to anyone who may have a similar problem in the future.
ExtendedCanvas.java
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import java.util.Stack;
public class ExtendedCanvas extends Canvas {
private final double ZOOM_SCALE = 0.1;
private final double MAX_ZOOM_SCALE = 3.0;
private final double MIN_ZOOM_SCALE = 0.2;
private double currentScale;
private final Stack<Image> undoStack;
private final Stack<Image> redoStack;
private final Canvas carbonCanvas;
private final GraphicsContext gc;
private final GraphicsContext carbonGc;
public ExtendedCanvas(double width, double height){
super(width, height);
carbonCanvas = new Canvas(width, height);
undoStack = new Stack<>();
redoStack = new Stack<>();
currentScale = 1.0;
gc = this.getGraphicsContext2D();
carbonGc = carbonCanvas.getGraphicsContext2D();
setEventHandlers();
}
private void setEventHandlers() {
this.setOnMousePressed(mouseEvent -> {
pushUndo();
gc.beginPath();
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
carbonGc.beginPath();
carbonGc.lineTo(mouseEvent.getX(), mouseEvent.getY());
});
this.setOnMouseDragged(mouseEvent -> {
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
gc.stroke();
carbonGc.lineTo(mouseEvent.getX(), mouseEvent.getY());
carbonGc.stroke();
});
this.setOnMouseReleased(mouseEvent -> {
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
gc.stroke();
gc.closePath();
carbonGc.lineTo(mouseEvent.getX(), mouseEvent.getY());
carbonGc.stroke();
carbonGc.closePath();
});
}
public void zoomIn() {
if (currentScale < MAX_ZOOM_SCALE ) {
currentScale += ZOOM_SCALE;
setScale(currentScale);
}
}
public void zoomOut() {
if (currentScale > MIN_ZOOM_SCALE) {
currentScale -= ZOOM_SCALE;
setScale(currentScale);
}
}
public void zoomNormal() {
currentScale = 1.0;
setScale(currentScale);
}
private void setScale(double value) {
this.setScaleX(value);
this.setScaleY(value);
carbonCanvas.setScaleX(value);
carbonCanvas.setScaleY(value);
}
private void pushUndo() {
redoStack.clear();
undoStack.push(getSnapshot());
}
private Image getSnapshot(){
carbonCanvas.setScaleX(1);
carbonCanvas.setScaleY(1);
Image snapshot = carbonCanvas.snapshot(null, null);
carbonCanvas.setScaleX(currentScale);
carbonCanvas.setScaleY(currentScale);
return snapshot;
}
public void undo() {
if (hasUndo()) {
Image redo = getSnapshot();
redoStack.push(redo);
Image undoImage = undoStack.pop();
gc.drawImage(undoImage, 0, 0);
carbonGc.drawImage(undoImage, 0, 0);
}
}
public void redo() {
if (hasRedo()) {
Image undo = getSnapshot();
undoStack.push(undo);
Image redoImage = redoStack.pop();
gc.drawImage(redoImage, 0, 0);
carbonGc.drawImage(redoImage, 0, 0);
}
}
public boolean hasUndo() {
return !undoStack.isEmpty();
}
public boolean hasRedo() {
return !redoStack.isEmpty();
}
}
Main.java
package com.felipepaschoal;
import javafx.application.Application;
import javafx.scene.Group;
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.stage.Stage;
public class Main extends Application {
ExtendedCanvas extendedCanvas;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
BorderPane borderPane = new BorderPane();
HBox hbox = new HBox(4);
Button btnUndo = new Button("Undo");
btnUndo.setOnAction(actionEvent -> extendedCanvas.undo());
Button btnRedo = new Button("Redo");
btnRedo.setOnAction(actionEvent -> extendedCanvas.redo());
Button btnDecreaseZoom = new Button("-");
btnDecreaseZoom.setOnAction(actionEvent -> extendedCanvas.zoomOut());
Button btnResetZoom = new Button("Reset");
btnResetZoom.setOnAction(event -> extendedCanvas.zoomNormal());
Button btnIncreaseZoom = new Button("+");
btnIncreaseZoom.setOnAction(actionEvent -> extendedCanvas.zoomIn());
hbox.getChildren().addAll(
btnUndo,
btnRedo,
btnDecreaseZoom,
btnResetZoom,
btnIncreaseZoom
);
ScrollPane scrollPane = new ScrollPane();
Group group = new Group();
extendedCanvas = new ExtendedCanvas(300,200);
group.getChildren().add(extendedCanvas);
scrollPane.setContent(group);
borderPane.setTop(hbox);
borderPane.setCenter(scrollPane);
Scene scene = new Scene(borderPane, 600, 400);
stage.setScene(scene);
stage.show();
}
}
This program first displays a bullseye created by three different sized circles.
Once the animate me button is clicked, the function animation() will make the existing circles shrink inwards until the size of the circles is zero.
Once the user presses the button named "Press to stop", the animation will then stop. If the user presses the button again, it will then keep going from the state it was stopped from, so on so forth.
Currently, this is not working as intended. It only creates about 9 circles (including the nine circles that the program began with). I know I will need to use the action listener in order to make the program run, but I'm having a hard time in terms of the documentation of the action listener. What am I supposed to put in the parameters of the listener? If you see any other ways around this, please feel free to let me know.
package target;
import javafx.animation.ScaleTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Target extends Application
{
Circle[] cir = new Circle[7];
Button btn = new Button("Animate me!");
StackPane root = new StackPane();
public static void main(String[] args)
{
launch(args);
}
/**
* start method will create the target and the start button first
* displayed on-screen to the user
*/
#Override
public void start(Stage primaryStage)
{
root.setStyle("-fx-border-color:black;");
cir[0] = new Circle(400, 250, 200);
cir[0].setFill(Color.RED);
cir[0].setStyle("-fx-border-color:black;");
cir[1] = new Circle(315, 165, 115);
cir[1].setFill(Color.WHITE);
cir[1].setStyle("-fx-border-color:black;");
cir[2] = new Circle(230, 80, 30);
cir[2].setFill(Color.RED);
cir[2].setStyle("-fx-border-color:black;");
root.getChildren().addAll(cir[0], cir[1], cir[2]);
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root));
primaryStage.show();
btn.setOnAction(e ->
{
animation();
btn.setText("Press to Stop");
});
}
public void animation()
{
//Timeline animation = new Timeline(
//)
ScaleTransition[] st = new ScaleTransition[7];
boolean recycleCircles = false;
st[0]= new ScaleTransition(Duration.seconds(7), cir[0]);
st[0].setToX(0.0f);
st[0].setToY(0.0f);
st[0].play();
st[1] = new ScaleTransition(Duration.seconds(5.5), cir[1]);
st[1].setToX(0.0f);
st[1].setToY(0.0f);
st[1].play();
st[2] = new ScaleTransition(Duration.seconds(4), cir[2]);
st[2].setToX(0.0f);
st[2].setToY(0.0f);
st[2].play();
// int delayInc = 1;
int delay = 1;
//will create circles (will rotate between white and red) and then add
//to scaleTransitions
//while(btn.isPressed() == false)
{
for(int i = 3; i<st.length; i++)
{
if(recycleCircles == true)
{
i = 0;
recycleCircles = false;
}
if(i % 2 == 1)
{
cir[i] = new Circle(400,250,200);
cir[i].setFill(Color.WHITE);
cir[i].setStyle("-fx-border-color:black;");
root.getChildren().add(cir[i]);
cir[i].toBack();
st[i] = new ScaleTransition(Duration.seconds(7), cir[i]);
st[i].setDelay(Duration.seconds(delay));
delay++;
st[i].setToX(0.0f);
st[i].setToY(0.0f);
st[i].play();
}
else if(i%2==0)
{
cir[i] = new Circle(400, 250, 200);
cir[i].setFill(Color.RED);
cir[i].setStyle("-fx-border-color:black;");
root.getChildren().add(cir[i]);
cir[i].toBack();
st[i] = new ScaleTransition(Duration.seconds(7), cir[i]);
st[i].setDelay(Duration.seconds(delay));
delay++;
st[i].setToX(0.0f);
st[i].setToY(0.0f);
st[i].play();
}
if(i == 6)
recycleCircles = true;
}
}
//btn.pressedProperty().addListener(listener);
btn.setOnMousePressed(event ->
{
});
btn.setOnMouseReleased(event ->
{
for(int y = 0; y<st.length;y++)
{
}
});
}
}
Not sure whether you have any specific use case with each circle. If your are using the circles only for the purpose of alternating row colors, then you can get similar effect with radial gradient's repeat option.
To the extent I understand the question, below program is what I can think of. May be this can help you.
Just to let you know, the overall effect is slightly different from your program. The main difference in effects is, your program gives an effect/impression that each circle are shrinking towards center, as the distance between each circle is always same till it shrinked completely.
My program gives the effect/.impression like the entire board is moving away from your sight till it vanishes. In my program the distance between each circle decreases proportianally till it shrinks.
import javafx.animation.ScaleTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TargetAnimation extends Application {
Button btn = new Button("Animate me!");
StackPane root = new StackPane();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
root.setPrefSize(400, 400);
root.setStyle("-fx-border-color:black;");
Circle board = new Circle();
board.setRadius(200);
board.setStyle("-fx-fill:radial-gradient(focus-angle 0deg , focus-distance 0% , center 50% 50% , radius 21% , repeat, red 44% , white 46% );-fx-stroke-width:1px;-fx-stroke:black;");
root.getChildren().addAll(board, btn);
primaryStage.setScene(new Scene(root));
primaryStage.show();
ScaleTransition transition = new ScaleTransition(Duration.seconds(7), board);
transition.setToX(0);
transition.setToY(0);
btn.setOnAction(e -> {
switch (transition.getStatus()) {
case RUNNING:
transition.pause();
break;
case PAUSED:
transition.play();
break;
default:
board.setScaleX(1);
board.setScaleY(1);
transition.playFromStart();
}
});
}
}
The code given to setOnAction is an EventHandler, which is a #FunctionalInterface with the single method handle. That means that you can give it a lambda expression instead. The method takes an argument, which is the ActionEvent of clicking the button (created for you by JavaFX), and runs the code you give it.
If you want to pause the animation, call Animation#pause, and if you want to resume it, call Animation#play. I suggest that you create a ParallelTransition with all of your ScaleTransitions as its children. Then call the above methods on the ParallelTransition in the event handler.
That means that the setup code, like naming the button and creates the animations, goes outside of the event handler.
I've googled enough but still can find solution to get only single resize event when user releases left mouse button. For example the following solution from here
stage.titleProperty().bind(
scene.widthProperty().asString().
concat(" : ").
concat(scene.heightProperty().asString()));
When user clicks mouse left button and starts resizing the stage we will get very many events (using property listeners) while he does resizing. However, I want to get only one event - when the user completes resizing and releases mouse left button.
Another solution is here This solution significantly decreases amount of events but still doesn't let to get only one.
How to get only one resize event after user releases mouse button?
As far as I know, the mouse event handlers that resize the stage are managed natively, and so there is no way to access those purely in JavaFX - to do this the way you describe would require writing native libraries and hooking into them.
If you are doing some heavy computation (or other work that takes a long time) in response to the change in size of the stage, your best bet is probably to write code that only processes one change at a time, and just processes the last known change when it can.
An example of this is:
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class StageResizeThrottling extends Application {
private Random rng = new Random();
#Override
public void start(Stage primaryStage) {
BlockingQueue<Point2D> dimensionChangeQueue = new ArrayBlockingQueue<>(1);
ChangeListener<Number> dimensionChangeListener = (obs, oldValue, newValue) -> {
dimensionChangeQueue.clear();
dimensionChangeQueue.add(new Point2D(primaryStage.getWidth(), primaryStage.getHeight()));
};
primaryStage.widthProperty().addListener(dimensionChangeListener);
primaryStage.heightProperty().addListener(dimensionChangeListener);
Thread processDimensionChangeThread = new Thread(() -> {
try {
while (true) {
System.out.println("Waiting for change in size");
Point2D size = dimensionChangeQueue.take();
System.out.printf("Detected change in size to [%.1f, %.1f]: processing%n", size.getX(), size.getY());
process(size, primaryStage);
System.out.println("Done processing");
}
} catch (InterruptedException letThreadExit) { }
});
processDimensionChangeThread.setDaemon(true);
processDimensionChangeThread.start();
Scene scene = new Scene(new StackPane(), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void process(Point2D stageDimension, Stage stage) throws InterruptedException {
// simulate slow process:
Thread.sleep(500 + rng.nextInt(1000));
final String title = String.format("Width: %.0f Height: %.0f", stageDimension.getX(), stageDimension.getY());
Platform.runLater(() -> stage.setTitle(title));
}
public static void main(String[] args) {
launch(args);
}
}
Note that this will always process the very first change immediately, and then process the latest change when each previously-processed change has finished processing. If no further changes have occurred, it will wait until one does occur and then process it immediately. If you like, you can combine this with the timer-based technique you linked for coalescing the changes in the listener, which will typically remove the very first change that is processed (which is usually redundant as it is almost always followed by subsequent changes). The following changes will wait until no resizes have occurred for 300ms before submitting one to the queue for processing (the thread still behaves the same way - it will process the latest change, and when that processing is complete, wait for another one):
BlockingQueue<Point2D> dimensionChangeQueue = new ArrayBlockingQueue<>(1);
PauseTransition coalesceChanges = new PauseTransition(Duration.millis(300));
coalesceChanges.setOnFinished(e -> {
dimensionChangeQueue.clear();
dimensionChangeQueue.add(new Point2D(primaryStage.getWidth(), primaryStage.getHeight()));
});
ChangeListener<Number> dimensionChangeListener = (obs, oldValue, newValue) ->
coalesceChanges.playFromStart();
primaryStage.widthProperty().addListener(dimensionChangeListener);
primaryStage.heightProperty().addListener(dimensionChangeListener);
There's some tuning here, which is a tradeoff between latency and over-eagerness in processing changes. You probably want the pause transition to last something shorter than the average processing time of the change in screen size, but not an order of magnitude shorter.
The code guarantees that no more than one change will be processed at a time and that the latest change will eventually be processed if no more changes occur. This is probably about as good as you can get without accessing native user events. (And it would also handle programmatic changes in the stage size, which a mouse handler would not handle.)
I tried to create an example to achieve what you are looking for, I ended up with this, it is not perfect but when I tested it, it looked like it could help:
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class OneEventJavaFX extends Application{
double originalWidth = 400; // the initial width of Scene when the program starts
double originalHeight = 400; // the initial height of Scene when the program starts
// boolean property to be observed in order to know the completion of stage resize
BooleanProperty completedProperty = new SimpleBooleanProperty(false);
Timeline timeline;
#Override
public void start(Stage stage) throws Exception {
Pane root = new Pane(); // simple root as example just for test purpose
Scene scene = new Scene(root, 400,400);
stage.setScene(scene);
stage.setTitle("OneEventJavaFX");
stage.show();
// because I could not find a way to implement MouseEvent.MOUSE_RELEASED
// on the stage to notify the completion on resizing, I had to use a TimeLine
// the duration should consider the time the user usually take to finish every resize
// duration is tricky, Very Slow Resizing V.S Very Fast Resizing!
timeline = new Timeline(new KeyFrame(Duration.seconds(1), e ->{
System.out.println("Resizing Should Be Completed By Now!");
originalWidth = scene.getWidth(); // record the new scene size
originalHeight = scene.getHeight();
completedProperty.setValue(false);
}));
// change listener, to be added to and removed from the scene
ChangeListener<Number> changeListener= (observable, oldValue, newValue) ->{
System.out.println("I am Detecting an Event!"); // test
// once the size changed
if(originalWidth-scene.getWidth()>1 || scene.getWidth()-originalWidth>1 ||
originalHeight-scene.getHeight()>1 || scene.getHeight()-originalHeight>1){
completedProperty.set(true); // notify that completion should be considered
System.out.println("I Stopped! No More Events!");
timeline.play(); // and start counting the time
}};
// add the change listener when the program starts up
scene.widthProperty().addListener(changeListener);
scene.heightProperty().addListener(changeListener);
System.out.println("ChangeListener Added At Startup!");
// now listen to the change of the boolean property value
// instead of the size changes, it should NOT take a lot of work
// then accordingly add and remove change listener!
completedProperty.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean notComplete, Boolean complete) {
if (complete) {
scene.widthProperty().removeListener(changeListener);
scene.heightProperty().removeListener(changeListener);
System.out.println("ChangeListener Removed!");
}
else{
scene.widthProperty().addListener(changeListener);
scene.heightProperty().addListener(changeListener);
System.out.println("ChangeListener Added Back!");
}
}
});
}
public static void main(String[] args) {
launch();
}
}
Test While Resizing
ChangeListener Added At Startup!
I am Detecting an Event!
I am Detecting an Event!
ChangeListener Removed!
I Stopped! No More Events!
Resizing Should Be Completed By Now!
ChangeListener Added Back!
UPDATE:
I have been working on solving this question, I believe this approach can achieve what you want.
The idea is as follows:
Create UNDECORATED Stage and Make it Resizable.
Create a Title Bar and add it to the Stage.
Now the Mouse Events can be detected on the Border of the Stage (because basically it happens on the Scene).
Create Double Property for both the Width and Height of Stage and add Change Listener to listen to the Changes.
The changes in the Stage Width & Height will only be recorded at the beginning of the drag and when user RELEASES the Mouse.
Explanations in Comments.
The whole solution can be found here as an archive file (Why? Because I tried to post it here fully but the Body Limit is 30000 Character!) .
OneEventStage Class:
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/**
* This class customize a given Stage to record the changes
* of its size only when user starts and finishes resizing (recording one event)
* #author Yahya Almardeny
* #version 28/05/2017
*/
public class OneEventStage{
private double originalWidth; // the initial width of Scene when the program starts
private double originalHeight; // the initial height of Scene when the program starts
private TitleBar titleBar; // can be customized by the setter method (by default I made it for Windows 10 style)
private boolean started, alreadyFullScreen;
private DoubleProperty widthChange, heightChange; // record the changes in size
public Scene s;
public BorderPane scene; // this will be considered as a Scene when used in the program
public OneEventStage(Stage stage, double width, double height){
originalWidth = width; originalHeight = height;
widthChange = new SimpleDoubleProperty(originalWidth);
heightChange = new SimpleDoubleProperty(originalHeight);
started = false;
titleBar = new TitleBar("");
scene = new BorderPane();
scene.setTop(titleBar.getTitleBar());
s = new Scene(scene, originalWidth,originalHeight);
stage.initStyle(StageStyle.UNDECORATED);
stage.setScene(s);
ResizeHelper.addResizeListener(stage);
Task<Void> task = new Task<Void>(){
#Override
protected Void call() throws Exception {
Platform.runLater(new Runnable(){
#Override
public void run() {
// change listener, to be added to and removed from the scene
ChangeListener<Number> changeListener= (observable, oldValue, newValue) ->{
if(isFullScreen()){
widthChange.setValue(stage.getWidth());
heightChange.setValue(stage.getHeight());
alreadyFullScreen=true;
}
else if (alreadyFullScreen){ // coming from full screen mode
widthChange.setValue(Screen.getPrimary().getVisualBounds().getWidth());
heightChange.setValue(Screen.getPrimary().getVisualBounds().getHeight());
widthChange.setValue(originalWidth);
heightChange.setValue(originalHeight);
alreadyFullScreen = false;
}
else if(!alreadyFullScreen && !started){
started = true; // to inform the detecting Mouse Release Event is required
}
};
s.setOnMouseReleased(e->{
if(started){ // if this happens particularly after changing the size/dragging
originalWidth = stage.getWidth(); // record the new scene size
originalHeight = stage.getHeight();
widthChange.setValue(originalWidth); // add it
heightChange.setValue(originalHeight);
started = false;
}
});
// add the change listener when the program starts up
s.widthProperty().addListener(changeListener);
s.heightProperty().addListener(changeListener);
}
});
return null;
}};
new Thread(task).start();
}
/*
* to detected if user clicked on maximize button or double click on the title bar
*/
private boolean isFullScreen(){
return this.s.getWindow().getWidth()==Screen.getPrimary().getVisualBounds().getWidth() &&
this.s.getWindow().getHeight()==Screen.getPrimary().getVisualBounds().getHeight();
}
public DoubleProperty getWidthChange() {
return widthChange;
}
public DoubleProperty getHeightChange() {
return heightChange;
}
public TitleBar getTitleBar() {
return titleBar;
}
public void setTitleBar(TitleBar titleBar) {
this.titleBar = titleBar;
}
public void setTitle(String title){
titleBar.getTitle().setText(title);
}
}
OneEventStageTest Class:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* Implementing an Example of OneEventStage to test it
* #author Yahya Almardeny
* #version 28/05/2017
*/
public class OneEventStageTest extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
// create stage
OneEventStage stage = new OneEventStage(primaryStage, 400,400);
stage.setTitle("One Event Stage");
// simple containers and its components for testing purpose
VBox container = new VBox();
container.setAlignment(Pos.CENTER);
HBox widthInfoContainer = new HBox();
widthInfoContainer.setAlignment(Pos.CENTER);
Label widthChangeL = new Label("Width Changes");
TextField widthChangeV = new TextField();
widthChangeV.setEditable(false);
widthInfoContainer.getChildren().addAll(widthChangeL, widthChangeV);
HBox.setMargin(widthChangeL, new Insets(10));
HBox.setMargin(widthChangeV, new Insets(10));
HBox heightInfoContainer = new HBox();
heightInfoContainer.setAlignment(Pos.CENTER);
Label heightChangeL = new Label("Height Changes");
TextField heightChangeV = new TextField();
heightChangeV.setEditable(false);
heightInfoContainer.getChildren().addAll(heightChangeL, heightChangeV);
HBox.setMargin(heightChangeL, new Insets(10));
HBox.setMargin(heightChangeV, new Insets(10));
container.getChildren().addAll(widthInfoContainer, heightInfoContainer);
//////////////////////////////////////////////////////////////////////////
DoubleProperty widthChange = stage.getWidthChange();
DoubleProperty heightChange = stage.getHeightChange();
// listen to the changes (Testing)
widthChange.addListener((obs, old, newV)->{
Platform.runLater(new Runnable(){
#Override
public void run() {
widthChangeV.setText("From(" + old.doubleValue() + ") To(" + newV.doubleValue() + ")");
}
});
});
heightChange.addListener((obs, old, newV)->{
Platform.runLater(new Runnable(){
#Override
public void run() {
heightChangeV.setText("From(" + old.doubleValue() + ") To(" + newV.doubleValue() + ")");
}
});
});
//////////////////////////////////////////////////////////////////////////////////////
// represent a root but in fact it's inside the real root (BorderPane in the OneEventStage Class!).
StackPane root = new StackPane();
root.setAlignment(Pos.CENTER);
root.getChildren().add(container);
stage.scene.setCenter(root);
primaryStage.show();
}
public static void main(String[] args) {
launch();
}
}
TitleBar Class:
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.stage.Screen;
import javafx.stage.Stage;
/**
* This class to create a default/customized Title Bar
* to be added to Undecorated Stage in JavaFX Application
* #author Yahya Almardeny
* #version 27/05/2017
*/
public class TitleBar {
private HBox titleBar;
private ImageView icon;
private StackPane close, minimize, maximize; // represent customized components for the title bar (by using the second constructor)
private Image maximizeBefore, maximizeAfter; // for changing maximize icon when it's full screen
private Label title;
private double height, stageWidth, stageHeight, x,y, offsetX, offsetY;
private double screenWidth = Screen.getPrimary().getVisualBounds().getWidth(),
screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
private Color backgroundColor;
private StackPane maximizeButton; // for default title bar
private Label minimizeButton, closeButton; // for default title bar
private Stage stage;
private boolean intialized = false, fromMax = false;
public static enum Components {ICON,TITLE,MINIMIZE,MAXIMIZE,CLOSE;}
/**
* the default constructor, appearance of Windows 10
* #param title
*/
public TitleBar(String title){
titleBar = new HBox();
icon = new ImageView(new Image(TitleBar.class.getResourceAsStream("/icon/icon.png")));
icon.setFitWidth(15); this.icon.setFitHeight(13);
closeButton = new Label("×");
closeButton.setFont(Font.font("Times New Roman", 25));
closeButton.setPrefWidth(46);
closeButton.setAlignment(Pos.CENTER);
minimizeButton = new Label("—");
minimizeButton.setFont(Font.font(10));
minimizeButton.setPrefWidth(46);
minimizeButton.setPrefHeight(29);
minimizeButton.setAlignment(Pos.CENTER);
maximizeButton = maximiazeButton();
this.title = new Label(title);
final Pane space = new Pane();
HBox.setHgrow(space,Priority.ALWAYS);
titleBar.getChildren().addAll(this.icon, this.title,space,this.minimizeButton, this.maximizeButton, this.closeButton);
titleBar.setAlignment(Pos.CENTER_RIGHT);
HBox.setMargin(this.icon, new Insets(0,5,0,10)); // top,right, bottom, left
initalize(); // private method to get the Stage for first time
setDefaultControlsFunctionality(); // private method to add the default controls functionality
}
/**
* This is constructor to create a custom title bar
* #param icon
* #param minimize
* #param maximize
* #param close
* #param title
*/
public TitleBar(Image icon, Image minimize, Image maximizeBefore, Image maximizeAfter, Image close, String title){
titleBar = new HBox();
this.icon = new ImageView(icon);
this.icon.setFitWidth(15); this.icon.setFitHeight(14); // values can be changed via setters
this.close = new StackPane();
this.close.setPrefSize(25, 20);
this.close.getChildren().add(new ImageView(close));
((ImageView) this.close.getChildren().get(0)).setFitWidth(20);
((ImageView) this.close.getChildren().get(0)).setFitHeight(20);
this.minimize = new StackPane();
this.minimize.setPrefSize(25, 20);
this.minimize.getChildren().add(new ImageView(minimize));
((ImageView) this.minimize.getChildren().get(0)).setFitWidth(20);
((ImageView) this.minimize.getChildren().get(0)).setFitHeight(20);
this.maximizeBefore = maximizeBefore;
this.maximize = new StackPane();
this.maximize.setPrefSize(25, 20);
this.maximize.getChildren().add(new ImageView(maximizeBefore));
((ImageView) this.maximize.getChildren().get(0)).setFitWidth(20);
((ImageView) this.maximize.getChildren().get(0)).setFitHeight(20);
this.maximizeAfter = maximizeAfter;
this.title = new Label(title);
final Pane space = new Pane();
HBox.setHgrow(space,Priority.ALWAYS);
titleBar.getChildren().addAll(this.icon, this.title,space,this.minimize, this.maximize, this.close);
titleBar.setAlignment(Pos.CENTER_RIGHT);
HBox.setMargin(this.icon, new Insets(0,5,0,10)); // top,right, bottom, left
HBox.setMargin(this.close, new Insets(0,5,0,0));
initalize();
setCustomizedControlsFunctionality();
}
/**
* create the default maximize button
* #return container
*/
private StackPane maximiazeButton(){
StackPane container = new StackPane();
Rectangle rect = new Rectangle(8,8);
rect.setFill(Color.TRANSPARENT);
rect.setStroke(Color.BLACK);
container.setPrefWidth(46);
container.getChildren().add(rect);
return container;
}
/**
* To get the Stage of the application for one time only
* as well as adding listener to iconifiedProperty()
*/
private void initalize(){
titleBar.setOnMouseEntered(e->{ // the entire block will be executed only once
if(!intialized){
// get the stage and assign it to the Stage field
stage = ((Stage)titleBar.getScene().getWindow());
// add listener toiconifiedProperty()
stage.iconifiedProperty().addListener(ee->{
if(!stage.isIconified()){
stage.setMaximized(true);
if(fromMax){ // if already maximized
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.setX(0);
stage.setY(0);
}
else{
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
}
try { // to remove the flash
Thread.sleep(10);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
stage.setOpacity(1.0);
}
});
intialized=true;
}
});
}
/**
* To add functionality to title bar controls
* via event listeners
*/
private void setDefaultControlsFunctionality(){
// Double-Click on Title Bar
titleBar.setOnMouseClicked(e->{
if(e.getClickCount()==2){
maximizefunctonality();
}
});
//Maximize Control
maximizeButton.setOnMouseEntered(e->{// highlight when hover
maximizeButton.setBackground(
new Background(new BackgroundFill(Color.LIGHTGRAY,null,null)));
((Rectangle)maximizeButton.getChildren().get(0)).setFill(Color.LIGHTGRAY);
if(maximizeButton.getChildren().size()==2){
((Rectangle)maximizeButton.getChildren().get(1)).setFill(Color.LIGHTGRAY);
}
});
maximizeButton.setOnMouseExited(e->{ // remove highlight
maximizeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
((Rectangle)maximizeButton.getChildren().get(0)).setFill(Color.TRANSPARENT);
if(maximizeButton.getChildren().size()==2){
((Rectangle)maximizeButton.getChildren().get(1)).setFill(Color.WHITE);
}
});
maximizeButton.setOnMouseClicked(e->{
maximizefunctonality();
});
//Close Control
closeButton.setOnMouseEntered(e->{
closeButton.setBackground(
new Background(new BackgroundFill(Color.CRIMSON,null,null)));
closeButton.setTextFill(Color.WHITE);
});
closeButton.setOnMouseExited(e->{
closeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
closeButton.setTextFill(Color.BLACK);
});
closeButton.setOnMouseClicked(e->{
stage.close();
});
//Minimize Control
minimizeButton.setOnMouseEntered(e->{
minimizeButton.setBackground(
new Background(new BackgroundFill(Color.LIGHTGRAY,null,null)));
});
minimizeButton.setOnMouseExited(e->{
minimizeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
});
minimizeButton.setOnMouseClicked(e->{
if(!stage.isIconified()){ // if it's not minimized
if(fromMax){ // check if it's already full screen(maximized)
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
else{ // if it's not -> record the size and position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
}
});
// to make title bar movable
titleBar.setOnMousePressed(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
offsetX = e.getScreenX() - stage.getX();
offsetY = e.getScreenY() - stage.getY();
}
});
titleBar.setOnMouseDragged(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
stage.setX(e.getScreenX() - offsetX);
stage.setY(e.getScreenY() - offsetY);
}
});
}
private void maximizefunctonality(){
Rectangle rect = (Rectangle) maximizeButton.getChildren().get(0);
if(stage.getWidth()<screenWidth||stage.getHeight()<screenHeight){
// get the previous size + position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
// maximize it
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.centerOnScreen();
// change the maximize button appearance
rect.setTranslateX(2);
rect.setTranslateY(-2);
Rectangle rect1 = new Rectangle(8,8);
rect1.setFill(Color.WHITE);
rect1.setStroke(Color.BLACK);
maximizeButton.getChildren().add(rect1);
fromMax = true;
}
else{ // if already maximized -> return to previous size + position
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
fromMax = false;
// change the maximize button appearance
rect.setTranslateX(0);
rect.setTranslateY(0);
maximizeButton.getChildren().remove(1);
}
}
private void setCustomizedControlsFunctionality(){
//Maximize Control
maximize.setOnMouseClicked(e->{
if(stage.getWidth()<screenWidth||stage.getHeight()<screenHeight){
// get the previous size + position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
// maximize it
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.centerOnScreen();
// change the maximize button appearance
((ImageView) maximize.getChildren().get(0)).setImage(maximizeAfter);
fromMax = true;
}
else{ // if already maximized -> return to previous size + position
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
fromMax = false;
// change the maximize button appearance
((ImageView) maximize.getChildren().get(0)).setImage(maximizeBefore);
}
});
close.setOnMouseClicked(e->{
stage.close();
});
//Minimize Control
minimize.setOnMouseClicked(e->{
if(!stage.isIconified()){ // if it's not minimized
if(fromMax){ // check if it's already full screen(maximized)
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
else{ // if it's not -> record the size and position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
}
});
// to make title bar movable
titleBar.setOnMousePressed(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
offsetX = e.getScreenX() - stage.getX();
offsetY = e.getScreenY() - stage.getY();
}
});
titleBar.setOnMouseDragged(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
stage.setX(e.getScreenX() - offsetX);
stage.setY(e.getScreenY() - offsetY);
}
});
}
/**
* To change margins/insets to the Title Bar components
* #param component
* #param top
* #param right
* #param bottom
* #param left
*/
public void setInsets(Components component, double top, double right, double bottom, double left){
switch(component){
case TITLE:
HBox.setMargin(title, new Insets(top, right, bottom ,left));
break;
case ICON:
HBox.setMargin(icon, new Insets(top, right, bottom ,left));
break;
case CLOSE:
HBox.setMargin(close, new Insets(top, right, bottom ,left));
break;
case MAXIMIZE:
HBox.setMargin(maximize, new Insets(top, right, bottom ,left));
break;
case MINIMIZE:
HBox.setMargin(minimize, new Insets(top, right, bottom ,left));
break;
}
}
public void setControlsSpace(Components component, double width, double height){
switch(component){
case CLOSE:
close.setPrefSize(width, height);
break;
case MAXIMIZE:
maximize.setPrefSize(width, height);
break;
case MINIMIZE:
minimize.setPrefSize(width, height);
break;
case TITLE:
//do nothing
break;
case ICON:
// do nothing
break;
}
}
public void addHoverEffect(Components component, Color defaultColor, Color onHover, Cursor cursor){
}
//reset of the class
{...}
}
ResizeHelper Class:
{....}
Test
I have a Pane in which i add and remove nodes during a computation. Therefor i save a boolean which is set to true if the computation is running. of course i do some handling on starting and terminating a computation.
What i want to do now is: disable all MouseEvents on the children of the Pane if the computation starts and reenable them if the computation is terminated.
My tries until now where limited to completly remove the EventHandlers, but then i can't add them again later.
unfortunately i couldn't find a way to do this, so i hope for help here :)
Thanks in advance
Assuming you have implemented the long-running computation as a Task or Service (and if you haven't, you should probably consider doing so), you can just do something along the following lines:
Pane pane ;
// ...
Task<ResultType> computation = ... ;
pane.disableProperty().bind(computation.runningProperty());
new Thread(computation).start();
Calling setDisable(true) on a node will disable all its child nodes, so this will disable all the children of the pane, and re-enable them when the task is no longer running.
Here's an SSCCE:
import javafx.application.Application;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
public class ComputationSimulation extends Application {
#Override
public void start(Stage primaryStage) {
// text fields for input:
TextField xInput = new TextField();
TextField yInput = new TextField();
// Service for performing the computation.
// (For demo here, the computation just computes the sum of
// the two input values. Obviously this doesn't take long, so
// a random pause is inserted.)
Service<Integer> service = new Service<Integer>() {
#Override
protected Task<Integer> createTask() {
final int x = readTextField(xInput);
final int y = readTextField(yInput);
return new Task<Integer>() {
#Override
public Integer call() throws Exception {
// simulate long-running computation...
Thread.sleep((int)(Math.random() * 2000) + 1000);
// this doesn't really take much time(!):
return x + y ;
}
};
}
};
// Label to show result. Just use binding to bind to value of computation:
Label result = new Label();
result.textProperty().bind(service.valueProperty().asString());
// Button starts computation by restarting service:
Button compute = new Button("Compute");
compute.setOnAction(e -> service.restart());
// Pane to hold controls:
GridPane pane = new GridPane();
// Disable pane (and consequently all its children) when computation is running:
pane.disableProperty().bind(service.runningProperty());
// layout etc:
pane.setHgap(5);
pane.setVgap(10);
pane.addRow(0, new Label("x:"), xInput);
pane.addRow(1, new Label("y:"), yInput);
pane.addRow(2, new Label("Total:"), result);
pane.add(compute, 1, 3);
ColumnConstraints left = new ColumnConstraints();
left.setHalignment(HPos.RIGHT);
left.setHgrow(Priority.NEVER);
pane.getColumnConstraints().addAll(left, new ColumnConstraints());
pane.setPadding(new Insets(10));
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
// converts text in text field to an int if possible
// returns 0 if not valid text, and sets text accordingly
private int readTextField(TextField text) {
try {
return Integer.parseInt(text.getText());
} catch (NumberFormatException e) {
text.setText("0");
return 0 ;
}
}
public static void main(String[] args) {
launch(args);
}
}
I hope I'm not duplicating a question, but I couldn't find one specifically for my issue.
I'm developing a small math flash card application, using JavaFX to create the GUI. The program should runs as follow:
user selects settings, then presses start button.
gui displays question and textfield for user input.
user inputs answer within X amount of seconds or gui automatically move onto the next question - alternatively, user can move onto next question immediately by pressing next button.
GUI displays score and average.
The problems is getText() from user textfield is processed as soon as start button is pressed, without giving the user a chance to enter an answer. How do I make the program wait for X amount of seconds or for the next button to be clicked before processing the user's answer? Here's my code:
//start button changes view and then runs startTest()
start.setOnAction(e -> {
setLeft(null);
setRight(null);
setCenter(test_container);
running_program_title.setText(getDifficulty().name() + " Test");
buttons_container.getChildren().clear();
buttons_container.getChildren().addAll(next, quit, submit);
startTest();
});
Here is the problem code... at least how I see it.
//startTest method calls askAdd() to ask an addition question
void startTest() {
int asked = 0;
int correct = 0;
while (asked < numberOfQuestions) {
if(askAdd()){
correct++;
asked++;
}
}
boolean askAdd() {
int a = (int) (Math.random() * getMultiplier());
int b = (int) (Math.random() * getMultiplier());
//ask question
question.setText("What is " + a + " + " + b + "?");
//code needed to pause method and wait for user input for X seconds
//retrieve user answer and return if its correct
return answer.getText().equalsIgnoreCase(String.valueOf(a+b));
}
I've tried using Thread.sleep(X) but that freezes the gui for however long I specify and then goes through the addAsk() method and the loop before going to the test screen. (I know because I had the program set up to print the questions and answer input to the console). It shows the last question and that's all.
I didn't include the next button code because I can't get the gui to go to the test page anyway.
Any help on any of the code is appreciated.
This can be achieved by various methods.
PauseTransition is one of the many apt solution present. It waits for X time interval and then performs a Task. It can start, restart, stop at any moment.
Here is an example of how it can used to achieve a similar result.
Complete Code
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.stream.IntStream;
public class Main extends Application {
int questionIndex = 0;
int noOfQuestions = 10;
#Override
public void start(Stage stage) {
VBox box = new VBox(10);
box.setPadding(new Insets(10));
Scene scene = new Scene(new ScrollPane(box), 500, 200);
ObservableList<String> questions =
FXCollections.observableArrayList("1) Whats your (full) name?",
"2) How old are you?",
"3) Whats your Birthday?",
"4) What starsign does that make it?",
"5) Whats your favourite colour?",
"6) Whats your lucky number?",
"7) Do you have any pets?",
"8) Where are you from?",
"9) How tall are you?",
"10) What shoe size are you?");
ObservableList<String> answers = FXCollections.observableArrayList();
final PauseTransition pt = new PauseTransition(Duration.millis(5000));
Label questionLabel = new Label(questions.get(questionIndex));
Label timerLabel = new Label("Time Remaining : ");
Label time = new Label();
time.setStyle("-fx-text-fill: RED");
TextField answerField = new TextField();
Button nextQuestion = new Button("Next");
pt.currentTimeProperty().addListener(new ChangeListener<Duration>() {
#Override
public void changed(ObservableValue<? extends Duration> observable, Duration oldValue, Duration newValue) {
time.setText(String.valueOf(5 - (int)newValue.toSeconds()));
}
});
box.getChildren().addAll(questionLabel, answerField, new HBox(timerLabel, time), nextQuestion);
nextQuestion.setOnAction( (ActionEvent event) -> {
answers.add(questionIndex, answerField.getText());
//Check if it is the last question
if(questionIndex == noOfQuestions-1) {
pt.stop();
box.getChildren().clear();
IntStream.range(0, noOfQuestions).forEach(i -> {
Label question = new Label("Question : " + questions.get(i));
question.setStyle("-fx-text-fill: RED");
Label answer = new Label("Answer : " + answers.get(i));
answer.setStyle("-fx-text-fill: GREEN");
box.getChildren().addAll(question, answer);
});
}
// All other time
else {
//Set new question
questionLabel.setText(questions.get(++questionIndex));
answerField.clear();
pt.playFromStart();
}
});
pt.setOnFinished( ( ActionEvent event ) -> {
nextQuestion.fire();
});
pt.play();
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
For the timer you should (IMO) use a Timeline. Here is an example:
public class MultiGame extends Application {
ProgressBar progressBar;
final int allowedTime = 5; //seconds
final DoubleProperty percentOfTimeUsed = new SimpleDoubleProperty(0);
final Timeline timer =
new Timeline(
new KeyFrame(
Duration.ZERO, new KeyValue(percentOfTimeUsed, 0)),
new KeyFrame(
Duration.seconds(allowedTime), new KeyValue(percentOfTimeUsed, 1))
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
progressBar = new ProgressBar();
progressBar.progressProperty().bindBidirectional(percentOfTimeUsed);
root.setTop(progressBar);
Button answer = new Button("Answer");
answer.setOnAction(ae -> restart());// the on answer handler
Button skip = new Button("Skip");
skip.setOnAction(ae -> restart());// the skip question handler
HBox mainContent = new HBox(15,
new Label("Your Question"), new TextField("The answer"), answer, skip);
root.setCenter(mainContent);
timer.setOnFinished(ae -> restart());// the end of timer handler
primaryStage.setScene(new Scene(root));
primaryStage.show();
restart();
}
void restart() { timer.stop(); timer.playFromStart(); }
void pause() { timer.pause(); }
void resume() { timer.play(); }
}
You just need to capture the text from the input in between the starting of the timeline and the restart method.