Skip to content

Map Interactions with Flutter and MapMetrics

This tutorial will show you how to handle various map interactions, events, and user gestures in your Flutter MapMetrics applications.

Basic Map Interactions

Here's a comprehensive example of handling different map interactions:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class MapInteractionsScreen extends StatefulWidget {
  @override
  _MapInteractionsScreenState createState() => _MapInteractionsScreenState();
}

class _MapInteractionsScreenState extends State<MapInteractionsScreen> {
  MapMetricsController? mapController;
  LatLng? lastTappedLocation;
  LatLng? lastLongPressedLocation;
  CameraPosition? currentCameraPosition;
  bool isMapMoving = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Map Interactions'),
        actions: [
          IconButton(
            icon: Icon(Icons.info),
            onPressed: _showInteractionInfo,
          ),
        ],
      ),
      body: Column(
        children: [
          // Interaction status panel
          Container(
            padding: EdgeInsets.all(16),
            color: Colors.grey[100],
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Map Status',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 8),
                Text('Moving: ${isMapMoving ? "Yes" : "No"}'),
                if (currentCameraPosition != null)
                  Text('Zoom: ${currentCameraPosition!.zoom.toStringAsFixed(2)}'),
                if (lastTappedLocation != null)
                  Text('Last Tap: ${lastTappedLocation!.latitude.toStringAsFixed(4)}, ${lastTappedLocation!.longitude.toStringAsFixed(4)}'),
              ],
            ),
          ),
          // Map
          Expanded(
            child: MapMetrics(
              styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
              onMapCreated: (MapMetricsController controller) {
                setState(() {
                  mapController = controller;
                });
              },
              onMapClick: (Point point, LatLng coordinates) {
                setState(() {
                  lastTappedLocation = coordinates;
                });
                _handleMapClick(coordinates);
              },
              onMapLongClick: (Point point, LatLng coordinates) {
                setState(() {
                  lastLongPressedLocation = coordinates;
                });
                _handleMapLongClick(coordinates);
              },
              onCameraMove: (CameraPosition position) {
                setState(() {
                  currentCameraPosition = position;
                  isMapMoving = true;
                });
              },
              onCameraIdle: () {
                setState(() {
                  isMapMoving = false;
                });
                _handleCameraIdle();
              },
              onStyleLoaded: () {
                print('Map style loaded successfully!');
              },
              initialCameraPosition: CameraPosition(
                target: LatLng(40.7128, -74.0060),
                zoom: 12.0,
              ),
            ),
          ),
        ],
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton.small(
            heroTag: "reset",
            onPressed: _resetMap,
            child: Icon(Icons.refresh),
          ),
          SizedBox(height: 8),
          FloatingActionButton.small(
            heroTag: "fit",
            onPressed: _fitToBounds,
            child: Icon(Icons.fit_screen),
          ),
        ],
      ),
    );
  }

  void _handleMapClick(LatLng coordinates) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Map Clicked'),
        content: Text(
          'Latitude: ${coordinates.latitude.toStringAsFixed(6)}\n'
          'Longitude: ${coordinates.longitude.toStringAsFixed(6)}',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('OK'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              _addMarkerAtLocation(coordinates);
            },
            child: Text('Add Marker'),
          ),
        ],
      ),
    );
  }

  void _handleMapLongClick(LatLng coordinates) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Map Long Pressed'),
        content: Text(
          'Latitude: ${coordinates.latitude.toStringAsFixed(6)}\n'
          'Longitude: ${coordinates.longitude.toStringAsFixed(6)}',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('OK'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              _flyToLocation(coordinates);
            },
            child: Text('Fly To'),
          ),
        ],
      ),
    );
  }

  void _handleCameraIdle() {
    print('Camera stopped moving');
    // You can perform actions when the map stops moving
    // For example, load data for the current viewport
  }

  void _addMarkerAtLocation(LatLng coordinates) {
    // Implementation for adding a marker
    print('Adding marker at: $coordinates');
  }

  void _flyToLocation(LatLng coordinates) {
    mapController?.animateCamera(
      CameraUpdate.newLatLngZoom(coordinates, 15.0),
    );
  }

  void _resetMap() {
    mapController?.animateCamera(
      CameraUpdate.newLatLngZoom(LatLng(40.7128, -74.0060), 12.0),
    );
  }

  void _fitToBounds() {
    mapController?.animateCamera(
      CameraUpdate.newLatLngBounds(
        LatLngBounds(
          southwest: LatLng(40.7, -74.1),
          northeast: LatLng(40.8, -73.9),
        ),
        50.0,
      ),
    );
  }

  void _showInteractionInfo() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Interaction Guide'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('• Tap: Show location info'),
            Text('• Long Press: Fly to location'),
            Text('• Drag: Pan the map'),
            Text('• Pinch: Zoom in/out'),
            Text('• Double Tap: Zoom in'),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('OK'),
          ),
        ],
      ),
    );
  }
}

Gesture Handling

Custom Gesture Recognition

dart
class GestureHandlingScreen extends StatefulWidget {
  @override
  _GestureHandlingScreenState createState() => _GestureHandlingScreenState();
}

class _GestureHandlingScreenState extends State<GestureHandlingScreen> {
  MapMetricsController? mapController;
  bool isGestureEnabled = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Gesture Handling'),
        actions: [
          Switch(
            value: isGestureEnabled,
            onChanged: (value) {
              setState(() {
                isGestureEnabled = value;
              });
              _toggleGestures();
            },
          ),
        ],
      ),
      body: MapMetrics(
        styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (controller) => mapController = controller,
        onMapClick: isGestureEnabled ? (point, coordinates) {
          _handleClick(coordinates);
        } : null,
        onMapLongClick: isGestureEnabled ? (point, coordinates) {
          _handleLongClick(coordinates);
        } : null,
        initialCameraPosition: CameraPosition(
          target: LatLng(40.7128, -74.0060),
          zoom: 12.0,
        ),
      ),
    );
  }

  void _toggleGestures() {
    if (isGestureEnabled) {
      print('Gestures enabled');
    } else {
      print('Gestures disabled');
    }
  }

  void _handleClick(LatLng coordinates) {
    print('Map clicked at: $coordinates');
  }

  void _handleLongClick(LatLng coordinates) {
    print('Map long clicked at: $coordinates');
  }
}

Camera Controls

Advanced Camera Operations

dart
class CameraControlsScreen extends StatefulWidget {
  @override
  _CameraControlsScreenState createState() => _CameraControlsScreenState();
}

class _CameraControlsScreenState extends State<CameraControlsScreen> {
  MapMetricsController? mapController;
  CameraPosition? currentPosition;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Camera Controls')),
      body: Stack(
        children: [
          MapMetrics(
            styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
            onMapCreated: (controller) => mapController = controller,
            onCameraMove: (position) {
              setState(() {
                currentPosition = position;
              });
            },
            initialCameraPosition: CameraPosition(
              target: LatLng(40.7128, -74.0060),
              zoom: 12.0,
            ),
          ),
          // Camera controls overlay
          Positioned(
            top: 20,
            right: 20,
            child: Column(
              children: [
                FloatingActionButton.small(
                  heroTag: "zoomIn",
                  onPressed: _zoomIn,
                  child: Icon(Icons.add),
                ),
                SizedBox(height: 8),
                FloatingActionButton.small(
                  heroTag: "zoomOut",
                  onPressed: _zoomOut,
                  child: Icon(Icons.remove),
                ),
                SizedBox(height: 8),
                FloatingActionButton.small(
                  heroTag: "rotate",
                  onPressed: _rotateMap,
                  child: Icon(Icons.rotate_right),
                ),
                SizedBox(height: 8),
                FloatingActionButton.small(
                  heroTag: "tilt",
                  onPressed: _tiltMap,
                  child: Icon(Icons.view_in_ar),
                ),
              ],
            ),
          ),
          // Camera info overlay
          Positioned(
            bottom: 20,
            left: 20,
            child: Container(
              padding: EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.white.withOpacity(0.9),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  if (currentPosition != null) ...[
                    Text('Zoom: ${currentPosition!.zoom.toStringAsFixed(2)}'),
                    Text('Bearing: ${currentPosition!.bearing.toStringAsFixed(1)}°'),
                    Text('Tilt: ${currentPosition!.tilt.toStringAsFixed(1)}°'),
                  ],
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _zoomIn() {
    mapController?.animateCamera(CameraUpdate.zoomIn());
  }

  void _zoomOut() {
    mapController?.animateCamera(CameraUpdate.zoomOut());
  }

  void _rotateMap() {
    final currentBearing = currentPosition?.bearing ?? 0.0;
    final newBearing = (currentBearing + 45) % 360;
    
    mapController?.animateCamera(
      CameraUpdate.rotateBy(newBearing - currentBearing),
    );
  }

  void _tiltMap() {
    final currentTilt = currentPosition?.tilt ?? 0.0;
    final newTilt = currentTilt > 0 ? 0.0 : 45.0;
    
    mapController?.animateCamera(
      CameraUpdate.tiltTo(newTilt),
    );
  }
}

Event Handling

Comprehensive Event Management

dart
class EventHandlingScreen extends StatefulWidget {
  @override
  _EventHandlingScreenState createState() => _EventHandlingScreenState();
}

class _EventHandlingScreenState extends State<EventHandlingScreen> {
  MapMetricsController? mapController;
  List<String> eventLog = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Event Handling'),
        actions: [
          IconButton(
            icon: Icon(Icons.clear),
            onPressed: _clearEventLog,
          ),
        ],
      ),
      body: Column(
        children: [
          // Event log
          Container(
            height: 200,
            padding: EdgeInsets.all(16),
            color: Colors.grey[100],
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Event Log',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 8),
                Expanded(
                  child: ListView.builder(
                    itemCount: eventLog.length,
                    itemBuilder: (context, index) {
                      return Text(
                        eventLog[eventLog.length - 1 - index],
                        style: TextStyle(fontSize: 12),
                      );
                    },
                  ),
                ),
              ],
            ),
          ),
          // Map
          Expanded(
            child: MapMetrics(
              styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
              onMapCreated: (controller) {
                mapController = controller;
                _logEvent('Map created');
              },
              onMapClick: (point, coordinates) {
                _logEvent('Map clicked at ${coordinates.latitude.toStringAsFixed(4)}, ${coordinates.longitude.toStringAsFixed(4)}');
              },
              onMapLongClick: (point, coordinates) {
                _logEvent('Map long clicked at ${coordinates.latitude.toStringAsFixed(4)}, ${coordinates.longitude.toStringAsFixed(4)}');
              },
              onCameraMove: (position) {
                _logEvent('Camera moving - Zoom: ${position.zoom.toStringAsFixed(2)}');
              },
              onCameraIdle: () {
                _logEvent('Camera idle');
              },
              onStyleLoaded: () {
                _logEvent('Style loaded');
              },
              onError: (error) {
                _logEvent('Error: $error');
              },
              initialCameraPosition: CameraPosition(
                target: LatLng(40.7128, -74.0060),
                zoom: 12.0,
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _logEvent(String event) {
    setState(() {
      final timestamp = DateTime.now().toString().substring(11, 19);
      eventLog.add('[$timestamp] $event');
      
      // Keep only last 50 events
      if (eventLog.length > 50) {
        eventLog.removeAt(0);
      }
    });
  }

  void _clearEventLog() {
    setState(() {
      eventLog.clear();
    });
  }
}

Performance Optimization

Debounced Interactions

dart
import 'dart:async';

class OptimizedInteractionsScreen extends StatefulWidget {
  @override
  _OptimizedInteractionsScreenState createState() => _OptimizedInteractionsScreenState();
}

class _OptimizedInteractionsScreenState extends State<OptimizedInteractionsScreen> {
  MapMetricsController? mapController;
  Timer? _debounceTimer;
  CameraPosition? lastCameraPosition;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Optimized Interactions')),
      body: MapMetrics(
        styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (controller) => mapController = controller,
        onCameraMove: _debouncedCameraMove,
        onMapClick: _debouncedMapClick,
        initialCameraPosition: CameraPosition(
          target: LatLng(40.7128, -74.0060),
          zoom: 12.0,
        ),
      ),
    );
  }

  void _debouncedCameraMove(CameraPosition position) {
    // Cancel previous timer
    _debounceTimer?.cancel();
    
    // Set new timer
    _debounceTimer = Timer(Duration(milliseconds: 300), () {
      _handleCameraMove(position);
    });
  }

  void _debouncedMapClick(Point point, LatLng coordinates) {
    // Cancel previous timer
    _debounceTimer?.cancel();
    
    // Set new timer
    _debounceTimer = Timer(Duration(milliseconds: 100), () {
      _handleMapClick(coordinates);
    });
  }

  void _handleCameraMove(CameraPosition position) {
    // Only process if position changed significantly
    if (lastCameraPosition == null ||
        (position.zoom - lastCameraPosition!.zoom).abs() > 0.5 ||
        _distance(position.target, lastCameraPosition!.target) > 0.001) {
      
      lastCameraPosition = position;
      print('Camera moved to: ${position.target}, zoom: ${position.zoom}');
      
      // Perform expensive operations here
      _loadDataForViewport(position);
    }
  }

  void _handleMapClick(LatLng coordinates) {
    print('Map clicked at: $coordinates');
    // Perform click-related operations
  }

  void _loadDataForViewport(CameraPosition position) {
    // Simulate loading data for the current viewport
    print('Loading data for viewport...');
  }

  double _distance(LatLng point1, LatLng point2) {
    final latDiff = point1.latitude - point2.latitude;
    final lngDiff = point1.longitude - point2.longitude;
    return sqrt(latDiff * latDiff + lngDiff * lngDiff);
  }

  @override
  void dispose() {
    _debounceTimer?.cancel();
    super.dispose();
  }
}

Best Practices

Interaction Guidelines

  1. Debounce Frequent Events: Use timers to debounce camera move events
  2. Batch Operations: Group related operations to improve performance
  3. Error Handling: Always handle potential errors in event callbacks
  4. Memory Management: Dispose of controllers and timers properly
  5. User Feedback: Provide visual feedback for user interactions

Common Patterns

dart
// Pattern 1: State-based interactions
class StateBasedInteractions {
  bool isMapReady = false;
  bool isUserInteracting = false;
  
  void onMapCreated(controller) {
    isMapReady = true;
    // Enable interactions
  }
  
  void onCameraMove(position) {
    isUserInteracting = true;
    // Handle movement
  }
  
  void onCameraIdle() {
    isUserInteracting = false;
    // Perform final actions
  }
}

// Pattern 2: Event-driven architecture
class EventDrivenInteractions {
  final StreamController<MapEvent> _eventController = StreamController.broadcast();
  
  Stream<MapEvent> get events => _eventController.stream;
  
  void onMapClick(point, coordinates) {
    _eventController.add(MapClickEvent(coordinates));
  }
  
  void onCameraMove(position) {
    _eventController.add(CameraMoveEvent(position));
  }
}

Next Steps

Now that you understand map interactions, try:


Pro Tip: Use debouncing for camera move events to improve performance when handling large datasets or performing expensive operations based on map position.