Skip to content

Fling direction is derived from the noisy last pointer segment on touch: fast swipes glide off-axis / zigzag since 8.3.0 #2225

Description

@JonasGrunau

What is the bug?

Since v8.3.0, fast swipes on touch devices launch the fling/momentum glide visibly off-axis: a fast, near-vertical flick coasts diagonally, and repeatedly flicking in the same direction makes the map zigzag left and right. Slow drags are unaffected (the finger-down pan itself tracks correctly) — only the direction of the post-release glide is wrong.

Reproduced on flutter_map 8.3.0 and 8.3.1 (and current master), Android and iOS physical devices. 8.2.2 behaves correctly.

Cause: #2158 (released in 8.3.0) changed the fling direction source in _handleScaleEnd from the gesture recognizer's velocity to the single last tracked pointer segment:

// 8.2.2
final direction = details.velocity.pixelsPerSecond / magnitude;
...
end: flingOffset - direction * distance,

// 8.3.0+ (master: flingDirection(), which prefers finalSegment)
final finalSegment = _prevFocalLocal - _lastFocalLocal;
direction = finalSegment / finalSegmentDistance;
...
end: flingOffset + direction * distance,

details.velocity comes from Flutter's VelocityTracker — a least-squares fit over the last ~100 ms of pointer samples — so its direction is stable. _prevFocalLocal - _lastFocalLocal is one single frame-to-frame delta: at fast swipe speeds this segment is dominated by touch-sampling noise and the small thumb-roll at lift-off, so its direction can deviate wildly from the actual swipe axis (routinely by 30–90° in our testing).

#2158 solved a real problem — on web/desktop the pointer can leave the window mid-drag, corrupting the velocity estimate — but that failure mode cannot occur with touch input, while the cost (noisy single-segment direction) hits touch hardest, since that's where fast flicks are a primary navigation gesture.

A related detail — the velocityDirection fallback from #2220 points backwards: the three candidate vectors in flingDirection() don't share a sign convention. finalSegment and flingOffset run opposite to the pointer motion (they're used as end: flingOffset + direction * distance), but the fallback passes details.velocity.pixelsPerSecond / magnitude, which points along the pointer motion (8.2.2 used it with - direction * distance). So in the degenerate zero/zero case the glide coasts against the swipe. Rarely visible (both tracked offsets must be zero-length), but worth fixing in the same place.

How can we reproduce it?

  1. Run any FlutterMap with InteractiveFlag.flingAnimation enabled on a physical phone (or flutter create + the flutter_map README example, unchanged).
  2. Flick the map quickly upwards several times in a row, as you would to travel a long distance.
  3. Watch the glide direction: instead of continuing along the swipe axis, each glide veers to a different side depending on sub-frame noise in the last pointer segment.
FlutterMap(
  options: MapOptions(
    initialCenter: const LatLng(52.0, 9.0),
    initialZoom: 13,
    // default flags include flingAnimation
  ),
  children: [
    TileLayer(
      urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
      userAgentPackageName: 'dev.fleaflet.flutter_map.example',
    ),
  ],
)

Do you have a potential solution?

Yes — keep #2158's tracked-segment approach where it's needed (mouse/trackpad, where the pointer can leave the window), and use the smoothed velocity direction for PointerDeviceKind.touch; also negate the velocity fallback so all three candidates share the same sign convention. PR incoming: it extends flingDirection() with a preferVelocityDirection flag set from the last pointer-down's device kind, plus tests.

(We currently ship with flingAnimation disabled as a stopgap, which loses momentum panning entirely — hence the report.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions