Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass trough all gestures between two widgets in Stack

Tags:

flutter

I'm working on an app where I display markers over a map like so:

enter image description here

The way it works is markers are rendered "over" the Map widget as Stack. My problem is that currently, the markers 'absorbs' the gestures used to control the map underneath (if the gesture starts on the marker).

I was therefore wondering, is there a way to pass through all gestures events between two widgets in a stack? Ideally, the marker would ignore (and pass through) all events except onTap (as I still want to be able to click on the markers).

Here my specific tree:

enter image description here

Cheers!

like image 314
Théo Champion Avatar asked Dec 12 '20 19:12

Théo Champion


1 Answers

When a widget that is (visually) on top of another widget in the same stack is hit, the stack will stop any further hit testing. So, in your case, the second child of the stack containing the GoogleMap widget must be made to report that it is not hit, so the stack will give GoogleMap a chance to react to pointer events. IgnorePointer can do that, however that widget will also not hit test its child, so its child gesture detectors will never be involved in any gesture. In simple cases, that can be worked-around by swapping the order of IgnorePointer and GestureDetector while setting the latter's behavior property to HitTestBehaviour.translucent. For example:

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        home: Stack(
          fit: StackFit.expand,
          children: [
            GestureDetector(
              onDoubleTap: () => print("double red"),
              child: Container(color: Colors.red),
            ),
            Positioned(
              top: 100,
              left: 100,
              right: 100,
              bottom: 100,
              child: GestureDetector(
                behavior: HitTestBehavior.translucent,
                onTap: () => print("green"),
                child: IgnorePointer(
                  child: Container(color: Colors.green),
                ),
              ),
            ),
          ],
        ),
      );
}

Your case is more complicated though. A more generic approach would be to create a new widget like IgnorePointer (let's call it TransparentPointer) that can act to it parent as if it is never hit, while still doing hit testing on its child. Here I've copied IgnorePointer and changed the behavior in that way (the only change with respect to RenderIgnorePointer is in RenderTransparentPointer.hitTest):

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        home: Stack(
          fit: StackFit.expand,
          children: [
            GestureDetector(
              onDoubleTap: () => print("double red"),
              child: Container(color: Colors.red),
            ),
            TransparentPointer(
              transparent: true,
              child: Stack(
                children: [
                  Positioned(
                    top: 100,
                    left: 100,
                    right: 100,
                    bottom: 100,
                    child: GestureDetector(
                      onTap: () => print("green"),
                      child: Container(color: Colors.green),
                    ),
                  )
                ],
              ),
            ),
          ],
        ),
      );
}

class TransparentPointer extends SingleChildRenderObjectWidget {
  const TransparentPointer({
    Key key,
    this.transparent = true,
    Widget child,
  })  : assert(transparent != null),
        super(key: key, child: child);

  final bool transparent;

  @override
  RenderTransparentPointer createRenderObject(BuildContext context) {
    return RenderTransparentPointer(
      transparent: transparent,
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderTransparentPointer renderObject) {
    renderObject
      ..transparent = transparent;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<bool>('transparent', transparent));
  }
}

class RenderTransparentPointer extends RenderProxyBox {
  RenderTransparentPointer({
    RenderBox child,
    bool transparent = true,
  })  : _transparent = transparent,
        super(child) {
    assert(_transparent != null);
  }

  bool get transparent => _transparent;
  bool _transparent;

  set transparent(bool value) {
    assert(value != null);
    if (value == _transparent) return;
    _transparent = value;
  }

  @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 !transparent && hit;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<bool>('transparent', transparent));
  }
}

I have published this code as a small package: https://pub.dev/packages/transparent_pointer

like image 84
spkersten Avatar answered Nov 15 '22 23:11

spkersten