Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

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:

How dragging works: there's no draggable/onDragEnd property on a marker object. Interactive markers are built with WidgetLayer — plain Flutter widgets pinned to map coordinates via Marker(point:, child:) — so dragging is implemented with a normal GestureDetector's onPanStart/onPanUpdate/onPanEnd, converting the drag's screen offset back to a map Position with MapController.toLngLat. While dragging, disable the map's own pan gesture (via MapGestures) so panning the marker doesn't also pan the camera.

Basic Draggable Marker

dart
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:

dart
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

PieceRole
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/onPanEndDrives 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


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.