flutter_map icon indicating copy to clipboard operation
flutter_map copied to clipboard

Allow exclusive panning or zooming when pinching

Open translibrius opened this issue 1 year ago • 5 comments

What do you want implemented?

Hey, love the package!

Simillar to #1396 I would love to have a way to disable pinch move while zooming. This is specifically an issue on a laptop's trackpad (windows tested) when a user wants to zoom in via trackpad, but that pans the map somewhere else.

What other alternatives are available?

Simply disabling the pinch move flag, however then the user will be unable to move the map with two fingers.

Can you provide any other information?

I have video footage for refrence. As you can see the map moves when zooming.

https://github.com/fleaflet/flutter_map/assets/54362693/19bb5f2a-08fc-4161-b95f-4407485deb60

I love this package, so I might take a look at potentially doing a PR for this if it's not too complicated.

Severity

Annoying: Currently have to use workarounds

translibrius avatar Jun 04 '24 08:06 translibrius

Hey @JaffaKetchup, sorry to tag you, it's been quite a while since I posted this. Users basically can't use the pinch zoom feature on laptops with no external mouse, which is really frustrating for 'em.

Do you plan on fixing this anytime soon or should I dip my head into the source and try to do it myself? I see that you are overhauling the gesture system currently so I'm not sure if trying it myself is the best idea since it will be reworked anyway?

If I should, could you give any pointers of how to get started with this? Thanks 😃

translibrius avatar Dec 05 '24 13:12 translibrius

Hey @translibrius, Sorry for not responding sooner, we've been letting the gesture issues stack up.

I'm not sure whether this is a bug or a design question. When using the scroll wheel, or double tapping, the zoom occurs at the pointer location (cursor or tap location). However, it looks like the trackpad is trying to do this (at the cursor location), but it feels kinda broken (as your video shows). I guess the question is whether the trackpad should zoom to the cursor location, or to the center of the map. I'm not sure how to detect whether there is a cursor or not, but maybe that's possible and useful? I'm not quite clear on which you'd prefer?

The work on gesture and event overhaul has been dormant for a while now, so feel free to have a dig yourself. Most of the code should be in the MapInteractiveViewer - but be warned, there's some amount of confusing methods going on :D. I'll have a quick look as well.

The code concerned is probably: https://github.com/fleaflet/flutter_map/blob/4dc3bcae5f58f5d287abbf434a45a3314342cf21/lib/src/gestures/map_interactive_viewer.dart#L575-L641

But I cannot be sure whether it also is used for touchscreen right now - I'll need to check. Indeed, replacing line 640 to oldCenterPt causes zooming into the map center on the trackpad (and maybe other input forms as well). It looks like the calculation here is not quite right for the trackpad (again, maybe its correct for other input methods that use this block).

JaffaKetchup avatar Dec 05 '24 17:12 JaffaKetchup

Thank you for the detailed response!

Indeed, I've set the line 640 to _camera.center, and indeed it seems to solve the issue perfectly for me on the trackpad, now it is working as it should. I could very easily just edit this into a fork or something, checking if the platform is windows, apply this workaround, and all my problems are gone, but I would kind of feel bad since this doesn't really solve it at scale.

I will look into the source more on the weekends in my spare time to try and better understand how it all works under the hood to hopefully make a solution. Perhaps an easy fix would be some interaction flag like FLAG_TRACKPAD_CENTER or something and just return the center if such a flag is set. I'm not sure if it's possible to detect if user is specifically using touchpad pinch as well. The flag could be a simple solution since probably not many people use this package with a trackpad like my app.

I will let you know if I find something useful about this :)

translibrius avatar Dec 06 '24 07:12 translibrius

Merged into #2001.

JaffaKetchup avatar Jan 08 '25 21:01 JaffaKetchup

Unmerged: #2001 is not directly relevant to this and...

See #2152 for the bug report which links https://github.com/flutter/flutter/issues/136029. This is the root cause of the issue which triggered this feature request. This feature request should be considered a workaround for that bug, which must be fixed in the Flutter engine.

The override-trackpad-pinch branch contains a new InteractionOptions.forceOnlySinglePinchGesture property, resolving this request.

It can be used with the following code, to workaround #2152/the engine issue. This is designed to wrap around the FlutterMap widget. It could be extended to only apply to Windows devices.

import 'dart:collection';

import 'package:flutter/gestures.dart';

class TrackpadBugDetector extends StatefulWidget {
  const TrackpadBugDetector({super.key, required this.builder});

  final Widget Function(BuildContext context, bool bugDetected) builder;

  @override
  State<TrackpadBugDetector> createState() => _TrackpadBugDetectorState();
}

class _TrackpadBugDetectorState extends State<TrackpadBugDetector> {
  // These can be further tuned if necessary

  /// Distance (in events) between two events to compare
  static const _numDiffEvents = 5;

  /// Absolute difference between two evts on either x/y axis
  static const _panDiffTrigger = 4;

  /// Absolute difference between two evts on the scale dimension
  static const _scaleDiffTrigger = 0.01;

  final _events = ListQueue<(Offset, double)>(_numDiffEvents);
  bool _latched = false;

  @override
  Widget build(BuildContext context) => Listener(
    onPointerPanZoomUpdate: (evt) {
      if (_latched || evt.kind != PointerDeviceKind.trackpad) return;

      _events.add((evt.pan, evt.scale));
      if (_events.length > _numDiffEvents) {
        final cmp = _events.removeFirst();

        final panDiff = evt.pan - cmp.$1;
        final scaleDiff = (evt.scale - cmp.$2).abs();

        if ((panDiff.dx.abs() > _panDiffTrigger ||
                panDiff.dy.abs() > _panDiffTrigger) &&
            scaleDiff > _scaleDiffTrigger) {
          setState(() => _latched = true);
        }
      }
    },
    child: widget.builder(context, _latched),
  );
}

JaffaKetchup avatar Sep 11 '25 14:09 JaffaKetchup