Customize Camera Animations in Flutter
This tutorial shows how to create custom camera animations — zooming, tilting, rotating, and combining multiple camera movements for cinematic map experiences.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Basic Camera Animations
Different types of camera movements with buttons:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class CameraAnimationsScreen extends StatefulWidget {
@override
_CameraAnimationsScreenState createState() =>
_CameraAnimationsScreenState();
}
class _CameraAnimationsScreenState extends State<CameraAnimationsScreen> {
MapMetricsController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Camera Animations')),
body: Column(
children: [
// Animation buttons
Container(
padding: EdgeInsets.all(8),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
_animButton('Zoom In', Icons.zoom_in, _zoomIn),
_animButton('Zoom Out', Icons.zoom_out, _zoomOut),
_animButton('Tilt', Icons.panorama_horizontal, _tilt),
_animButton('Rotate', Icons.rotate_right, _rotate),
_animButton('Bird\'s Eye', Icons.flight, _birdsEye),
_animButton('Reset', Icons.refresh, _reset),
],
),
),
// Map
Expanded(
child: 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,
),
),
),
],
),
);
}
Widget _animButton(String label, IconData icon, VoidCallback onPressed) {
return ElevatedButton.icon(
onPressed: onPressed,
icon: Icon(icon, size: 18),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
);
}
void _zoomIn() {
mapController?.animateCamera(
CameraUpdate.zoomTo(18.0),
);
}
void _zoomOut() {
mapController?.animateCamera(
CameraUpdate.zoomTo(10.0),
);
}
void _tilt() {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(48.8584, 2.2945),
zoom: 16.0,
tilt: 60.0,
bearing: 0.0,
),
),
);
}
void _rotate() {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(48.8584, 2.2945),
zoom: 16.0,
tilt: 45.0,
bearing: 180.0,
),
),
);
}
void _birdsEye() {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(48.8584, 2.2945),
zoom: 17.0,
tilt: 75.0,
bearing: 45.0,
),
),
);
}
void _reset() {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(48.8584, 2.2945),
zoom: 15.0,
tilt: 0.0,
bearing: 0.0,
),
),
);
}
}Cinematic City Tour
Automatically fly through a sequence of locations with different camera angles:
dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class CityTourScreen extends StatefulWidget {
@override
_CityTourScreenState createState() => _CityTourScreenState();
}
class _CityTourScreenState extends State<CityTourScreen> {
MapMetricsController? mapController;
bool isTouring = false;
int currentStop = 0;
final List<Map<String, dynamic>> tourStops = [
{
'name': 'Eiffel Tower',
'position': CameraPosition(
target: LatLng(48.8584, 2.2945),
zoom: 17.0,
tilt: 60.0,
bearing: 45.0,
),
},
{
'name': 'Arc de Triomphe',
'position': CameraPosition(
target: LatLng(48.8738, 2.2950),
zoom: 17.0,
tilt: 55.0,
bearing: 135.0,
),
},
{
'name': 'Louvre Museum',
'position': CameraPosition(
target: LatLng(48.8606, 2.3376),
zoom: 16.5,
tilt: 50.0,
bearing: 220.0,
),
},
{
'name': 'Notre-Dame',
'position': CameraPosition(
target: LatLng(48.8530, 2.3499),
zoom: 17.0,
tilt: 65.0,
bearing: 310.0,
),
},
{
'name': 'Sacre-Coeur',
'position': CameraPosition(
target: LatLng(48.8867, 2.3431),
zoom: 16.0,
tilt: 70.0,
bearing: 180.0,
),
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('City Tour')),
body: Stack(
children: [
MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.8566, 2.3200),
zoom: 12.0,
),
),
// Tour info bar
Positioned(
top: 16,
left: 16,
right: 16,
child: Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isTouring
? tourStops[currentStop]['name']
: 'Paris City Tour',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
if (isTouring)
Padding(
padding: EdgeInsets.only(top: 8),
child: LinearProgressIndicator(
value: (currentStop + 1) / tourStops.length,
),
),
],
),
),
),
),
// Tour control
Positioned(
bottom: 24,
left: 0,
right: 0,
child: Center(
child: ElevatedButton.icon(
onPressed: isTouring ? null : _startTour,
icon: Icon(isTouring ? Icons.pause : Icons.play_arrow),
label: Text(isTouring ? 'Touring...' : 'Start Tour'),
style: ElevatedButton.styleFrom(
padding:
EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
),
),
],
),
);
}
Future<void> _startTour() async {
setState(() {
isTouring = true;
currentStop = 0;
});
for (int i = 0; i < tourStops.length; i++) {
if (!mounted) return;
setState(() {
currentStop = i;
});
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
tourStops[i]['position'] as CameraPosition,
),
);
// Wait at each stop
await Future.delayed(Duration(seconds: 4));
}
if (mounted) {
setState(() {
isTouring = false;
});
}
}
}Smooth Zoom with Duration
Control animation speed using moveCamera (instant) vs animateCamera (smooth):
dart
void _smoothZoomToLocation() {
// Smooth animated transition (default ~300ms)
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(48.8584, 2.2945),
zoom: 18.0,
tilt: 60.0,
bearing: 30.0,
),
),
);
}
void _instantJumpToLocation() {
// Instant jump — no animation
mapController?.moveCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(48.8584, 2.2945),
zoom: 18.0,
tilt: 60.0,
bearing: 30.0,
),
),
);
}Camera Update Methods
| Method | Animation | Use Case |
|---|---|---|
animateCamera | Smooth transition | User-facing navigation |
moveCamera | Instant jump | Loading, resetting |
CameraUpdate.zoomTo(level) | Zoom only | Quick zoom change |
CameraUpdate.newLatLng(pos) | Pan only | Move without zoom change |
CameraUpdate.newLatLngZoom(pos, zoom) | Pan + zoom | Navigate to a location |
CameraUpdate.newCameraPosition(...) | Full control | Tilt, bearing, zoom, pan |
CameraUpdate.newLatLngBounds(bounds, padding) | Fit area | Show all markers |
Next Steps
- Fly to a Location — Basic fly-to animation
- Slowly Fly to Location — Slow cinematic flight
- Animate Camera Around Point — Orbit animation
Tip: Combine tilt (0-60) and bearing (0-360) for dramatic 3D views. Higher tilt values give a more ground-level perspective, which works best at zoom levels 15+.