Welcome toVigges Developer Community-Open, Learning,Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.8k views
in Technique[技术] by (71.8m points)

multithreading - JavaFX make bar chart changing bars with delay in UI Thread

I got JavaFX main thread, there I create new Thread that extends Task and sort and replace bars. Everyhing is good, but I want to make some delays(like 100ms) while replacing to show step by step sorting, maybe with animation. The problem is when I use Thread.sleep() or TranslateTransition() it just sum all delays miliseconds together in one big delay that happens before changing bars. How can I make delay that will work properly in UI thread?

In main class:

       Sorting sorting = new Sorting();
       sortThread = new Thread(sorting, "sort");
       sortThread.start();
       sortThread.join();

And my class Sorting extends Task

  public class Sorting extends Task<Void> {

  //some stuff here

    @Override
    protected Void call() throws Exception {


        taskThread = new Thread(counter, "time");
        taskThread.setDaemon(true);
        taskThread.start();

        int n = array_tmp.length;
        int  temp;
        for (int i = 0; i < n; i++) {
            for (int j = 1; j < (n - i); j++) {
                if (array_tmp[j - 1] > array_tmp[j]) {

                        //replacing bars
                        Node n1 = barChart.getData().get(j-1).getData().get(0).getNode();
                        Node n2 = barChart.getData().get(j).getData().get(0).getNode();

                        double x1 = n1.getTranslateX() + ((barChart.getWidth()-69)/array_tmp.length);
                        double x2 = n2.getTranslateX() - ((barChart.getWidth()-69)/array_tmp.length);

                        n1.setTranslateX(x1);
                        n2.setTranslateX(x2);

                        barChart.getData().get(j-1).getData().get(0).setNode(n2);
                        barChart.getData().get(j).getData().get(0).setNode(n1);


                    temp = array_tmp[j - 1];
                    array_tmp[j - 1] = array_tmp[j];
                    array_tmp[j] = temp;
                }
            }
        }
  }
     }
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

There are two basic rules to threading in JavaFX:

  1. UI components (nodes) that are part of a scene graph that is actually displayed can only be accessed from the JavaFX application thread. Some other operations (such as creating a new Stage) are also subject to this rule.
  2. Any long-running or blocking operation should be run on a background thread (i.e. not the JavaFX application thread). This is because the JavaFX application thread is the one needed to render the UI and respond to user interaction. Consequently, if you block the FX Application Thread, then the UI cannot be rendered and the application will become unresponsive until your operation completes.

The javafx.concurrent API provides facilities for managing code that can be run on background threads and executing callbacks on the FX application thread.

The javafx.animation API additionally provides classes that allow UI code to be executed on the JavaFX application thread at specific times. Note that the animation API avoids creating background threads at all.

So for your use case, if you want to animate the swapping of two bars in the bar chart, you can do so with the animation API. A general method that creates an animation that performs such a swap might look like this:

private <T> Animation createSwapAnimation(Data<?, T> first, Data<?, T> second) {
    double firstX = first.getNode().getParent().localToScene(first.getNode().getBoundsInParent()).getMinX();
    double secondX = first.getNode().getParent().localToScene(second.getNode().getBoundsInParent()).getMinX();

    double firstStartTranslate = first.getNode().getTranslateX();
    double secondStartTranslate = second.getNode().getTranslateX();

    TranslateTransition firstTranslate = new TranslateTransition(Duration.millis(500), first.getNode());
    firstTranslate.setByX(secondX - firstX);
    TranslateTransition secondTranslate = new TranslateTransition(Duration.millis(500), second.getNode());
    secondTranslate.setByX(firstX - secondX);
    ParallelTransition translate = new ParallelTransition(firstTranslate, secondTranslate);

    translate.statusProperty().addListener((obs, oldStatus, newStatus) -> {
        if (oldStatus == Animation.Status.RUNNING) {
            T temp = first.getYValue();
            first.setYValue(second.getYValue());
            second.setYValue(temp);
            first.getNode().setTranslateX(firstStartTranslate);
            second.getNode().setTranslateX(secondStartTranslate);
        }
    });

    return translate;
}

The basic idea here is pretty simple: we measure the distance in the x-coordinates between the two nodes; make a note of their current translateX properties, and then create two transitions which move the nodes so they take each others positions. Those two transitions are executed in parallel. When the transitions are complete (indicated by the status of the transition changing from RUNNING to something else), the values in the chart are exchanged and the translateX properties reset to their previous values (the effect of these will cancel out visually, but now the chart data will reflect the fact that the two have been exchanged).

If you want to perform a sort algorithm which animates the exchanges in the sorting, pausing between each step of the algorithm, you can do this using a background thread (you may be able to do this with an animation too - but this seems simple enough and is perhaps more instructional).

The idea here is to create a Task whose call() method performs the sort algorithm, pausing at various points to allow the use to see what is happening. Because we are pausing (blocking), this cannot be run on the FX Application Thread, as the blocking would prevent the UI being updated until the entire process was complete.

