Skip to content

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:

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

ApproachSpeedSmoothnessBest For
Timer.periodic 50msFastVery smoothVisual demos
Timer.periodic 200msMediumSmoothTracking UIs
Timer.periodic 500msSlowStep-by-stepDelivery tracking
Interpolate waypointsExtra smoothLong routes with few waypoints

Next Steps


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.