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

Add an Animated Icon to the Map in Flutter

Static pins render better with MarkerLayer

This page uses WidgetLayer, which repositions each marker in Dart every frame — it lags a frame while panning, ignores tilt and bearing, and does not scale past a few dozen markers. If your markers are plain pins with no Flutter content or gestures, use MarkerLayer instead; see Markers and Annotations.

This tutorial shows how to create animated marker icons — rotating, scaling, or changing appearance over time — for live tracking, alerts, or eye-catching points of interest.

Prerequisites

Before you begin, ensure you have:

Animate the Widget, Not a Bitmap

Because WidgetLayer markers take a real Flutter child widget (not a rasterized BitmapDescriptor), you don't need to redraw a Canvas on every frame and re-encode it to PNG bytes. Drive the marker's child with normal Flutter animation widgets — AnimationController, RotationTransition, ColorTween — the same way you'd animate any other widget.

Rotating Icon Marker

A compass-style icon that spins continuously:

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

class RotatingIconScreen extends StatefulWidget {
  const RotatingIconScreen({super.key});

  @override
  State<RotatingIconScreen> createState() => _RotatingIconScreenState();
}

class _RotatingIconScreenState extends State<RotatingIconScreen>
    with SingleTickerProviderStateMixin {
  MapController? mapController;
  late final AnimationController _animController;

  static const _position = Position(2.2945, 48.8584);

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Rotating Icon')),
      body: MapMetricsView(
        options: MapOptions(
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
          initCenter: _position,
          initZoom: 15,
        ),
        onMapCreated: (controller) => mapController = controller,
        mapChildren: [
          WidgetLayer(
            markers: [
              Marker(
                point: _position,
                size: const Size.square(48),
                alignment: Alignment.center,
                child: RotationTransition(
                  turns: _animController,
                  child: Container(
                    decoration: const BoxDecoration(
                      color: Colors.blue,
                      shape: BoxShape.circle,
                    ),
                    child: const Icon(
                      Icons.navigation,
                      color: Colors.white,
                      size: 28,
                    ),
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

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

Color-Cycling Alert Icon

A marker that pulses between red and yellow to draw attention, using AnimatedContainer/ColorTween:

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

class AlertIconScreen extends StatefulWidget {
  const AlertIconScreen({super.key});

  @override
  State<AlertIconScreen> createState() => _AlertIconScreenState();
}

class _AlertIconScreenState extends State<AlertIconScreen>
    with SingleTickerProviderStateMixin {
  MapController? mapController;
  late final AnimationController _animController;
  late final Animation<Color?> _colorAnimation;

  final _alertLocations = const [
    Position(2.340, 48.860),
    Position(2.350, 48.855),
    Position(2.325, 48.865),
  ];

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

    _colorAnimation = ColorTween(
      begin: Colors.red,
      end: Colors.yellow,
    ).animate(_animController);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Alert Icons')),
      body: MapMetricsView(
        options: MapOptions(
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
          initCenter: Position(2.340, 48.858),
          initZoom: 14,
        ),
        onMapCreated: (controller) => mapController = controller,
        mapChildren: [
          WidgetLayer(
            markers: _alertLocations.asMap().entries.map((e) {
              return Marker(
                point: e.value,
                size: const Size.square(48),
                alignment: Alignment.center,
                child: AnimatedBuilder(
                  animation: _animController,
                  builder: (context, _) {
                    final color = _colorAnimation.value ?? Colors.red;
                    return Container(
                      decoration: BoxDecoration(
                        color: color.withValues(alpha: 0.3),
                        shape: BoxShape.circle,
                      ),
                      alignment: Alignment.center,
                      child: Container(
                        width: 24,
                        height: 24,
                        decoration: BoxDecoration(
                          color: color,
                          shape: BoxShape.circle,
                          border: Border.all(color: Colors.white, width: 2),
                        ),
                        alignment: Alignment.center,
                        child: const Text(
                          '!',
                          style: TextStyle(
                            color: Colors.white,
                            fontSize: 14,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                    );
                  },
                ),
              );
            }).toList(),
          ),
        ],
      ),
    );
  }

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

Frame-by-Frame Sprite Animation

Cycle through pre-made image frames the same way you'd animate an Image widget anywhere else in Flutter — no BitmapDescriptor frame list required:

dart
import 'dart:async';

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

class SpriteAnimationScreen extends StatefulWidget {
  const SpriteAnimationScreen({super.key});

  @override
  State<SpriteAnimationScreen> createState() => _SpriteAnimationScreenState();
}

class _SpriteAnimationScreenState extends State<SpriteAnimationScreen> {
  MapController? mapController;
  Timer? _frameTimer;
  int _currentFrame = 0;
  static const _frameCount = 8;

  static const _position = Position(2.2945, 48.8584);

  @override
  void initState() {
    super.initState();
    _frameTimer = Timer.periodic(const Duration(milliseconds: 150), (_) {
      setState(() {
        _currentFrame = (_currentFrame + 1) % _frameCount;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sprite Animation')),
      body: MapMetricsView(
        options: MapOptions(
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
          initCenter: _position,
          initZoom: 15,
        ),
        onMapCreated: (controller) => mapController = controller,
        mapChildren: [
          WidgetLayer(
            markers: [
              Marker(
                point: _position,
                size: const Size.square(48),
                alignment: Alignment.center,
                child: Image.asset('assets/anim/frame_$_currentFrame.png'),
              ),
            ],
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _frameTimer?.cancel();
    super.dispose();
  }
}

Declare the frames in pubspec.yaml:

yaml
flutter:
  assets:
    - assets/anim/

Animating a Style-Layer Icon Instead

If your icon lives on a native SymbolStyleLayer (loaded once via StyleController.addImage) rather than a WidgetLayer marker, you can still animate rotation by periodically removing and re-adding the layer with a new icon-rotate paint value, or by driving icon-rotate off a ['get', 'bearing'] expression fed by StyleController.updateGeoJsonSource. There's no setPaintProperty-style single-property setter — updates to an existing style layer go through removeLayer + addLayer.

Animation Approaches

ApproachBest For
RotationTransition / AnimatedBuilder on a WidgetLayer markerRotating, pulsing, color-changing icons — the default choice
Image.asset frame cycling in a WidgetLayer markerPre-rendered sprite-sheet style animation
removeLayer + addLayer on a SymbolStyleLayerIcons baked into the base style/vector tiles rather than widget markers

Next Steps


Tip: Because WidgetLayer markers are ordinary Flutter widgets, prefer Flutter's own animation primitives (AnimationController, Tween, AnimatedBuilder) over generating raster frames — it's both simpler and cheaper than rasterizing a Canvas every tick.