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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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:
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:
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:
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:
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
| Approach | Best For |
|---|---|
RotationTransition / AnimatedBuilder on a WidgetLayer marker | Rotating, pulsing, color-changing icons — the default choice |
Image.asset frame cycling in a WidgetLayer marker | Pre-rendered sprite-sheet style animation |
removeLayer + addLayer on a SymbolStyleLayer | Icons baked into the base style/vector tiles rather than widget markers |
Next Steps
- Add a Popup — Show details when an animated marker is tapped
- Add a Cluster — Group many markers together
- Add a Polyline — Animate a line growing along a route
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.