user2499946
user2499946

Reputation: 689

JavaFx Combobox lazy loading images

I am using the JavaFX Combobox for the first time... and I have over 2000 icons in the combobox (they can be filtered via AutoCompleteComboBoxListener that I found from StackOverflow).

I am planning to use the ExecutorService to fetch the images from a zip-file.

Now, the problem is that how can I figure out the currently visible items in the Combobox?

I am setting a custom ListCellFactory for the ComboBox, and I have a custom ListCell class, that also displays the icon. Can I somehow check from within the ListCell object whether the item "is showing" ?

Thanks.

Upvotes: 2

Views: 1423

Answers (3)

tomsontom
tomsontom

Reputation: 5887

Even if your list has 2000 items javafx will only create listcell objects for the visible cells (plus one or two more for half visible cells) so there's not really a lot TODO for you to load Images lazy - just load them when updateItem is called - and maybe cache already loaded Images in a lifo Cache so that not all of them stay in memory

Upvotes: 1

James_D
James_D

Reputation: 209225

Just note first that if you are loading the images from individual files instead of from a zip file, there is a mechanism that avoids having to work directly with any kind of threading at all:

ComboBox<MyDataType> comboBox = new ComboBox<>();
comboBox.setCellFactory(listView -> new ListCell<MyDataType>() {

    private ImageView imageView = new ImageView();

    @Override
    public void updateItem(MyDataType item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setGraphic(null);
        } else {
            String imageURL = item.getImageURL();
            Image image = new Image(imageURL, true); // true means load in background
            imageView.setImage(image);
            setGraphic(imageView);
        }
    }
});

Unfortunately, if you're loading from a zip file, I don't think you can use this, so you'll need to create your own background tasks. You need to just be a little careful to make sure that you don't use an image loaded in the background if the item in the cell changes during the loading process (which is pretty likely if the user scrolls a lot).

(Update note: I changed this to listen for changes in the itemProperty() of the cell, instead of using the updateItem(...) method. The updateItem(...) method can be called more frequently than when the actual item displayed by the cell changes, so this approach avoids unnecessary "reloading" of images.)

Something like:

ExecutorService exec = ... ;
ComboBox<MyDataType> comboBox = new ComboBox<>();
comboBox.setCellFactory(listView -> {

    ListCell<MyDataType> cell = new ListCell<MyDataType>() ;

    ImageView imageView = new ImageView();
    ObjectProperty<Task<Image>> loadingTask = new SimpleObjectProperty<>();

    cell.emptyProperty().addListener((obs, wasEmpty, isNotEmpty) -> {
        if (isNowEmpty) {
            cell.setGraphic(null);
            cell.setText(null);
        } else {
            cell.setGraphic(imageView);
        }
    });

    cell.itemProperty().addListener((obs, oldItem, newItem) ->  {
        if (loadingTask.get() != null && 
            loadingTask.get().getState() != Worker.State.SUCCEEDED &&
            loadingTask.get().getState() != Worker.State.FAILED) {

            loadingTask.get().cancel();
        }
        loadingTask.set(null) ;
        if (newItem != null) {
            Task<Image> task = new Task<Image>() {
                @Override
                public Image call() throws Exception {
                    Image image = ... ; // retrieve image for item
                    return image ;
                }
            };
            loadingTask.set(task);
            task.setOnSucceeded(event -> imageView.setImage(task.getValue()));
            exec.submit(task);

            cell.setText(...); // some text from item...
        }
    }); 

    return cell ;
});

Some thoughts on performance here:

First, the "virtualized" mechanism of the ComboBox means that only a small number of these cells will ever be created, so you don't need to worry that you're immediately starting thousands of threads loading images, or indeed that you will ever have thousands of images in memory.

When the user scrolls through the list, the itemProperty(...) may change quite frequently as the cells are reused for new items. It's important to make sure you don't use images from threads that are started but don't finish before the item changes again; that's the purpose of canceling an existing task at the beginning of the item change listener. Canceling the task will prevent the onSucceeded handler from being invoked. However, you will still have those threads running, so if possible the implementation of your call() method should check the isCancelled() flag and abort as quickly as possible if it returns true. This might be tricky to implement: I would experiment and see how it works with a simple implementation first.

Upvotes: 5

Current visible item implies the current selected item on combobox. You can get the selected item using

comboboxname.getSelectionModel().getSelectedItem();

Upvotes: 0

Related Questions