Théo Champion
Théo Champion

Reputation: 1978

Pass trough all gestures between two widgets in Stack

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!

Upvotes: 25

Views: 6935

Answers (2)

Lukas Pierce
Lukas Pierce

Reputation: 1239

You can use the gestureRecognizers property of the GoogleMap widget. These recognizers do not block map gestures and work in parallel.

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class ExampleMap extends StatelessWidget {
  const ExampleMap({super.key});

  @override
  Widget build(BuildContext context) {
    return GoogleMap(
      gestureRecognizers: {
        // This gesture recognrizer is not blocking GoogleMap gestures
        // and works parallely
        Factory<PanGestureRecognizer>(() {
          return PanGestureRecognizer()
            ..onDown = (DragDownDetails details) {
              // [details] contains global and local position pixel coordinates
              // at which the pointer contacted the screen
              //
              // I think you have already calculated coordinates
              // of all markers judging by your Stack.
              //
              // So you can find your marker which includes
              // this coordinate from [details]
              // Even serveral markers and choose from them toppes.
              //
              // And if such marker found then run
              // your 'markerTapHandler' from here
              print(
                '[onDown]:\n'
                '  localPosition: ${details.localPosition}\n'
                '  globalPosition: ${details.globalPosition}',
              );
            };
        }),
      },

      // Los Angeles
      initialCameraPosition: const CameraPosition(
        target: LatLng(34.051253266721325, -118.23510468006135),
        zoom: 10.313197135925293,
      ),
    );
  }
}

Upvotes: 0

spkersten
spkersten

Reputation: 3386

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

Upvotes: 41

Related Questions