Flutter scroll screen when pointer reaches edge

544

Here's how I'm solving it. Using TickerProviderStateMixin, you can obtain a Ticker that invokes a callback once per frame, where you can adjust the scroll offset by a small amount for a smooth scroll. I used a Stack to add dummy DragTargets to the top and bottom of the list area which control the tickers. I used two per edge, to allow different scrolling speeds. You could probably use a Listener to interpolate the speed using the cursor position if you want finer-grained control.

https://www.dartpad.dev/acb83fdbbbbb0fd765cd5afa414a8942

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(),
    body: Stack(
      children: [
        ListView.separated(
          controller: controller,
          itemCount: 50,
          itemBuilder: (context, index) {
            return buildLongPressDraggable(index);
          },
          separatorBuilder: (context, index) {
            return Divider();
          },
        ),
        Positioned(
            top: 0, left: 0, right: 0, height: 25, child: buildEdgeScroller(-10)),
        Positioned(
            top: 25, left: 0, right: 0, height: 25, child: buildEdgeScroller(-5)),
        Positioned(
            bottom: 25, left: 0, right: 0, height: 25, child: buildEdgeScroller(5)),
        Positioned(
            bottom: 0, left: 0, right: 0, height: 25, child: buildEdgeScroller(10)),
      ],
    ),
  );
}

Widget buildEdgeScroller(double offsetPerFrame) {
  return DragTarget<int>(
    builder: (context, candidateData, rejectedData) => Container(),
    onWillAccept: (data) {
      scrollTicker = this.createTicker((elapsed) {
        if (!controller.hasClients) {
          return;
        }
        final position = controller.position;
        if ((offsetPerFrame < 0 && position.pixels <= position.minScrollExtent) ||
            (offsetPerFrame > 0 && position.pixels >= position.maxScrollExtent)) {
          scrollTicker.stop();
          scrollTicker.dispose();
          scrollTicker = null;
        } else {
          controller.jumpTo(controller.offset + offsetPerFrame);
        }
      });
      scrollTicker.start();
      return false;
    },
    onLeave: (data) {
      scrollTicker?.stop();
      scrollTicker?.dispose();
      scrollTicker = null;
    },
  );
}
Share:
544
SeriousMonk
Author by

SeriousMonk

Updated on December 29, 2022

Comments

  • SeriousMonk
    SeriousMonk over 1 year

    I have a GridView that contains draggable items. When an item is dragged to the top/bottom of the screen I want to scroll the GridView in that direction.

    Currently I wrapped each draggable item in a Listener like so:

    Listener(
          child: _wrap(widget.children[i], i),
          onPointerMove: (PointerMoveEvent event) {
            if (event.position.dy >= MediaQuery.of(context).size.height - 100) {
              // 120 is height of your draggable.
              widget.scrollController.animateTo(
                widget.scrollController.offset + 120,
                curve: Curves.easeOut,
                duration: const Duration(milliseconds: 200));
            }if (event.position.dy <= kToolbarHeight + MediaQueryData.fromWindow(window).padding.top + 100) {
              // 120 is height of your draggable.
              widget.scrollController.animateTo(
                  widget.scrollController.offset - 120,
                  curve: Curves.easeOut,
                  duration: const Duration(milliseconds: 200));
            }
          }
        )
    

    It works, but the scroll is not smooth at all and looks kind of laggy. I would need it to work on web too.

    Does anyone have a better solution for this?

    • Abhilash Chandran
      Abhilash Chandran about 3 years
      Could you share a demo in dartpad. Would be helpful to check the problem directly. One thing I am aware is often the position returned in case of web turnsout to be zero.
  • SeriousMonk
    SeriousMonk about 3 years
    Thank you! I thought about using drag targets for this but didn't know how. I'm currently migrating my app to null-safety, but as soon as I'm done with that I'll try you solution. I am very confident it'll work just fine :)
  • SeriousMonk
    SeriousMonk about 3 years
    Works like a charm! For people using null-safety just use Ticker? scrollTicker insted of Ticker scrollTicker and inside the buildEdgeScroller method use scrollTicker!.stop(), scrollTicker!.dispose() and scrollTicker?.start()
  • Chuender
    Chuender about 2 years
    You rock, @blake raymond! Thanks!