9

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.

spkersten
  • 2,849
  • 21
  • 20
  • I don't know if it can help, but cuold you try chanhe outer `GestureDetector` to `Listener`? – Andrii Turkovskyi Nov 09 '18 at 20:01
  • @AndreyTurkovsky I'm not sure if I understand what you mean. Just replacing the outer detector with a Listener and using onPointerDown etc. like I used onTapDown does fix the problem. (This is a reduced example, in the real code the outer GestureDetector is further up the stack in a different Widget. So I'd prefer no to change that GestureDetector if possible) – spkersten Nov 09 '18 at 20:21
  • I encountered the same issue, did you find a solution for this? – rasitayaz Oct 08 '21 at 21:58
  • 1
    @RasitAyaz I've created https://pub.dev/packages/transparent_pointer for this. – spkersten Oct 12 '21 at 11:32
  • I tried that package but it doesn't seem to work in my situation. Parent's `onTap` function is not being triggered but its `onTapDown` function is being triggered. Any suggestions? – rasitayaz Oct 14 '21 at 19:24

3 Answers3

2

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,
      ),
    )
  ],
)
NiklasPor
  • 9,116
  • 1
  • 45
  • 47
1

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.

spkersten
  • 2,849
  • 21
  • 20
0

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).

  1. Create a notifier
final textSelectionInProgress = StateProvider<bool>((ref) {
  return false;
});
  1. When there is an action (in my case onTap) on the widget, wrap widget with a Focus, or use the focusNode of the widget, and the following code in initState
  @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();
  }
  1. Add listener in the build of the widget you want to stop scrolling
bool textSelectionOn = ref.watch(textSelectionInProgress);
  1. Set ScrollPhysics appropriately
physics: textSelectionOn
            ? NeverScrollableScrollPhysics()
            : <you choice>
Sunil Gupta
  • 666
  • 6
  • 20