Here is an implementation of a bubble sort (for simplicity). On each iteration of the sort, we:

  • highlight the two bars to be compared in green*
  • pause so the user can see that
  • if the values need to be exchanged:
    • get the animation defined above and run it*
  • pause again, and
  • reset the colors*.

Steps marked * in the above psuedocode change the UI, so they must be executed on the FX Application thread, so they need to be wrapped in a call to Platform.runLater(...), which causes the provided code to be executed on the FX Application Thread.

The last tricky part here (and this is unusually tricky) is that the animation, of course, takes some time to execute. So we must arrange for our background thread to wait until the animation is complete. We do this by creating a CountDownLatch with a count of 1. When the animation is complete, we count the latch down. Then after submitting the animation to Platform.runLater(..), our background thread just waits for the latch to count down before proceeding, by calling latch.await(). It is quite unusual for a background thread to need to wait for something to run on the FX Application Thread, but this is one technique to do that in a case where you do need it.

The implementation of the bubble sort thus looks like

private Task<Void> createSortingTask(Series<String, Number> series) {
    return new Task<Void>() {
        @Override
        protected Void call() throws Exception {

            ObservableList<Data<String, Number>> data = series.getData();
            for (int i = data.size() - 1; i >= 0; i--) {
                for (int j = 0 ; j < i; j++) {

                    Data<String, Number> first = data.get(j);
                    Data<String, Number> second = data.get(j + 1);

                    Platform.runLater(() -> {
                        first.getNode().setStyle("-fx-background-color: green ;");
                        second.getNode().setStyle("-fx-background-color: green ;");
                    });

                    Thread.sleep(500);

                    if (first.getYValue().doubleValue() > second.getYValue().doubleValue()) {
                        CountDownLatch latch = new CountDownLatch(1);
                        Platform.runLater(() -> {
                            Animation swap = createSwapAnimation(first, second);
                            swap.setOnFinished(e -> latch.countDown());
                            swap.play();
                        });
                        latch.await();
                    }
                    Thread.sleep(500);

                    Platform.runLater(() -> {
                        first.getNode().setStyle("");
                        second.getNode().setStyle("");
                    });
                }
            }
            return null;
        }
    };
}

Here is a complete demo. Since the sort algorithm, with its pauses, is encapsulated as a Task, we can leverage its callbacks and state properties if we need. As an example, we disable the buttons before starting the task, and use the onSucceeded handler to enable them again when it completes. It would be easy to add a "cancel" option too.

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javafx.animation.Animation;
import javafx.animation.ParallelTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class AnimatedBubbleSort extends Application {

    private Random rng = new Random();

    private ExecutorService exec = Executors.newCachedThreadPool(runnable -> {
        Thread t = new Thread(runnable);
        t.setDaemon(true);
        return t;
    });

    @Override
    public void start(Stage primaryStage) {
        BarChart<String, Number> chart = new BarChart<>(new CategoryAxis(), new NumberAxis());
        chart.setAnimated(false);
        Series<String, Number> series = generateRandomIntegerSeries(10);
        chart.getData().add(series);

        Button sort = new Button("Sort");

        Button reset = new Button("Reset");
        reset.setOnAction(e -> chart.getData().set(0, generateRandomIntegerSeries(10)));

        HBox buttons = new HBox(5, sort, reset);
        buttons.setAlignment(Pos.CENTER);
        buttons.setPadding(new Insets(5));

        sort.setOnAction(e -> {
            Task<Void> animateSortTask = createSortingTask(chart.getData().get(0));
            buttons.setDisable(true);
            animateSortTask.setOnSucceeded(event -> buttons.setDisable(false));
            exec.submit(animateSortTask);
        });

        BorderPane root = new BorderPane(chart);
        root.setBottom(buttons);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Task<Void> createSortingTask(Series<String, Number> series) {
        return new Task<Void>() {
            @Override
            protected Void call() throws Exception {

                ObservableList<Data<String, Number>> data = series.getData();
                for (int i = data.size() - 1; i >= 0; i--) {
                    for (int j = 0 ; j < i; j++) {

                        Data<String, Number> first = data.get(j);
                        Data<String, Number> second = data.get(j + 1);

                        Platform.runLater(() -> {
                            first.getNode().setStyle("-fx-background-color: green ;");
                            second.getNode().setStyle("-fx-background-color: green ;");
                        });

                        Thread.sleep(500);

                        if (first.getYValue().doubleValue() > second.getYValue().doubleValue()) {
                            CountDownLatch latch = new CountDownLatch(1);
                            Platform.runLater(() -> {
                                Animation swap = createSwapAnimation(first, second);
                                swap.setOnFinished(e -> latch.countDown());
                                swap.play();
                            });
                            latch.await();
                        }
                        Thread.sleep(500);

                        Platform.runLater(() -> {
                            first.getNode().setStyle("");
                            second.getNode().setStyle("");
                        });
                    }
        

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to Vigges Developer Community for programmer and developer-Open, Learning and Share
...