Reputation: 3396
I have a Stack in which several widget can be dragged around. In addition, the container that the Stack is in has a GestureDetector to trigger on onTapDown and onTapUp. I want those onTap events only to be triggered when the user taps outside of the widget in the Stack. I've tried the following code:
class Gestures extends StatefulWidget {
@override
State<StatefulWidget> createState() => _GesturesState();
}
class _GesturesState extends State<Gestures> {
Color background;
Offset pos;
@override
void initState() {
super.initState();
pos = Offset(10.0, 10.0);
}
@override
Widget build(BuildContext context) => GestureDetector(
onTapDown: (_) => setState(() => background = Colors.green),
onTapUp: (_) => setState(() => background = Colors.grey),
onTapCancel: () => setState(() => background = Colors.grey),
child: Container(
color: background,
child: Stack(
children: <Widget>[
Positioned(
top: pos.dy,
left: pos.dx,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanUpdate: _onPanUpdate,
// onTapDown: (_) {}, Doesn't affect the problem
child: Container(
width: 30.0,
height: 30.0,
color: Colors.red,
),
),
)
],
),
),
);
void _onPanUpdate(DragUpdateDetails details) {
RenderBox renderBox = context.findRenderObject();
setState(() {
pos = renderBox.globalToLocal(details.globalPosition);
});
}
}
However, when starting to drag the widget, the onTap of the outermost container is triggered as well, making the background momentarily go green in this case. Settings HitTestBehavior.opaque
doesn't seem to work like I'd expect. Neither does adding a handler for onTapDown to the widget in the Stack.
So, how do I prevent onTapDown from being triggered on the outermost GestureDetector when the user interacts with the widget inside of the Stack?
Update:
An even simpler example of the problem I'm encountering:
GestureDetector(
onTapDown: (_) {
print("Green");
},
child: Container(
color: Colors.green,
width: 300.0,
height: 300.0,
child: Center(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (_) {
print("Red");
},
child: Container(
color: Colors.red,
width: 50.0,
height: 50.0,
),
),
),
),
);
When I tap and hold the red container, both "Red" and "Green" are printed even though the inner GestureDetector has HitTestBehavior.opaque
.
Upvotes: 12
Views: 8444
Reputation: 832
I use RiverPod, and have succeeded with these steps. This is a general process, so should work for all use cases (and with your state manager)
(The use case here is to prevent a ListView from scrolling, when I select text on a widget using the mouse).
final textSelectionInProgress = StateProvider<bool>((ref) {
return false;
});
@override
initState() {
super.initState();
focusN = FocusNode();
focusN.addListener(() {
if (focusN.hasFocus) {
widget.ref.read(textSelectionInProgress.notifier).state = true;
} else {
widget.ref.read(textSelectionInProgress.notifier).state = false;
}
});
}
Ensure you add this in the onDispose:
@override
void dispose() {
widget.ref.read(textSelectionInProgress.notifier).state = false;
super.Dispose();
}
bool textSelectionOn = ref.watch(textSelectionInProgress);
physics: textSelectionOn
? NeverScrollableScrollPhysics()
: <you choice>
Upvotes: 0
Reputation: 3396
I found that I could get the behavior that I wanted (making a widget appear transparent to it parent, while still responding to pointer events) by creating a render object with hit test behavior like this:
@override
bool hitTest(BoxHitTestResult result, {@required Offset position}) {
// forward hits to our child:
final hit = super.hitTest(result, position: position);
// but report to our parent that we are not hit when `transparent` is true:
return false;
}
I've published a package with a widget having this behavior here: https://pub.dev/packages/transparent_pointer.
Upvotes: 2
Reputation: 9785
In this answer, I'll solve the simpler example you have given. You are creating the following Widget hierarchy:
- GestureDetector // green
- Container
- Center
- GestureDetector // red
- Container
Therefore the red GestureDetector is a child Widget of the green GestureDetector. The green GestureDetector has the default HitTestBehavior: HitTestBehavior.deferToChild
. That is why onTapDown
is fired for both containers.
Targets that defer to their children receive events within their bounds only if one of their children is hit by the hit test.
Instead, you can use a Stack to build your UI:
- Stack
- GestureDetector // green
- Container
- GestureDetector // red
- Container
This structure would result in the follwing sourcecode. It looks the same, but the behavior is the one you desired:
Stack(
alignment: Alignment.center,
children: <Widget>[
GestureDetector(
onTapDown: (_) {
print("Green");
},
child: Container(
color: Colors.green,
width: 300.0,
height: 300.0,
),
),
GestureDetector(
onTapDown: (_) {
print("Red");
},
child: Container(
color: Colors.red,
width: 50.0,
height: 50.0,
),
)
],
)
Upvotes: 4