JavaFX: custom ListCell

JavaFX: custom ListCell

In my GeoFroggerFX (replacement for my GeoCachingFrogger based on Netbeans), I have a list of geocaches. This post shows the development of the list from a simple list to a multiline list with icons.

Simple List

The first and simplest version had some textual rows.
simple list

Custom CellList with more information

But this isn´t too sexy and I also wanted more information in my list. So I decided to use a custom ListCell to support more information in multiple rows and also some icons. The first version just added more information as text to the row.

public class CacheListCell extends ListCell {  
  @Override 
  public void updateItem(Cache cache, boolean empty) { 
    super.updateItem(cache, empty); 
    if (empty) { 
      setText(null); 
      setGraphic(null); 
    } else { 
      setText(cache.getName() + " (" + cache.getDifficulty() + "/" + cache.getTerrain() + " )"); 
      setGraphic(null); 
    } 
  } 
}

list with more information

Custom CellList with multiple columns and icon

The second version splits the information in multiple rows and adds an icon to the cell. I use FontAwesome as icon and not an image.

public class CacheListCell extends ListCell {  
    @Override
    public void updateItem(Cache cache, boolean empty) {
        super.updateItem(cache, empty);
        if (empty) {
            setText(null);
            setGraphic(null);
        } else {
            setText(null);

            // DO NOT CREATE INSTANCES IN THIS METHOD, THIS IS BAD!
            GridPane grid = new GridPane();
            grid.setHgap(10);
            grid.setVgap(4);
            grid.setPadding(new Insets(0, 10, 0, 10));

            // DO NOT CREATE INSTANCES IN THIS METHOD, THIS IS BAD!
            Label icon = new Label(GeocachingIcons.getIcon(cache).toString());
            icon.setFont(Font.font("FontAwesome", FontWeight.BOLD, 24));
            icon.getStyleClass().add("cache-list-icon");
            grid.add(icon, 0, 0, 1, 2);            

            // DO NOT CREATE INSTANCES IN THIS METHOD, THIS IS BAD!
            Label name = new Label(cache.getName());
            name.getStyleClass().add("cache-list-name");
            grid.add(name, 1, 0);

            // DO NOT CREATE INSTANCES IN THIS METHOD, THIS IS BAD!
            Label dt = new Label(cache.getDifficulty()+" / "+ cache.getTerrain());
            grid.add(dt, 1, 1);            
            dt.getStyleClass().add("cache-list-dt");

            if (CacheUtils.hasUserFoundCache(cache, new Long(3906456))) {
                JavaFXUtils.addClasses(this, CACHE_LIST_FOUND_CLASS);
                JavaFXUtils.removeClasses(this, CACHE_LIST_NOT_FOUND_CLASS);
            } else {
                JavaFXUtils.addClasses(this, CACHE_LIST_NOT_FOUND_CLASS);
                JavaFXUtils.removeClasses(this, CACHE_LIST_FOUND_CLASS);
            }

            setGraphic(grid);
        }
    }
}

list with icons

Styling ListCell with css

I added some classes to the nodes to style the list with those classes.

.cache-list-name {  
    -fx-font-size: 1.2em;
    -fx-font-weight: bold;
}

.cache-list-icon {
    -fx-text-fill: #444444;
    -fx-effect: dropshadow( three-pass-box, rgba(0,0,0,0.4), 3, 0.0, 1, 1);
}

.cache-list-found .cache-list-icon {
    -fx-text-fill: #669900;
}

This brings some colour into the list and the name of the cache is bigger than the rest.

listcell_with_color

Jonathans tip how to do it THE RIGHT WAY

Jonathan Giles wrote me on twitter:

@frosch95 You shouldn’t create new instances of nodes in the updateItem method - instantiate them once in the cell constructor and reuse

So I refactored the class to reuse the objects and for better readability.

public class CacheListCell extends ListCell {  
    private static final String CACHE_LIST_FOUND_CLASS = "cache-list-found";
    private static final String CACHE_LIST_NOT_FOUND_CLASS = "cache-list-not-found";
    private static final String CACHE_LIST_NAME_CLASS = "cache-list-name";
    private static final String CACHE_LIST_DT_CLASS = "cache-list-dt";
    private static final String CACHE_LIST_ICON_CLASS = "cache-list-icon";
    private static final String FONT_AWESOME = "FontAwesome";

    private GridPane grid = new GridPane();
    private Label icon = new Label();
    private Label name = new Label();
    private Label dt = new Label();

    public CacheListCell() {
        configureGrid();        
        configureIcon();
        configureName();
        configureDifficultyTerrain();
        addControlsToGrid();            
    }

    private void configureGrid() {
        grid.setHgap(10);
        grid.setVgap(4);
        grid.setPadding(new Insets(0, 10, 0, 10));
    }

    private void configureIcon() {
        icon.setFont(Font.font(FONT_AWESOME, FontWeight.BOLD, 24));
        icon.getStyleClass().add(CACHE_LIST_ICON_CLASS);
    }

    private void configureName() {
        name.getStyleClass().add(CACHE_LIST_NAME_CLASS);
    }

    private void configureDifficultyTerrain() {
        dt.getStyleClass().add(CACHE_LIST_DT_CLASS);
    }

    private void addControlsToGrid() {
        grid.add(icon, 0, 0, 1, 2);                    
        grid.add(name, 1, 0);        
        grid.add(dt, 1, 1);
    }

    @Override
    public void updateItem(Cache cache, boolean empty) {
        super.updateItem(cache, empty);
        if (empty) {
            clearContent();
        } else {
            addContent(cache);
        }
    }

    private void clearContent() {
        setText(null);
        setGraphic(null);
    }

    private void addContent(Cache cache) {
        setText(null);
        icon.setText(GeocachingIcons.getIcon(cache).toString());
        name.setText(cache.getName());
        dt.setText(cache.getDifficulty()+" / "+ cache.getTerrain());
        setStyleClassDependingOnFoundState(cache);        
        setGraphic(grid);
    }

    private void setStyleClassDependingOnFoundState(Cache cache) {
        if (CacheUtils.hasUserFoundCache(cache, new Long(3906456))) {
            addClasses(this, CACHE_LIST_FOUND_CLASS);
            removeClasses(this, CACHE_LIST_NOT_FOUND_CLASS);
        } else {
            addClasses(this, CACHE_LIST_NOT_FOUND_CLASS);
            removeClasses(this, CACHE_LIST_FOUND_CLASS);
        }
    }
}

Comments