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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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
| Approach | FPS | Best For |
|---|---|---|
| Canvas drawing + Timer | 15-30 | Rotating/color-changing icons |
| Pre-loaded sprite frames | 10-15 | Complex custom animations |
| AnimationController | 60 | Smooth transitions |
Next Steps
- Animate a Point — Moving/bouncing markers
- Add Icon to Map — Static custom icons
- Add Custom Icons with Markers — Custom marker styles
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.