Reputation: 2849
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
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 Node
s 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
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:
updateSelected(boolean)
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