Animate a Point Along a Route in Flutter
This tutorial shows how to smoothly animate a marker moving along a defined route path — great for delivery tracking, ride-hailing, or tour animations.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Basic Point Animation Along Route
Move a marker along a European city route with Start/Stop/Reset controls:
dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class AnimatePointAlongRouteScreen extends StatefulWidget {
@override
_AnimatePointAlongRouteScreenState createState() =>
_AnimatePointAlongRouteScreenState();
}
class _AnimatePointAlongRouteScreenState
extends State<AnimatePointAlongRouteScreen> {
MapMetricsController? mapController;
Timer? animationTimer;
int currentIndex = 0;
bool isAnimating = false;
// Route waypoints (European capitals)
final List<LatLng> waypoints = [
LatLng(40.4168, -3.7038), // Madrid
LatLng(48.8566, 2.3522), // Paris
LatLng(51.5074, -0.1276), // London
LatLng(52.52, 13.405), // Berlin
LatLng(48.2082, 16.3738), // Vienna
LatLng(41.9028, 12.4964), // Rome
];
// Interpolated points for smooth animation
late List<LatLng> smoothRoute;
@override
void initState() {
super.initState();
smoothRoute = _interpolateRoute(waypoints, 50);
}
/// Create smooth intermediate points between waypoints
List<LatLng> _interpolateRoute(List<LatLng> points, int stepsPerSegment) {
final result = <LatLng>[];
for (int i = 0; i < points.length - 1; i++) {
final from = points[i];
final to = points[i + 1];
for (int s = 0; s < stepsPerSegment; s++) {
final t = s / stepsPerSegment;
result.add(LatLng(
from.latitude + (to.latitude - from.latitude) * t,
from.longitude + (to.longitude - from.longitude) * t,
));
}
}
result.add(points.last);
return result;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animate Point Along Route')),
body: Column(
children: [
// Controls
Container(
padding: EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: isAnimating ? null : _startAnimation,
icon: Icon(Icons.play_arrow),
label: Text('Start'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
SizedBox(width: 8),
ElevatedButton.icon(
onPressed: isAnimating ? _stopAnimation : null,
icon: Icon(Icons.stop),
label: Text('Stop'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _resetAnimation,
icon: Icon(Icons.replay),
label: Text('Reset'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
foregroundColor: Colors.white,
),
),
],
),
),
// Map
Expanded(
child: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.0, 5.0),
zoom: 4.0,
),
onStyleLoaded: () {
_addRouteLineLayer();
},
markers: {
Marker(
markerId: MarkerId('moving_point'),
position: smoothRoute[currentIndex],
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueBlue),
infoWindow: InfoWindow(title: 'Traveling...'),
),
},
polylines: {
Polyline(
polylineId: PolylineId('route'),
points: waypoints,
color: Colors.blue.withOpacity(0.4),
width: 3,
),
},
),
),
],
),
);
}
void _addRouteLineLayer() {
// Add city markers at each waypoint
final cityNames = [
'Madrid', 'Paris', 'London', 'Berlin', 'Vienna', 'Rome'
];
for (int i = 0; i < waypoints.length; i++) {
// City markers are added via the markers set in the widget
}
}
void _startAnimation() {
setState(() {
isAnimating = true;
});
animationTimer = Timer.periodic(Duration(milliseconds: 50), (timer) {
if (currentIndex >= smoothRoute.length - 1) {
_stopAnimation();
return;
}
setState(() {
currentIndex++;
});
// Optionally follow the marker with the camera
mapController?.animateCamera(
CameraUpdate.newLatLng(smoothRoute[currentIndex]),
);
});
}
void _stopAnimation() {
animationTimer?.cancel();
setState(() {
isAnimating = false;
});
}
void _resetAnimation() {
_stopAnimation();
setState(() {
currentIndex = 0;
});
mapController?.animateCamera(
CameraUpdate.newLatLngZoom(LatLng(48.0, 5.0), 4.0),
);
}
@override
void dispose() {
animationTimer?.cancel();
super.dispose();
}
}Delivery Tracker Example
A practical example showing a delivery moving along a path with status updates:
dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class DeliveryTrackerScreen extends StatefulWidget {
@override
_DeliveryTrackerScreenState createState() => _DeliveryTrackerScreenState();
}
class _DeliveryTrackerScreenState extends State<DeliveryTrackerScreen> {
MapMetricsController? mapController;
Timer? animationTimer;
int currentIndex = 0;
bool isDelivering = false;
// Delivery route through Paris streets
final List<LatLng> deliveryRoute = [
LatLng(48.8738, 2.2950), // Arc de Triomphe (pickup)
LatLng(48.8700, 2.3050),
LatLng(48.8650, 2.3150),
LatLng(48.8620, 2.3250),
LatLng(48.8600, 2.3350),
LatLng(48.8580, 2.3400),
LatLng(48.8566, 2.3522), // City center
LatLng(48.8550, 2.3550),
LatLng(48.8530, 2.3499), // Notre-Dame (delivery)
];
String get statusText {
final progress = currentIndex / (deliveryRoute.length - 1);
if (progress == 0) return 'Ready for pickup';
if (progress < 0.3) return 'Picked up — on the way';
if (progress < 0.7) return 'In transit';
if (progress < 1.0) return 'Almost there!';
return 'Delivered!';
}
@override
Widget build(BuildContext context) {
final progress = currentIndex / (deliveryRoute.length - 1);
return Scaffold(
appBar: AppBar(title: Text('Delivery Tracker')),
body: Column(
children: [
// Status bar
Container(
padding: EdgeInsets.all(16),
color: Colors.blue[50],
child: Column(
children: [
Text(statusText,
style:
TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey[300],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
],
),
),
// Map
Expanded(
child: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.8620, 2.3250),
zoom: 14.0,
),
markers: {
// Pickup point
Marker(
markerId: MarkerId('pickup'),
position: deliveryRoute.first,
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueGreen),
infoWindow: InfoWindow(title: 'Pickup'),
),
// Delivery point
Marker(
markerId: MarkerId('delivery'),
position: deliveryRoute.last,
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueRed),
infoWindow: InfoWindow(title: 'Delivery'),
),
// Moving delivery marker
Marker(
markerId: MarkerId('driver'),
position: deliveryRoute[currentIndex],
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueBlue),
infoWindow: InfoWindow(title: 'Driver'),
),
},
polylines: {
Polyline(
polylineId: PolylineId('delivery_route'),
points: deliveryRoute,
color: Colors.blue,
width: 4,
),
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: isDelivering ? null : _startDelivery,
backgroundColor: isDelivering ? Colors.grey : Colors.blue,
child: Icon(isDelivering ? Icons.local_shipping : Icons.play_arrow),
),
);
}
void _startDelivery() {
setState(() {
isDelivering = true;
currentIndex = 0;
});
animationTimer = Timer.periodic(Duration(milliseconds: 500), (timer) {
if (currentIndex >= deliveryRoute.length - 1) {
timer.cancel();
setState(() {
isDelivering = false;
});
return;
}
setState(() {
currentIndex++;
});
});
}
@override
void dispose() {
animationTimer?.cancel();
super.dispose();
}
}Animation Tips
| Approach | Speed | Smoothness | Best For |
|---|---|---|---|
Timer.periodic 50ms | Fast | Very smooth | Visual demos |
Timer.periodic 200ms | Medium | Smooth | Tracking UIs |
Timer.periodic 500ms | Slow | Step-by-step | Delivery tracking |
| Interpolate waypoints | — | Extra smooth | Long routes with few waypoints |
Next Steps
- Animate a Marker — Bounce and pulse animations
- Animate a Line — Draw a line progressively
- Fly to a Location — Smooth camera transitions
Tip: For production tracking apps, receive real GPS coordinates from a backend and update the marker position — the same setState pattern works with live data from a WebSocket or polling API.