Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ListView with custom CellFactory trims invisible nodes

My layout issue

I have a little issue with ListView and I'm not sure if it's because of some knowledge I missing or if my approach is flawed. Have to admit I'm not yet clear with how JavaFX handle the layout in the many possible cases.

ListView trims my attempt at layout

The above screenshot shows the result I get twice with the exact same code, except that on the second one an invisible shape I use for coherent layout is made visible for debug.

The various classes involved by the CellFactory extend Group, I tried with some other Parent without much success so far.


How to reproduce

Rather than sharing my StarShape, StarRow and some other misc classes (I'd be happy to if requested) I wrote a sample reproducing the issue. The class extends Application and overrides the start(...) method as such:

@Override
public void start(Stage primaryStage) throws Exception {
    final StackPane root = new StackPane();
    final Scene scene = new Scene(root, 400, 600);

    final ListView<Boolean> listView = new ListView<>();
    listView.setCellFactory(this::cellFactory);

    for (int i = 0; i < 5 ; i++) {
        listView.getItems().add(true);
        listView.getItems().add(false);
    }

    root.getChildren().add(listView);

    primaryStage.setScene(scene);
    primaryStage.setTitle("ListView trims the invisible");
    primaryStage.show();
}

where this::cellFactory is

private ListCell<Boolean> cellFactory(ListView<Boolean> listView) {
    return new ListCell<Boolean>() {
        @Override
        protected void updateItem(Boolean item, boolean empty) {
            super.updateItem(item, empty);

            if (empty || item == null) {
                setText(null);
            } else {
                final Rectangle tabShape = new Rectangle();
                tabShape.setHeight(20);
                tabShape.setWidth(40);
                tabShape.setVisible(item);

                final Label label = new Label(item.toString());
                label.setLayoutX(40);

                final Group cellRoot = new Group();
                cellRoot.getChildren().add(tabShape);
                cellRoot.getChildren().add(label);

                setGraphic(cellRoot);
            }
        }
    };
}

The above will display a ListView<Boolean> with black shapes in front of true items (because of the tabShape.setVisible(item); bit). The false items are looking like regular Label objects as if the invisible shape in their Group wasn't there (but it is).


Closing comments

Debugging this, it turns out groups with the invisible shapes are given negative layoutX property values. Thus Label controls aren't aligned as I'd like them to be. It doesn't happen when I call setLayoutX and setLayoutY outside of a ListView (the invisible shapes do force offsets), but it's probably not the only place where it would happen.

What's happening and how to avoid it? Alternatively, as I'm guessing I'm approaching this wrong, what'd be the right way? In other words, what is the question I should be asking instead of this?

like image 925
Lulero Avatar asked Mar 25 '17 13:03

Lulero


2 Answers

Taking from @dlatikay's comment, instead of setting the placeholder items to invisible, you can render them transparent by setting their opacity to 0.0.

Applied to the MCVE from your question, this would be done by replacing:

tabShape.setVisible(item);

with:

tabShape.setOpacity(item ? 1.0 : 0.0);

In terms of user experience, you could take this one step further. Instead of setting the "inactive" stars to fully transparent, you could set them to be near-transparent, as in this mockup (with opacity set to 0.1):

mockup

The benefits that I see are:

  1. It indicates not only the rating of an item in the list, but also the maximum rating.
  2. It avoids awkward empty spaces for list items with zero stars.
like image 162
Robby Cornelissen Avatar answered Nov 07 '22 07:11

Robby Cornelissen


I'm guessing I'm approaching this wrong

No, you're not. As with all layouts, there's often multiple ways to approach the same problem. Your approach is actually correct, and you're very close to a working solution.

You can achieve what you're after with a mere 1 line change. That is, changing the Group to an HBox.

An HBox ensures that elements are ordered horizontally, one after another. They also allow invisible elements to still take up space.

I also commented out one line: label.setLayoutX(40). I did this because HBox will not respect this setting, and actually you don't need it to. It will automatically shift the elements horizontally by as much is required.

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

    if (empty || item == null) {
        setText(null);
    }
    else {
        final Rectangle tabShape = new Rectangle();
        tabShape.setHeight(20);
        tabShape.setWidth(40);
        tabShape.setVisible(item);

        final Label label = new Label(item.toString());
        //label.setLayoutX(40);

        final HBox cellRoot = new HBox();
        cellRoot.getChildren().add(tabShape);
        cellRoot.getChildren().add(label);

        setGraphic(cellRoot);
    }
}

When I make those changes, your layout will render like so:

enter image description here


Important: Your example and your screenshots are slightly different. You may want to use a VBox for your star example (V for 'vertical', H for 'horizontal').

like image 4
Michael Avatar answered Nov 07 '22 07:11

Michael