Fit to Bounding Box in Flutter
This tutorial shows how to adjust the map camera to fit a set of coordinates or a bounding box within the visible viewport.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Basic Fit to Bounds
Fit the camera to show a specific rectangular area with MapController.fitBounds. LngLatBounds takes four scalar fields — longitudeWest, longitudeEast, latitudeSouth, latitudeNorth — rather than a pair of coordinate objects:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class FitBoundsScreen extends StatefulWidget {
@override
_FitBoundsScreenState createState() => _FitBoundsScreenState();
}
class _FitBoundsScreenState extends State<FitBoundsScreen> {
MapController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Fit to Bounds')),
body: Column(
children: [
// Region buttons
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.all(8),
child: Row(
children: [
_regionButton(
'Paris',
const LngLatBounds(
longitudeWest: 2.225,
longitudeEast: 2.470,
latitudeSouth: 48.815,
latitudeNorth: 48.902,
),
),
SizedBox(width: 8),
_regionButton(
'Manhattan',
const LngLatBounds(
longitudeWest: -74.020,
longitudeEast: -73.930,
latitudeSouth: 40.700,
latitudeNorth: 40.800,
),
),
SizedBox(width: 8),
_regionButton(
'Central London',
const LngLatBounds(
longitudeWest: -0.180,
longitudeEast: -0.070,
latitudeSouth: 51.490,
latitudeNorth: 51.530,
),
),
SizedBox(width: 8),
_regionButton(
'Tokyo Center',
const LngLatBounds(
longitudeWest: 139.700,
longitudeEast: 139.780,
latitudeSouth: 35.650,
latitudeNorth: 35.700,
),
),
],
),
),
// Map
Expanded(
child: MapMetricsView(
options: MapOptions(
initCenter: Position(2.3522, 48.8566), // lng, lat — Paris
initZoom: 5,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (controller) => mapController = controller,
),
),
],
),
);
}
Widget _regionButton(String label, LngLatBounds bounds) {
return ElevatedButton(
onPressed: () => _fitToBounds(bounds),
child: Text(label),
);
}
void _fitToBounds(LngLatBounds bounds) {
mapController?.fitBounds(
bounds: bounds,
padding: const EdgeInsets.all(50), // padding in pixels
);
}
}Fit to Markers
Automatically calculate bounds from a set of marker points and zoom to show them all:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class FitToMarkersScreen extends StatefulWidget {
@override
_FitToMarkersScreenState createState() => _FitToMarkersScreenState();
}
class _FitToMarkersScreenState extends State<FitToMarkersScreen> {
MapController? mapController;
final List<Position> markerPositions = [
Position(2.2945, 48.8584), // Eiffel Tower
Position(2.3376, 48.8606), // Louvre
Position(2.3499, 48.8530), // Notre-Dame
Position(2.3431, 48.8867), // Sacré-Cœur
Position(2.2950, 48.8738), // Arc de Triomphe
];
List<Point> get points =>
markerPositions.map((p) => Point(coordinates: p)).toList();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Fit to Markers')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(2.3522, 48.8566), // lng, lat — Paris
initZoom: 10,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (controller) {
mapController = controller;
// Fit to all markers after map loads
_fitToAllMarkers();
},
layers: [
MarkerLayer(points: points, iconAnchor: IconAnchor.bottom),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _fitToAllMarkers,
child: Icon(Icons.fit_screen),
tooltip: 'Fit All Markers',
),
);
}
void _fitToAllMarkers() {
if (markerPositions.isEmpty) return;
// Calculate bounds from all marker positions
double minLat = markerPositions.first.lat.toDouble();
double maxLat = markerPositions.first.lat.toDouble();
double minLng = markerPositions.first.lng.toDouble();
double maxLng = markerPositions.first.lng.toDouble();
for (final position in markerPositions) {
final lat = position.lat.toDouble();
final lng = position.lng.toDouble();
if (lat < minLat) minLat = lat;
if (lat > maxLat) maxLat = lat;
if (lng < minLng) minLng = lng;
if (lng > maxLng) maxLng = lng;
}
mapController?.fitBounds(
bounds: LngLatBounds(
longitudeWest: minLng,
longitudeEast: maxLng,
latitudeSouth: minLat,
latitudeNorth: maxLat,
),
padding: const EdgeInsets.all(60), // padding
);
}
}Fit Bounds with Custom Padding
fitBounds takes a Flutter EdgeInsets for padding, so each side can be controlled independently:
// Uniform padding
mapController?.fitBounds(
bounds: bounds,
padding: const EdgeInsets.all(50),
);
// Asymmetric padding — useful for keeping content clear of a bottom sheet
// or app bar
mapController?.fitBounds(
bounds: bounds,
padding: const EdgeInsets.only(top: 80, bottom: 200, left: 40, right: 40),
);LngLatBounds Properties
| Property | Type | Description |
|---|---|---|
longitudeWest | double | Western (left) edge of the bounding box |
longitudeEast | double | Eastern (right) edge of the bounding box |
latitudeSouth | double | Southern (bottom) edge of the bounding box |
latitudeNorth | double | Northern (top) edge of the bounding box |
Next Steps
- Restrict Map Panning — Prevent users from panning outside bounds
- Fly to a Location — Animate camera to a single point
- Jump to Locations — Navigate through a series of locations
Tip: Always add some padding (40–80 pixels) when fitting bounds so markers at the edges are not clipped by the screen border.