There is a lot at play here that make customizing ListView horizontal scrollbar behavior difficult to deal with. In addition to that, common misunderstandings on how ListView works can cause other problems.
The main issue to address is that the width of the ListCells will not automatically adapt when the vertical scrollbar becomes visible. Therefore, the moment it is, suddenly the contents are too wide to fit between the left edge of the ListView and the left edge of the vertical scrollbar, triggering a horizontal scrollbar. There is also the default padding of a ListCell as well as the border widths of the ListView itself to consider when determining the proper binding to set.
The following class that extends ListView:
public class WidthBoundList extends ListView {
private final BooleanProperty vbarVisibleProperty = new SimpleBooleanProperty(false);
private final boolean bindPrefWidth;
private final double scrollbarThickness;
private final double sumBorderSides;
public WidthBoundList(double scrollbarThickness, double sumBorderSides, boolean bindPrefWidth) {
this.scrollbarThickness = scrollbarThickness;
this.sumBorderSides = sumBorderSides;
this.bindPrefWidth = bindPrefWidth;
Platform.runLater(()->{
findScroller();
});
}
private void findScroller() {
if (!this.getChildren().isEmpty()) {
VirtualFlow flow = (VirtualFlow)this.getChildren().get(0);
if (flow != null) {
List<Node> flowChildren = flow.getChildrenUnmodifiable();
int len = flowChildren .size();
for (int i = 0; i < len; i++) {
Node n = flowChildren .get(i);
if (n.getClass().equals(VirtualScrollBar.class)) {
final ScrollBar bar = (ScrollBar) n;
if (bar.getOrientation().equals(Orientation.VERTICAL)) {
vbarVisibleProperty.bind(bar.visibleProperty());
bar.setPrefWidth(scrollbarThickness);
bar.setMinWidth(scrollbarThickness);
bar.setMaxWidth(scrollbarThickness);
} else if (bar.getOrientation().equals(Orientation.HORIZONTAL)) {
bar.setPrefHeight(scrollbarThickness);
bar.setMinHeight(scrollbarThickness);
bar.setMaxHeight(scrollbarThickness);
}
}
}
} else {
Platform.runLater(()->{
findScroller();
});
}
} else {
Platform.runLater(()->{
findScroller();
});
}
}
public void bindWidthScrollCondition(Region node) {
node.maxWidthProperty().unbind();
node.prefWidthProperty().unbind();
node.maxWidthProperty().bind(
Bindings.when(vbarVisibleProperty)
.then(this.widthProperty().subtract(scrollbarThickness).subtract(sumBorderSides))
.otherwise(this.widthProperty().subtract(sumBorderSides))
);
if (bindPrefWidth) {
node.prefWidthProperty().bind(node.maxWidthProperty());
}
}
}
Regarding your code, your bindings could cause problems. A ListCell's updateItem()
method is not only called when the ListCell is created. A ListView can contain a pretty large list of data, so to improve the performance only the ListCells scrolled into view (and possibly a few before and after) need their graphic rendered. The updateItem()
method handles this. In your code, a Region is being created over and over again and each and every one of them is being bound to the width of your ListView. Instead, the ListCell itself should be bound.
The following class extends ListCell and the method to bind the HBox is called in the constructor:
public class BoundListCell extends ListCell<String> {
private final HBox hbox;
private final Label label;
public BoundListCell(WidthBoundList widthBoundList) {
this.setPadding(Insets.EMPTY);
hbox = new HBox();
label = new Label();
hbox.setPadding(new Insets(2, 4, 2, 4));
hbox.getChildren().add(label);
widthBoundList.bindWidthScrollCondition(this);
}
@Override
public void updateItem(String data, boolean empty) {
super.updateItem(data, empty);
if (empty || data == null) {
label.setText("");
setGraphic(null);
setText(null);
} else {
label.setText(data);
setGraphic(hbox);
}
}
}
The scrollbarThickness
parameter of WidthBoundList constructor has been set to 12. The sumBorderSides
parameter has been set to 2 because my WidthBoundList has a one pixel border on the right and left. The bindPrefWidth
parameter has been set to true to prevent the horizontal scroller from showing at all (labels have ellipses, any non-text nodes that you might add to the hbox will simply be clipped). Set bindPrefWidth
to false to allow a horizontal scrollbar, and with these proper bindings it should only show when needed. An implementation:
private final WidthBoundList myListView = new WidthBoundList(12, 2, true);
public static void main(final String... a) {
Application.launch(a);
}
@Override
public void start(final Stage primaryStage) throws Exception {
myListView.setCellFactory(c -> new BoundListCell(myListView));
VBox vBox = new VBox();
vBox.setFillWidth(true);
vBox.setAlignment(Pos.CENTER);
vBox.setSpacing(5);
Button button = new Button("APPEND");
button.setOnAction((e)->{
myListView.getItems().add("THIS IS LIST ITEM NUMBER " + myListView.getItems().size());
});
vBox.getChildren().addAll(myListView, button);
myListView.maxWidthProperty().bind(vBox.widthProperty().subtract(20));
myListView.prefHeightProperty().bind(vBox.heightProperty().subtract(20));
primaryStage.setScene(new Scene(vBox, 200, 400));
primaryStage.show();
}