Skip to content

Animate a Point in Flutter

This tutorial shows how to animate a point (marker) bouncing, pulsing, or moving on the map using Flutter's built-in animation system.

Prerequisites

Before you begin, ensure you have:

Bouncing Marker

Animate a marker that bounces up and down continuously:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class BouncingMarkerScreen extends StatefulWidget {
  @override
  _BouncingMarkerScreenState createState() => _BouncingMarkerScreenState();
}

class _BouncingMarkerScreenState extends State<BouncingMarkerScreen>
    with SingleTickerProviderStateMixin {
  MapMetricsController? mapController;
  late AnimationController _animController;
  late Animation<double> _bounceAnimation;

  final LatLng markerPosition = LatLng(48.8584, 2.2945); // Eiffel Tower

  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      duration: Duration(milliseconds: 800),
      vsync: this,
    )..repeat(reverse: true);

    _bounceAnimation = Tween<double>(begin: 0.0, end: 0.003).animate(
      CurvedAnimation(parent: _animController, curve: Curves.easeInOut),
    );

    _animController.addListener(() {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    // Offset the latitude slightly to simulate bouncing
    final animatedPosition = LatLng(
      markerPosition.latitude + _bounceAnimation.value,
      markerPosition.longitude,
    );

    return Scaffold(
      appBar: AppBar(title: Text('Bouncing Marker')),
      body: MapMetrics(
        styleUrl:
            'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (MapMetricsController controller) {
          mapController = controller;
        },
        initialCameraPosition: CameraPosition(
          target: markerPosition,
          zoom: 15.0,
        ),
        markers: {
          Marker(
            markerId: MarkerId('bouncing'),
            position: animatedPosition,
            infoWindow: InfoWindow(title: 'Eiffel Tower'),
          ),
        },
      ),
    );
  }

  @override
  void dispose() {
    _animController.dispose();
    super.dispose();
  }
}

Pulsing Circle

Show a pulsing circle that grows and fades around a location:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class PulsingCircleScreen extends StatefulWidget {
  @override
  _PulsingCircleScreenState createState() => _PulsingCircleScreenState();
}

class _PulsingCircleScreenState extends State<PulsingCircleScreen>
    with SingleTickerProviderStateMixin {
  MapMetricsController? mapController;
  late AnimationController _animController;
  late Animation<double> _radiusAnimation;
  late Animation<double> _opacityAnimation;

  final LatLng center = LatLng(48.8566, 2.3522); // Paris center

  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    )..repeat();

    _radiusAnimation = Tween<double>(begin: 100.0, end: 500.0).animate(
      CurvedAnimation(parent: _animController, curve: Curves.easeOut),
    );

    _opacityAnimation = Tween<double>(begin: 0.4, end: 0.0).animate(
      CurvedAnimation(parent: _animController, curve: Curves.easeOut),
    );

    _animController.addListener(() {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Pulsing Circle')),
      body: MapMetrics(
        styleUrl:
            'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (MapMetricsController controller) {
          mapController = controller;
        },
        initialCameraPosition: CameraPosition(
          target: center,
          zoom: 14.0,
        ),
        circles: {
          // Pulsing circle
          Circle(
            circleId: CircleId('pulse'),
            center: center,
            radius: _radiusAnimation.value,
            fillColor: Colors.blue.withOpacity(_opacityAnimation.value),
            strokeColor: Colors.blue.withOpacity(_opacityAnimation.value),
            strokeWidth: 2,
          ),
          // Static center dot
          Circle(
            circleId: CircleId('center_dot'),
            center: center,
            radius: 30,
            fillColor: Colors.blue.withOpacity(0.8),
            strokeColor: Colors.white,
            strokeWidth: 3,
          ),
        },
        markers: {
          Marker(
            markerId: MarkerId('center'),
            position: center,
            infoWindow: InfoWindow(title: 'Your Location'),
          ),
        },
      ),
    );
  }

  @override
  void dispose() {
    _animController.dispose();
    super.dispose();
  }
}

Moving Point Between Locations

Smoothly move a point from one location to another:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class MovingPointScreen extends StatefulWidget {
  @override
  _MovingPointScreenState createState() => _MovingPointScreenState();
}

class _MovingPointScreenState extends State<MovingPointScreen>
    with SingleTickerProviderStateMixin {
  MapMetricsController? mapController;
  late AnimationController _animController;
  late Animation<double> _latAnimation;
  late Animation<double> _lngAnimation;

  final List<Map<String, dynamic>> destinations = [
    {'name': 'Paris', 'lat': 48.8566, 'lng': 2.3522},
    {'name': 'London', 'lat': 51.5074, 'lng': -0.1276},
    {'name': 'Berlin', 'lat': 52.52, 'lng': 13.405},
    {'name': 'Rome', 'lat': 41.9028, 'lng': 12.4964},
  ];
  int currentDestination = 0;

  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _setAnimationToNextDestination();

    _animController.addListener(() {
      setState(() {});
    });

    _animController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        // Move to next destination
        setState(() {
          currentDestination =
              (currentDestination + 1) % destinations.length;
        });
        _setAnimationToNextDestination();
        _animController.forward(from: 0.0);
      }
    });

    _animController.forward();
  }

  void _setAnimationToNextDestination() {
    final from = destinations[currentDestination];
    final to =
        destinations[(currentDestination + 1) % destinations.length];

    _latAnimation = Tween<double>(
      begin: from['lat'],
      end: to['lat'],
    ).animate(CurvedAnimation(
      parent: _animController,
      curve: Curves.easeInOut,
    ));

    _lngAnimation = Tween<double>(
      begin: from['lng'],
      end: to['lng'],
    ).animate(CurvedAnimation(
      parent: _animController,
      curve: Curves.easeInOut,
    ));
  }

  @override
  Widget build(BuildContext context) {
    final currentPosition = LatLng(
      _latAnimation.value,
      _lngAnimation.value,
    );
    final destName = destinations[
        (currentDestination + 1) % destinations.length]['name'];

    return Scaffold(
      appBar: AppBar(title: Text('Moving Point')),
      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.0, 8.0),
              zoom: 4.0,
            ),
            markers: {
              // Destination markers
              ...destinations.map((d) => Marker(
                    markerId: MarkerId(d['name']),
                    position: LatLng(d['lat'], d['lng']),
                    icon: BitmapDescriptor.defaultMarkerWithHue(
                        BitmapDescriptor.hueOrange),
                    infoWindow: InfoWindow(title: d['name']),
                  )),
              // Moving point
              Marker(
                markerId: MarkerId('moving'),
                position: currentPosition,
                icon: BitmapDescriptor.defaultMarkerWithHue(
                    BitmapDescriptor.hueBlue),
              ),
            },
          ),
          // Status bar
          Positioned(
            top: 16,
            left: 16,
            right: 16,
            child: Card(
              child: Padding(
                padding: EdgeInsets.all(12),
                child: Text(
                  'Moving to $destName...',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _animController.dispose();
    super.dispose();
  }
}

Animation Options

TypeUse CaseFlutter Class
BounceDraw attention to a markerAnimationController + Curves.easeInOut
PulseShow live location or alertsAnimationController + repeat()
MoveTransition between locationsTween<double> for lat/lng
RotateCompass or direction indicatorTween<double> for bearing

Next Steps


Tip: Use SingleTickerProviderStateMixin for a single animation or TickerProviderStateMixin if your screen has multiple independent animations running simultaneously.