Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter_map_example/pages/debouncing_tile_update_transformer.dar
import 'package:flutter_map_example/pages/epsg3996_crs.dart';
import 'package:flutter_map_example/pages/epsg4326_crs.dart';
import 'package:flutter_map_example/pages/fallback_url_page.dart';
import 'package:flutter_map_example/pages/fling_animation_damping.dart';
import 'package:flutter_map_example/pages/home.dart';
import 'package:flutter_map_example/pages/interactive_test_page.dart';
import 'package:flutter_map_example/pages/latlng_to_screen_point.dart';
Expand Down Expand Up @@ -79,6 +80,8 @@ class MyApp extends StatelessWidget {
TileBuilderPage.route: (context) => const TileBuilderPage(),
ErrorTileBuilder.route: (context) => const ErrorTileBuilder(),
InteractiveFlagsPage.route: (context) => const InteractiveFlagsPage(),
FlingAnimationDampingPage.route: (context) =>
const FlingAnimationDampingPage(),
ManyMarkersPage.route: (context) => const ManyMarkersPage(),
MapInsideListViewPage.route: (context) => const MapInsideListViewPage(),
ResetTileLayerPage.route: (context) => const ResetTileLayerPage(),
Expand Down
129 changes: 129 additions & 0 deletions example/lib/pages/fling_animation_damping.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/misc/tile_providers.dart';
import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
import 'package:latlong2/latlong.dart';

class FlingAnimationDampingPage extends StatefulWidget {
static const String route = '/fling_animation_damping';

const FlingAnimationDampingPage({super.key});

@override
State<FlingAnimationDampingPage> createState() =>
_FlingAnimationDampingPageState();
}

class _FlingAnimationDampingPageState extends State<FlingAnimationDampingPage> {
double _dampingRatio = 2;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Fling Animation Damping')),
drawer: const MenuDrawer(FlingAnimationDampingPage.route),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Damping Ratio: ${_dampingRatio.toStringAsFixed(1)}',
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Text(
'Drag the map and release to see the fling animation. '
'Lower values = less momentum, stops quicker. '
'Higher values = more momentum, bouncier feel. ',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
const SizedBox(height: 8),
Row(
children: [
const Text('Damped (1)'),
Expanded(
child: Slider(
value: _dampingRatio,
min: 1,
max: 10,
divisions: 19,
label: _dampingRatio.toStringAsFixed(1),
onChanged: (value) {
setState(() {
_dampingRatio = value;
});
},
),
),
const Text('Damped (10)'),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
ElevatedButton(
onPressed: () => setState(() => _dampingRatio = 1),
child: const Text('Very Damped (1)'),
),
ElevatedButton(
onPressed: () => setState(() => _dampingRatio = 2),
child: const Text('Damped (2)'),
),
ElevatedButton(
onPressed: () => setState(() => _dampingRatio = 5),
child: const Text('Default (5)'),
),
ElevatedButton(
onPressed: () => setState(() => _dampingRatio = 7),
child: const Text('Bouncy (4)'),
),
ElevatedButton(
onPressed: () => setState(() => _dampingRatio = 10),
child: const Text('Very Bouncy (10)'),
),
],
),
],
),
),
Expanded(
child: FlutterMap(
options: MapOptions(
initialCenter: const LatLng(51.5, -0.09),
initialZoom: 11,
interactionOptions: InteractionOptions(
flags: InteractiveFlag.all,
flingAnimationDampingRatio: _dampingRatio,
),
),
children: [
openStreetMapTileLayer,
Center(
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'Drag and release to see the fling effect!',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
],
),
),
],
),
);
}
}
6 changes: 6 additions & 0 deletions example/lib/widgets/drawer/menu_drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:flutter_map_example/pages/debouncing_tile_update_transformer.dar
import 'package:flutter_map_example/pages/epsg3996_crs.dart';
import 'package:flutter_map_example/pages/epsg4326_crs.dart';
import 'package:flutter_map_example/pages/fallback_url_page.dart';
import 'package:flutter_map_example/pages/fling_animation_damping.dart';
import 'package:flutter_map_example/pages/home.dart';
import 'package:flutter_map_example/pages/interactive_test_page.dart';
import 'package:flutter_map_example/pages/latlng_to_screen_point.dart';
Expand Down Expand Up @@ -190,6 +191,11 @@ class MenuDrawer extends StatelessWidget {
routeName: InteractiveFlagsPage.route,
currentRoute: currentRoute,
),
MenuItemWidget(
caption: 'Fling Animation Damping',
routeName: FlingAnimationDampingPage.route,
currentRoute: currentRoute,
),
const Divider(),
MenuItemWidget(
caption: 'WMS Sourced Map',
Expand Down
28 changes: 23 additions & 5 deletions lib/src/gestures/map_interactive_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
late double _lastRotation;
late double _lastScale;
late Offset _lastFocalLocal;
late Offset _prevFocalLocal;
late LatLng _mapCenterStart;
late double _mapZoomStart;
late Offset _focalStartLocal;
Expand Down Expand Up @@ -539,7 +540,8 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>

_mapZoomStart = _camera.zoom;
_mapCenterStart = _camera.center;
_focalStartLocal = _lastFocalLocal = details.localFocalPoint;
_focalStartLocal =
_lastFocalLocal = _prevFocalLocal = details.localFocalPoint;
_focalStartLatLng = _camera.offsetToCrs(_focalStartLocal);

_dragStarted = false;
Expand Down Expand Up @@ -567,6 +569,7 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>

_lastRotation = currentRotation;
_lastScale = details.scale;
_prevFocalLocal = _lastFocalLocal;
_lastFocalLocal = details.localFocalPoint;
}

