I would like to implement a scrollable ListView with gestures, just like in mobiles and tablets, to scroll up and down my list with a finger. But my current list selects an item as soon as I click down on the list. How can I achieve this? I couldn't find any example at the Oracle tutorials.
private ObservableList<Document> items = FXCollections.observableArrayList();
@FXML ListView<Document> listView;
{
...
listView.setItems(items);
listView.getStylesheets().add("style/listview.css");
listView.setStyle("-fx-background-insets: 0 ;"); // remove 1px border of listview container
listView.setCellFactory(new Callback<ListView<Document>, ListCell<Document>>() {
@Override
public ListCell<Document> call(ListView<Document> listView) {
return new DocumentArrayAdapter();
}
});
...
}
public void loadListView(List<Document> ldoc){
if (!ldoc.isEmpty()){
items.addAll(ldoc);
}
}
This is what I've made
public class CustomListCell extends ListCell<Document>{
private double lastYposition = 0;
public CustomListCell(){
setOnMousePressed(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
lastYposition = event.getSceneY();
}
});
setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
double newYposition = event.getSceneY();
double diff = newYposition - lastYposition;
lastYposition = newYposition;
CustomScrollEvent cse = new CustomScrollEvent();
cse.fireVerticalScroll((int)diff, DocumentArrayAdapter.this, (EventTarget) event.getSource());
}
});
and
package myproject.utils;
import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.scene.input.ScrollEvent;
public class CustomScrollEvent {
public void fireVerticalScroll(int deltaY, Object source, EventTarget target){
ScrollEvent newScrollEvent = null;
newScrollEvent = new ScrollEvent(source,
target,
ScrollEvent.SCROLL,
0,
0,
0,
0,
false,
false,
false,
false,
false,
false,
0,
deltaY,
0,
0,
ScrollEvent.HorizontalTextScrollUnits.CHARACTERS,
0,
ScrollEvent.VerticalTextScrollUnits.NONE,
deltaY,
0,
null);
Event.fireEvent(target, newScrollEvent);
}
}
Although I've implemented the listener in my own ListCell, I guess it would also work implementing the listeners directly at the ListView, with listView.setOnMousePressed and listView.setOnMouseDragged
Here is another approach:
public class ScrollListener {
private BooleanProperty scrolling = new ReadOnlyBooleanWrapper(false);
private EventHandler<? super MouseEvent> dragDetectedFilter = e -> scrolling.set(true);
private EventHandler<? super MouseEvent> mouseClickedFilter = evt -> {
if (scrolling.get()) {
scrolling.set(false);
evt.consume();
}
};
private EventHandler<? super MouseEvent> mouseExitedHandler = e -> scrolling.set(false);
private Node observableNode;
public ScrollListener(Node observableNode) {
this.observableNode = observableNode;
}
public void enable() {
observableNode.addEventFilter(MouseEvent.DRAG_DETECTED, dragDetectedFilter);
observableNode.addEventFilter(MouseEvent.MOUSE_CLICKED, mouseClickedFilter);
observableNode.addEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedHandler);
}
public void disable() {
observableNode.removeEventFilter(MouseEvent.DRAG_DETECTED, dragDetectedFilter);
observableNode.removeEventFilter(MouseEvent.MOUSE_CLICKED, mouseClickedFilter);
observableNode.removeEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedHandler);
}
public ReadOnlyBooleanProperty scrollingProperty() {
return scrolling;
}
public boolean isScrolling() {
return scrolling.get();
}
}
When you add the listener to a ListView
by:
ScrollListener scrollListener = new ScrollListener(yourListView);
scrollListener.enable();
MouseEvent.MOUSE_CLICKED
will get consumed while you are scrolling the list
I found a different solution to this problem:
public class ScrollList {
protected double lastYposition = 0;
private double last_value = 0;
protected double firstYposition = 0;
protected ScrollBar scrollbar = null;
private int autoValue = 0;
private int maxScroll = 0;
public ScrollList(ListView list) {
list.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
lastYposition = event.getSceneY();
//Config.prints(lastYposition + "");
firstYposition = lastYposition;
}
});
Platform.runLater(new Runnable() {
@Override
public void run() {
if (scrollbar == null) {
for (Node node : list.lookupAll(".scroll-bar")) {
if (node instanceof ScrollBar) {
ScrollBar bar = (ScrollBar) node;
if (bar.getOrientation().equals(Orientation.VERTICAL)) {
scrollbar = bar;
}
}
}
scrollbar.setOnMouseReleased(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
lastYposition = event.getSceneY();
last_value = autoValue;
}
});
/* //in case the list has already a scroll value, like when
//return to fontes list, it should has the last choosed fontes selected
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
autoValue = first.getIndex();*/
VirtualFlow flow = (VirtualFlow) list.lookup(".virtual-flow");
last_value = autoValue;
maxScroll = flow.getCellCount();
flow.addEventFilter(Event.ANY, new EventHandler<Event>() {
@Override
public void handle(Event eventus) {
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
autoValue = first.getIndex();
Config.prints(last_value + "");
}
});
}
}
});
list.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
double newYposition = event.getSceneY();
//Config.prints(newYposition + "");
double diff = newYposition - lastYposition;
lastYposition = newYposition;
if ((firstYposition - lastYposition) > Config.TOUCH_SCREEN_NOISE && (firstYposition - lastYposition) > 0 || (firstYposition - lastYposition) < (Config.TOUCH_SCREEN_NOISE * -1) && (firstYposition - lastYposition) < 0) {
list.getSelectionModel().clearSelection();
}
if (diff > 0 && diff > Config.MIN_SCROLL_VELOCITY) {
diff = Config.MIN_SCROLL_VELOCITY;
} else if (diff < 0 && diff < Config.MIN_SCROLL_VELOCITY * -1) {
diff = Config.MIN_SCROLL_VELOCITY * -1;
}
if (diff > 0 && diff > Config.MAX_SCROLL_VELOCITY) {
diff = Config.MAX_SCROLL_VELOCITY;
} else if (diff < 0 && diff < Config.MAX_SCROLL_VELOCITY * -1) {
diff = Config.MAX_SCROLL_VELOCITY * -1;
}
//prints("diff=>" + diff + " || last_value=>" + last_value);
last_value -= diff;
//Config.prints("last_value=>" + last_value);
if (last_value < 0) {
last_value = 0;
}else if(last_value > maxScroll){
last_value = maxScroll;
}
list.scrollTo((int) last_value);
}
});
}
//if you want the scroll to start from a specific position
public void setAutoValue(int index) {
this.autoValue = index;
}
}
}
public static double MAX_SCROLL_VELOCITY = 1;
public static double MIN_SCROLL_VELOCITY = 0.5;
I put this code inside an object "ScrollList" and used it like this.
protected ArrayList<ScrollList> scrolls = new ArrayList<>();
protected void setScroll(ListView list, int... index) {
ScrollList scrollList = new ScrollList(list);
if (index.length > 0) {
scrollList.setAutoValue(index[0]);
}
scrolls.add(scrollList);
}
the ArrayList is because, in my case, I have some views that I have more then one listview.
GC
Simple to do so:
import com.sun.javafx.scene.control.skin.VirtualFlow;
import com.sun.javafx.scene.control.skin.VirtualScrollBar;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
public class ListViewHelper
{
public static VirtualFlow getVirtualFlow(ListView lv)
{
return (VirtualFlow) lv.lookup(".virtual-flow");
}
public static VirtualScrollBar getVbar(VirtualFlow vf)
{
try {
final Method method = VirtualFlow.class.getDeclaredMethod("getVbar");
method.setAccessible(true);
return (VirtualScrollBar) method.invoke(vf);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException |
InvocationTargetException ex) {
throw new RuntimeException(ex);
}
}
public static VirtualScrollBar getHbar(VirtualFlow vf)
{
try {
final Method method = VirtualFlow.class.getDeclaredMethod("getHbar");
method.setAccessible(true);
return (VirtualScrollBar) method.invoke(vf);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException |
InvocationTargetException ex) {
throw new RuntimeException(ex);
}
}
}
Just call getVbar(getVirtualFlow(listViewInstance)) and you will have a scrollbar in your hands. Then you might add listener to the valueProperty of the scrollbar.