Animate Camera Around a Point in Flutter
This tutorial shows how to smoothly rotate the camera around a fixed point on the map, creating a cinematic orbiting effect.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Basic Rotation Animation
Use a Flutter AnimationController to continuously rotate the bearing around a point:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class AnimateCameraScreen extends StatefulWidget {
@override
_AnimateCameraScreenState createState() => _AnimateCameraScreenState();
}
class _AnimateCameraScreenState extends State<AnimateCameraScreen>
with SingleTickerProviderStateMixin {
MapMetricsController? mapController;
late AnimationController _animationController;
bool isRotating = false;
double currentBearing = 0.0;
final LatLng center = LatLng(40.7128, -74.0060); // New York
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 60), // Full rotation in 60 seconds
);
_animationController.addListener(() {
if (mapController != null && isRotating) {
currentBearing = _animationController.value * 360;
mapController?.moveCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: center,
zoom: 15.0,
bearing: currentBearing,
tilt: 45.0,
),
),
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Orbiting Camera')),
body: Stack(
children: [
MapMetrics(
styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (controller) => mapController = controller,
initialCameraPosition: CameraPosition(
target: center,
zoom: 15.0,
tilt: 45.0,
),
),
// Play/Stop button
Positioned(
bottom: 24,
left: 0,
right: 0,
child: Center(
child: FloatingActionButton.extended(
onPressed: _toggleRotation,
icon: Icon(isRotating ? Icons.stop : Icons.play_arrow),
label: Text(isRotating ? 'Stop' : 'Orbit'),
),
),
),
],
),
);
}
void _toggleRotation() {
setState(() {
isRotating = !isRotating;
});
if (isRotating) {
_animationController.repeat();
} else {
_animationController.stop();
}
}
@override
void dispose() {
_animationController.dispose();
mapController?.dispose();
super.dispose();
}
}Adjustable Speed and Tilt
Let users control the orbit speed and tilt angle:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class CustomOrbitScreen extends StatefulWidget {
@override
_CustomOrbitScreenState createState() => _CustomOrbitScreenState();
}
class _CustomOrbitScreenState extends State<CustomOrbitScreen>
with SingleTickerProviderStateMixin {
MapMetricsController? mapController;
late AnimationController _animationController;
bool isRotating = false;
double tilt = 45.0;
double speed = 1.0; // rotations per minute
final LatLng center = LatLng(48.8584, 2.2945); // Eiffel Tower
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 60),
);
_animationController.addListener(_onAnimationTick);
}
void _onAnimationTick() {
if (mapController != null && isRotating) {
final bearing = (_animationController.value * 360 * speed) % 360;
mapController?.moveCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: center,
zoom: 16.0,
bearing: bearing,
tilt: tilt,
),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Orbit')),
body: Column(
children: [
// Controls
Container(
padding: EdgeInsets.all(12),
color: Colors.grey[100],
child: Column(
children: [
Row(
children: [
SizedBox(width: 60, child: Text('Tilt: ${tilt.toInt()}°')),
Expanded(
child: Slider(
value: tilt,
min: 0,
max: 60,
onChanged: (value) => setState(() => tilt = value),
),
),
],
),
Row(
children: [
SizedBox(width: 60, child: Text('Speed: ${speed.toStringAsFixed(1)}x')),
Expanded(
child: Slider(
value: speed,
min: 0.2,
max: 5.0,
onChanged: (value) => setState(() => speed = value),
),
),
],
),
],
),
),
// Map
Expanded(
child: Stack(
children: [
MapMetrics(
styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (controller) => mapController = controller,
initialCameraPosition: CameraPosition(
target: center,
zoom: 16.0,
tilt: tilt,
),
),
Positioned(
bottom: 24,
left: 0,
right: 0,
child: Center(
child: FloatingActionButton.extended(
onPressed: _toggleRotation,
icon: Icon(isRotating ? Icons.stop : Icons.play_arrow),
label: Text(isRotating ? 'Stop' : 'Start Orbit'),
),
),
),
],
),
),
],
),
);
}
void _toggleRotation() {
setState(() {
isRotating = !isRotating;
});
if (isRotating) {
_animationController.repeat();
} else {
_animationController.stop();
}
}
@override
void dispose() {
_animationController.dispose();
mapController?.dispose();
super.dispose();
}
}Key Concepts
| Concept | Details |
|---|---|
AnimationController | Drives the continuous rotation loop |
SingleTickerProviderStateMixin | Required mixin for AnimationController |
moveCamera | Used instead of animateCamera for frame-by-frame updates |
repeat() | Makes the animation loop continuously |
bearing | Incremented each frame to create rotation |
Next Steps
- Set Pitch and Bearing — Manual pitch/bearing control
- Fly to a Location — Animated camera transitions
- Jump to Locations — Tour through multiple locations
Tip: Use moveCamera (not animateCamera) inside the animation listener for smooth frame-by-frame updates. animateCamera adds its own easing which conflicts with the animation controller.