Flutter Web "smooth scrolling" on WheelEvent within a PageView
Asked Answered
D

4

7

With the code below

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) => MaterialApp(
        home: const MyHomePage(),
      );
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) => DefaultTabController(
        length: 2,
        child: Scaffold(
          appBar: AppBar(
            title: const Center(
            child: Text('use the mouse wheel to scroll')),
            bottom: TabBar(
              tabs: const [
                Center(child: Text('ScrollView')),
                Center(child: Text('PageView'))
              ],
            ),
          ),
          body: TabBarView(
            children: [
              SingleChildScrollView(
                child: Column(
                  children: [
                    for (int i = 0; i < 10; i++)
                      Container(
                        height: MediaQuery.of(context).size.height,
                        child: const Center(
                          child: FlutterLogo(size: 80),
                        ),
                      ),
                  ],
                ),
              ),
              PageView(
                scrollDirection: Axis.vertical,
                children: [
                  for (int i = 0; i < 10; ++i)
                    const Center(
                      child: FlutterLogo(size: 80),
                    ),
                ],
              ),
            ],
          ),
        ),
      );
}

You can see, running it on dartpad or from this video,

that using the mouse wheel to scroll a PageView provides a mediocre experience (at best),

This is a known issue #35687 #32120, but I'm trying to find a workaround

to achieve either smooth scrolling for the PageView or at least prevent the "stutter".

Can someone help me out or point me in the right direction?

I'm not sure the issue is with PageScrollPhysics;

I have a gut feeling that the problem might be with WheelEvent

since swiping with multitouch scroll works perfectly

Duero answered 25/8, 2020 at 14:59 Comment(1)
here is a super lame hack dartpad.dartlang.org/c3476573514298b9885b78a0275c744cDuero
R
8

The problem arises from chain of events:

  1. user rotate mouse wheel by one notch,
  2. Scrollable receives PointerSignal and calls jumpTo method,
  3. _PagePosition's jumpTo method (derived from ScrollPositionWithSingleContext) updates scroll position and calls goBallistic method,
  4. requested from PageScrollPhysics simulation reverts position back to initial value, since produced by one notch offset is too small to turn the page,
  5. another notch and process repeated from step (1).

One way to fix issue is perform a delay before calling goBallistic method. This can be done in _PagePosition class, however class is private and we have to patch the Flutter SDK:

// <FlutterSDK>/packages/flutter/lib/src/widgets/page_view.dart
// ...

class _PagePosition extends ScrollPositionWithSingleContext implements PageMetrics {
  //...

  // add this code to fix issue (mostly borrowed from ScrollPositionWithSingleContext):
  Timer timer;

  @override
  void jumpTo(double value) {
    goIdle();
    if (pixels != value) {
      final double oldPixels = pixels;
      forcePixels(value);
      didStartScroll();
      didUpdateScrollPositionBy(pixels - oldPixels);
      didEndScroll();
    }
    if (timer != null) timer.cancel();
    timer = Timer(Duration(milliseconds: 200), () {
      goBallistic(0.0);
      timer = null;
    });
  }

  // ...
}

Another way is to replace jumpTo with animateTo. This can be done without patching Flutter SDK, but looks more complicated because we need to disable default PointerSignalEvent listener:

import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class PageViewLab extends StatefulWidget {
  @override
  _PageViewLabState createState() => _PageViewLabState();
}

class _PageViewLabState extends State<PageViewLab> {
  final sink = StreamController<double>();
  final pager = PageController();

  @override
  void initState() {
    super.initState();
    throttle(sink.stream).listen((offset) {
      pager.animateTo(
        offset,
        duration: Duration(milliseconds: 200),
        curve: Curves.ease,
      );
    });
  }

  @override
  void dispose() {
    sink.close();
    pager.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Mouse Wheel with PageView'),
      ),
      body: Container(
        constraints: BoxConstraints.expand(),
        child: Listener(
          onPointerSignal: _handlePointerSignal,
          child: _IgnorePointerSignal(
            child: PageView.builder(
              controller: pager,
              scrollDirection: Axis.vertical,
              itemCount: Colors.primaries.length,
              itemBuilder: (context, index) {
                return Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Container(color: Colors.primaries[index]),
                );
              },
            ),
          ),
        ),
      ),
    );
  }

  Stream<double> throttle(Stream<double> src) async* {
    double offset = pager.position.pixels;
    DateTime dt = DateTime.now();
    await for (var delta in src) {
      if (DateTime.now().difference(dt) > Duration(milliseconds: 200)) {
        offset = pager.position.pixels;
      }
      dt = DateTime.now();
      offset += delta;
      yield offset;
    }
  }

  void _handlePointerSignal(PointerSignalEvent e) {
    if (e is PointerScrollEvent && e.scrollDelta.dy != 0) {
      sink.add(e.scrollDelta.dy);
    }
  }
}

