maplibre / flutter-maplibre-gl

Customizable, performant and vendor-free vector and raster maps, flutter wrapper for maplibre-native and maplibre-gl-js (fork of flutter-mapbox-gl/maps)
https://pub.dev/packages/maplibre_gl
Other
226 stars 125 forks source link

[Web] How to disambiguate click and double click? #305

Open ouvreboite opened 1 year ago

ouvreboite commented 1 year ago

On the web platform, onMapClick will be called when performing a zoom (double-clicking).

It seems it's somehow expected for the web (cf old comment on mapbox-gl-js: https://github.com/mapbox/mapbox-gl-js/issues/6524#issuecomment-382034961):

Receiving two click events followed by a dblclick is the expected behavior

But that's not the case for iOS/Android: there, the onMapClick is distinct from the double-click-to-zoom.

This is unhelpful, especially considering that onMapLongClick is not supported on the web, meaning there is no simple way to trigger an event via the mouse on the web that won't interfere with the double click.

From my point of view, either:

m0nac0 commented 1 year ago

That is a very good question I don't have a good answer for. I think this projects goal is to be somewhere in between those two, leaning more towards abstraction, wherever its feasible to abstract the differences away.

In this case, though, I don't think there is a good way for us to do that. We could intercept the click events and only forward them if no double click event occurs shortly after, but I think that is not a good solution. So if you need this changed, I think you should file a feature request upstream. We could and probably should also document this behavior though.

ouvreboite commented 1 year ago

Thank you. A workaround is simple, so I won't push to have it fixed upstream.

Talking about documenting web-specific click behavior: in web, the click events are received by the map even though another widget is in front of the map (the dropdown of a SearchAnchor, for example). This is not a MapLibre bug, but more a known limitation of Flutter's HtmlElementView. The solution is to wrap said widget with a PointerInterceptor.

For reference, here is my own solution, using a mixin to isolate the logic.

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

mixin DoubleClickDisambiguate on Widget{
  DateTime? lastClick;
  int millisecondsDoubleClickThreshold = 500;

  /// On web platform, ensure the provided callback is only called for single click, and not for double-click.
  /// On non-web platforms, the provided callback is called immediately (no disambiguation needed)
  Future<void> disambiguateSingleClick(Function() callback) async {
    // disambiguation is only needed on web
    if(!kIsWeb){
      await callback();
      return;
    }

    var isSecondClick = _isLastClickWithinDoubleClickThreshold();
    lastClick = DateTime.now();

    if(!isSecondClick){
      await Future.delayed(Duration(milliseconds: millisecondsDoubleClickThreshold+1));
      var wasSingleClick = !_isLastClickWithinDoubleClickThreshold();
      if (wasSingleClick) {
        await callback();
      }
    }
  }

  _isLastClickWithinDoubleClickThreshold(){
    return lastClick != null 
    && lastClick!.millisecondsSinceEpoch + millisecondsDoubleClickThreshold > DateTime.now().millisecondsSinceEpoch;
  }
}

It can be called like that:

class MapWidget extends StatelessWidget with DoubleClickDisambiguate {

  @override
  Widget build(BuildContext context){
    return MaplibreMap(
        onMapClick: (point, coordinates) async {
              await disambiguateSingleClick(() => print("Map single-clicked on $coordinates"));
          },
      );
  }
}
ouvreboite commented 1 year ago

Sadly, the disambiguation does not work on all web platforms: chrome for Android does not support dbClick, so maplibre-gl-js relies on touchstart/touchend to detect a "double-tap". In a double-tap context, only a single "click" event is fired, so there is no way to know if it's part of a double-tap or a legitimate single click.

cf https://github.com/maplibre/maplibre-gl-js/issues/3242

From what I see, this leaves a few possibilities:

m0nac0 commented 1 year ago

Thank you for investigating this and sharing your findings.

I like the last option the most, because I think this would be a useful feature for other users of maplibre-gl-js as well. But if that is not possible or realistically will not be implemented soon, the third option also sounds fine to me (especially if someone could contribute a PR).