Expand Down Expand Up @@ -789,13 +792,28 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
return;
}

final direction = details.velocity.pixelsPerSecond / magnitude;
// Use the tracked offset to determine direction instead of the velocity
// direction, which can be incorrect on web when the pointer leaves the
// window. Use the final segment direction to correctly handle curved
// gestures where the user changes direction during the drag.
final flingOffset = _focalStartLocal - _lastFocalLocal;
final finalSegment = _prevFocalLocal - _lastFocalLocal;
final finalSegmentDistance = finalSegment.distance;

// Use final segment direction if available, otherwise fall back to overall
// direction for edge cases where the final segment has no movement.
final Offset direction;
if (finalSegmentDistance > 0) {
direction = finalSegment / finalSegmentDistance;
} else {
final flingDistance = flingOffset.distance;
direction = flingOffset / flingDistance;
}
final distance = (Offset.zero & _camera.nonRotatedSize).shortestSide;

final flingOffset = _focalStartLocal - _lastFocalLocal;
_flingAnimation = Tween<Offset>(
begin: flingOffset,
end: flingOffset - direction * distance,
end: flingOffset + direction * distance,
).animate(_flingController);

_flingController
Expand All @@ -805,7 +823,7 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
springDescription: SpringDescription.withDampingRatio(
mass: 1,
stiffness: 1000,
ratio: 5,
ratio: _interactionOptions.flingAnimationDampingRatio,
));
}

Expand Down
16 changes: 16 additions & 0 deletions lib/src/map/options/interaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ class InteractionOptions {
/// Defaults to [Curves.fastOutSlowIn].
final Curve doubleTapZoomCurve;

/// The damping ratio for the fling animation spring simulation
///
/// This controls how the fling animation decelerates after a drag gesture.
/// Lower values result in less damping (more momentum, bouncier).
/// Higher values result in more damping (stops quicker, less bouncy).
///
/// Defaults to 5.0.
final double flingAnimationDampingRatio;

/// Options to configure cursor/keyboard rotation
///
/// Cursor/keyboard rotation is designed for desktop platforms, and allows the
Expand Down Expand Up @@ -129,6 +138,7 @@ class InteractionOptions {
defaultDoubleTapDragZoomChangeCalculator,
this.doubleTapZoomDuration = const Duration(milliseconds: 200),
this.doubleTapZoomCurve = Curves.fastOutSlowIn,
this.flingAnimationDampingRatio = 5.0,
this.cursorKeyboardRotationOptions = const CursorKeyboardRotationOptions(),
this.keyboardOptions = const KeyboardOptions(),
}) : assert(
Expand All @@ -142,6 +152,10 @@ class InteractionOptions {
assert(
pinchMoveThreshold >= 0.0,
'`pinchMoveThreshold` must be positive',
),
assert(
flingAnimationDampingRatio > 0.0,
'`flingAnimationDampingRatio` must be positive',
);

/// Default calculator function for [doubleTapDragZoomChangeCalculator]
Expand Down Expand Up @@ -171,6 +185,7 @@ class InteractionOptions {
other.doubleTapDragZoomChangeCalculator &&
doubleTapZoomDuration == other.doubleTapZoomDuration &&
doubleTapZoomCurve == other.doubleTapZoomCurve &&
flingAnimationDampingRatio == other.flingAnimationDampingRatio &&
keyboardOptions == other.keyboardOptions;

@override
Expand All @@ -188,6 +203,7 @@ class InteractionOptions {
doubleTapDragZoomChangeCalculator,
doubleTapZoomDuration,
doubleTapZoomCurve,
flingAnimationDampingRatio,
keyboardOptions,
);
}