Introduction

In JavaFX, ListViews are everywhere. They’re used as the pop-up in ComboBoxes and some other controls, so many programmers are familiar with them even if they haven’t directly used ListView. Most programmers, however, never go very deep into understanding ListView, they’ll simply create an ObservableList of String and then stuff it into a vanilla ListView to put a scrolling list on the screen. To do anything more interesting, they’ll jump over to TableView instead.

But ListView can do so much more! In many cases, a ListView can be a much more appropriate vehicle to display complex data than TableView.

In this article, we’ll begin by showing the simplest usage of ListView, then we’ll take a look at how to do something much more involved.

The Basic ListView

The simplest implementation of a ListView is to use it to display a list of Strings or some other object that supports toString() nicely. In this case, you just create the ListView, connect it to your ObservableList and you’re done. Let’s take a look at that:

public class BasicListViewApplication extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Scene scene = new Scene(createContent());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Parent createContent() {
        ListView<String> listView = new ListView<>();
        ObservableList<String> animals = 
              FXCollections.observableArrayList("Bear", "Bird", "Hedgehog", 
                                                "Lobster", "Owl", "Shark");
        listView.setItems(animals);
        return listView;
    }
}

And it looks like this:

Basic ListView

Using Non-String Values

Anything that can support Object.toString() can be used as a data type in a ListView. We’re going to use a custom Enum type to show some more techniques, so let’s introduce it here and show how it works with a basic ListView

public enum Animal {

    BEAR("Bear", "bear.png"),
    BIRD("Bird", "bird.png"),
    HEDGEHOG("Hedgehog", "hedgehog.png"),
    LOBSTER("Lobster", "lobster.png"),
    OWL("Owl", "owl.png"),
    SHARK("Shark", "shark.png");
    private final ImageView imageView;
    private final String name;

    Animal(String name, String imageName) {
        this.name = name;
        this.imageView = new ImageView(BasicListViewApplication.class.getResource("images/" + imageName).toExternalForm());
    }

    public ImageView getImageView() {
        return imageView;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }
}

Using an ImageView right in the Enum isn’t really a good idea, but it’s a little cheat that will keep the first example that uses it simple. We’ll fix this up later.

and now the application would look like this:

public class BasicListViewApplication extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Scene scene = new Scene(createContent());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Parent createContent() {
        ListView<Animal> listView = new ListView<>();
        ObservableList<Animal> animals = 
               FXCollections.observableArrayList(Animal.BEAR, Animal.BIRD, Animal.HEDGEHOG, 
                                                 Animal.LOBSTER, Animal.OWL, Animal.SHARK);
        listView.setItems(animals);
        return listView;
    }
}

Which gives exactly the same display as before. We just let the Animal.toString() function do the heavy lifting.

For a lot of programmers, that’s all they’ll ever did with ListView, and that’s a shame. ListView can do so much more.

Customizing the Display

The basic display unit of a ListView is the ListCell. ListCell is a subclass of Labeled which means that, in many ways, it behaves just like a Label. Most importantly, it has both a Graphic and a Text component, both of which can be displayed together or independantly. Ordinarily, we just see the Text component as the default behaviour of ListCell is just to use toString() on the value and put it in Text component.

Loading Custom Cells

You can customize the ListCell to make its behaviour change. This involves first telling your ListView that it needs to load a custom ListCell. This is done via the ListView.setCellFactory() method. Let’s look at the signature of this:

public final void setCellFactory(Callback<ListView<T>,ListCell<T>> value)

This method takes a Callback. A Callback is basically the same as a Function, but the name implies that it’s supposed to be passed as a parameter of a method which will invoke the Function (it will “call back” to the Function) as part of what it does.

In this case, we expect that some piece of internal JavaFX code will take our ListView, pass it to the Callback and get a ListCell back. So we need to write the logic in our Callback which will create our custom ListCell on demand.

Here’s the code that will do that:

public class BasicListViewApplication extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Scene scene = new Scene(createContent());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Parent createContent() {
        ListView<Animal> listView = new ListView<>();
        listView.setCellFactory(lv -> createCell());
        ObservableList<Animal> animals = 
                 FXCollections.observableArrayList(Animal.BEAR, Animal.BIRD, Animal.HEDGEHOG, 
                                                   Animal.LOBSTER, Animal.OWL, Animal.SHARK);
        listView.setItems(animals);
        return listView;
    }

    private ListCell<Animal> createCell() {
        return new ListCell<>() {
            @Override
            public void updateItem(Animal item, boolean isEmpty) {
                super.updateItem(item, isEmpty);
                if (!isEmpty && (item != null)) {
                    setGraphic(item.getImageView());
                    setText(item.getName());
                } else {
                    setGraphic(null);
                    setText(null);
                }
            }
        };
    }
}

The line:

listView.setCellFactory(lv -> createCell());

sets the Callback. The createCell() method does the work, which in this case is to create an anonymous inner class which extends ListCell and overrides updateItem().

When you run it, it looks like this:

List With Images

About Cell.updateItem()

Every ListCell has to support ListCell.updateItem() and the super class that this comes from is Cell, not ListCell. Cell.updateItem() is the basic mechanism by which the data is put into your ListCell. It passes two values: the actual data, and a flag that indicates if the ListCell is empty. This second element is used when your ListView's viewport is longer than the number of items in the list, and it needs to leave some of the rows empty.

It’s important that we clear out all of the visual elements when isEmpty is true. Otherwise we’ll get random data in our empty rows (maybe).

In our custom ListCell, we’re simply putting values into the Graphic and Text elements of the standard ListCell, pulling them from the appropriate parts of item when it’s loaded in.

One of the most important aspects of updateItem() is that it must call super.updateItem(). We’ll see why in a little bit, but for now you should know that if you forget to do this (and you will), then you’re likely to get very strange results in your ListView.

Understanding VirtualFlow

ListView is one of the screen elements in JavaFX that uses something called VirtualFlow. This is a class that extends Region and is used in the skins of a number of classes with many elements inside a scrolling region. We’ve already seen one aspect of this with our call to setCellFactory(), which is part of the VirtualFlow interface.

One of the main characteristics of VirtualFlow is that is a virtualized region of many elements. What this means is that you’ll have a limited number of real screen elements, but they are used and re-used over and over to present a potentially unlimited number of elements in whatever list is backing it. In terms of ListView, this means that if the ObservableList that you install using ListView.setItems() is huge, you’ll still only have a limited number of ListCells created to display those on the screen. Typically, this amount is only slightly more than the number of rows that can be displayed on the screen at one time. If you do something at runtime to make the viewport of the ListView bigger, then it will create a few more ListCells so that it can fill the viewport.

Why do you care about this?

You care about this because this radically alters the approach you need to take when designing your ListCells. For the most part, all of the ListCells that you’ll have are created when the ListView is first shown on the screen. Then those handful of ListCells are going to be loaded via Cell.updateItem() with the data from the underlying List that are going to be shown in the viewport of the ListView. As the user scrolls through the ListView, some items will scroll out of the viewport and off the screen, the ListCells that held them are now free to be resused, and they will be filled with data from items that are scrolling into the viewport.

The implication of this is that Cell.updateItem() is going to get called a lot.

Seriously…a lot.

So you want it to run lightning fast. And this is why the commentary from the JavaDocs is misleading:

The updateItem method should not be called by developers, but it is the best method for developers to override to allow for them to customise the visuals of the cell.

and then:

However, the purpose of having the updateItem method is so that developers, when specifying custom cell factories (again, like the ListView cell factory), the updateItem method can be overridden to allow for complete customisation of the cell.

If you do as this says, and “customise the visuals of the cell” in Cell.updateItem(), then your application will choke when the users scroll through your ListView with any speed. That’s because creating a layout in JavaFX is fairly heavy-weight, and it’s slow.

Everybody knows this.

Everbody understands that a better approach is to create your layout in your ListCell constructor and then just put the data into it via Cell.updateItem().

Let’s take a look at this with our Animals example:

Customizing the Graphic Layout