// workaround https://github.com/flutter/flutter/issues/35723
class _IgnorePointerSignal extends SingleChildRenderObjectWidget {
  _IgnorePointerSignal({Key key, Widget child}) : super(key: key, child: child);

  @override
  RenderObject createRenderObject(_) => _IgnorePointerSignalRenderObject();
}

class _IgnorePointerSignalRenderObject extends RenderProxyBox {
  @override
  bool hitTest(BoxHitTestResult result, {Offset position}) {
    final res = super.hitTest(result, position: position);
    result.path.forEach((item) {
      final target = item.target;
      if (target is RenderPointerListener) {
        target.onPointerSignal = null;
      }
    });
    return res;
  }
}

Here is demo on CodePen.

Rollandrollaway answered 2/9, 2020 at 13:16 Comment(1)
I tried using animateTo but I got this error: at page_view._PagePosition.new.get pixels [as pixels] (localhost:49511/packages/flutter/src/widgets/…) at main_layout._MainLayoutState.new.throttle (localhost:49511/packages/one_page/…) at throttle.next (<anonymous>) at _AsyncStarImpl.new.runBody (localhost:49511/dart_sdk.js:31858:40) at Object._microtaskLoop dart_sdk.js:39175:13) at _startMicrotaskLoop (dart_sdk.js:39181:13) at localhost:49511/dart_sdk.js:34688:9Rough
W
2

Quite similar but easier to setup:

add smooth_scroll_web ^0.0.4 to your pubspec.yaml

...
dependencies:
    ...
    smooth_scroll_web: ^0.0.4
...

Usage:

import 'package:smooth_scroll_web/smooth_scroll_web.dart';
import 'package:flutter/material.dart';
import 'dart:math'; // only for demo

class Page extends StatefulWidget {
  @override
  PageState createState() => PageState();
}

class PageState extends State<Page> {
  final ScrollController _controller = new ScrollController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("SmoothScroll Example"),
      ),
      body: SmoothScrollWeb(
        controller: controller,
        child: Container(
            height: 1000,
            child: ListView(
              physics: NeverScrollableScrollPhysics(),
              controller: _controller,
              children: [
                // Your content goes here, thoses children are only for demo
                for (int i = 0; i < 100; i++)
                  Container(
                    height: 60,
                    color: Color.fromARGB(1, 
                      Random.secure().nextInt(255),
                      Random.secure().nextInt(255),
                      Random.secure().nextInt(255)),
                  ),
              ],
            ),
          ),
      ),
    );
  }
}

Thanks you hobbister !

Refer to flutter's issue #32120 on Github.

Wilkins answered 9/4, 2021 at 18:43 Comment(3)
thanks, I'm aware of the issue github.com/flutter/flutter/issues/32120#issuecomment-725913608Duero
also I stringly recommend NOT to use packages w/o tests gitlab.com/dezso15/smoothscrollweb/-/tree/master/…Duero
I love this library, it makes flutter scrolling so much better, but just today I noticed if I scroll using the scroll controller, the smooth scroll does not "notice" that and when I try to scroll with my mouse wheel, it teleports me back to the place where I clicked on the button (Which used the scroll controller for scrolling)Clarence
S
2

I know that it has been almost 1.5 year from this question, but I found a way that works smoothly. Maybe this will be very helpful whoever read it. Add a listener to your pageview controller with this code (You can make adjustments on duration or nextPage/animateToPage/jumpToPage etc.):

pageController.addListener(() {
  if (pageController.position.userScrollDirection == ScrollDirection.reverse) {
    pageController.nextPage(duration: const Duration(milliseconds: 60), curve: Curves.easeIn);
  } else if (pageController.position.userScrollDirection == ScrollDirection.forward) {
    pageController.previousPage(duration: const Duration(milliseconds: 60), curve: Curves.easeIn);
  }
});
Subcelestial answered 27/2, 2022 at 21:6 Comment(0)
B
-2

The issue is with the user settings, how the end-user has set the scrolling to happen with his mouse. I have a Logitech mouse that allows me to turn on or off the smooth scrolling capability via Logitech Options. When I enable smooth scrolling it works perfectly and scrolls as required but in case of disabling the smooth scroll it gets disabled on the project as well. The behavior is as set by the end-user.

Still, if there's a requirement to force the scroll to smooth scroll than can only be done by setting relevant animations. There's no direct way as of now.

enter image description here

Bronze answered 29/8, 2020 at 20:17 Comment(1)
sorry pal, but I'm afraid you are missing my point, as you can see from both the issues I linked above either the "WheelEvent" or the "PageViewScrollPhysics" are not behaving properly, this is an assertion, NOT a question. I'm certain that eventually the bug will be fixed / the feature implemented, I'm looking for a workaround or some useful input on how to fix it myself in the meantime, or even just mitigate the problem. Telling the users to change their mouse setting is NOT A SOLUTIONDuero

© 2022 - 2024 — McMap. All rights reserved.