Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smooth scrolling #27

Closed
mikehearn opened this issue Aug 29, 2016 · 8 comments
Closed

Smooth scrolling #27

mikehearn opened this issue Aug 29, 2016 · 8 comments

Comments

@mikehearn
Copy link

VirtualFlow calculates a reasonable jump size when using the mouse wheel or scroll bar increment/decrement buttons. This leads to reasonable scrolling behaviour but with large content (e.g. a many page PDF) the scroll amount can be quite large, like 20 pixels or so, and this leads to a jerky feeling when scrolling.

Seeing as how ReactFX has the nice Val.animate() method it would probably be quite easy to fix this. I had a quick go at doing so without hacking Flowless itself but it seems the methods I'd need are all (package) private. Perhaps there's a simple trick to get this; I guess inserting a simple animated val with short duration between the desired scroll position and the actual position is sufficient.

@JordanMartinez
Copy link
Contributor

JordanMartinez commented Mar 31, 2017

Now that I've documented the code in #33, I don't know if your Val.animate idea is feasible. In my understanding, the viewport lays out its content first, and then checks whether there is unused space. If there is, it may relayout that content a second time. Since the scroll values are estimates calculated from the averages of the displayed cells' nodes, I'd guess that these jerky value changes are a result of these averages being recalculated when the displayed nodes are repositioned or nodes are added/removed.

Instead, I'd suggest the code does something similar to the RichTexfFX approach of suspending a value when the view is being updated and resuming it when its finished.

@JordanMartinez
Copy link
Contributor

Can you give a standalone test that demonstrates this jerkiness? I'm assuming the solution would be to wrap the layoutChildren and any show()-related methods in a Suspendables.combine(estimatedScrollX, estimatedScrollY).suspendWhile() block

@palexdev
Copy link

Still no support for smooth scrolling?
I tried to make it work with the same strategy I use for JavaFX's scroll panes, see here: MaterialFX ScrollPane, but it won't work

@Jugen
Copy link
Contributor

Jugen commented Mar 21, 2021

What happens when you try your method on a VirtualFlow ?
Can you provide a demo with both a ScrollPane and a VirtualFlow with the same content that show/compares the behavior you want ?

@palexdev
Copy link

2021-03-21_14-07-36.mp4

As you can see the handler in never executed with the VirtualizedScrollPane.
I had to modify the code a bit though because VirtualizedScrollPane doesn't have vvalue and hvalue properties.
I had to use reflection and change the smooth scroll code to:

    private static void customScrolling(MFXVirtualizedScrollPane<?> scrollPane, DoubleProperty scrollDirection, Function<Bounds, Double> sizeFunc) {
        final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
        final double[] pushes = {1};
        final double[] derivatives = new double[frictions.length];

        Timeline timeline = new Timeline();
        final EventHandler<MouseEvent> dragHandler = event -> timeline.stop();
        final EventHandler<ScrollEvent> scrollHandler = event -> {
            System.out.println("Smooth Scrolling");
            if (event.getEventType() == ScrollEvent.SCROLL) {
                int direction = event.getDeltaY() > 0 ? -1 : 1;
                for (int i = 0; i < pushes.length; i++) {
                    derivatives[i] += direction * pushes[i];
                }
                if (timeline.getStatus() == Animation.Status.STOPPED) {
                    timeline.play();
                }
                event.consume();
            }
        };
        if (scrollPane.getContent().getParent() != null) {
            scrollPane.getContent().getParent().addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
            scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
        }
        scrollPane.getContent().parentProperty().addListener((observable, oldValue, newValue) -> {
            if (oldValue != null) {
                oldValue.removeEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
                oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
            }
            if (newValue != null) {
                newValue.addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
                newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
            }
        });
        timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), (event) -> {
            for (int i = 0; i < derivatives.length; i++) {
                derivatives[i] *= frictions[i];
            }
            for (int i = 1; i < derivatives.length; i++) {
                derivatives[i] += derivatives[i - 1];
            }
            double dy = derivatives[derivatives.length - 1];
            double size = sizeFunc.apply(scrollPane.getContent().getLayoutBounds());
            scrollDirection.set(Math.min(Math.max(scrollDirection.get() + dy / size, 0), 1));
            if (Math.abs(dy) < 0.001) {
                timeline.stop();
            }
        }));
        timeline.setCycleCount(Animation.INDEFINITE);
    }
    
    public static void smoothVScrolling(MFXVirtualizedScrollPane<?> scrollPane) {
        try {
            Field vvalue = VirtualizedScrollPane.class.getDeclaredField("vBarValue");
            vvalue.setAccessible(true);
            Var<Double> obj = (Var<Double>) vvalue.get(scrollPane);
            DoubleProperty doubleProperty = new SimpleDoubleProperty();
            doubleProperty.bind(obj);
            customScrolling(scrollPane, doubleProperty, Bounds::getHeight);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

@Jugen
Copy link
Contributor

Jugen commented Mar 25, 2021

Here you go, try this slightly modified version of the code you provided:

// Changed method signature to use VirtualFlow and its estimated scroll property directly
private static void customScrolling( VirtualFlow<?,?> flowPane, Var<Double> scrollDirection ) {
    final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
    final double[] pushes = {1};
    final double[] derivatives = new double[frictions.length];

    Timeline timeline = new Timeline();
    final EventHandler<MouseEvent> dragHandler = event -> timeline.stop();
    final EventHandler<ScrollEvent> scrollHandler = event -> {
        if (event.getEventType() == ScrollEvent.SCROLL) {
            System.out.println("Smooth Scrolling");
            int direction = event.getDeltaY() > 0 ? -1 : 1;
            for (int i = 0; i < pushes.length; i++) {
                derivatives[i] += direction * pushes[i];
            }
            if (timeline.getStatus() == Animation.Status.STOPPED) {
                timeline.play();
            }
            event.consume();
        }
    };

    // Changed this to get ScrollEvents to work, "Smooth Scrolling" prints now :-)
    flowPane.addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
    flowPane.addEventHandler(ScrollEvent.ANY, scrollHandler);

    timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), (event) -> {
        for (int i = 0; i < derivatives.length; i++) {
            derivatives[i] *= frictions[i];
        }
        for (int i = 1; i < derivatives.length; i++) {
            derivatives[i] += derivatives[i - 1];
        }
        double dy = derivatives[derivatives.length - 1];

        // Changed this as values aren't between 0 & 1
        scrollDirection.setValue(scrollDirection.getValue() + dy);

        if (Math.abs(dy) < 0.001) {
            timeline.stop();
        }
    }));
    timeline.setCycleCount(Animation.INDEFINITE);
}

