flutterflutter-layoutflutter-webflutter-gridview

Flutter scroll screen when pointer reaches edge


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?


Solution

  • 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;
        },
      );
    }