mr mcwolf
mr mcwolf

Reputation: 2849

listview with different cell height

I'm trying to do a ListView in which the selected cell has a different UI (and a different height, respectively). The cells are declared in FXML and custom controls (DisplayRowDefault and DisplayRowSelected) are created that load the corresponding FXML files.

I also have a cell factory set up where I manage the rendering of the cell, depending on whether it is selected or not.

listView.setCellFactory(lv -> new ListCell<>() {
        private DisplayRowSelected selectedGraphics;
        private DisplayRowDefault defaultGraphics;

        {
            defaultGraphics = new DisplayRowDefault();
            selectedGraphics = new DisplayRowSelected();
        }

        @Override
        protected void updateItem(Item item, boolean empty) {
            super.updateItem(item, empty);

            if(empty || item == null) {
                setContentDisplay(ContentDisplay.TEXT_ONLY);
                setGraphic(null);
            }
            else {
                selectedGraphics.setIndex(getListView().getItems().indexOf(item));
                selectedGraphics.setItem(item);

                setGraphic(isSelected() ? selectedGraphics : defaultGraphics);
                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            }
        }
    }
);

Everything works "perfectly" except that the cell size remains the same.

edit: I found a solution to the problem. When override computePrefHeight cells are resized correctly. The disadvantage of this method is that the prefHeight must be explicitly specified.

listView.setCellFactory(lv -> new ListCell<>() {
        private DisplayRowSelected selectedGraphics;
        private DisplayRowDefault defaultGraphics;

        {
            defaultGraphics = new DisplayRowDefault();
            defaultGraphics.setPrefHeight(50);

            selectedGraphics = new DisplayRowSelected();
            selectedGraphics.setPrefHeight(100);
        }

        @Override
        protected void updateItem(Item item, boolean empty) {
            super.updateItem(item, empty);

            if(empty || item == null) {
                setContentDisplay(ContentDisplay.TEXT_ONLY);
                setGraphic(null);
            }
            else {
                selectedGraphics.setIndex(getListView().getItems().indexOf(item));
                selectedGraphics.setItem(item);

                setGraphic(isSelected() ? selectedGraphics : defaultGraphics);
                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            }

            setPrefHeight(Region.USE_COMPUTED_SIZE);
        }

        @Override
        protected double computePrefHeight(double v) {
            if(getContentDisplay() == ContentDisplay.GRAPHIC_ONLY) {
                return isSelected() ? selectedGraphics.getPrefHeight() : defaultGraphics.getPrefHeight();
            }

            return super.computePrefHeight(v);
        }
    }
);

edit: MCVE

public class Sample extends Application {
    private class SelectedCell extends VBox {
        public SelectedCell() {
            getChildren().add(new Label("---"));
            getChildren().add(new Label("Selected Cell"));
            getChildren().add(new Label("---"));
        }
    }

    private class DefaultCell extends VBox {
        public DefaultCell() {
            getChildren().add(new Label("Default Cell"));
        }
    }

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        List<String> items = List.of("Item A", "Item B", "Item C", "Item D");

        ListView<String> listView = new ListView<>();
        listView.getItems().setAll(items);

        listView.setCellFactory(lv -> new ListCell<>() {
            private SelectedCell selectedCell = new SelectedCell();
            private DefaultCell defaultCell = new DefaultCell();

            @Override
            protected void updateItem(String s, boolean b) {
                super.updateItem(s, b);

                if(s == null || b) {
                    setContentDisplay(ContentDisplay.TEXT_ONLY);
                    setGraphic(null);
                }
                else {
                    setGraphic(isSelected() ? selectedCell : defaultCell);
                    setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                }
            }
        });

        Scene scene = new Scene(listView, 200, 500);

        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

Upvotes: 3

Views: 394

Answers (2)

mr mcwolf
mr mcwolf

Reputation: 2849

Although the solution by explicitly asking prefHeight (which I mentioned in the question) works, it is not a good solution. The reason for this is that the dimensions of the individual Nodes depend on the context in which they are placed. So, a prefHeight value cannot always be determined.

By design, ListView is designed so that in edit mode, cells use a different UI, and changing modes causes the cell size to be correctly re-calculated. So I choose the approach where I start/stop editing the current cell when changing the selection.

This is the solution I found and it works correctly:

public class Sample extends Application {
    private class SelectedCell extends VBox {
        public SelectedCell() {
            getChildren().add(new Label("---"));
            getChildren().add(new Label("Selected Cell"));
            getChildren().add(new Label("---"));
        }
    }

    private class DefaultCell extends VBox {
        public DefaultCell() {
            getChildren().add(new Label("Default Cell"));
        }
    }

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        List<String> items = List.of("Item A", "Item B", "Item C", "Item D");

        ListView<String> listView = new ListView<>();
        listView.getItems().setAll(items);

        listView.setEditable(true);

        listView.setCellFactory(lv -> new ListCell<>() {
            private SelectedCell selectedCell = new SelectedCell();
            private DefaultCell defaultCell = new DefaultCell();

            {
                selectedProperty().addListener((observable, oldValue, newValue) -> {
                    if (oldValue != null && oldValue) {
                        cancelEdit();
                    }

                    if (newValue != null && newValue) {
                        startEdit();
                    }
                });
            }

            @Override
            public void startEdit() {
                super.startEdit();
                setGraphic(selectedCell);
            }

            @Override
            public void cancelEdit() {
                if(!isSelected()) {
                    super.cancelEdit();
                    setGraphic(defaultCell);
                }
            }

            @Override
            protected void updateItem(String s, boolean b) {
                super.updateItem(s, b);

                if(s == null || b) {
                    setContentDisplay(ContentDisplay.TEXT_ONLY);
                    setGraphic(null);
                }
                else {
                    setGraphic(isEditing() ? selectedCell : defaultCell);
                    setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                }
            }
        });

        Scene scene = new Scene(listView, 200, 500);

        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

Upvotes: 2

kleopatra
kleopatra

Reputation: 51525

As noted in the comments this might be a bug (or not - a cell has many properties and their interplay is not completely specified). Hacky workarounds - here: triggering fake edit transitions or manually setting the height - often are needed.

The fact that a fake edit transition on listening to the selected property hacks the problem indicates that we need to update the graphic "earlier" (than updateItem) in the change notification chain, that is when the selected changes:

  • the method to hook into (instead of manually listening) is updateSelected(boolean)
  • override and change the graphic as needed

A code snippet:

@Override
public void updateSelected(boolean selected) {
    super.updateSelected(selected);
    setGraphic(selected ? selectedCell : defaultCell);
}



@Override
protected void updateItem(String s, boolean b) {
    super.updateItem(s, b);

    if(s == null || b) {
        setContentDisplay(ContentDisplay.TEXT_ONLY);
        setGraphic(null);
    }
    else {
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        setGraphic(isSelected() ? selectedCell : defaultCell);
    }
}

to override the cell's updateSelected(boolean) method and set the cell's graphic as needed:

Upvotes: 2

Related Questions