JavaFX Tip 28: Pretty List View

When I look at the list views on my mobile phone I always notice that they display their scrollbar (normally a vertical one) very differently than JavaFX does. The same is true for applications running on MacOS X. Below you can see a snapshot of Apple’s calendar app. You will notice that the scrollbar is actually on top of the content.

In JavaFX the scrollbar would be placed to the right of the view. However on mobile devices in order to save space the scrollbar shows up on top of the content.

So obviously I wanted to see how I could achieve the same thing for my JavaFX apps. The trick to do this is to first “style away” the original scrollbars. Then you add your own scrollbar and lay it out in such a way that it will be on top of the content (the ListView). Next you style your own scrollbar to look nice and lightweight, and finally you bind your scrollbar to the original scrollbar.

In the end the result will look like this:

Below you can see the class PrettyListView, which extends a standard ListView.

import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.scene.Node; import javafx.scene.control.ListView; import javafx.scene.control.ScrollBar; import javafx.scene.control.ScrollPane; import java.util.Set; public class PrettyListView<T> extends ListView<T> { private ScrollBar vBar = new ScrollBar(); private ScrollBar hBar = new ScrollBar(); public PrettyListView() { super(); skinProperty().addListener(it -> { // first bind, then add new scrollbars, otherwise the new bars will be found bindScrollBars(); getChildren().addAll(vBar, hBar); }); getStyleClass().add("pretty-list-view"); vBar.setManaged(false); vBar.setOrientation(Orientation.VERTICAL); vBar.getStyleClass().add("pretty-scroll-bar"); vBar.visibleProperty().bind(vBar.visibleAmountProperty().isNotEqualTo(0)); hBar.setManaged(false); hBar.setOrientation(Orientation.HORIZONTAL); hBar.getStyleClass().add("pretty-scroll-bar"); hBar.visibleProperty().bind(hBar.visibleAmountProperty().isNotEqualTo(0)); } private void bindScrollBars() { final Set<Node> nodes = lookupAll("VirtualScrollBar"); for (Node node : nodes) { if (node instanceof ScrollBar) { ScrollBar bar = (ScrollBar) node; if (bar.getOrientation().equals(Orientation.VERTICAL)) { bindScrollBars(vBar, bar); } else if (bar.getOrientation().equals(Orientation.HORIZONTAL)) { bindScrollBars(hBar, bar); } } } } private void bindScrollBars(ScrollBar scrollBarA, ScrollBar scrollBarB) { scrollBarA.valueProperty().bindBidirectional(scrollBarB.valueProperty()); scrollBarA.minProperty().bindBidirectional(scrollBarB.minProperty()); scrollBarA.maxProperty().bindBidirectional(scrollBarB.maxProperty()); scrollBarA.visibleAmountProperty().bindBidirectional(scrollBarB.visibleAmountProperty()); scrollBarA.unitIncrementProperty().bindBidirectional(scrollBarB.unitIncrementProperty()); scrollBarA.blockIncrementProperty().bindBidirectional(scrollBarB.blockIncrementProperty()); } @Override protected void layoutChildren() { super.layoutChildren(); Insets insets = getInsets(); double w = getWidth(); double h = getHeight(); final double prefWidth = vBar.prefWidth(-1); vBar.resizeRelocate(w - prefWidth - insets.getRight(), insets.getTop(), prefWidth, h - insets.getTop() - insets.getBottom()); final double prefHeight = hBar.prefHeight(-1); hBar.resizeRelocate(insets.getLeft(), h - prefHeight - insets.getBottom(), w - insets.getLeft() - insets.getRight(), prefHeight); } }

The CSS looks like this:

.pretty-list-view > .virtual-flow > .scroll-bar, .pretty-list-view > .virtual-flow > .scroll-bar .decrement-arrow, .pretty-list-view > .virtual-flow > .scroll-bar .increment-arrow, .pretty-list-view > .virtual-flow > .scroll-bar .decrement-button, .pretty-list-view > .virtual-flow > .scroll-bar .increment-button { -fx-pref-width: 0; -fx-pref-height: 0; } .pretty-list-view .pretty-scroll-bar:vertical .thumb { -fx-background-insets: 0 2 0 0; } .pretty-list-view .pretty-scroll-bar:horizontal .thumb { -fx-background-insets: 0 0 2 0; } .pretty-list-view .pretty-scroll-bar .decrement-arrow, .pretty-list-view .pretty-scroll-bar .increment-arrow { -fx-pref-width: 0; -fx-pref-height: 0; } .pretty-list-view .pretty-scroll-bar { -fx-background-color: transparent; -fx-pref-width: 12; -fx-pref-height: 12; -fx-padding: 2; } .pretty-list-view .pretty-scroll-bar .thumb { -fx-background-color: rgba(0, 0, 0, .2); -fx-background-radius: 1000; -fx-pref-width: 12; -fx-pref-height: 12; }