spkersten
spkersten

Reputation: 3396

How do I prevent onTapDown from being triggered on a parent widgets GestureDetector?

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

Answers (3)

Sunil Gupta
Sunil Gupta

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

  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>

Upvotes: 0

spkersten
spkersten

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

NiklasPor
NiklasPor

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

Related Questions