JavaFX 中的线程有两个基本规则:
- 作为实际显示的场景图一部分的 UI 组件(节点)只能从 JavaFX 应用程序线程访问。其他一些操作(例如创建新的
Stage)也受此规则约束。
- 任何长时间运行或阻塞的操作都应该在后台线程(即不是 JavaFX 应用程序线程)上运行。这是因为 JavaFX 应用程序线程是呈现 UI 和响应用户交互所需的线程。因此,如果您阻止 FX 应用程序线程,则无法呈现 UI,并且应用程序将变得无响应,直到您的操作完成。
javafx.concurrent API 提供了用于管理可在后台线程上运行的代码和在 FX 应用程序线程上执行回调的工具。
javafx.animation API 还提供了允许在特定时间在 JavaFX 应用程序线程上执行 UI 代码的类。请注意,动画 API 完全避免创建后台线程。
因此,对于您的用例,如果您想为条形图中两个条形的交换设置动画,您可以使用动画 API 来实现。创建执行此类交换的动画的一般方法可能如下所示:
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;
}
这里的基本思想非常简单:我们测量两个节点之间的 x 坐标距离;记下它们当前的translateX 属性,然后创建两个转换来移动节点,以便它们占据彼此的位置。这两个转换是并行执行的。当转换完成时(由转换状态从RUNNING 更改为其他值来指示),图表中的值被交换,translateX 属性重置为之前的值(这些效果将在视觉上抵消,但现在图表数据将反映两者已交换的事实)。
如果您想执行一种排序算法,在排序中为交换设置动画,在算法的每个步骤之间暂停,您可以使用后台线程执行此操作(您也可以使用动画执行此操作 - 但这看起来很简单,也许更有指导意义)。
这里的想法是创建一个Task,其call() 方法执行排序算法,在不同点暂停以允许用户查看正在发生的事情。因为我们正在暂停(阻塞),所以这不能在 FX 应用程序线程上运行,因为阻塞会阻止 UI 更新,直到整个过程完成。
这是一个冒泡排序的实现(为简单起见)。在排序的每次迭代中,我们:
- 用绿色突出显示要比较的两个条*
- 暂停以便用户可以看到
- 如果需要交换值:
- 再次暂停,然后
- 重置颜色*。
上述伪代码中标有 * 的步骤更改了 UI,因此它们必须在 FX 应用程序线程上执行,因此需要将它们包装在对 Platform.runLater(...) 的调用中,这会导致提供的代码在 FX 上执行应用程序线程。
这里最后一个棘手的部分(这非常棘手)当然是动画需要一些时间来执行。所以我们必须安排我们的后台线程等到动画完成。我们通过创建一个计数为 1 的CountDownLatch 来做到这一点。动画完成后,我们将锁存器倒数。然后在将动画提交到Platform.runLater(..) 之后,我们的后台线程通过调用latch.await() 等待闩锁倒计时再继续。后台线程需要等待某些东西在 FX 应用程序线程上运行是很不寻常的,但这是在您确实需要它的情况下执行此操作的一种技术。
冒泡排序的实现如下所示
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;
}
};
}
这是一个完整的演示。由于排序算法及其暂停被封装为Task,因此如果需要,我们可以利用它的回调和状态属性。例如,我们在开始任务之前禁用按钮,并在完成时使用onSucceeded 处理程序再次启用它们。添加“取消”选项也很容易。
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("");
});
}
}
return null;
}
};
}
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;
}
private Series<String, Number> generateRandomIntegerSeries(int n) {
Series<String, Number> series = new Series<>();
for (int i = 1; i <= n; i++) {
series.getData().add(new Data<>(Integer.toString(i), rng.nextInt(90) + 10));
}
return series;
}
public static void main(String[] args) {
launch(args);
}
}