In Flutter, how can a positioned Widget feel taps outside of its parent Stack area?

12,593

Solution 1

This behavior occurs because the stack checks whether the pointer is inside its bounds before checking whether a child got hit:

Class: RenderBox (which RenderStack extends)

bool hitTest(BoxHitTestResult result, { @required Offset position }) {
    ...
    if (_size.contains(position)) {
      if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
        result.add(BoxHitTestEntry(this, position));
        return true;
      }
    }
    return false;
}

My workaround is deleting the

if (_size.contains(position))

check. Unfortunately, this is not possible without copying code from the framework.

Here is what I did:

  • Copied the Stack class and named it Stack2
  • Copied RenderStack and named it RenderStack2
  • Made Stack2 reference RenderStack2
  • Added the hitTest method from above without the _size.contains check
  • Copied Positioned and named it Positioned2 and made it reference Stack2 as its generic parameter
  • Used Stack2 and Positioned2 in my code

This solution is by no means optimal, but it achieves the desired behavior.

Solution 2

I had a similar issue. Basically since the stack's children don't use the fully overflown box size for their hit testing, i used a nested stack and an arbitrary big height so that i can capture the clicks of the nested stack's overflown boxes. Not sure if it can work for you but here goes nothing :)

So in your example maybe you could try something like that

Stack(
  clipBehavior: Clip.none,
  children: [
    Positioned(
    top: 0.0,
    left: 0.0,
    height : 500.0 // biggest possible child size or just very big 
    child: Stack(
      children: [MyWidget()]
    ),
  )],
);

Solution 3

You can consider using inheritance to copy the hitTest method to break the hit rule, example

class Stack2 extends Stack {
  Stack2({
    Key key,
    AlignmentGeometry alignment = AlignmentDirectional.topStart,
    TextDirection textDirection,
    StackFit fit = StackFit.loose,
    Overflow overflow = Overflow.clip,
    List<Widget> children = const <Widget>[],
  }) : super(
          key: key,
          alignment: alignment,
          textDirection: textDirection,
          fit: fit,
          overflow: overflow,
          children: children,
        );
  @override
  RenderStack createRenderObject(BuildContext context) {
    return RenderStack2(
      alignment: alignment,
      textDirection: textDirection ?? Directionality.of(context),
      fit: fit,
      overflow: overflow,
    );
  }
}
class RenderStack2 extends RenderStack {
  RenderStack2({
    List<RenderBox> children,
    AlignmentGeometry alignment = AlignmentDirectional.topStart,
    TextDirection textDirection,
    StackFit fit = StackFit.loose,
    Overflow overflow = Overflow.clip,
  }) : super(
          children: children,
          alignment: alignment,
          textDirection: textDirection,
          fit: fit,
          overflow: overflow,
        );
  @override
  bool hitTest(BoxHitTestResult result, {Offset position}) {
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
    return false;
  }
}

Solution 4

Ok, I did a workaround about this, basically I added a GestureDetector on the parent and implemented the onTapDown. Also you have to keep track your Widget using GlobalKey to get the current position.

When the Tap at the parent level is detected check if the tap position is inside your widget.

The code below:

final GlobalKey key = new GlobalKey();
      void onTapDown(BuildContext context, TapDownDetails details) {
        final RenderBox box = context.findRenderObject();
        final Offset localOffset = box.globalToLocal(details.globalPosition);
        final RenderBox containerBox = key.currentContext.findRenderObject();
        final Offset containerOffset = containerBox.localToGlobal(localOffset);
        final onTap = containerBox.paintBounds.contains(containerOffset);
        if (onTap){
          print("DO YOUR STUFF...");
        }
      }
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onTapDown: (TapDownDetails details) => onTapDown(context, details),
          child: Container(
            color: Colors.red,
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
            child: Align(
              alignment: Alignment.topLeft,
                      child: SizedBox(
                width: 200.0,
                height: 400.0,
                child: Container(
                  color: Colors.black,
                    child: Stack(
                      overflow: Overflow.visible,
                      children: [
                        Positioned(
                          top: 0.0, left: 0.0,
                                          child: Container(
                            key: key,
                            width: 500.0,
                            height: 200.0,
                            color: Colors.blue,
                          ),
                        ),
                      ],
                    ),
                ),
              ),
            ),
          ),
        );
      } 
Share:
12,593
MarcG
Author by

MarcG

Currently living in São Paulo and Rio de Janeiro - Brazil. I hold both Brazilian and European (Polish) passports. Aeronautical-Mechanical Engineer (Technological Institute of Aeronautics ITA, the "Brazilian MIT"). MBA (PUC-RJ) Software Architect and Developer. Previously C++, but now mostly Dart/Flutter (see https://pub.dev/publishers/glasberg.dev/packages), JavaScript (Typescript) and Java. Love TDD, BDD and Clean Code. Also very interested in Usability and Gamification. Excellent communication skills and technical writing. I program since I was 10 yo. Love travelling, astronomy, science and games (playing and making them). Links: https://github.com/marcglasberg https://www.linkedin.com/in/marcglasberg/ https://medium.com/flutter-community/https-medium-com-marcglasberg-async-redux-33ac5e27d5f6 https://medium.com/flutter-community/i18n-extension-flutter-b966f4c65df9 Patents: https://www.google.com/patents/US7917437 https://www.google.com/patents/US7596530 Fluent English and Portuguese, good Spanish, some French. Come visit Rio and rent my place in AirBnB: https://www.airbnb.com/rooms/13830632 [email protected]

Updated on December 05, 2022

Comments

  • MarcG
    MarcG 13 minutes

    A Stack contains MyWidget inside of a Positioned.

    Stack(
      overflow: Overflow.visible,
      children: [
        Positioned(
        top: 0.0,
        left: 0.0,
        child: MyWidget(),
      )],
    );
    

    Since overflow is Overflow.visible and MyWidget is larger than the Stack, it displays outside of the Stack, which is what I want.

    However, I can't tap in the area of MyWidget which is outside of the Stack area. It simply ignores the tap there.

    How can I make sure MyWidget accepts gestures there?

    • diegoveloper
      diegoveloper over 4 years
      It's a duplicated question : stackoverflow.com/questions/51188344/…
    • MarcG
      MarcG over 4 years
      It's not a duplicate. That question only asks if that's intended behavior, which I know it is. I just want to know a workaround to make it accept gestures anyway.
    • Rémi Rousselet
      Rémi Rousselet over 4 years
      That is not possible. The only solution is to refactor your layout to remove the overflow.
    • Rémi Rousselet
      Rémi Rousselet over 4 years
      Depending on what you want, instead of Stack you may simply want to use Overlay instead.
  • MarcG
    MarcG over 4 years
    So, you are actually detecting if MyWidget is tapped? Assuming it's rectangular. Hm, I guess this is useful for some use cases, so I'll vote this answer up. But in my case MyWidget is a complex widget, and it needs to receive its own gestures to make its job. For example, if it contains a ListView and buttons, this solution still doesn't solve the problem.
  • buttonsrtoys
    buttonsrtoys over 2 years
    Has changing the framework been proposed to the Flutter team on Github? Seems like a property could be added to Stack to allow pointer events outside of the bounds of the Stack
  • Benno
    Benno 6 months
    I set up a demo and posted this issue to the Flutter team's GitHub github.com/flutter/flutter/issues/104519