You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A fling (pan-release) gesture can corrupt the map camera's position with NaN/Infinity, which then crashes with:
Unhandled error: Unsupported operation: Infinity or NaN toInt
#0 double.toInt (dart:core-patch/double.dart)
#1 double.floor (dart:core-patch/double.dart:201:34)
#2 _floor (package:flutter_map/src/layer/tile_layer/tile_range.dart:36:25)
#3 new DiscreteTileRange.fromPixelBounds (package:flutter_map/src/layer/tile_layer/tile_range.dart:61:9)
#4 TileRangeCalculator.calculate (package:flutter_map/src/layer/tile_layer/tile_range_calculator.dart:31:30)
#5 _TileLayerState._onTileUpdateEvent (package:flutter_map/src/layer/tile_layer/tile_layer.dart:632:51)
Once the camera's center is NaN, other layers that read MapCamera.visibleBounds
also fail, e.g. flutter_map_marker_cluster throws:
The north latitude can't be bigger than 90.0: NaN
'package:flutter_map/src/geo/latlng_bounds.dart': line 78: 'north <= maxLatitude'
#2 new LatLngBounds.worldSafe (package:flutter_map/src/geo/latlng_bounds.dart:78:16)
#3 MapCamera._computeVisibleBounds (package:flutter_map/src/map/camera/camera.dart:71:25)
Expected vs actual behaviour
Expected: flinging the map, however fast or however the gesture curves,
never produces a non-finite camera position.
Actual: under the above conditions, the camera's center/zoom become NaN, and the app crashes on the very next tile-range or visible-bounds
computation.
Root cause
#2158
(c5e4909, "improve fling behaviour when pointer leaves window", released
in 8.3.0) changed MapInteractiveViewerState._handleScaleEnd to derive the
fling direction from tracked pointer-position deltas instead of from details.velocity directly:
final flingOffset = _focalStartLocal - _lastFocalLocal;
final finalSegment = _prevFocalLocal - _lastFocalLocal;
final finalSegmentDistance = finalSegment.distance;
finalOffset direction;
if (finalSegmentDistance >0) {
direction = finalSegment / finalSegmentDistance;
} else {
final flingDistance = flingOffset.distance;
direction = flingOffset / flingDistance;
}
The guard above this only checks that details.velocity.pixelsPerSecond.distance
(the recognizer's fitted velocity) is above the fling threshold - it does not guarantee that the tracked position deltas (finalSegment, flingOffset) are non-zero. Velocity and sampled position deltas are
independently computed by the gesture recognizer, so it's entirely possible
for a fast flick to report a high velocity while the last two tracked pointer
samples coincide and the overall drag ends back near its start point. In
that case both finalSegment and flingOffset are Offset.zero, so flingOffset / flingDistance evaluates to 0.0 / 0.0 = NaN. This NaN
direction is fed straight into the fling Tween, and every subsequent
animation tick then moves the camera to a NaN/Infinity position.
I confirmed Flutter's own VelocityTracker / LeastSquaresSolver cannot be
the source of a NaN velocity here - they fall back to Offset.zero / null on ill-conditioned samples rather than producing NaN - so the bug
is isolated to the division above.
Affected versions
Reproduces on 8.3.0 and 8.3.1.
Does not reproduce on 8.2.2 (confirmed by manual testing on the
same device) - 8.2.2 derives the fling direction directly from details.velocity.pixelsPerSecond / magnitude, which is safe because magnitude is already guarded to be non-zero at that point.
git clone https://github.com/nisenbeck/flutter_map_mre
cd flutter_map_mre
git checkout repro/fling-nan-crash
flutter pub get
flutter run
Or in any FlutterMap (any TileLayer, default MapOptions) on a physical
touchscreen device (debug or release):
Perform a fast pan/flick gesture where the finger ends close to where it
started (e.g. a quick back-and-forth "shake" or a fast flick that curves
back near the start point) - the pattern that reliably triggers it is a
fling with high recorded velocity but where the last couple of tracked
pointer samples happen to coincide.
The app crashes (or, if the exception is caught by a higher-level error
handler, the map silently jumps to an invalid/blank state).
This only reproduces reliably on a real touchscreen - the exact micro-timing
of pointer samples is very hard to force through synthetic WidgetTester
gestures, which is presumably why this hasn't shown up in the existing test
suite.
Screen recording of the reproduction is attached below.
What happened
A fling (pan-release) gesture can corrupt the map camera's position with
NaN/Infinity, which then crashes with:Once the camera's center is
NaN, other layers that readMapCamera.visibleBoundsalso fail, e.g.
flutter_map_marker_clusterthrows:Expected vs actual behaviour
never produces a non-finite camera position.
NaN, and the app crashes on the very next tile-range or visible-boundscomputation.
Root cause
#2158
(
c5e4909, "improve fling behaviour when pointer leaves window", releasedin 8.3.0) changed
MapInteractiveViewerState._handleScaleEndto derive thefling direction from tracked pointer-position deltas instead of from
details.velocitydirectly:The guard above this only checks that
details.velocity.pixelsPerSecond.distance(the recognizer's fitted velocity) is above the fling threshold - it does
not guarantee that the tracked position deltas (
finalSegment,flingOffset) are non-zero. Velocity and sampled position deltas areindependently computed by the gesture recognizer, so it's entirely possible
for a fast flick to report a high velocity while the last two tracked pointer
samples coincide and the overall drag ends back near its start point. In
that case both
finalSegmentandflingOffsetareOffset.zero, soflingOffset / flingDistanceevaluates to0.0 / 0.0 = NaN. ThisNaNdirection is fed straight into the fling
Tween, and every subsequentanimation tick then moves the camera to a
NaN/Infinityposition.I confirmed Flutter's own
VelocityTracker/LeastSquaresSolvercannot bethe source of a
NaNvelocity here - they fall back toOffset.zero/nullon ill-conditioned samples rather than producingNaN- so the bugis isolated to the division above.
Affected versions
same device) - 8.2.2 derives the fling direction directly from
details.velocity.pixelsPerSecond / magnitude, which is safe becausemagnitudeis already guarded to be non-zero at that point.How can we reproduce it?
A minimal reproduction is available at
https://github.com/nisenbeck/flutter_map_mre, branch
repro/fling-nan-crash:git clone https://github.com/nisenbeck/flutter_map_mre cd flutter_map_mre git checkout repro/fling-nan-crash flutter pub get flutter runOr in any
FlutterMap(anyTileLayer, defaultMapOptions) on a physicaltouchscreen device (debug or release):
started (e.g. a quick back-and-forth "shake" or a fast flick that curves
back near the start point) - the pattern that reliably triggers it is a
fling with high recorded velocity but where the last couple of tracked
pointer samples happen to coincide.
handler, the map silently jumps to an invalid/blank state).
This only reproduces reliably on a real touchscreen - the exact micro-timing
of pointer samples is very hard to force through synthetic
WidgetTestergestures, which is presumably why this hasn't shown up in the existing test
suite.
Screen recording of the reproduction is attached below.
fling-crash.mp4
Do you have a potential solution?
Fixed in #2220.