Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

Animate a Point in Flutter

This tutorial shows how to animate a point (marker) bouncing, pulsing, or moving on the map using Flutter's built-in animation system.

Prerequisites

Before you begin, ensure you have:

There is no markers: / circles: / Marker / Circle widget API on MapMetricsView. Points are rendered with the declarative layers: list instead — MarkerLayer for icon/text markers and CircleLayer for dots — built from Point(coordinates: Position(lng, lat)) values. Because layers: is an ordinary Flutter widget property, rebuilding it from setState inside an AnimationController listener animates the point exactly like the original Marker/Circle re-creation pattern did.

One real constraint to know up front: CircleLayer.radius is a pixel radius (screen-space), not a geographic radius in meters — there is no geo-radius circle type in the SDK.

Bouncing Marker

Animate a marker that bounces up and down continuously:

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

class BouncingMarkerScreen extends StatefulWidget {
  @override
  _BouncingMarkerScreenState createState() => _BouncingMarkerScreenState();
}

class _BouncingMarkerScreenState extends State<BouncingMarkerScreen>
    with SingleTickerProviderStateMixin {
  MapController? mapController;
  late AnimationController _animController;
  late Animation<double> _bounceAnimation;

  final Position markerPosition = Position(2.2945, 48.8584); // Eiffel Tower

  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      duration: Duration(milliseconds: 800),
      vsync: this,
    )..repeat(reverse: true);

    _bounceAnimation = Tween<double>(begin: 0.0, end: 0.003).animate(
      CurvedAnimation(parent: _animController, curve: Curves.easeInOut),
    );

    _animController.addListener(() {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    // Offset the latitude slightly to simulate bouncing
    final animatedPosition = Position(
      markerPosition.lng,
      markerPosition.lat + _bounceAnimation.value,
    );

    return Scaffold(
      appBar: AppBar(title: Text('Bouncing Marker')),
      body: MapMetricsView(
        options: MapOptions(
          initCenter: markerPosition,
          initZoom: 15.0,
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
        ),
        onMapCreated: (MapController controller) {
          mapController = controller;
        },
        layers: [
          MarkerLayer(
            points: [Point(coordinates: animatedPosition)],
            textField: 'Eiffel Tower',
            textOffset: const [0, 1],
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _animController.dispose();
    super.dispose();
  }
}

Pulsing Circle

Show a pulsing circle that grows and fades around a location:

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

class PulsingCircleScreen extends StatefulWidget {
  @override
  _PulsingCircleScreenState createState() => _PulsingCircleScreenState();
}

class _PulsingCircleScreenState extends State<PulsingCircleScreen>
    with SingleTickerProviderStateMixin {
  MapController? mapController;
  late AnimationController _animController;
  late Animation<double> _radiusAnimation;
  late Animation<double> _opacityAnimation;

  final Position center = Position(2.3522, 48.8566); // Paris center

  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    )..repeat();

    // Radius is in screen pixels, not meters.
    _radiusAnimation = Tween<double>(begin: 10.0, end: 60.0).animate(
      CurvedAnimation(parent: _animController, curve: Curves.easeOut),
    );

    _opacityAnimation = Tween<double>(begin: 0.4, end: 0.0).animate(
      CurvedAnimation(parent: _animController, curve: Curves.easeOut),
    );

    _animController.addListener(() {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Pulsing Circle')),
      body: MapMetricsView(
        options: MapOptions(
          initCenter: center,
          initZoom: 14.0,
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
        ),
        onMapCreated: (MapController controller) {
          mapController = controller;
        },
        layers: [
          // Pulsing ring
          CircleLayer(
            points: [Point(coordinates: center)],
            radius: _radiusAnimation.value.round(),
            color: Colors.blue.withValues(alpha: _opacityAnimation.value),
            strokeColor: Colors.blue.withValues(alpha: _opacityAnimation.value),
            strokeWidth: 2,
          ),
          // Static center dot
          CircleLayer(
            points: [Point(coordinates: center)],
            radius: 10,
            color: Colors.blue.withValues(alpha: 0.8),
            strokeColor: Colors.white,
            strokeWidth: 3,
          ),
          // Label
          MarkerLayer(
            points: [Point(coordinates: center)],
            textField: 'Your Location',
            textOffset: const [0, 2],
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _animController.dispose();
    super.dispose();
  }
}

Moving Point Between Locations

Smoothly move a point from one location to another. Each destination is its own single-point MarkerLayer so it can carry its own label — MarkerLayer applies one textField uniformly to every point it holds, so heterogeneous labels need one layer instance per label, all listed together in layers::

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

class MovingPointScreen extends StatefulWidget {
  @override
  _MovingPointScreenState createState() => _MovingPointScreenState();
}

class _MovingPointScreenState extends State<MovingPointScreen>
    with SingleTickerProviderStateMixin {
  MapController? mapController;
  late AnimationController _animController;
  late Animation<double> _latAnimation;
  late Animation<double> _lngAnimation;

  // lng/lat order.
  final List<Map<String, dynamic>> destinations = [
    {'name': 'Paris', 'lng': 2.3522, 'lat': 48.8566},
    {'name': 'London', 'lng': -0.1276, 'lat': 51.5074},
    {'name': 'Berlin', 'lng': 13.405, 'lat': 52.52},
    {'name': 'Rome', 'lng': 12.4964, 'lat': 41.9028},
  ];
  int currentDestination = 0;

  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _setAnimationToNextDestination();

    _animController.addListener(() {
      setState(() {});
    });

    _animController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        // Move to next destination
        setState(() {
          currentDestination =
              (currentDestination + 1) % destinations.length;
        });
        _setAnimationToNextDestination();
        _animController.forward(from: 0.0);
      }
    });

    _animController.forward();
  }

  void _setAnimationToNextDestination() {
    final from = destinations[currentDestination];
    final to =
        destinations[(currentDestination + 1) % destinations.length];

    _latAnimation = Tween<double>(
      begin: from['lat'],
      end: to['lat'],
    ).animate(CurvedAnimation(
      parent: _animController,
      curve: Curves.easeInOut,
    ));

    _lngAnimation = Tween<double>(
      begin: from['lng'],
      end: to['lng'],
    ).animate(CurvedAnimation(
      parent: _animController,
      curve: Curves.easeInOut,
    ));
  }

  @override
  Widget build(BuildContext context) {
    final currentPosition = Position(
      _lngAnimation.value,
      _latAnimation.value,
    );
    final destName = destinations[
        (currentDestination + 1) % destinations.length]['name'];

    return Scaffold(
      appBar: AppBar(title: Text('Moving Point')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initCenter: Position(8.0, 48.0),
              initZoom: 4.0,
              initStyle:
                  'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
            ),
            onMapCreated: (MapController controller) {
              mapController = controller;
            },
            layers: [
              // Destination markers (one layer per point so each keeps its
              // own label)
              for (final d in destinations)
                MarkerLayer(
                  points: [Point(coordinates: Position(d['lng'], d['lat']))],
                  textField: d['name'] as String,
                  textOffset: const [0, 1],
                ),
              // Destination dots
              for (final d in destinations)
                CircleLayer(
                  points: [Point(coordinates: Position(d['lng'], d['lat']))],
                  radius: 6,
                  color: Colors.orange,
                  strokeColor: Colors.white,
                  strokeWidth: 2,
                ),
              // Moving point
              CircleLayer(
                points: [Point(coordinates: currentPosition)],
                radius: 7,
                color: Colors.blue,
                strokeColor: Colors.white,
                strokeWidth: 2,
              ),
            ],
          ),
          // Status bar
          Positioned(
            top: 16,
            left: 16,
            right: 16,
            child: Card(
              child: Padding(
                padding: EdgeInsets.all(12),
                child: Text(
                  'Moving to $destName...',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _animController.dispose();
    super.dispose();
  }
}

Animation Options

TypeUse CaseFlutter Class
BounceDraw attention to a markerAnimationController + Curves.easeInOut
PulseShow live location or alertsAnimationController + repeat()
MoveTransition between locationsTween<double> for lat/lng
RotateCompass or direction indicatorTween<double> for bearing

Next Steps


Tip: Use SingleTickerProviderStateMixin for a single animation or TickerProviderStateMixin if your screen has multiple independent animations running simultaneously. Rebuilding the layers: list every frame is cheap for a handful of points — for hundreds of moving points, prefer the GeoJsonSource + StyleController.updateGeoJsonSource approach shown in Animate a Marker.