handle nested scrolls but not only scroll in Flutter


Sometimes we should handle nested scrolls but not only scroll widget, such as zoomable widget, in such case we can not use NestedScrollView or NotificationListener because zoomable widget may not receive ScrollNotification.

For example, we have such a widget:

class ScrollableWidget extends StatelessWidget {
  const ScrollableWidget({super.key});

  @override
  Widget build(BuildContext context) {
    final Controller controller = Get.put(Controller());
    return Listener(
      onPointerSignal: (e) {
        if (e is PointerScrollEvent) {
              x.value++;
          } else if (e.scrollDelta.dy < 0) {
              x.value--;
          }
        }
      },
      child: Obx(() => Text('on mouse wheel up/down ${x.value}')),
    );
  }
}

We build it inside a SingleChildScrollView:

Obviously, it is a bad experience. How could we handle this case? A quick way is to controll the ScrollPhysics of scrollable widget.

First we create a custom ScrollPhysics and override allowUserScrolling:

      home: SingleChildScrollView(
        physics: CustomScrollPhysics(Get.put(Controller())),
        child: Column(
          children: [
            ...List.generate(7, (index) => Text('Item $index')),
            ScrollableWidget(),
            ...List.generate(7, (index) => Text('Item $index')),
          ],
        ),
      ),

      class CustomScrollPhysics extends ScrollPhysics {
      const CustomScrollPhysics(this.controll, {super.parent});

      final Controller controll;

      @override
      ScrollPhysics applyTo(ScrollPhysics? ancestor) {
        return CustomScrollPhysics(controll, parent: buildParent(ancestor));
      }

      @override
      bool get allowUserScrolling => controll.enableScroll;
    }

Then we handle the enableScroll from our widget:

class ScrollableWidget extends StatelessWidget {
  const ScrollableWidget({super.key});

  @override
  Widget build(BuildContext context) {
    final Controller controller = Get.put(Controller());
    return Listener(
      onPointerSignal: (e) {
        if (e is PointerScrollEvent) {
          if (e.scrollDelta.dy > 0) {
            if (x < 10) {
              controller.enableScroll = false;
              x.value++;
            } else {
              controller.enableScroll = true;
            }
          } else if (e.scrollDelta.dy < 0) {
            if (x > 0) {
              Get.put(Controller()).enableScroll = false;
              x.value--;
            } else {
              controller.enableScroll = true;
            }
          }
        }
      },
      child: Obx(() => Text('on mouse wheel up/down ${x.value}')),
    );
  }
}

That’s it!

It is not the only way to handle the case, sometimes you need override the handleEvent method. For example, we extends ZoomPanBehavior when we use syncfusion_flutter_charts package:

class CustomZoomPanBehavior extends ZoomPanBehavior {
  CustomZoomPanBehavior(
  		// ...
  });

  @override
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
    if (event is PointerScrollEvent) {
      if (event.scrollDelta.dy < 0) {
        enableScroll = axisController.zoomFactor <= 0.1;
      } else {
        enableScroll = axisController.zoomFactor >= 1.0;
      }
    } else {
      enableScroll = true;
    }
    super.handleEvent(event, entry);
  }
}