Skip to content

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:

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


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.