Draggable Marker in Flutter
This tutorial shows how to create markers that users can drag to a new position on the map. This is useful for letting users pick a location, adjust a pin, or reposition points of interest.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
How dragging works: there's no
draggable/onDragEndproperty on a marker object. Interactive markers are built withWidgetLayer— plain Flutter widgets pinned to map coordinates viaMarker(point:, child:)— so dragging is implemented with a normalGestureDetector'sonPanStart/onPanUpdate/onPanEnd, converting the drag's screen offset back to a mapPositionwithMapController.toLngLat. While dragging, disable the map's ownpangesture (viaMapGestures) so panning the marker doesn't also pan the camera.
Basic Draggable Marker
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class DraggableMarkerScreen extends StatefulWidget {
@override
_DraggableMarkerScreenState createState() => _DraggableMarkerScreenState();
}
class _DraggableMarkerScreenState extends State<DraggableMarkerScreen> {
late final MapController mapController;
final _mapKey = GlobalKey();
Position markerPosition = Position(2.3522, 48.8566);
MapGestures _gestures = const MapGestures.all();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Draggable Marker')),
body: Column(
children: [
// Coordinate display
Container(
width: double.infinity,
padding: EdgeInsets.all(12),
color: Colors.grey[100],
child: Text(
'Position: ${markerPosition.lat.toStringAsFixed(6)}, '
'${markerPosition.lng.toStringAsFixed(6)}',
style: TextStyle(fontFamily: 'monospace', fontSize: 14),
),
),
// Map
Expanded(
child: MapMetricsView(
key: _mapKey,
options: MapOptions(
initCenter: markerPosition,
initZoom: 14.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
gestures: _gestures,
),
onMapCreated: (controller) => mapController = controller,
mapChildren: [
WidgetLayer(
allowInteraction: true,
markers: [
Marker(
point: markerPosition,
size: const Size.square(44),
alignment: Alignment.bottomCenter,
child: GestureDetector(
onPanStart: (_) {
// Disable camera panning while the marker is dragged
setState(() => _gestures = const MapGestures.all(pan: false));
},
onPanUpdate: (details) async {
final newPosition = await _toLngLat(details.globalPosition);
setState(() => markerPosition = newPosition);
},
onPanEnd: (_) {
setState(() => _gestures = const MapGestures.all());
},
child: const Icon(
Icons.location_on,
color: Colors.red,
size: 44,
),
),
),
],
),
],
),
),
],
),
);
}
Future<Position> _toLngLat(Offset eventOffset) async {
// Only Android returns screen pixels, other platforms return logical pixels.
final pixelRatio = (!kIsWeb && Platform.isAndroid)
? MediaQuery.devicePixelRatioOf(context)
: 1.0;
final mapRenderBox = _mapKey.currentContext?.findRenderObject() as RenderBox?;
final mapOffset = mapRenderBox!.localToGlobal(Offset.zero);
final offset = Offset(
eventOffset.dx - mapOffset.dx,
eventOffset.dy - mapOffset.dy,
);
return mapController.toLngLat(offset.scale(pixelRatio, pixelRatio));
}
}Drag the marker and drop it at a new location. The coordinate display updates in real time while onPanUpdate fires.
Complete Example: Location Picker
A practical example where the user drags a marker to select a delivery address, with a confirm button and drag-state feedback:
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class LocationPickerScreen extends StatefulWidget {
@override
_LocationPickerScreenState createState() => _LocationPickerScreenState();
}
class _LocationPickerScreenState extends State<LocationPickerScreen> {
late final MapController mapController;
final _mapKey = GlobalKey();
Position selectedPosition = Position(2.3522, 48.8566);
MapGestures _gestures = const MapGestures.all();
bool isDragging = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Pick a Location')),
body: Stack(
children: [
MapMetricsView(
key: _mapKey,
options: MapOptions(
initCenter: selectedPosition,
initZoom: 15.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
gestures: _gestures,
),
onMapCreated: (controller) => mapController = controller,
mapChildren: [
WidgetLayer(
allowInteraction: true,
markers: [
Marker(
point: selectedPosition,
size: const Size.square(44),
alignment: Alignment.bottomCenter,
child: GestureDetector(
onPanStart: (_) {
setState(() {
isDragging = true;
_gestures = const MapGestures.all(pan: false);
});
},
onPanUpdate: (details) async {
final newPosition = await _toLngLat(details.globalPosition);
setState(() => selectedPosition = newPosition);
},
onPanEnd: (_) {
setState(() {
isDragging = false;
_gestures = const MapGestures.all();
});
},
child: const Icon(
Icons.location_on,
color: Colors.blue,
size: 44,
),
),
),
],
),
],
),
// Bottom card with confirm button
Positioned(
bottom: 24,
left: 16,
right: 16,
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isDragging ? 'Release to select...' : 'Drag the pin to your location',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
),
SizedBox(height: 8),
Text(
'Lat: ${selectedPosition.lat.toStringAsFixed(6)}\n'
'Lng: ${selectedPosition.lng.toStringAsFixed(6)}',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 13,
color: Colors.grey[600],
),
),
SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isDragging
? null
: () {
// Use selectedPosition for your app logic
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Location confirmed: '
'${selectedPosition.lat.toStringAsFixed(4)}, '
'${selectedPosition.lng.toStringAsFixed(4)}',
),
),
);
},
child: Text('Confirm Location'),
),
),
],
),
),
),
),
],
),
);
}
Future<Position> _toLngLat(Offset eventOffset) async {
final pixelRatio = (!kIsWeb && Platform.isAndroid)
? MediaQuery.devicePixelRatioOf(context)
: 1.0;
final mapRenderBox = _mapKey.currentContext?.findRenderObject() as RenderBox?;
final mapOffset = mapRenderBox!.localToGlobal(Offset.zero);
final offset = Offset(
eventOffset.dx - mapOffset.dx,
eventOffset.dy - mapOffset.dy,
);
return mapController.toLngLat(offset.scale(pixelRatio, pixelRatio));
}
}Drag Handling Building Blocks
| Piece | Role |
|---|---|
WidgetLayer(allowInteraction: true, markers: [...]) | Hosts one or more widget-based Markers that can receive gestures |
Marker(point:, size:, alignment:, child:) | Positions a Flutter widget at a Position on the map |
GestureDetector.onPanStart/onPanUpdate/onPanEnd | Drives the drag lifecycle for a marker's child widget |
MapController.toLngLat(Offset) | Converts the drag's screen offset to a map Position |
MapGestures.all(pan: false) | Temporarily disables camera panning while a marker is being dragged |
Next Steps
- Add a Popup — Show info when tapping markers
- Markers and Annotations — Learn more about marker customization
- Map Interactions — Handle all types of user interaction
Tip: Disabling the map's pan gesture while dragging (and restoring it in onPanEnd) keeps the camera from fighting the marker for the same drag gesture.