Restrict Map Panning in Flutter
This tutorial shows how to limit the map to a specific geographic area so users cannot pan or zoom outside of it.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
How bounds restriction works on this SDK
There is no mapController.setMaxBounds(...) method and no MinMaxZoomPreference class. maxBounds, minZoom, and maxZoom are fields on MapOptions itself — set them when you construct MapOptions, and change them by rebuilding the widget with a new MapOptions (typically via setState). This is the same pattern the SDK's own example app uses to drive zoom/pitch/bounds sliders live.
Set Max Bounds
Restrict the map to only show Paris:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class RestrictPanningScreen extends StatefulWidget {
@override
_RestrictPanningScreenState createState() => _RestrictPanningScreenState();
}
class _RestrictPanningScreenState extends State<RestrictPanningScreen> {
MapController? mapController;
static const parisBounds = LngLatBounds(
longitudeWest: 2.220,
longitudeEast: 2.470,
latitudeSouth: 48.800,
latitudeNorth: 48.920,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Restricted to Paris')),
body: MapMetricsView(
options: MapOptions(
initStyle: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: Position(2.3522, 48.8566), // lng, lat
initZoom: 12.0,
minZoom: 10.0,
maxZoom: 18.0,
maxBounds: parisBounds,
),
onMapCreated: (controller) {
mapController = controller;
},
),
);
}
}Users can pan and zoom within Paris but the map will bounce back if they try to go outside the boundary.
Restrict with Min/Max Zoom
Combine bounds with zoom limits — both are plain MapOptions fields:
MapMetricsView(
options: MapOptions(
initStyle: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: Position(2.3522, 48.8566), // lng, lat
initZoom: 12.0,
maxBounds: const LngLatBounds(
longitudeWest: 2.220,
longitudeEast: 2.470,
latitudeSouth: 48.800,
latitudeNorth: 48.920,
),
minZoom: 10.0, // Can't zoom out further than this
maxZoom: 18.0, // Can't zoom in further than this
),
)Toggle Bounds On/Off
Let users switch between restricted and free panning by rebuilding MapOptions with a setState:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class ToggleBoundsScreen extends StatefulWidget {
@override
_ToggleBoundsScreenState createState() => _ToggleBoundsScreenState();
}
class _ToggleBoundsScreenState extends State<ToggleBoundsScreen> {
MapController? mapController;
bool isRestricted = true;
final LngLatBounds parisBounds = const LngLatBounds(
longitudeWest: 2.220,
longitudeEast: 2.470,
latitudeSouth: 48.800,
latitudeNorth: 48.920,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(isRestricted ? 'Restricted' : 'Free Panning'),
actions: [
TextButton.icon(
onPressed: _toggleBounds,
icon: Icon(
isRestricted ? Icons.lock : Icons.lock_open,
color: Colors.white,
),
label: Text(
isRestricted ? 'Unlock' : 'Lock',
style: TextStyle(color: Colors.white),
),
),
],
),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initStyle: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: Position(2.3522, 48.8566), // lng, lat
initZoom: 12.0,
maxBounds: isRestricted ? parisBounds : null,
),
onMapCreated: (controller) {
mapController = controller;
},
),
// Bounds indicator
if (isRestricted)
Positioned(
bottom: 24,
left: 16,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.9),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.lock, size: 16, color: Colors.white),
SizedBox(width: 6),
Text(
'Map restricted to Paris',
style: TextStyle(color: Colors.white, fontSize: 13),
),
],
),
),
),
],
),
);
}
void _toggleBounds() {
setState(() {
isRestricted = !isRestricted;
});
}
}Switchable Regions
Let users select which region to restrict to. Rebuild MapOptions.maxBounds for the hard restriction, and use MapController.fitBounds to animate the camera into the new region:
final Map<String, LngLatBounds> regions = {
'Paris': const LngLatBounds(
longitudeWest: 2.220,
longitudeEast: 2.470,
latitudeSouth: 48.800,
latitudeNorth: 48.920,
),
'Manhattan': const LngLatBounds(
longitudeWest: -74.020,
longitudeEast: -73.930,
latitudeSouth: 40.700,
latitudeNorth: 40.800,
),
'Central London': const LngLatBounds(
longitudeWest: -0.180,
longitudeEast: -0.070,
latitudeSouth: 51.490,
latitudeNorth: 51.530,
),
};
LngLatBounds? activeBounds;
Future<void> switchRegion(
MapController controller,
String regionName,
void Function(VoidCallback fn) setState,
) async {
final bounds = regions[regionName];
if (bounds == null) return;
setState(() => activeBounds = bounds);
await controller.fitBounds(
bounds: bounds,
padding: const EdgeInsets.all(50),
);
}Then pass maxBounds: activeBounds into MapOptions the next time you build the widget.
Restriction Options
| Option | Description |
|---|---|
MapOptions(maxBounds: bounds) | Restrict panning to a bounding box — set at construction, changed via setState + rebuild |
MapOptions(maxBounds: null) | Remove bounds restriction |
MapOptions(minZoom: ..., maxZoom: ...) | Restrict zoom levels |
MapController.fitBounds(bounds: ...) | Animate the camera to fit a bounding box (doesn't restrict future panning by itself) |
Next Steps
- Render World Copies — World wrapping behavior
- Navigation Controls — Zoom, compass, and location controls
- Offset the Vanishing Point — Shift the visual center with UI overlays
Tip: Combine maxBounds with tight minZoom/maxZoom values to prevent users from zooming out far enough to see the bounds edges, giving a seamless restricted experience.