Skip to content

[BUG] When using allowInteraction: true on WidgetLayer, gestures on the map are disabled #378

@NunoCMatos

Description

@NunoCMatos

Platforms

Web

Version of flutter-maplibre

0.3.0

Bug Description

I'm using the exact same code of the Example App but I can't pan, zoom, pitch or rotate the map. However, if I use allowInteraction: false, then I can make them all.

Steps to Reproduce

  1. Copy the same code of the Example App
  2. Try to pan, zoom, pitch or zoom.

Expected Results

I can do all the gestures that I enable in the map options.

Actual Results

I can't do any of them.

Code Sample

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

class MapScreen extends StatefulWidget {
  const MapScreen({super.key});

  @override
  State<MapScreen> createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  late final MapController _mapController;
  final GlobalKey _mapKey = GlobalKey();

  final _markerPositions = [
    const Geographic(lon: -10, lat: 0),
    const Geographic(lon: -5, lat: 0),
    const Geographic(lon: 0, lat: 0),
    const Geographic(lon: 5, lat: 0),
  ];

  Geographic? _originalPosition;
  MapGestures _mapGestures = const MapGestures.all();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Interactive Widget Layer')),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Padding(
            padding: EdgeInsets.only(left: 16, bottom: 8),
            child: Text(
              'Long tap map: Create marker.\nTap marker: Show dialog.\nLong tap marker: Show popup menu.\nTap+drag marker: Move marker.',
              textAlign: TextAlign.left,
            ),
          ),
          Expanded(
            child: MapLibreMap(
              key: _mapKey,
              options: MapOptions(
                initZoom: 3,
                initCenter: const Geographic(lon: 0, lat: 0),
                gestures: _mapGestures,
              ),
              onMapCreated: (controller) => _mapController = controller,
              onEvent: (event) async {
                if (event is MapEventLongClick) {
                  final position = event.point;
                  _markerPositions.add(position);

                  setState(() {});
                }
              },
              children: [
                WidgetLayer(
                  allowInteraction: false,
                  markers: List.generate(
                    _markerPositions.length,
                    (index) => Marker(
                      size: const Size.square(50),
                      point: _markerPositions[index],
                      child: GestureDetector(
                        onTap: () => _onTap(index),
                        onLongPressStart: (details) =>
                            _onLongPress(index, details),
                        onPanStart: (details) => _onPanStart(details, index),
                        onPanUpdate: (details) async =>
                            _onPanUpdate(details, index),
                        onPanEnd: (details) async => _onPanEnd(details, index),
                        child: const Icon(
                          Icons.location_on,
                          color: Colors.red,
                          size: 50,
                        ),
                      ),
                      alignment: Alignment.bottomCenter,
                    ),
                  ),
                ),
                // display the UI widgets above the widget markers.
                const MapScalebar(),
                const SourceAttribution(),
                const MapControlButtons(),
                const MapCompass(),
              ],
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    super.dispose();
  }

  Future<Geographic> _toLngLat(Offset eventOffset) async {
    final mapRenderBox =
        _mapKey.currentContext?.findRenderObject() as RenderBox?;

    assert(mapRenderBox != null, 'RenderBox of Map should never be null');

    final mapOffset = mapRenderBox!.localToGlobal(Offset.zero);

    final offset = Offset(
      eventOffset.dx - mapOffset.dx,
      eventOffset.dy - mapOffset.dy,
    );

    return _mapController.toLngLat(offset);
  }

  void _onLongPress(int index, LongPressStartDetails details) {
    final offset = details.globalPosition;

    showMenu(
      context: context,
      position: RelativeRect.fromLTRB(
        offset.dx,
        offset.dy,
        MediaQuery.of(context).size.width - offset.dx,
        MediaQuery.of(context).size.height - offset.dy,
      ),
      items: [
        const PopupMenuItem<void>(child: Text('Edit')),
        PopupMenuItem<void>(
          onTap: () async {
            final isConfirmed = await _showConfirmationDialogDelete(index);

            if (isConfirmed) {
              _markerPositions.removeAt(index);

              setState(() {});
            }
          },
          child: const Text('Delete'),
        ),
      ],
    );
  }

  Future<void> _onPanEnd(DragEndDetails details, int index) async {
    final isAccepted = await _showConfirmationDialogMove();

    if (!isAccepted) {
      _markerPositions[index] = _originalPosition!;
    } else {
      final newPosition = await _toLngLat(details.globalPosition);
      _markerPositions[index] = newPosition;
    }

    _originalPosition = null;

    setState(() {
      _mapGestures = const MapGestures.all();
    });
  }

  void _onPanStart(DragStartDetails details, int index) {
    // Keep original position in case of discarded move
    _originalPosition = Geographic.from(_markerPositions[index]);

    setState(() {
      // Disable camera panning while a marker gets moved.
      _mapGestures = const MapGestures.all(pan: false);
    });
  }

  Future<void> _onPanUpdate(DragUpdateDetails details, int index) async {
    final newPosition = await _toLngLat(details.globalPosition);
    _markerPositions[index] = newPosition;

    setState(() {});
  }

  void _onTap(int index) {
    _showMarkerDetails(index);
  }

  Future<bool> _showConfirmationDialogDelete(int index) async {
    final isConfirmed = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Delete marker [$index]?'),
        actions: <Widget>[
          TextButton(
            style: TextButton.styleFrom(
              textStyle: Theme.of(context).textTheme.labelLarge,
            ),
            child: const Text('Cancel'),
            onPressed: () {
              Navigator.of(context).pop(false);
            },
          ),
          TextButton(
            style: TextButton.styleFrom(
              textStyle: Theme.of(context).textTheme.labelLarge,
            ),
            child: const Text('Delete'),
            onPressed: () {
              Navigator.of(context).pop(true);
            },
          ),
        ],
      ),
    );

    return isConfirmed ?? false;
  }

  Future<bool> _showConfirmationDialogMove() async {
    final isConfirmed = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Accept new position?'),
        actions: <Widget>[
          TextButton(
            style: TextButton.styleFrom(
              textStyle: Theme.of(context).textTheme.labelLarge,
            ),
            child: const Text('Discard'),
            onPressed: () {
              Navigator.of(context).pop(false);
            },
          ),
          TextButton(
            style: TextButton.styleFrom(
              textStyle: Theme.of(context).textTheme.labelLarge,
            ),
            child: const Text('Accept'),
            onPressed: () {
              Navigator.of(context).pop(true);
            },
          ),
        ],
      ),
    );

    return isConfirmed ?? false;
  }

  Future<void> _showMarkerDetails(int index) async {
    await showDialog<void>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Details marker with index: $index'),
        content: Text('Show here the details of Marker with index $index'),
        actions: <Widget>[
          TextButton(
            style: TextButton.styleFrom(
              textStyle: Theme.of(context).textTheme.labelLarge,
            ),
            child: const Text('Cancel'),
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
        ],
      ),
    );

    return;
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    Status

    Done

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions