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
- Debounce Frequent Events: Use timers to debounce camera move events
- Batch Operations: Group related operations to improve performance
- Error Handling: Always handle potential errors in event callbacks
- Memory Management: Dispose of controllers and timers properly
- 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.