简体   繁体   中英

Javafx listview disable horizontal scrolling

I'm trying to avoid horizontal scrolling in ListView. The ListView instance holds list of HBox items, each item has a different width.

So far I'm using such a cell factory:

public class ListViewCell extends ListCell<Data>
{
    @Override
    public void updateItem(Data data, boolean empty)
    {
        super.updateItem(data, empty);
        if(empty || data == null){
            setGraphic(null);
            setText(null);
        }
        if(data != null)
        {
            Region region = createRow(data);
            region.prefWidthProperty().bind(mListView.widthProperty().subtract(20));
            region.maxWidthProperty().bind(mListView.widthProperty().subtract(20));
            setGraphic(region);
        }
    }
}

Unfortunately it is not enough. Usually after adding several items ListView's horizontal scrollbar appears. Even if it seems to be unnecessary.

How can I assure, that ListViewCell will not exceed it's parent width and horizontal scrollbar will not appear?

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();
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM