Skip to content

Double-tap-drag zoom expires while the second tap is held before dragging #2246

Description

@smolikja

What is the bug?

InteractiveFlag.doubleTapDragZoom stops recognizing the gesture if the user
holds the second tap briefly before starting to drag.

The gesture works only when dragging begins before the 250 ms timer—started
after the first tap is released—expires. The timer is not cancelled when the
second pointer goes down. It is cancelled only after the first
ScaleUpdateDetails is received.

This means the effective time available to begin dragging is:

250 ms - delay between first tap-up and second tap-down

For example:

  • First tap is released at t=0 ms.
  • The second pointer goes down at t=100 ms, which is a valid double tap.
  • The user holds the pointer.
  • At t=250 ms, the double-tap-hold state is reset.
  • A subsequent vertical drag no longer zooms. Depending on enabled flags, it
    either pans the map or does nothing.

An immediate double-tap-and-drag works, which makes the issue appear as if the
gesture is unusually difficult or unreliable.

The relevant implementation is in
MapInteractiveViewerState._handleOnTapUp and _handleDoubleTapHold.

Expected behaviour:

The timeout should limit only the interval between releasing the first tap and
pressing the second tap. Once the second pointer is down within that interval,
the user should be able to hold it and start dragging later. The gesture should
remain active until pointer-up or pointer-cancel.

Environment:

  • Observed with flutter_map 8.3.1
  • Verified that the implementation is unchanged in 8.3.2
  • Flutter 3.44.4
  • Dart 3.12.2
  • macOS 26.6.2
  • iOS Simulator using the primary mouse button

The affected state machine is shared by touch and mouse pointer input.

How can we reproduce it?

Run this minimal application:

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';

void main() {
  runApp(const ExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: FlutterMap(
          options: const MapOptions(
            initialCenter: LatLng(50.0755, 14.4378),
            initialZoom: 13,
            interactionOptions: InteractionOptions(
              flags: InteractiveFlag.all,
            ),
          ),
          children: const [
            TileLayer(
              urlTemplate:
                  'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
              userAgentPackageName: 'dev.example.quick_zoom_reproduction',
            ),
          ],
        ),
      ),
    );
  }
}

Steps:

  1. Tap or click once and release.
  2. Within 250 ms, press the same location again.
  3. Keep the second pointer down for approximately 500 ms.
  4. Without releasing it, drag vertically.

Actual result:
The map does not perform double-tap-drag zoom. With normal dragging enabled, it
usually pans instead.

Control test:
Repeat the gesture but begin dragging immediately after the second pointer
goes down. Zooming works.

The issue can also be covered by a widget test:

  1. Complete the first tap.
  2. Wait 100 ms.
  3. Send the second pointer-down event.
  4. Wait longer than the original 250 ms timeout.
  5. Move the second pointer vertically.
  6. Assert that the camera zoom changes while its center remains unchanged.

Do you have a potential solution?

Treat the 250 ms timer strictly as an inter-tap timeout.

When the second primary pointer goes down while a valid first-tap candidate is
pending, cancel the inter-tap timer and preserve the double-tap-drag candidate
until:

  • the pointer moves far enough to start zooming,
  • the pointer is released,
  • the pointer is cancelled, or
  • another pointer invalidates the gesture.

Simply increasing the 250 ms value would not fully solve the issue because the
timer would still cover the wrong phase of the gesture.

A regression test should hold the second pointer for longer than 250 ms before
moving it. It would also be useful to run the test with both
PointerDeviceKind.touch and PointerDeviceKind.mouse.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    Medium

    Effort

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions