JavaFX SequentialTransition IllegalStateException: Cannot stop when embedded in another animation - javafx

The test program below reproduces the problem. I understand why the exception is thrown but I would like to know how can I work around it or use a different construct in JavaFX to get what I want.
The full application is a robot simulator with multiple robots that move autonomously, independently, and simultaneously around a field. Each robot has its own SequentialTransition for its particular set of movements. The program adds the SequentialTransitions to a ParallelTransition, which it then plays. Everything was fine until I put in a listener that notices if a robot runs into an obstacle. I've simplified the collision detection in the test program to apply to only one robot and one wall. The point of the error is marked with //** BROKEN!! IllegalStateException on next line.
I really do want to stop the SequentialTransition for a robot that runs into an obstacle but let the other robot(s) continue. How can I do this?
The error comes up in Java 8 but also in Java 11 and JavaFX 15.
package sample;
import javafx.animation.ParallelTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
private static final double FIELD_WIDTH = 600;
private static final double FIELD_HEIGHT = 600;
private Pane field = new Pane();
ParallelTransition parallel = new ParallelTransition();
SequentialTransition sequentialRobot1 = new SequentialTransition();
SequentialTransition sequentialRobot2 = new SequentialTransition();
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
// Place one field boundary for testing.
Line northBoundary = new Line(0, 0, FIELD_WIDTH, 0);
northBoundary.setStrokeWidth(5.0);
field.getChildren().add(northBoundary);
// Place the robots on the field.
// The first robot.
Rectangle robotBody1 = new Rectangle(100, 300, 60, 60);
robotBody1.setArcHeight(15);
robotBody1.setArcWidth(15);
robotBody1.setStroke(Color.BLACK);
robotBody1.setFill(Color.CRIMSON);
field.getChildren().add(robotBody1);
robotBody1.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
if (northBoundary.getBoundsInParent().intersects(robotBody1.getBoundsInParent())) {
//** BROKEN!! IllegalStateException on next line
sequentialRobot1.stop();
System.out.println("Collision detected");
parallel.play();
}
});
TranslateTransition translateTransition1 = new TranslateTransition();
translateTransition1.setNode(robotBody1);
translateTransition1.setByX(0);
translateTransition1.setByY(-300);
translateTransition1.setDuration(Duration.seconds(1));
translateTransition1.setOnFinished(event -> {
robotBody1.setLayoutX(robotBody1.getLayoutX() + robotBody1.getTranslateX());
robotBody1.setLayoutY(robotBody1.getLayoutY() + robotBody1.getTranslateY());
robotBody1.setTranslateX(0);
robotBody1.setTranslateY(0);
});
sequentialRobot1.getChildren().add(translateTransition1);
// The second robot.
Rectangle robotBody2 = new Rectangle(300, 300, 60, 60);
robotBody2.setArcHeight(15);
robotBody2.setArcWidth(15);
robotBody2.setStroke(Color.BLACK);
robotBody2.setFill(Color.CYAN);
field.getChildren().add(robotBody2);
TranslateTransition translateTransition2 = new TranslateTransition();
translateTransition2.setNode(robotBody2);
translateTransition2.setByX(0);
translateTransition2.setByY(-100);
translateTransition2.setDuration(Duration.seconds(1));
translateTransition2.setOnFinished(event -> {
robotBody2.setLayoutX(robotBody2.getLayoutX() + robotBody2.getTranslateX());
robotBody2.setLayoutY(robotBody2.getLayoutY() + robotBody2.getTranslateY());
robotBody2.setTranslateX(0);
robotBody2.setTranslateY(0);
});
sequentialRobot2.getChildren().add(translateTransition2);
parallel.getChildren().addAll(sequentialRobot1, sequentialRobot2);
parallel.play();
primaryStage.setTitle("Field");
primaryStage.setScene(new Scene(field, FIELD_WIDTH, FIELD_HEIGHT, Color.GRAY));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

#Slaw's idea of using an AnimationTimer is probably the best direction to go, but the obvious answer is to not use the ParallelTransition at all. Since the robot animations are going to be independent, just use the SequentialTransitions and start them all at the same time by calling play() on each.

Related

JavaFX Alert with multiple colors

