Animations in JavaFX using RxJava without blocking GUI - java

This is a simple class that uses bubble sort to sort a List of Integers and push the result of every iteration to a Subject from RxJava library, with some delay.
public class Sorter {
private PublishSubject<List<Integer>> subject = PublishSubject.create();
public void bubbleSort(int delay) throws InterruptedException {
List<Integer> ints = randomIntegers(0,200,10);
for (int i = 0; i < ints.size(); i++) {
for (int j = 0; j < ints.size() - 1; j++) {
if (ints.get(j) > ints.get(j + 1)) {
Collections.swap(ints, j, j + 1);
subject.onNext(ints);
Thread.sleep(delay);
}
}
}
}
private List<Integer> randomIntegers(int origin, int bound, int limit) {
return new Random()
.ints(origin, bound)
.limit(limit)
.boxed()
.collect(Collectors.toList());
}
public PublishSubject<List<Integer>> getSubject() {
return subject;
}
}
I can observe the result very easily with a sample code:
int delay = 100;
Sorter sorter = new Sorter();
PublishSubject<List<Integer>> subject = sorter.getSubject();
subject.subscribe(System.out::println);
sorter.bubbleSort(delay);
I want, however, to see the results of the iterations as moving rectangles of different height representing the numbers. I use JavaFX. After every iteration, I want to clear the application window and redraw everything.
public class Main extends Application {
private Group root;
#Override
public void start(Stage primaryStage) throws InterruptedException {
root = new Group();
Scene scene = new Scene(root, 500, 500, Color.WHITE);
primaryStage.show();
primaryStage.setScene(scene);
Sorter sorter = new Sorter();
PublishSubject<List<Integer>> subject = sorter.getSubject();
subject.subscribe(this::drawRectangles);
sorter.bubbleSort(100);
}
private void drawRectangles(List<Integer> integers) {
root.getChildren().clear();
rectangles.clear();
for (int i = 0; i<integers.size(); i++) {
Rectangle rectangle = new Rectangle(i * 20, 500 - integers.get(i), 10, integers.get(i));
rectangle.setFill(Color.BLACK);
rectangles.add(rectangle);
}
root.getChildren().addAll(rectangles);
}
public static void main(String[] args) {
launch(args);
}
}
However, I only see the last iteration's result. Prior to that, I can only see that the application is running and the GUI thread is blocked.
How can I make the background RxJava calculations non-blocking to my GUI?
EDIT: I solved the issue by running the computation code in another thread, like so:
#Override
public void start(Stage primaryStage) throws InterruptedException {
new Thread(() -> {
Sorter sorter = new Sorter();
sorter.getSubject().subscribe(this::drawRectangles);
try {
sorter.bubbleSort(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
root = new Group();
Scene scene = new Scene(root, 1500, 500, Color.WHITE);
primaryStage.show();
primaryStage.setScene(scene);
}
And wrapper the drawRectangles in Platform.runLater(() -> {} expression.
This seems to be working, although I feel like there might me a possibility to run this with observeOn somehow.

Use RxJavaFX scheduler your UI tasks.
You need do the sort task sorter.bubbleSort in another thread instead of UI thread. And
observeOn(JavaFxScheduler.getInstance())
to schedule UI task back into UI thread.
Only change your start method to:
#Override
public void start(Stage primaryStage) throws InterruptedException {
root = new Group();
Scene scene = new Scene(root, 500, 500, Color.WHITE);
primaryStage.show();
primaryStage.setScene(scene);
Sorter sorter = new Sorter();
sorter.getSubject()
.observeOn(JavaFxScheduler.platform())
.subscribe(this::drawRectangles);
new Thread(() -> uncheck(() -> sorter.bubbleSort(100))).start();
}

Related

JavaFX rectangle doesn't update

I'm trying to do a maze resolver with a cellular automaton but I have a display problem
for each new generation of a grid of an automaton we try to display the cells in the form of rectangle.The initialization works well and the grid is displayed,the last generation of the simulation is displayed too but not the intermediate steps.
pic of first generation
last generation
//Initialise la liste des rectangles
public void initRectList() {
for(int height = 0; height < this.mazeA.grid.getHeight(); height++) {
for(int width = 0; width < this.mazeA.grid.getWidth(); width++) {
this.rectList[height][width] = new Rectangle(REC_HEIGHT,REC_WIDTH, getColorCell(this.mazeA.grid, height, width));
}
}
}
//Dessine le labyrinthe
public void drawGrid() {
for (int height = 0; height < this.mazeA.grid.getHeight(); height++) {
for(int width = 0; width < this.mazeA.grid.getWidth(); width++) {
tilePane.getChildren().add(this.rectList[height][width]);
}
}
}
public class MazeFX {
private HBox root = new HBox();
private Scene scene = new Scene(root,1100,800);
private TilePane tilePane = new TilePane();
private Grid grid = new Grid(30,30);
private Rectangle[][] rectList = new Rectangle[30][30];
private VBox buttons = new VBox();
private Button reset = new Button("Reset");
private Button pas = new Button("Play");
private Button load = new Button("Load");
private MazeAutomaton mazeA;
private final double REC_HEIGHT = 20.;
private final double REC_WIDTH = 20.;
public MazeFX(Stage stage) throws InterruptedException {
scene.getStylesheets().add(getClass().getResource("/src/application.css").toExternalForm());
initButton();
initLayout();
initGridByTopologie();
mazeA = new MazeAutomaton(this.grid);
initRectList();
drawGrid();
pressedButtons(stage);
setWindow(stage);
displayWindow(stage);
}
to start the next generation you press a button.
//Action de l'utilisateur sur l'un des bouttons
public void pressedButtons(Stage stage) {
pressReset();
pressPAS();
pressLoad(stage);
}
//Boutton Play/Stop préssé
public void pressPAS() {
this.pas.setOnMouseClicked(e -> {
for (int i = 0; i < 30; i++) {
mazeA.nextStep();
try {
Thread.sleep(100);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
updateRectColor();
}
}
);
}
the problem seems to be that we are stuck in the method setOnMouseClicked() and that the update of the rectangles is not done, with a textual display I see the evolution of the automaton which indicates us that the simulation works and that the problem seems to come from JavaFX
The JavaFX Application Thread runs as a loop. Effectively (the actual implementation details are far more complex) that loop does the following (in pseudocode):
while (applicationIsRunning()) {
if (thereAreEventsToHandle()) {
handleEvents();
}
if (thereAreAnimationsToUpdate()) {
updateAnimations();
}
if (itsTimeToRenderTheScene()) {
renderScene();
}
}
By default, JavaFX renders the scene at most 60 times per second.
The code in your event handler is executed on the FX Application Thread (it's invoked by the first block in the pseudocode loop above). Since it sleeps on that thread, the FX Application Thread never gets to the third part of the loop (rendering the Scene) until the entire event handler completes. Consequently, you never see the intermediate updates, because the scene is never rendered.
Assuming mazeA.nextStep() doesn't block (or take a long time to run), it's best to refactor this as an Animation, e.g. a Timeline:
public void pressPAS() {
this.pas.setOnMouseClicked(e -> {
KeyFrame updateMaze = new KeyFrame(Duration.ZERO, evt -> mazeA.nextStep());
KeyFrame updateRect = new KeyFrame(Duration.millis(100), evt -> updateRectColor());
Timeline timeline = new Timeline(updateMaze, updateRect);
timeline.setCycleCount(30);
timeline.play();
});
}
The timeline.play() method simply starts the animation and returns immediately, allowing the FX Application Thread to proceed. When the FX Application Thread checks for running animations, it will check if it's time to execute either of the handlers in the key frames, and if so will execute them. Then it will render the scene as usual.

Repaint BorderPane (javaFx)

I have an application that can create a rectangle that decreases in size for example a lapse of time of 10 sec, but here is when I try to shrink the rectangle, the window bug (nothing is displayed in the scene) and wait until the countdown is finished to stop bugging (and then display the rectangle not diminished).
I tried to find on the Internet the equivalent of repaint in Swing but not average: /
this.requestLayout () -> I found this on the internet but it does not work.
Here is my code of my countdown:
public class Compteur {
DemoBorderPane p ;
public DemoBorderPane getPan() {
if(p==null) {
p = new DemoBorderPane();
}
return p;
}
public Compteur() {
}
public void lancerCompteur() throws InterruptedException {
int leTempsEnMillisecondes=1000;
for (int i=5;i>=0;i--) {
try {
Thread.sleep (leTempsEnMillisecondes);
}
catch (InterruptedException e) {
System.out.print("erreur");
}
System.out.println(i);
getPan().diminuerRect(35);
}
}
}
There is my Borderpane code :
public class DemoBorderPane extends BorderPane {
private Rectangle r;
public Rectangle getRect() {
if(r==null) {
r = new Rectangle();
r.setWidth(350);
r.setHeight(100);
r.setArcWidth(30);
r.setArcHeight(30);
r.setFill( //on remplie notre rectangle avec un dégradé
new LinearGradient(0f, 0f, 0f, 1f, true, CycleMethod.NO_CYCLE,
new Stop[] {
new Stop(0, Color.web("#333333")),
new Stop(1, Color.web("#000000"))
}
)
);
}
return r;
}
public void diminuerRect(int a) {
getRect().setWidth(getRect().getWidth()-a);
int c= (int) (getRect().getWidth()-a);
System.out.println(c);
this.requestLayout();
//this.requestFocus();
}
public DemoBorderPane() {
this.setBottom(getRect());
}
}
There is my Main code :
public class Main extends Application {
private DemoBorderPane p;
public DemoBorderPane getPan() {
if(p==null) {
p = new DemoBorderPane();
}
return p;
}
#Override
public void start(Stage primaryStage) {
Compteur c = new Compteur();
try {
//Group root = new Group();
Scene scene = new Scene(getPan(),800,600);
//scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
//root.getChildren().add(getPan());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
try {
c.lancerCompteur();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
/*Son s = null;
try {
s = new Son();
} catch (LineUnavailableException | IOException | UnsupportedAudioFileException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
s.volume(0.1);
s.jouer();
c.lancerCompteur();
s.arreter();*/
}
}
Thank ;)
As long as you keep the JavaFX application thread busy it cannot perform layout/rendering. For this reason it's important to make sure any methods that run on the application thread, like e.g. Application.start or event handlers on input events return fast.
lancerCompteur however blocks the application thread for 5 seconds so the only result you see is the final one after the method completes.
In general you can run code like this on a different thread and use Platform.runLater to update the ui.
In this case you could take advantage of the Timeline class which allows you to trigger an event handler on the application thread after a given delay:
#Override
public void start(Stage primaryStage) {
Scene scene = new Scene(getPan(), 800, 600);
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> {
getPan().diminuerRect(35);
}));
timeline.setCycleCount(5);
timeline.play();
primaryStage.setScene(scene);
primaryStage.show();
}
You also use different instances of DemoBorderPane in your Main class and the Compteur class; the Rectangle shown in the scene was never subject to an update.
there's no need to call requestLayout in diminuerRect. This happens automatically when the Rectangle's size is modified.
Lazy initialisation is pointless, if you know for sure the getter will be invoked during the object's creation. DemoBorderPane.getRect is invoked from it's constructor so moving the initialisation to the constructor would allow you to get rid of the if check without affecting functionality.

New to JavaFX and still trying to figure out a shrinking bullseye Application

So far everything in my application is working.
Function:
The bullseye will appear, then once the animate me button is clicked, the first three circles inside the bullseye will shrink. Then new circles will be created in alternating colors.
Problem:
Everything is working thus far except for the pause and play functions I'm trying to implement. I'm planning on making the bullseye stop when the user hits the stop button in the middle, yet pick up wherever it was left off with no hiccups, no skipped circles, etc. Like i said, I'm fairly new to this so I'm just wondering if the problem was that I didn't use the Timeline class in order to try and manage how it was paused and played? Or is the problem with the last two event handlers (btn.setOnMousedPressed and btn.setOnMouseReleased) and the for loops not reaching a certain index?
Thanks for the help!
public class BullsEye extends Application
{
Circle[] cir = new Circle[100];
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.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()
{
ScaleTransition[] st = new ScaleTransition[100];
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;
for(int i = 3; i<st.length; i++)
{
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].setDuration(Duration.seconds(7));
//st[i].setNode(cir[i]);
st[i].setDelay(Duration.seconds(delay));
delay++;
st[i].setToX(0.0f);
st[i].setToY(0.0f);
st[i].play();
}
}
btn.setOnMousePressed(event ->
{
for(int x = 0; x<st.length;x++)
st[x].pause();
btn.setText("Release to Resume");
});
btn.setOnMouseReleased(event ->
{
for(int y = 0; y<st.length;y++)
st[y].play();
});
}
}

Loading JavaFX GUI in background

I'm currently trying to create a Splash Screen for my program since it takes some time to start up.
The problem is that it takes a while to create the GUI (creating dialogues, updating tables etc.). And I can't move the GUI creation to a background thread (like the "Task" class), since I'll get an "Not on FXApplication Thread" exception.
I tried using:
Platform.runLater(new Runnable() {
public void run() {
//create GUI
}
}
And the "call" method of a Task:
public class InitWorker extends Task<Void> {
private Model model;
private ViewJFX view;
public InitWorker(Model model) {
this.model = model;
}
#Override
protected Void call() throws Exception {
View view = new View();
Collection collection = new Collection();
//do stuff
}
}
When I wrote the program in Swing I could just display and update the Splash Screen on the EventDispatchThread, without any real concurreny. The code looked like this:
public void build() {
MainOld.updateProgressBar(MainOld.PROGRESSBAR_VALUE++, "Creating Menus");
menuCreator = new MenuCreatorOld (model, this);
menuCreator.createMenu();
MainOld.updateProgressBar(MainOld.PROGRESSBAR_VALUE, "Creating Toolbar");
toolBar = menuCreator.createToolBar();
createWesternPanelToolBar();
shoppingPanel = new ShoppingListOld(model, this, collectionController, shoppingController, controller);
centerTabbedPane = new JTabbedPane();
MainOld.updateProgressBar(MainOld.PROGRESSBAR_VALUE++, "Creating Collection");
collectionPanel = new CollectionOld(model, collectionController, this, controller);
MainOld.updateProgressBar(MainOld.PROGRESSBAR_VALUE++, "Creating Wish List");
wishPanel = new WishListOld(model, this, collectionController, wishController, controller);
MainOld.updateProgressBar(MainOld.PROGRESSBAR_VALUE++, "Creating Folders Table");
//and so on
}
public static void updateProgressBar(int progressValue, String text) {
System.out.println("Loading Bar Value:"+progressValue);
progressBar.setValue(progressValue);
loadingLabel.setText(text);
progressBar.setString(text);
}
Is there any way to create the GUI in the background while displaying a Splash Screen with a loading bar?
Edit:
I had a look at my code and was able to decrease the startup time by 5 seconds. Most of the dialogs pull data from the database when they are created. So I moved the creation of the dialogs into their getter methods. That resulted in an improvement of 3 seconds. But I would still like to know if there is in a way to create the GUI on a background thread.
Edit:
As suggested, I also tried using "RunLater" in a "Task".
This way I can create the GUI and display the SplashScreen, but I can't update the progress bar and progress label, since the GUI creation blocks the JavaFX application thread. The progress bar and label are only updated, after the GUI has been fully created.
Here's an example you guys can run (I removed the splash screen and only kept the progress bar and progress label):
public class InitWorker extends Task<Void> {
private static ProgressBar progressBar;
private static Label progressLabel;
private static double PROGRESS_MAX = 5;
private double loadingValue;
public InitWorker() {
loadingValue = 0;
}
#Override
protected void succeeded() {
System.out.println("Succeeded");
}
#Override
protected void failed() {
System.out.println("Failed");
}
#Override
protected Void call() throws Exception {
System.out.println("RUNNING");
Platform.runLater(new Runnable() {
public void run() {
displaySplashScreen();
for(int i=0; i<10; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
updateProgressBar(loadingValue++, "Label "+i);
Stage stage = new Stage();
Label label = new Label("Label " + i);
VBox panel = new VBox();
panel.getChildren().add(label);
Scene scene = new Scene(panel);
stage.setScene(scene);
stage.centerOnScreen();
stage.show();
}
// updateProgressBar(1, "Initializing...");
}});
return null;
}
public void updateProgressBar(double loadingValue, String text) {
progressBar.setProgress(loadingValue / PROGRESS_MAX);
progressLabel.setText(text);
}
public static void displaySplashScreen() {
Stage progressBarStage = new Stage();
progressBar = new ProgressBar();
Scene progressBarScene = new Scene(progressBar);
progressBarStage.setScene(progressBarScene);
Stage progressLabelStage = new Stage();
progressLabel = new Label("Loading...");
progressLabel.setPadding(new Insets(5));
progressLabel.setStyle("-fx-background-color: red");
Scene progressLabelScene = new Scene(progressLabel);
progressLabelStage.setScene(progressLabelScene);
double progressBarWidth = 500;
double progressBarHeight = 75;
//muss angezeigt werden, um sie abhängig von Größe zu positionieren
progressBarStage.show();
progressLabelStage.show();
//
progressBarStage.setWidth(progressBarWidth);
progressBarStage.setHeight(progressBarHeight);
progressBarStage.centerOnScreen();
progressBarStage.centerOnScreen();
progressLabelStage.setY(progressLabelStage.getY() + 25);
}
}
See Task documentation titled "A Task Which Modifies The Scene Graph", which provides an example:
final Group group = new Group();
Task<Void> task = new Task<Void>() {
#Override protected Void call() throws Exception {
for (int i=0; i<100; i++) {
if (isCancelled()) break;
final Rectangle r = new Rectangle(10, 10);
r.setX(10 * i);
Platform.runLater(new Runnable() {
#Override public void run() {
group.getChildren().add(r);
}
});
}
return null;
}
};
The above example add the rectangles to the scene graph via a 100 runLater calls. A more efficient way to do this would be to add the rectangles to a group not attached to the active scene graph, then only add the group to the active scene graph in the runLater call. For example:
final Group groupInSceneGraph = new Group();
Task<Void> task = new Task<Void>() {
#Override protected Void call() throws Exception {
final Group localGroup = new Group();
for (int i=0; i<100; i++) {
if (isCancelled()) break;
final Rectangle r = new Rectangle(10, 10);
r.setX(10 * i);
localGroup.getChildren().add(r);
}
Platform.runLater(new Runnable() {
#Override public void run() {
groupInSceneGraph.add(localGroup);
}
});
return null;
}
};
You can create and modify most scene graph objects off of the JavaFX application thread (including loading FXML), as long as the objects aren't attached to the active scene graph. By active scene graph I mean a scene graph which is currently attached as a scene to a displayed stage. (A complicated control such as a WebView may be an exception to this rule and may require creation on the JavaFX application thread).
You must only attach the scene graph objects created off of the JavaFX application thread to the active scene graph on the JavaFX application thread (for example using Platform.runLater()). And, you must work with them on the JavaFX application thread as long they continue to be attached to the active scene graph.

JavaFX veil screen

When I try to load some files from JSON I want to create a progress bar that veils the screen for some seconds. The loading from JSON works, the progress bar works the only problem I have is with the veil.
So, I have my application that is running and when I try to load the JSON file I try to set the scene with the progress bar for the stage. All the things are going fine until now (even the new scene is showing the progress bar). The problem comes when I the progress bar finishes the progress (100%) it shows me blank ...and doesn't show me the old application scene. How can I resolve this ?
This is my code in the progress loader:
public Scene createContent() {
final StackPane g = new StackPane();
Region veil = new Region();
veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.4)");
veil.setOpacity(0.8);
final ProgressIndicator p1 = new ProgressIndicator();
p1.setPrefSize(100, 100);
p1.setMaxSize(150, 150);
p1.progressProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue ov, Number oldVal, Number newVal) {
if (p1.getProgress() < 0.25) {
p1.setStyle("-fx-progress-color: red;");
} else if (p1.getProgress() < 0.5) {
p1.setStyle("-fx-progress-color: orange;");
} else {
p1.setStyle("-fx-progress-color: green;");
}
}
});
// animate the styled ProgressIndicator
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.setAutoReverse(true);
final KeyValue kv = new KeyValue(p1.progressProperty(), 1);
final KeyFrame kf1 = new KeyFrame(Duration.millis(3000), kv);
timeline.getKeyFrames().add(kf1);
g.getChildren().addAll(veil,p1);
g.setAlignment(Pos.CENTER);
Task task = new Task() {
#Override
protected Object call() throws Exception {
for (int i = 0; i < 500; i++) {
updateProgress(i, 500);
Thread.sleep(5);
}
stage.hide();
return null;
}
};
p1.progressProperty().bind(task.progressProperty());
veil.visibleProperty().bind(task.runningProperty());
p1.visibleProperty().bind(task.runningProperty());
new Thread(task).start();
Scene scene = new Scene(g, 200, 200);
return scene;
}
public void play() {
timeline.play();
}
public void stop() {
timeline.stop();
}
public void start(Stage stage) {
this.stage=stage;
this.stage.setScene(createContent());
this.stage.show();
}
And this is in the JSON loader class:
ProgressLoader pl=new ProgressLoader();
pl.start(VisualAppFactory.getStage());
I do not know what you are trying to achieve excactly and what you mean by "veil", but your problem most certainly comes from calling stage.hide() while not being on the FX-Thread. Check out the documentation of the method or surround the call with a try block
try {
stage.hide();
} catch (Exception e) {
e.printStackTrace();
}
to see the effect.
Use Platform.runLater to execute the call on the FX-Thread:
Platform.runLater(()-> stage.hide());
With task.setOnSucceeded(...) you get notified when the task finished so you can set your old view into the stage or something.

Categories

Resources