Animate a Point in Flutter
This tutorial shows how to animate a point (marker) bouncing, pulsing, or moving on the map using Flutter's built-in animation system.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
There is no markers: / circles: / Marker / Circle widget API on MapMetricsView. Points are rendered with the declarative layers: list instead — MarkerLayer for icon/text markers and CircleLayer for dots — built from Point(coordinates: Position(lng, lat)) values. Because layers: is an ordinary Flutter widget property, rebuilding it from setState inside an AnimationController listener animates the point exactly like the original Marker/Circle re-creation pattern did.
One real constraint to know up front: CircleLayer.radius is a pixel radius (screen-space), not a geographic radius in meters — there is no geo-radius circle type in the SDK.
Bouncing Marker
Animate a marker that bounces up and down continuously:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class BouncingMarkerScreen extends StatefulWidget {
@override
_BouncingMarkerScreenState createState() => _BouncingMarkerScreenState();
}
class _BouncingMarkerScreenState extends State<BouncingMarkerScreen>
with SingleTickerProviderStateMixin {
MapController? mapController;
late AnimationController _animController;
late Animation<double> _bounceAnimation;
final Position markerPosition = Position(2.2945, 48.8584); // Eiffel Tower
@override
void initState() {
super.initState();
_animController = AnimationController(
duration: Duration(milliseconds: 800),
vsync: this,
)..repeat(reverse: true);
_bounceAnimation = Tween<double>(begin: 0.0, end: 0.003).animate(
CurvedAnimation(parent: _animController, curve: Curves.easeInOut),
);
_animController.addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
// Offset the latitude slightly to simulate bouncing
final animatedPosition = Position(
markerPosition.lng,
markerPosition.lat + _bounceAnimation.value,
);
return Scaffold(
appBar: AppBar(title: Text('Bouncing Marker')),
body: MapMetricsView(
options: MapOptions(
initCenter: markerPosition,
initZoom: 15.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
layers: [
MarkerLayer(
points: [Point(coordinates: animatedPosition)],
textField: 'Eiffel Tower',
textOffset: const [0, 1],
),
],
),
);
}
@override
void dispose() {
_animController.dispose();
super.dispose();
}
}Pulsing Circle
Show a pulsing circle that grows and fades around a location:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class PulsingCircleScreen extends StatefulWidget {
@override
_PulsingCircleScreenState createState() => _PulsingCircleScreenState();
}
class _PulsingCircleScreenState extends State<PulsingCircleScreen>
with SingleTickerProviderStateMixin {
MapController? mapController;
late AnimationController _animController;
late Animation<double> _radiusAnimation;
late Animation<double> _opacityAnimation;
final Position center = Position(2.3522, 48.8566); // Paris center
@override
void initState() {
super.initState();
_animController = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat();
// Radius is in screen pixels, not meters.
_radiusAnimation = Tween<double>(begin: 10.0, end: 60.0).animate(
CurvedAnimation(parent: _animController, curve: Curves.easeOut),
);
_opacityAnimation = Tween<double>(begin: 0.4, end: 0.0).animate(
CurvedAnimation(parent: _animController, curve: Curves.easeOut),
);
_animController.addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Pulsing Circle')),
body: MapMetricsView(
options: MapOptions(
initCenter: center,
initZoom: 14.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
layers: [
// Pulsing ring
CircleLayer(
points: [Point(coordinates: center)],
radius: _radiusAnimation.value.round(),
color: Colors.blue.withValues(alpha: _opacityAnimation.value),
strokeColor: Colors.blue.withValues(alpha: _opacityAnimation.value),
strokeWidth: 2,
),
// Static center dot
CircleLayer(
points: [Point(coordinates: center)],
radius: 10,
color: Colors.blue.withValues(alpha: 0.8),
strokeColor: Colors.white,
strokeWidth: 3,
),
// Label
MarkerLayer(
points: [Point(coordinates: center)],
textField: 'Your Location',
textOffset: const [0, 2],
),
],
),
);
}
@override
void dispose() {
_animController.dispose();
super.dispose();
}
}Moving Point Between Locations
Smoothly move a point from one location to another. Each destination is its own single-point MarkerLayer so it can carry its own label — MarkerLayer applies one textField uniformly to every point it holds, so heterogeneous labels need one layer instance per label, all listed together in layers::
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class MovingPointScreen extends StatefulWidget {
@override
_MovingPointScreenState createState() => _MovingPointScreenState();
}
class _MovingPointScreenState extends State<MovingPointScreen>
with SingleTickerProviderStateMixin {
MapController? mapController;
late AnimationController _animController;
late Animation<double> _latAnimation;
late Animation<double> _lngAnimation;
// lng/lat order.
final List<Map<String, dynamic>> destinations = [
{'name': 'Paris', 'lng': 2.3522, 'lat': 48.8566},
{'name': 'London', 'lng': -0.1276, 'lat': 51.5074},
{'name': 'Berlin', 'lng': 13.405, 'lat': 52.52},
{'name': 'Rome', 'lng': 12.4964, 'lat': 41.9028},
];
int currentDestination = 0;
@override
void initState() {
super.initState();
_animController = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_setAnimationToNextDestination();
_animController.addListener(() {
setState(() {});
});
_animController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// Move to next destination
setState(() {
currentDestination =
(currentDestination + 1) % destinations.length;
});
_setAnimationToNextDestination();
_animController.forward(from: 0.0);
}
});
_animController.forward();
}
void _setAnimationToNextDestination() {
final from = destinations[currentDestination];
final to =
destinations[(currentDestination + 1) % destinations.length];
_latAnimation = Tween<double>(
begin: from['lat'],
end: to['lat'],
).animate(CurvedAnimation(
parent: _animController,
curve: Curves.easeInOut,
));
_lngAnimation = Tween<double>(
begin: from['lng'],
end: to['lng'],
).animate(CurvedAnimation(
parent: _animController,
curve: Curves.easeInOut,
));
}
@override
Widget build(BuildContext context) {
final currentPosition = Position(
_lngAnimation.value,
_latAnimation.value,
);
final destName = destinations[
(currentDestination + 1) % destinations.length]['name'];
return Scaffold(
appBar: AppBar(title: Text('Moving Point')),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initCenter: Position(8.0, 48.0),
initZoom: 4.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
layers: [
// Destination markers (one layer per point so each keeps its
// own label)
for (final d in destinations)
MarkerLayer(
points: [Point(coordinates: Position(d['lng'], d['lat']))],
textField: d['name'] as String,
textOffset: const [0, 1],
),
// Destination dots
for (final d in destinations)
CircleLayer(
points: [Point(coordinates: Position(d['lng'], d['lat']))],
radius: 6,
color: Colors.orange,
strokeColor: Colors.white,
strokeWidth: 2,
),
// Moving point
CircleLayer(
points: [Point(coordinates: currentPosition)],
radius: 7,
color: Colors.blue,
strokeColor: Colors.white,
strokeWidth: 2,
),
],
),
// Status bar
Positioned(
top: 16,
left: 16,
right: 16,
child: Card(
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
'Moving to $destName...',
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
),
],
),
);
}
@override
void dispose() {
_animController.dispose();
super.dispose();
}
}Animation Options
| Type | Use Case | Flutter Class |
|---|---|---|
| Bounce | Draw attention to a marker | AnimationController + Curves.easeInOut |
| Pulse | Show live location or alerts | AnimationController + repeat() |
| Move | Transition between locations | Tween<double> for lat/lng |
| Rotate | Compass or direction indicator | Tween<double> for bearing |
Next Steps
- Animate Point Along Route — Move along a path
- Animate a Marker — More marker animations
- Animate a Line — Progressive line drawing
Tip: Use SingleTickerProviderStateMixin for a single animation or TickerProviderStateMixin if your screen has multiple independent animations running simultaneously. Rebuilding the layers: list every frame is cheap for a handful of points — for hundreds of moving points, prefer the GeoJsonSource + StyleController.updateGeoJsonSource approach shown in Animate a Marker.