Skip to content

Add an Animated Icon to the Map in Flutter

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:

Rotating Icon Marker

Create a marker icon that rotates continuously (e.g., a compass or loading indicator):

dart
import 'dart:math';
import 'dart:ui' as ui;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class RotatingIconScreen extends StatefulWidget {
  @override
  _RotatingIconScreenState createState() => _RotatingIconScreenState();
}

class _RotatingIconScreenState extends State<RotatingIconScreen>
    with SingleTickerProviderStateMixin {
  MapMetricsController? mapController;
  late AnimationController _animController;
  BitmapDescriptor? currentIcon;

  final LatLng position = LatLng(48.8584, 2.2945);

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

    _animController.addListener(() {
      _generateRotatedIcon(_animController.value * 2 * pi);
    });
  }

  Future<void> _generateRotatedIcon(double angle) async {
    final size = 64.0;
    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);

    // Background circle
    canvas.drawCircle(
      Offset(size / 2, size / 2),
      size / 2,
      Paint()..color = Colors.blue,
    );

    // Rotating arrow
    canvas.save();
    canvas.translate(size / 2, size / 2);
    canvas.rotate(angle);

    final arrowPaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3
      ..strokeCap = StrokeCap.round;

    // Arrow pointing up
    canvas.drawLine(Offset(0, 12), Offset(0, -12), arrowPaint);
    canvas.drawLine(Offset(0, -12), Offset(-6, -4), arrowPaint);
    canvas.drawLine(Offset(0, -12), Offset(6, -4), arrowPaint);

    canvas.restore();

    final picture = recorder.endRecording();
    final image = await picture.toImage(size.toInt(), size.toInt());
    final bytes = await image.toByteData(format: ui.ImageByteFormat.png);

    if (mounted) {
      setState(() {
        currentIcon = BitmapDescriptor.fromBytes(bytes!.buffer.asUint8List());
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Rotating Icon')),
      body: MapMetrics(
        styleUrl:
            'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (MapMetricsController controller) {
          mapController = controller;
        },
        initialCameraPosition: CameraPosition(
          target: position,
          zoom: 15.0,
        ),
        markers: currentIcon != null
            ? {
                Marker(
                  markerId: MarkerId('rotating'),
                  position: position,
                  icon: currentIcon!,
                  anchor: Offset(0.5, 0.5),
                ),
              }
            : {},
      ),
    );
  }

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

Color-Cycling Alert Icon

A marker that cycles through colors to draw attention:

dart
import 'dart:ui' as ui;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class AlertIconScreen extends StatefulWidget {
  @override
  _AlertIconScreenState createState() => _AlertIconScreenState();
}

class _AlertIconScreenState extends State<AlertIconScreen>
    with SingleTickerProviderStateMixin {
  MapMetricsController? mapController;
  late AnimationController _animController;
  late Animation<Color?> _colorAnimation;
  BitmapDescriptor? currentIcon;

  final List<LatLng> alertLocations = [
    LatLng(48.860, 2.340),
    LatLng(48.855, 2.350),
    LatLng(48.865, 2.325),
  ];

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

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

    _animController.addListener(() {
      _generateColoredIcon(_colorAnimation.value!);
    });
  }

  Future<void> _generateColoredIcon(Color color) async {
    final size = 48.0;
    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);

    // Outer glow
    canvas.drawCircle(
      Offset(size / 2, size / 2),
      size / 2,
      Paint()..color = color.withOpacity(0.3),
    );

    // Inner circle
    canvas.drawCircle(
      Offset(size / 2, size / 2),
      size / 3,
      Paint()..color = color,
    );

    // White border
    canvas.drawCircle(
      Offset(size / 2, size / 2),
      size / 3,
      Paint()
        ..color = Colors.white
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2,
    );

    // Exclamation mark
    final textPainter = TextPainter(
      text: TextSpan(
        text: '!',
        style: TextStyle(
          color: Colors.white,
          fontSize: 18,
          fontWeight: FontWeight.bold,
        ),
      ),
      textDirection: TextDirection.ltr,
    );
    textPainter.layout();
    textPainter.paint(
      canvas,
      Offset((size - textPainter.width) / 2, (size - textPainter.height) / 2),
    );

    final picture = recorder.endRecording();
    final image = await picture.toImage(size.toInt(), size.toInt());
    final bytes = await image.toByteData(format: ui.ImageByteFormat.png);

    if (mounted) {
      setState(() {
        currentIcon = BitmapDescriptor.fromBytes(bytes!.buffer.asUint8List());
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Alert Icons')),
      body: MapMetrics(
        styleUrl:
            'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (MapMetricsController controller) {
          mapController = controller;
        },
        initialCameraPosition: CameraPosition(
          target: LatLng(48.858, 2.340),
          zoom: 14.0,
        ),
        markers: currentIcon != null
            ? alertLocations.asMap().entries.map((e) {
                return Marker(
                  markerId: MarkerId('alert_${e.key}'),
                  position: e.value,
                  icon: currentIcon!,
                  anchor: Offset(0.5, 0.5),
                  infoWindow: InfoWindow(title: 'Alert ${e.key + 1}'),
                );
              }).toSet()
            : {},
      ),
    );
  }

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

Frame-by-Frame Sprite Animation

Cycle through pre-made icon frames for sprite-sheet style animation:

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

class SpriteAnimationScreen extends StatefulWidget {
  @override
  _SpriteAnimationScreenState createState() => _SpriteAnimationScreenState();
}

class _SpriteAnimationScreenState extends State<SpriteAnimationScreen> {
  MapMetricsController? mapController;
  Timer? frameTimer;
  int currentFrame = 0;
  List<BitmapDescriptor> frames = [];

  @override
  void initState() {
    super.initState();
    _loadFrames();
  }

  Future<void> _loadFrames() async {
    // Load animation frames from assets
    // e.g., assets/anim/frame_0.png, frame_1.png, frame_2.png...
    final loadedFrames = <BitmapDescriptor>[];
    for (int i = 0; i < 8; i++) {
      final icon = await BitmapDescriptor.fromAssetImage(
        ImageConfiguration(size: Size(48, 48)),
        'assets/anim/frame_$i.png',
      );
      loadedFrames.add(icon);
    }

    setState(() {
      frames = loadedFrames;
    });

    // Start frame cycling
    frameTimer = Timer.periodic(Duration(milliseconds: 150), (_) {
      setState(() {
        currentFrame = (currentFrame + 1) % frames.length;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Sprite Animation')),
      body: MapMetrics(
        styleUrl:
            'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (MapMetricsController controller) {
          mapController = controller;
        },
        initialCameraPosition: CameraPosition(
          target: LatLng(48.8584, 2.2945),
          zoom: 15.0,
        ),
        markers: frames.isNotEmpty
            ? {
                Marker(
                  markerId: MarkerId('animated'),
                  position: LatLng(48.8584, 2.2945),
                  icon: frames[currentFrame],
                ),
              }
            : {},
      ),
    );
  }

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

Declare frames in pubspec.yaml:

yaml
flutter:
  assets:
    - assets/anim/

Animation Approaches

ApproachFPSBest For
Canvas drawing + Timer15-30Rotating/color-changing icons
Pre-loaded sprite frames10-15Complex custom animations
AnimationController60Smooth transitions

Next Steps


Tip: Generating icons on every animation frame is CPU-intensive. For production apps, pre-generate all frames during initState and store them in a list, then just swap the icon property each frame — this is much more efficient than drawing on every tick.