Animate a Line in Flutter
This tutorial shows how to animate a polyline being drawn on the map step by step, as if tracing a route in real time.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Basic Line Animation
Draw a route one segment at a time using an AnimationController:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class AnimateLineScreen extends StatefulWidget {
@override
_AnimateLineScreenState createState() => _AnimateLineScreenState();
}
class _AnimateLineScreenState extends State<AnimateLineScreen>
with SingleTickerProviderStateMixin {
MapMetricsController? mapController;
late AnimationController _animationController;
bool isAnimating = false;
// Full route coordinates (Paris walking route)
final List<LatLng> fullRoute = [
LatLng(48.8584, 2.2945), // Eiffel Tower
LatLng(48.8575, 2.3050),
LatLng(48.8560, 2.3150),
LatLng(48.8580, 2.3250),
LatLng(48.8606, 2.3376), // Louvre
LatLng(48.8590, 2.3420),
LatLng(48.8560, 2.3460),
LatLng(48.8530, 2.3499), // Notre-Dame
];
List<LatLng> get visibleRoute {
final progress = _animationController.value;
final totalPoints = fullRoute.length;
final visibleCount = (progress * totalPoints).ceil().clamp(1, totalPoints);
return fullRoute.sublist(0, visibleCount);
}
Set<Polyline> get polylines => {
// Faded full route (background)
Polyline(
polylineId: PolylineId('full_route'),
points: fullRoute,
color: Colors.blue.withOpacity(0.2),
width: 3,
patterns: [PatternItem.dash(8), PatternItem.gap(6)],
),
// Animated route (foreground)
if (visibleRoute.length >= 2)
Polyline(
polylineId: PolylineId('animated_route'),
points: visibleRoute,
color: Colors.blue,
width: 4,
),
};
Set<Marker> get markers => {
// Start marker
Marker(
markerId: MarkerId('start'),
position: fullRoute.first,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
infoWindow: InfoWindow(title: 'Start'),
),
// End marker (only show when animation is complete)
if (_animationController.value >= 1.0)
Marker(
markerId: MarkerId('end'),
position: fullRoute.last,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
infoWindow: InfoWindow(title: 'End'),
),
// Current position marker
if (visibleRoute.isNotEmpty && _animationController.value < 1.0)
Marker(
markerId: MarkerId('current'),
position: visibleRoute.last,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
),
};
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 5),
);
_animationController.addListener(() => setState(() {}));
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() => isAnimating = false);
}
});
}
@override
Widget build(BuildContext context) {
final progress = (_animationController.value * 100).toInt();
return Scaffold(
appBar: AppBar(title: Text('Animate a Line')),
body: Column(
children: [
// Progress bar
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.grey[100],
child: Row(
children: [
Text('Progress: $progress%'),
SizedBox(width: 12),
Expanded(
child: LinearProgressIndicator(
value: _animationController.value,
backgroundColor: Colors.grey[300],
),
),
],
),
),
// Map
Expanded(
child: MapMetrics(
styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (controller) => mapController = controller,
initialCameraPosition: CameraPosition(
target: LatLng(48.8570, 2.3200),
zoom: 14.0,
),
polylines: polylines,
markers: markers,
),
),
],
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton.small(
heroTag: 'reset',
onPressed: _reset,
child: Icon(Icons.replay),
),
SizedBox(width: 8),
FloatingActionButton.extended(
heroTag: 'play',
onPressed: isAnimating ? _pause : _play,
icon: Icon(isAnimating ? Icons.pause : Icons.play_arrow),
label: Text(isAnimating ? 'Pause' : 'Draw'),
),
],
),
);
}
void _play() {
setState(() => isAnimating = true);
if (_animationController.value >= 1.0) {
_animationController.reset();
}
_animationController.forward();
}
void _pause() {
_animationController.stop();
setState(() => isAnimating = false);
}
void _reset() {
_animationController.reset();
setState(() => isAnimating = false);
}
@override
void dispose() {
_animationController.dispose();
mapController?.dispose();
super.dispose();
}
}Smooth Interpolated Animation
For smoother line drawing, interpolate between points:
dart
List<LatLng> get smoothRoute {
final progress = _animationController.value;
final totalSegments = fullRoute.length - 1;
final exactPosition = progress * totalSegments;
final segmentIndex = exactPosition.floor().clamp(0, totalSegments - 1);
final t = exactPosition - segmentIndex;
// All completed segments plus interpolated current segment
final result = fullRoute.sublist(0, segmentIndex + 1);
if (segmentIndex < totalSegments) {
final from = fullRoute[segmentIndex];
final to = fullRoute[segmentIndex + 1];
result.add(LatLng(
from.latitude + (to.latitude - from.latitude) * t,
from.longitude + (to.longitude - from.longitude) * t,
));
}
return result;
}Next Steps
- Animate a Marker — Move a marker along a route
- Add a Polyline — Static polyline styling
- Fly to a Location — Animate the camera along with the line
Tip: Show a faded version of the full route as a background layer so users can see where the line is heading, while the solid animated line shows progress.