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.
CodePudding user response:
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.