45
votes

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?

2
the fact that the labels are centered inside their container, is problem #1 and likely unrelated to the rectangle shape issue. something like label.setAlignment(Pos.CENTER_LEFT); #2: you seem to need the invisible shape as a placeholder for items without a star rating? try make the rectangle transparent instead of invisible. or take the code that renders the rating bar, and add a version with zero stars. - Cee McSharpface
...and now nobody will dare to elaborate this into an answer because of this disclaimer on meta - Cee McSharpface
@dlatikay Well... Users still have the power to ignore the disclaimer and answer you know... and whoever is strong enough to do that and gets 2 votes from the community gets half the bounty ;) - T J
@dlatikay: Yes the place holder is needed so all "starred labels" have same width and height. Your suggestions are good, can't try them out right now but will soon. - Lulero
@dlatikay The disclaimer had been voided by OP. The bounty is still available as it cannot be awarded to myself. - Robby Cornelissen

2 Answers

13
votes

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.
4
votes

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').