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

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:

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:

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

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

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

dart
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

OptionDescription
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


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.