The first thing we’re going to do is clean up Animals and remove the ImageView, which is a screen Node and replace it with Image, which isn’t a screen Node:

public enum Animal {
    BEAR("Bear", "bear.png"),
    BIRD("Bird", "bird.png"),
    HEDGEHOG("Hedgehog", "hedgehog.png"),
    LOBSTER("Lobster", "lobster.png"),
    OWL("Owl", "owl.png"),
    SHARK("Shark", "shark.png");
    private final Image image;
    private final String name;

    Animal(String name, String imageName) {
        this.name = name;
        this.image = new Image(BasicListViewApplication.class.getResource("images/" + imageName).toExternalForm());
    }

    public Image getImage() {
        return image;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }
}

Now we can’t just take the ImageView and put it into Graphic, we need to create a layout in the ListCell that has an ImageView in it, then stuff the Image into the ImageView through Cell.updateItem():

public class BasicListViewApplication extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Scene scene = new Scene(createContent());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Parent createContent() {
        ListView<Animal> listView = new ListView<>();
        listView.setCellFactory(lv -> createCell());
        ObservableList<Animal> animals = FXCollections.observableArrayList(Animal.BEAR, Animal.BIRD, Animal.HEDGEHOG, Animal.LOBSTER, Animal.OWL, Animal.SHARK);
        listView.setItems(animals);
        return listView;
    }

    private ListCell<Animal> createCell() {
        return new ListCell<>() {
            private final ImageView imageView = new ImageView();

            @Override
            public void updateItem(Animal item, boolean isEmpty) {
                super.updateItem(item, isEmpty);
                if (!isEmpty && (item != null)) {
                    imageView.setImage(item.getImage());
                    setGraphic(imageView);
                    setText(item.getName());
                } else {
                    setGraphic(null);
                    setText(null);
                }
            }
        };
    }
}

That’s not really a whole lot of change, but it’s philosophically huge. Let’s go one step further and put the ImageView inside a styled StackPane so that it looks like a layout:

public class BasicListViewApplication2 extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Scene scene = new Scene(createContent());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Parent createContent() {
        ListView<Animal> listView = new ListView<>();
        listView.setCellFactory(lv -> createCell());
        ObservableList<Animal> animals = FXCollections.observableArrayList(Animal.BEAR, Animal.BIRD, Animal.HEDGEHOG, Animal.LOBSTER, Animal.OWL, Animal.SHARK);
        listView.setItems(animals);
        return listView;
    }

    private ListCell<Animal> createCell() {
        return new ListCell<>() {
            private final StackPane layout = new StackPane();
            private final ImageView imageView = new ImageView();

            {
                layout.getChildren().add(imageView);
                layout.setPadding(new Insets(3));
                layout.setBackground(new Background(new BackgroundFill(Color.CYAN, null, null)));
            }

            @Override
            public void updateItem(Animal item, boolean isEmpty) {
                super.updateItem(item, isEmpty);
                if (!isEmpty && (item != null)) {
                    imageView.setImage(item.getImage());
                    setGraphic(layout);
                    setText(item.getName());
                } else {
                    setGraphic(null);
                    setText(null);
                }
            }
        };
    }
}

And now it will look like:

List With Cyan Square

Ok, that’s more like a layout. Let’s look at what’s happening here…

With an anonymous inner class you use the {} for the constructor, so you can see that we’re creating the layout in the constructor. The layout is just a StackPane, and we’ve got some padding and a BackgroundFill on it. Once it’s done, though, it’s done. We’re not going to mess with the layout outside that constructor.

Now updateItem() does two things. First, it stuffs the Image into the ImageView. Then it sets Graphic to our layout, and Text to the name.

Conclusion

Hopefully, you now have a good understanding about how ListView works, and how the data from your underlying List gets into the ListCells. We’ve gone from the generic ListView showing a list of Strings in a scrolling pane, to handling the data transfer into each Cell and customizing how it’s displayed.

This is a big step forward, but we’re still treating Cell very much like a Label. In the next article we’ll look at how to treat ListView as a vehicle for displaying custom layouts.

Categories:

Updated: