Skip to content

Animate a Marker in Flutter

This tutorial shows how to smoothly animate a marker's position along a path or between waypoints.

Prerequisites

Before you begin, ensure you have:

Basic Marker Animation

Animate a marker along a predefined route using AnimationController:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class AnimateMarkerScreen extends StatefulWidget {
  @override
  _AnimateMarkerScreenState createState() => _AnimateMarkerScreenState();
}

class _AnimateMarkerScreenState extends State<AnimateMarkerScreen>
    with SingleTickerProviderStateMixin {
  MapMetricsController? mapController;
  late AnimationController _animationController;
  LatLng currentPosition = LatLng(48.8584, 2.2945);
  bool isAnimating = false;

  // Route waypoints (Paris landmarks)
  final List<LatLng> route = [
    LatLng(48.8584, 2.2945),  // Eiffel Tower
    LatLng(48.8606, 2.3376),  // Louvre
    LatLng(48.8530, 2.3499),  // Notre-Dame
    LatLng(48.8867, 2.3431),  // Sacré-Cœur
    LatLng(48.8738, 2.2950),  // Arc de Triomphe
    LatLng(48.8584, 2.2945),  // Back to Eiffel Tower
  ];

  Set<Marker> get markers => {
    Marker(
      markerId: MarkerId('moving'),
      position: currentPosition,
      icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
      infoWindow: InfoWindow(title: 'Moving Marker'),
    ),
  };

  // Show the route as a polyline
  Set<Polyline> get polylines => {
    Polyline(
      polylineId: PolylineId('route'),
      points: route,
      color: Colors.blue.withOpacity(0.5),
      width: 3,
      patterns: [PatternItem.dash(10), PatternItem.gap(8)],
    ),
  };

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(seconds: 15), // Total animation time
    );

    _animationController.addListener(_updateMarkerPosition);
    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        setState(() => isAnimating = false);
      }
    });
  }

  void _updateMarkerPosition() {
    final progress = _animationController.value;
    final totalSegments = route.length - 1;
    final segmentProgress = progress * totalSegments;
    final segmentIndex = segmentProgress.floor().clamp(0, totalSegments - 1);
    final t = segmentProgress - segmentIndex;

    final from = route[segmentIndex];
    final to = route[segmentIndex + 1];

    setState(() {
      currentPosition = LatLng(
        from.latitude + (to.latitude - from.latitude) * t,
        from.longitude + (to.longitude - from.longitude) * t,
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Animate Marker')),
      body: Stack(
        children: [
          MapMetrics(
            styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
            onMapCreated: (controller) => mapController = controller,
            initialCameraPosition: CameraPosition(
              target: LatLng(48.8620, 2.3200),
              zoom: 13.0,
            ),
            markers: markers,
            polylines: polylines,
          ),
          // Controls
          Positioned(
            bottom: 24,
            left: 0,
            right: 0,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FloatingActionButton.extended(
                  heroTag: 'play',
                  onPressed: isAnimating ? _stopAnimation : _startAnimation,
                  icon: Icon(isAnimating ? Icons.stop : Icons.play_arrow),
                  label: Text(isAnimating ? 'Stop' : 'Start'),
                ),
                SizedBox(width: 12),
                FloatingActionButton.small(
                  heroTag: 'reset',
                  onPressed: _resetAnimation,
                  child: Icon(Icons.replay),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  void _startAnimation() {
    setState(() => isAnimating = true);
    _animationController.forward();
  }

  void _stopAnimation() {
    _animationController.stop();
    setState(() => isAnimating = false);
  }

  void _resetAnimation() {
    _animationController.reset();
    setState(() {
      isAnimating = false;
      currentPosition = route.first;
    });
  }

  @override
  void dispose() {
    _animationController.dispose();
    mapController?.dispose();
    super.dispose();
  }
}

Looping Animation

Make the marker loop continuously along the route:

dart
void _startLoopAnimation() {
  setState(() => isAnimating = true);
  _animationController.repeat(); // Loops forever
}

Camera Follows Marker

Keep the camera centered on the moving marker:

dart
void _updateMarkerPosition() {
  // ... calculate currentPosition as above ...

  setState(() {
    currentPosition = interpolatedPosition;
  });

  // Camera follows the marker
  mapController?.moveCamera(
    CameraUpdate.newLatLng(currentPosition),
  );
}

Easing Curves

Use different animation curves for natural movement:

dart
// In initState, wrap with a CurvedAnimation:
final curvedAnimation = CurvedAnimation(
  parent: _animationController,
  curve: Curves.easeInOut, // Smooth start and end
);

curvedAnimation.addListener(() {
  final progress = curvedAnimation.value;
  // ... use progress for interpolation
});
CurveEffect
Curves.linearConstant speed (default)
Curves.easeInOutSlow start and end, fast middle
Curves.easeInSlow start, fast end
Curves.easeOutFast start, slow end
Curves.bounceOutBounce effect at the end

Next Steps


Tip: For smooth marker animation, use SingleTickerProviderStateMixin and keep the AnimationController duration proportional to the route length. Shorter routes need shorter durations.