Draw a Circle in Flutter
This tutorial shows how to draw circles on your MapMetrics Flutter map. Circles are useful for showing a radius around a point — like a search area, delivery zone, or coverage range.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
CircleLayerradius is in screen pixels, not meters. Unlike a geographic circle that represents a fixed real-world distance and grows/shrinks on screen as you zoom,CircleLayer.radiusis a constant pixel size — it stays the same visual size on screen at every zoom level. To draw a circle that represents an actual radius in meters (e.g. "2 km delivery zone"), convert meters to pixels withMapController.getMetersPerPixelAtLatitude(lat)and recompute whenever the camera zoom changes.
Basic Circle
Draw a circle whose on-screen size tracks a real 2 km radius, recomputing the pixel radius whenever the camera moves:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class CircleExampleScreen extends StatefulWidget {
@override
_CircleExampleScreenState createState() => _CircleExampleScreenState();
}
class _CircleExampleScreenState extends State<CircleExampleScreen> {
MapController? mapController;
static const _center = Position(2.3522, 48.8566);
static const _radiusMeters = 2000.0; // 2 km radius
double _radiusPixels = 40;
Future<void> _updateRadiusPixels() async {
final controller = mapController;
if (controller == null) return;
final metersPerPixel =
await controller.getMetersPerPixelAtLatitude(_center.lat);
if (!mounted) return;
setState(() {
_radiusPixels = _radiusMeters / metersPerPixel;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Circle Example')),
body: MapMetricsView(
options: MapOptions(
initCenter: _center,
initZoom: 13.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) async {
mapController = controller;
await _updateRadiusPixels();
},
onEvent: (event) {
if (event is MapEventMoveCamera) {
_updateRadiusPixels();
}
},
layers: [
CircleLayer(
points: [Point(coordinates: _center)],
radius: _radiusPixels.round(),
strokeWidth: 2,
strokeColor: Colors.blue,
color: Colors.blue.withValues(alpha: 0.15),
),
],
),
);
}
}Multiple Circles with Different Radii
Show concentric circles for multiple zones — since each ring has its own fixed pixel radius, each is its own CircleLayer, and each is converted from meters independently:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class MultipleCirclesScreen extends StatefulWidget {
@override
_MultipleCirclesScreenState createState() => _MultipleCirclesScreenState();
}
class _MultipleCirclesScreenState extends State<MultipleCirclesScreen> {
MapController? mapController;
static const _center = Position(-74.0060, 40.7128);
static const _zonesMeters = [1000.0, 3000.0, 5000.0]; // inner, middle, outer
List<double> _zonesPixels = [20, 60, 100];
Future<void> _updateZonePixels() async {
final controller = mapController;
if (controller == null) return;
final metersPerPixel =
await controller.getMetersPerPixelAtLatitude(_center.lat);
if (!mounted) return;
setState(() {
_zonesPixels =
_zonesMeters.map((m) => m / metersPerPixel).toList();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Coverage Zones')),
body: MapMetricsView(
options: MapOptions(
initCenter: _center,
initZoom: 12.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (controller) async {
mapController = controller;
await _updateZonePixels();
},
onEvent: (event) {
if (event is MapEventMoveCamera) {
_updateZonePixels();
}
},
layers: [
// Outer zone - 5 km (drawn first so it sits behind the others)
CircleLayer(
points: [Point(coordinates: _center)],
radius: _zonesPixels[2].round(),
strokeWidth: 2,
strokeColor: Colors.red,
color: Colors.red.withValues(alpha: 0.05),
),
// Middle zone - 3 km
CircleLayer(
points: [Point(coordinates: _center)],
radius: _zonesPixels[1].round(),
strokeWidth: 2,
strokeColor: Colors.orange,
color: Colors.orange.withValues(alpha: 0.1),
),
// Inner zone - 1 km
CircleLayer(
points: [Point(coordinates: _center)],
radius: _zonesPixels[0].round(),
strokeWidth: 2,
strokeColor: Colors.green,
color: Colors.green.withValues(alpha: 0.2),
),
// Center label
MarkerLayer(
points: [Point(coordinates: _center)],
textField: '1 km / 3 km / 5 km zones',
textOffset: const [0, 1],
),
],
),
);
}
}Dynamic Circle: Tap to Place
Let users tap the map to place a circle at any location. Because a CircleLayer applies one radius to every point it contains, all circles placed with this screen share the current slider value — moving the slider resizes every circle already on the map, not just the next one you place:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class DynamicCircleScreen extends StatefulWidget {
@override
_DynamicCircleScreenState createState() => _DynamicCircleScreenState();
}
class _DynamicCircleScreenState extends State<DynamicCircleScreen> {
MapController? mapController;
List<Point> circlePoints = [];
double radiusInMeters = 1000;
double _radiusPixels = 20;
Future<void> _updateRadiusPixels() async {
final controller = mapController;
if (controller == null || circlePoints.isEmpty) return;
final lat = circlePoints.last.coordinates.lat;
final metersPerPixel = await controller.getMetersPerPixelAtLatitude(lat);
if (!mounted) return;
setState(() {
_radiusPixels = radiusInMeters / metersPerPixel;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Tap to Draw Circle')),
body: Column(
children: [
// Radius slider
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Text('Radius: ${radiusInMeters.toInt()} m'),
Expanded(
child: Slider(
value: radiusInMeters,
min: 200,
max: 5000,
divisions: 24,
onChanged: (value) {
setState(() {
radiusInMeters = value;
});
_updateRadiusPixels();
},
),
),
],
),
),
// Map
Expanded(
child: MapMetricsView(
options: MapOptions(
initCenter: Position(2.3522, 48.8566),
initZoom: 13.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (controller) => mapController = controller,
onEvent: (event) {
if (event is MapEventClick) {
setState(() {
circlePoints.add(Point(coordinates: event.point));
});
_updateRadiusPixels();
}
},
layers: [
CircleLayer(
points: circlePoints,
radius: _radiusPixels.round(),
strokeWidth: 2,
strokeColor: Colors.blue,
color: Colors.blue.withValues(alpha: 0.15),
),
],
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
circlePoints.clear();
});
},
child: Icon(Icons.clear_all),
),
);
}
}Circle Layer Properties
| Property | Type | Description |
|---|---|---|
points | List<Point> | Centers of every circle drawn by this layer (they all share the properties below) |
radius | int | Radius in screen pixels — constant size on screen regardless of zoom |
strokeWidth | int | Border width in pixels |
strokeColor | Color | Border color |
color | Color | Fill color (use Color.withValues(alpha: ...) for transparency) |
blur | double | Blur amount; 1 blurs so only the centerpoint stays fully opaque |
To make a circle represent a fixed geographic radius (in meters) instead of a fixed pixel radius, convert with MapController.getMetersPerPixelAtLatitude(lat) and recompute on every MapEventMoveCamera event, as shown above.
Next Steps
- Add a Polygon — Draw custom shapes on the map
- Add a Polyline — Draw lines and routes
- Markers and Annotations — Add markers with popups
Tip: Debounce _updateRadiusPixels() calls on MapEventMoveCamera (it fires continuously during a pan/zoom gesture) if you have many circles or a heavy computation — recompute at the end of a gesture rather than on every frame.