@Jugen
Copy link
Contributor

Jugen commented Mar 25, 2021

Sorry the method signature can be reduced to:

private static void customScrolling( VirtualFlow<?,?> flowPane, Var<Double> scrollDirection )

which then is invoked with:

customScrolling( virtualFlow, virtualFlow.estimatedScrollYProperty() );

@palexdev
Copy link

palexdev commented Apr 11, 2021

@Jugen Hello, sorry for the late response.
I can confirm that now the smooth scrolling works, however there still seems to be some issues:

  1. The DRAG_DETECTED handler is not working. So if the view is scrolling, and you click on the scrollbar it doesn't stop scrolling.
  2. There's some sort of "jump" when the scrolling starts which should not happen
2021-04-11.12-41-49_Trim.mp4

EDIT: I was wondering if I could fix it by changing the type of handler... Yep, everything works now
The code now is:

    public static void setSmoothScrolling(VirtualFlow<?, ?> flow, Var<Double> scrollDirection) {
        final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
        final double[] pushes = {1};
        final double[] derivatives = new double[frictions.length];

        Timeline timeline = new Timeline();
        final EventHandler<MouseEvent> dragHandler = event -> {
            System.out.println("STOP!");
            timeline.stop();
        };

        final EventHandler<ScrollEvent> scrollHandler = event -> {
            if (event.getEventType() == ScrollEvent.SCROLL) {
                System.out.println("Smooth Scrolling");
                int direction = event.getDeltaY() > 0 ? -1 : 1;
                for (int i = 0; i < pushes.length; i++) {
                    derivatives[i] += direction * pushes[i];
                }
                if (timeline.getStatus() == Animation.Status.STOPPED) {
                    timeline.play();
                }
                event.consume();
            }
        };

        if (flow.getParent() != null) {
            flow.getParent().addEventFilter(MouseEvent.DRAG_DETECTED, dragHandler);
        }
       flow.parentProperty().addListener((observable, oldValue, newValue) -> {
            if (oldValue != null) {
                oldValue.removeEventFilter(MouseEvent.DRAG_DETECTED, dragHandler);
            }
            if (newValue != null) {
                newValue.addEventFilter(MouseEvent.DRAG_DETECTED, dragHandler);
            }
        });
        flow.addEventFilter(MouseEvent.DRAG_DETECTED, dragHandler);
        flow.addEventFilter(ScrollEvent.ANY, scrollHandler);

        timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), (event) -> {
            for (int i = 0; i < derivatives.length; i++) {
                derivatives[i] *= frictions[i];
            }
            for (int i = 1; i < derivatives.length; i++) {
                derivatives[i] += derivatives[i - 1];
            }
            double dy = derivatives[derivatives.length - 1];

            scrollDirection.setValue(scrollDirection.getValue() + dy);

            if (Math.abs(dy) < 0.001) {
                timeline.stop();
            }
        }));
        timeline.setCycleCount(Animation.INDEFINITE);
    }

I changed the handlers to filters and everything seems to work good.
The only thing I'm going to change is making this method private and create a new public method that accepts a list view as a parameter because the virtual flow is in the skin class.
This method will probably look like this:

    public static void setSmoothScrolling(AbstractMFXFlowlessListView<?, ?, ?> listView) {
        if (listView.getScene() != null) {
            VirtualFlow<?, ?> flow = (VirtualFlow<?, ?>) listView.lookup(".virtual-flow");
            setSmoothScrolling(flow, flow.estimatedScrollYProperty());
        } else {
            listView.skinProperty().addListener(new ChangeListener<>() {
                @Override
                public void changed(ObservableValue<? extends Skin<?>> observable, Skin<?> oldValue, Skin<?> newValue) {
                    if (newValue != null) {
                        VirtualFlow<?, ?> flow = (VirtualFlow<?, ?>) listView.lookup(".virtual-flow");
                        setSmoothScrolling(flow, flow.estimatedScrollYProperty());
                        listView.skinProperty().removeListener(this);
                    }
                }
            });
        }
    }

@Jugen Jugen closed this as completed Jun 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants