Skip to content

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:

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

ConceptDetails
AnimationControllerDrives the continuous rotation loop
SingleTickerProviderStateMixinRequired mixin for AnimationController
moveCameraUsed instead of animateCamera for frame-by-frame updates
repeat()Makes the animation loop continuously
bearingIncremented each frame to create rotation

Next Steps


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.