I have a program that at some point (may) displays two warnings - one about errors - those are in red, and one about warnings - those are in orange.
I wonder however if there is a way - using css - to have just one warning with some text red and some text orange.
Here is an example of what I want to achieve (the two can be separated into "sections"):
RED ERROR1
RED ERROR2
RED ERROR3
ORANGE WARNING1
ORANGE WARNING2
I've seen some answers pointing to RichTextFX like this one, however I don't see (or don't know) how that could apply to generic Alerts. Is that even possible, without writing some custom ExpandedAlert class?
The Alert class inherits from Dialog, which provides a pretty rich API and allows arbitrarily complex scene graphs to be set via the content property.
If you just want static text with different colors, the simplest approach is probably to add labels to a VBox; though you could also use more complex structures such as TextFlow or the third-party RichTextFX mentioned in the question if you need.
A simple example is:
import java.util.Random;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class App extends Application {
private final Random rng = new Random();
private void showErrorAlert(Stage stage) {
Alert alert = new Alert(Alert.AlertType.ERROR);
int numErrors = 2 + rng.nextInt(3);
int numWarnings = 2 + rng.nextInt(3);
VBox errorList = new VBox();
for (int i = 1 ; i <= numErrors ; i++) {
Label label = new Label("Error "+i);
label.setStyle("-fx-text-fill: red; ");
errorList.getChildren().add(label);
}
for (int i = 1 ; i <= numWarnings ; i++) {
Label label = new Label("Warning "+i);
label.setStyle("-fx-text-fill: orange; ");
errorList.getChildren().add(label);
}
alert.getDialogPane().setContent(errorList);
alert.initOwner(stage);
alert.show();
}
#Override
public void start(Stage stage) {
Button showErrors = new Button("Show Errors");
showErrors.setOnAction(e -> showErrorAlert(stage));
BorderPane root = new BorderPane(showErrors);
Scene scene = new Scene(root, 400, 400);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
which gives this result:

JavaFX Slider : How to drag the thumb only by increments

I am trying to implement the Slider such that user can drag only by given increments. I tried in different ways by using the Slider API, but didnt get the desired results. Below is a quick demo of what I had tried. I am expecting to drag the thumb only in increments of 10 not with intermediate values. snapToTicks is doing what I required, but only after finishing the drag. I am trying to not move the thumb till the next desired block increment is reached.
Can anyone let me know how can i achieve this. Below is the screenshot while dragging.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class SliderDemo extends Application {
public static void main(String... args){
Application.launch(args);
}
#Override
public void start(Stage stage) throws Exception {
Label label = new Label();
label.setStyle("-fx-font-size:30px");
Slider slider = new Slider(5,240,5);
slider.setBlockIncrement(10);
slider.setMajorTickUnit(10);
slider.setMinorTickCount(0);
slider.setShowTickLabels(true);
slider.setShowTickMarks(true);
slider.setSnapToTicks(true);
slider.valueProperty().addListener((obs,old,val)->label.setText((int)Math.round(val.doubleValue())+""));
VBox root = new VBox(slider,label);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(20));
root.setSpacing(20);
Scene scene = new Scene(root,600,200);
stage.setScene(scene);
stage.show();
}
}
The solution is to set the value of the slider directly inside of the listener. The listener will not be called again
final ChangeListener<Number> numberChangeListener = (obs, old, val) -> {
final double roundedValue = Math.floor(val.doubleValue() / 10.0) * 10.0;
slider.valueProperty().set(roundedValue);
label.setText(Double.toString(roundedValue));
};
slider.valueProperty().addListener(numberChangeListener);
If you use Math.floor() instead of round you get a more intuatuive behavior of the thumb.

Adding value from textfield

I a m creating a java application and I have a problem.
Here is the code.
package javastackoverflow;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class Javastackoverflow extends Application {
TextField deduct2;
Label text;
double ammount = 0.0;
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
btn.setText("Apply");
text = new Label(Double.toString(ammount));
btn.setOnAction((e->{
double getamount = Double.parseDouble(deduct2.getText());
text.setText(Double.toString(getamount)+ ammount);
//this is where the program is suppose to get the amount and add it to amount, notice the + sign.
}))
;
deduct2 = new TextField();
FlowPane root = new FlowPane();
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(btn,deduct2,text);
Scene scene = new Scene(root, 400, 450);
primaryStage.setTitle("Yo Stack");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
What it is suppose to do
When the user enters a number into the textfield the program is suppose to
to take that number and add it to ammount = 0;
My problem
But when ever the using enters a new number the text changes to That number. Remember I would like it to add to the currant number like (currant number = 23; user enters new Number) new number = 3; I would like results to equal = 26 but right now the program shows the result 3;
What I think
I think the problem is in the onAction() method.
I think that the text.setText() method is displaying the currant text entered into the textfield, rather then adding it to ammount.
I alse don't think I am using the correct operator from this line of code. That may be part of the problem.
text.setText(Double.toString(getamount)+ ammount);
notice how I use the + sign, that + adds the getamount to ammount..or its supposed to. But when I change that plus sign to - or * I get this error
===============================
bad operand types for binary operator '*'
first type: String
second type: TextField
===============================
As you can probably tell I really would like this code to be correct so if you don't understand something please comment before you report me. Then I can change it fast. Thank you!
You are adding the amount to the getAmount variable when amount = 0.0
Try adding the new value to getAmount
package javastackoverflow;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class Javastackoverflow extends Application {
TextField deduct2;
Label text;
double getamount = 0.0; //Edit 1
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
btn.setText("Apply");
text = new Label(Double.toString(ammount));
btn.setOnAction((e->{
getamount += Double.parseDouble(deduct2.getText()); //Edit 2
text.setText(Double.toString(getamount));
//this is where the program is suppose to get the amount and add it to amount, notice the + sign.
}))
;
deduct2 = new TextField();
FlowPane root = new FlowPane();
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(btn,deduct2,text);
Scene scene = new Scene(root, 400, 450);
primaryStage.setTitle("Yo Stack");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}

javafx 8 listview first and last row

Is there a way to determine the first and last visible row of a listview? In other words I'm looking for two indexes into an array that populates a listview which represent the top and the bottom row of the 'display window'.
You could get the VirtualFlow of the ListView which has methods for getting the first and last rows.
Example:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import com.sun.javafx.scene.control.skin.VirtualFlow;
public class ListViewSample extends Application {
#Override
public void start(Stage stage) {
VBox box = new VBox();
ListView<Integer> list = new ListView<>();
ObservableList<Integer> items = FXCollections.observableArrayList();
for( int i=0; i < 100; i++) {
items.add(i);
}
list.setItems(items);
box.getChildren().add(list);
VBox.setVgrow(list, Priority.ALWAYS);
Scene scene = new Scene(box, 200, 200);
stage.setScene(scene);
stage.show();
VirtualFlow flow = (VirtualFlow) list.lookup( ".virtual-flow");
flow.addEventFilter(Event.ANY, event -> {
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println( list.getItems().get( first.getIndex()) + " - " + list.getItems().get( last.getIndex()) );
});
}
public static void main(String[] args) {
launch(args);
}
}
You see the fully visible first and last items in the console.
ps: I leave the no data check and event handling to you
Alternate version without css lookup:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import com.sun.javafx.scene.control.skin.VirtualFlow;
public class ListViewSample extends Application {
ListView<String> list = new ListView<String>();
#Override
public void start(Stage stage) {
VBox box = new VBox();
ListView<Integer> list = new ListView<>();
ObservableList<Integer> items = FXCollections.observableArrayList();
for( int i=0; i < 100; i++) {
items.add(i);
}
list.setItems(items);
box.getChildren().add(list);
VBox.setVgrow(list, Priority.ALWAYS);
Scene scene = new Scene(box, 200, 200);
stage.setScene(scene);
stage.show();
VirtualFlow virtualFlow = null;
for( Node node: list.getChildrenUnmodifiable()) {
if( node instanceof VirtualFlow) {
virtualFlow = (VirtualFlow) node;
}
}
final VirtualFlow flow = virtualFlow;
flow.addEventFilter(Event.ANY, event -> {
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println( list.getItems().get( first.getIndex()) + " - " + list.getItems().get( last.getIndex()) );
});
}
public static void main(String[] args) {
launch(args);
}
}
UPDATE
VirtualFlow is available only after the ListView has been rendered, because it uses Layout parameters which are not available until after the ListView is visible on the stage. So I had to make sure that I got the VirtualFlow when it was certain that the ListView had been rendered. Since I was manipulating the list with various methods I call this method at the end of each method:
private VirtualFlow flow;
private void updateListView(int centreIndex) {
if (flow == null)
flow = (VirtualFlow) myListView.lookup(".virtual-flow");
if (flow != null){
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println(first.getIndex() + " - " + last.getIndex());
}
// Now the list can be selectively 'redrawn' using the scollTo() method,
// and using the .getSelectionModel().select(centreIndex) to set the
// desired cell
}
It's bit of a hack, but it works. Using layout parameters does have a drawback though that needs to be considered. If the height of the ListView is only 1 pixel less than the total height of all rows, n number of rows will be visible, but the flow will report n-1 rows which will appear to be a discrepancy at first. Hence keeping a fixed layout height is imperative. At least now by using scrollTo(..) I have control over the position of the selected item in the list (I want to keep it centred in the list display when an item is dragged through the list). This solution leaves me feeling uneasy, but it seems to be the only 'simple' way.
Just a note on the odd-looking logic. It seems that getting the flow takes time, while the program keeps executing. The second (flow != null) is necessary to avoid a NullPointerException.
UPDATE 2
My hack turns out not to work. The whole hack is dependent on timing. Rendering is done on a different thread and as soon as I changed the order of instantiation of classes in my app, I got a NullPointerException again. I turned to the Java doc:
"JavaFX is not thread safe and all JavaFX manipulation should be run on the JavaFX processing thread. If you allow a JavaFX application to interact with a thread other than the main processing thread, unpredictable errors will occur"
And they do! So forget the above - it does not work and will make you scratch your head (and more!) trying to debug it ;-)

Make JavaFX wait and continue with code

Basically I am trying to make a short effect using JavaFX. I have the shape of a heart (added together from two circles and a polygon) that I can vary in size using the double value p. "Standart Size" would be p = 1.0;.
I am trying to add a pumping effect to the heart. I have the method pumpOnce():
public void pumpOnce(){
p = p + 1;
initHeart();
//Here goes what ever it takes to make stuff working!!
p = p - 1;
initHeart();
}
initHeart() draws the heart based on p.
I have found out that Thread.sleep(); or similar methods will not work due to the thread philosophy in JavaFX.
But what can I use instead?
The JavaFX animations are probably the way to go, but the "thread philosophy" in JavaFX isn't hard to work with if you want to roll your own, or do other, more complicated things in background threads.
The following code will pause and change the value in a label (full disclosure, I'm reusing code I wrote for another question):
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javax.xml.datatype.Duration;
public class DelayWithTask extends Application {
private static Label label;
public static void main(String[] args) { launch(args); }
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
label = new Label();
label.setText("Waiting...");
StackPane root = new StackPane();
root.getChildren().add(label);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
delay(5000, () -> label.setText("Hello World"));
}
public static void delay(long millis, Runnable continuation) {
Task<Void> sleeper = new Task<Void>() {
#Override
protected Void call() throws Exception {
try { Thread.sleep(millis); }
catch (InterruptedException e) { }
return null;
}
};
sleeper.setOnSucceeded(event -> continuation.run());
new Thread(sleeper).start();
}
}
The basic JavaFX background tool is the Task, any JavaFX application that actually does anything will probably be littered with these all over. Learn how to use them.
Dave's solution is great for general purpose off thread based work in JavaFX.
If you wish to use the animation facilities of JavaFX, the solutions below demonstrate this using a Timeline or a ScaleTransition. The timeline implements a discrete scale of the UI element, so every quarter of a second the UI element is scaled larger or back to it's original size. The scale transition implements a smooth scale of the UI element, so the UI element gradually gets larger then smaller using an interpolated scale factor with the default easing interpolator.
import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class BeatingHeart extends Application {
public static void main(String[] args) {
launch(args);
}
public void start(Stage stage) {
ImageView heart = new ImageView(HEART_IMAGE_LOC);
animateUsingTimeline(heart);
// animateUsingScaleTransition(heart);
StackPane layout = new StackPane(heart);
layout.setPrefWidth(heart.getImage().getWidth() * 2);
layout.setPrefHeight(heart.getImage().getHeight() * 2);
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
private void animateUsingTimeline(ImageView heart) {
DoubleProperty scale = new SimpleDoubleProperty(1);
heart.scaleXProperty().bind(scale);
heart.scaleYProperty().bind(scale);
Timeline beat = new Timeline(
new KeyFrame(Duration.ZERO, event -> scale.setValue(1)),
new KeyFrame(Duration.seconds(0.5), event -> scale.setValue(1.1))
);
beat.setAutoReverse(true);
beat.setCycleCount(Timeline.INDEFINITE);
beat.play();
}
private void animateUsingScaleTransition(ImageView heart) {
ScaleTransition scaleTransition = new ScaleTransition(
Duration.seconds(1), heart
);
scaleTransition.setFromX(1);
scaleTransition.setFromY(1);
scaleTransition.setFromZ(1);
scaleTransition.setToX(1.1);
scaleTransition.setToY(1.1);
scaleTransition.setToZ(1.1);
scaleTransition.setAutoReverse(true);
scaleTransition.setCycleCount(Animation.INDEFINITE);
scaleTransition.play();
}
private static final String HEART_IMAGE_LOC =
"http://icons.iconarchive.com/icons/mirella-gabriele/valentine/128/Heart-red-icon.png";
// icon obtained from: http://www.iconarchive.com/show/valentine-icons-by-mirella-gabriele/Heart-red-icon.html
// icon license: Free for non-commercial use, commercial use not allowed.
}

Resources