JavaFX ListView with touch events for scrolling up and down
Asked Answered
A

4

5

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);
    }
}
Approachable answered 23/10, 2014 at 21:11 Comment(1)
Please take a look at my answer. A very simple answer without extensions or so.Carilla
A
9

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

Approachable answered 3/11, 2014 at 21:55 Comment(2)
When I run this I get: The constructor ScrollEvent(Object, EventTarget, EventType<ScrollEvent>, int, int, int, int, boolean, boolean, boolean, boolean, boolean, boolean, int, int, int, int, ScrollEvent.HorizontalTextScrollUnits, int, ScrollEvent.VerticalTextScrollUnits, int, int, null) is undefinedTetratomic
Nice work. I used it on a TableView to simulate panning without having a visible scroll bar. Set the scrollbar as target and the table as source.Finnell
T
0

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

Tia answered 30/6, 2016 at 15:32 Comment(0)
N
0

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

Nonentity answered 30/6, 2016 at 20:17 Comment(0)
C
0

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.

Carilla answered 3/9, 2016 at 8:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.