Stop event bubbling from Flutter Listener widget

Issue

I’m making an Android app in Flutter and I want to add an interactive widget that listens to raw pointer events and reacts to them. This is easy to do using the Listener widget:

class _MyWidget extends State<MyWidget> {
  @override
  build(BuildContext ctx) {
    return Listener(
      onPointerMove: (event) {
        event.delta // use this to do some magic...
      },
      child: ...
    );
  }
}

However, this widget is rendered inside a PageView, which allows the user to swipe in order to change pages. This is a behavior I don’t want to get rid of – except for the time when the user swipes over MyWidget. Since the MyWidget requires the user to touch-and-drag, I don’t want them to accidentaly nagivate to a different page.

In a browser, this would be extremely simple to implement, I’d just call event.stopPropagation() and the event wouldn’t propagate (ie. bubble) from MyWidget to its ancestor PageView. How do I stop propagation of an event in Flutter?

I could, in theory, make MyWidget set the application state, and use that to disable PageView while I’m swiping over MyWidget. However, that would go against the natural dataflow of Flutter’s widgets: it would require me to add callbacks on multiple places and make all the widgets more intertwined and less reusable. I would much prefer to prevent the event from bubbling, locally.


EDIT: I’ve tried using AbsorbPointer, however it seems to be "blocking propagation" in the wrong direction. It blocks all children of AbsorbPointer from recieving pointer events. What I want is quite the opposite – stop pointer events on a child from propagating to its ancestors.

Solution

In Flutter, usually there is only 1 widget that would respond to user touch events, instead of everything at the touch location responding together. To determine which widget should be the one answering a touch event, Flutter uses something called "gesture arena" to determine a "winner".

Listener is a raw listening widget that does not compete in the "gesture arena", so the underlying PageView will still "win" in the "gesture arena" even though Listener is technically closer to the user (and would’ve won).

On the other hand, more advanced (less raw) widgets such as GestureDetector does enter the "gesture arena" competition, and will "win", thus causing the PageView to "lose" in the arena, so the page view would not move.

For example, try putting this code inside a PageView and drag on it:

GestureDetector(
  onHorizontalDragUpdate: (DragUpdateDetails details) {
    print('on Horizontal Drag Update');
  },
  onVerticalDragUpdate: (DragUpdateDetails details) {
    print('on Vertical Drag Update');
  },
  child: Container(
    width: 100,
    height: 100,
    color: Colors.red,
  ),
)

You should notice that the PageView no longer scrolls.

However, depending on the scroll axis of the PageView (whether it’s scrolling horizontally or vertically), removing one of the event on GestureDetector above (e.g. remove onHorizontalDragUpdate event listener on a horizontally scrolling PageView) would enable the PageView to scroll again. This is because horizontal swiping and vertical swiping are "non-conflicting events" that enter different gesture arenas.

So, go back to your original question, can you rewrite your business logic using these "more advanced" widgets, instead of dealing with the raw level data Listener? If so, the problem will be solved by the framework for you.

If you must use Listener for some reason, I have another possible workaround for you: In PageView widget, you can pass in physics: const NeverScrollableScrollPhysics() to make it stop scrolling. So you can perhaps monitor touch events, and set/clear property accordingly. Essentially, at "touch down", lock the PageView, and at "touch up", free it.

Answered By – user1032613

Answer Checked By – David Marino (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *