4

我正在尝试显示 ProgressIndicator执行异步后台ListView项目加载的时间。我想要的行为是:

  1. 在开始加载ListView项目之前,显示ProgressIndicator一个不确定的进度;
  2. 异步开始加载ListView项目;
  3. 项目加载完成后ListView,隐藏ProgressIndicator.

这是我不成功的尝试的一部分:

public class AsyncLoadingExample extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        final ListView<String> listView = new ListView<String>();
        final ObservableList<String> listItems = FXCollections.observableArrayList();
        final ProgressIndicator loadingIndicator = new ProgressIndicator();
        final Button button = new Button("Click me to start loading");

        primaryStage.setTitle("Async Loading Example");        

        listView.setPrefSize(200, 250);
        listView.setItems(listItems);

        loadingIndicator.setVisible(false);

        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                // I have hoped it whould start displaying the loading indicator (actually, at the end of this
                // method execution (EventHandler.handle(ActionEvent))
                loadingIndicator.setVisible(true); 

                // asynchronously loads the list view items
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(2000l); // just emulates some loading time

                            // populates the list view with dummy items
                            while (listItems.size() < 10) listItems.add("Item " + listItems.size());
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally { 
                            loadingIndicator.setVisible(false);  // stop displaying the loading indicator
                        }                   
                    }
                });
            }
        });

        VBox root = VBoxBuilder.create()
            .children(
                StackPaneBuilder.create().children(listView, loadingIndicator).build(), 
                button                  
            )
            .build();

        primaryStage.setScene(new Scene(root, 200, 250));
        primaryStage.show();
    }
}

在此示例中,ListView项目是异步加载的。但是, ProgressIndicator并没有出现。仍然在这个例子中,如果我省略所有Platform.runLater(...)代码,ProgressIndicator就会显示出来,但是,当然,ListView项目没有加载。

因此,我怎样才能实现所需的行为?

4

2 回答 2

6

Crferreira 的自我回答非常好。

这个答案只是演示了一个替代实现,它不需要使用任何Platform.runLater调用,而是使用 JavaFX任务(以及Java 8 lambda 语法)。

import javafx.application.Application;
import javafx.collections.*;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.*;

public class AsyncLoadingExample extends Application {
    private void loadItems(final ObservableList<String> listItems, final ProgressIndicator loadingIndicator) {
        if (loadingIndicator.isVisible()) {
            return;
        }

        // clears the list items and start displaying the loading indicator at the Application Thread
        listItems.clear();
        loadingIndicator.setVisible(true);

        // loads the items at another thread, asynchronously
        Task listLoader = new Task<List<String>>() {
            {
                setOnSucceeded(workerStateEvent -> {
                    listItems.setAll(getValue());
                    loadingIndicator.setVisible(false); // stop displaying the loading indicator
                });

                setOnFailed(workerStateEvent -> getException().printStackTrace());
            }

            @Override
            protected List<String> call() throws Exception {
                final List<String> loadedItems = new LinkedList<>();

                Thread.sleep(2000l); // just emulates some loading time

                // populates the list view with dummy items
                while (loadedItems.size() < 10) {
                    loadedItems.add("Item " + loadedItems.size());
                }

                return loadedItems;
            }
        };

        Thread loadingThread = new Thread(listLoader, "list-loader");
        loadingThread.setDaemon(true);
        loadingThread.start();
    }

    @Override
    public void start(Stage primaryStage) {
        final ListView<String> listView = new ListView<>();
        final ObservableList<String> listItems = FXCollections.observableArrayList();
        final ProgressIndicator loadingIndicator = new ProgressIndicator();
        final Button button = new Button("Click me to start loading");

        primaryStage.setTitle("Async Loading Example");        

        listView.setPrefSize(200, 250);
        listView.setItems(listItems);

        loadingIndicator.setVisible(false);

        button.setOnAction(event -> loadItems(listItems, loadingIndicator));

        VBox root = new VBox(
                new StackPane(
                        listView,
                        loadingIndicator
                ),
                button
        );

        primaryStage.setScene(new Scene(root, 200, 250));
        primaryStage.show();
    }

    public static void main(String[] args) { launch(args); }
}
于 2013-09-20T20:30:01.460 回答
5

问题是,在给出的示例中,我滥用了该Platform.runLater(...)方法,因此滥用了 JavaFX 应用程序线程。

Platform.runLater()方法文档中所述,此方法

在未来某个未指定的时间在 JavaFX 应用程序线程上运行指定的 Runnable。

并且,JavaFX 应用程序线程是开发人员代码可以从中访问和修改 JavaFX 场景图的线程,直观地反映了执行的修改。

因此,当我开始从该线程加载项目时,UI 会变得无响应(此处ListView也有说明),直到加载完成。

为了解决这个问题,ListView项目必须在另一个线程中加载,并且只有ListView更新必须在应用程序线程中执行。

上述修正如下:

public class AsyncLoadingExample extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        final ListView<String> listView = new ListView<String>();
        final ObservableList<String> listItems = FXCollections.observableArrayList();
        final ProgressIndicator loadingIndicator = new ProgressIndicator();
        final Button button = new Button("Click me to start loading");

        primaryStage.setTitle("Async Loading Example");        

        listView.setPrefSize(200, 250);
        listView.setItems(listItems);

        loadingIndicator.setVisible(false);

        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                final List<String> loadedItems = new LinkedList<String>();

                // clears the list items and start displaying the loading indicator at the Application Thread
                listItems.clear();
                loadingIndicator.setVisible(true); 

                // loads the items at another thread, asynchronously
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(2000l); // just emulates some loading time

                            // populates the list view with dummy items
                            while (loadedItems.size() < 10) loadedItems.add("Item " + loadedItems.size());
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            // just updates the list view items at the
                            // Application Thread
                            Platform.runLater(new Runnable() {
                                @Override
                                public void run() {
                                    listItems.addAll(loadedItems);
                                    loadingIndicator.setVisible(false); // stop displaying the loading indicator
                                }
                            });
                        }
                    }
                }).start();
            }
        });

        VBox root = VBoxBuilder.create()
            .children(
                StackPaneBuilder.create().children(listView, loadingIndicator).build(), 
                button                  
            )
            .build();

        primaryStage.setScene(new Scene(root, 200, 250));
        primaryStage.show();
    }
}
于 2013-09-20T19:56:18.457 回答