In real-time tracking apps, like ride-sharing or delivery services, simply making a map marker jump from one location to the next can feel jarring. A smooth, animated transition provides a much better user experience, showing a clear path of movement.
While the google_maps_flutter
package doesn’t offer a built-in “move” animation, you can create one yourself using Flutter’s powerful animation framework.
The Core Concept: Manual Animation
The secret to smooth movement is to rapidly update the marker’s position along a path between its start and end points. Instead of teleporting from A to B, we’ll calculate dozens of intermediate points (C, D, E…) and tell the marker to redraw itself at each one, creating the illusion of motion.
To achieve this, we’ll use a StatefulWidget
along with an AnimationController
to handle the timing and a Tween
to manage the animation’s progress.
Step 1: Setting Up the State
First, you need a StatefulWidget
to manage the marker’s changing position and the animation’s state. You must also mix in TickerProviderStateMixin
to provide the “ticker” that drives the animation forward frame by frame.
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
// Add TickerProviderStateMixin to your State class
class _MapScreenState extends State<MapScreen> with TickerProviderStateMixin {
// We'll add our map and animation logic here
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
);
}
}
DartStep 2: Initializing the Animation Controller
Inside your State
class, declare your GoogleMapController
, a Marker
, and most importantly, an AnimationController
. We will initialize the controller in the initState
method.
We will also keep track of the marker’s current and target positions.
class _MapScreenState extends State<MapScreen> with TickerProviderStateMixin {
late GoogleMapController _mapController;
// Store the current marker position
LatLng _markerPosition = const LatLng(33.6844, 73.0479); // Initial position (e.g., Islamabad)
late Marker _marker;
late AnimationController _animationController;
late Animation<double> _animation;
// A list of locations to simulate movement
final List<LatLng> _locations = const [
LatLng(33.6844, 73.0479), // Islamabad
LatLng(33.7380, 73.0844), // Faisal Mosque
LatLng(33.7295, 73.0379), // Daman-e-Koh
LatLng(33.6938, 72.9752), // Golra Sharif
];
int _locationIndex = 0;
@override
void initState() {
super.initState();
_marker = Marker(
markerId: const MarkerId('my_marker'),
position: _markerPosition,
);
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2), // Animation duration
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}
// ... rest of the class
}
DartStep 3: Interpolating Between Two LatLng Points
The core of the animation is calculating the intermediate LatLng
values. We can create a helper function that performs linear interpolation (lerp
) between a start and end location based on the animation’s progress (t
, a value from 0.0 to 1.0).
LatLng _lerp(LatLng start, LatLng end, double t) {
return LatLng(
start.latitude + (end.latitude - start.latitude) * t,
start.longitude + (end.longitude - start.longitude) * t,
);
}
DartStep 4: Driving the Animation
Now, we’ll create a method to start the animation. This method will:
- Determine the start and end points.
- Add a listener to our animation that updates the marker’s position on every tick.
- Start the animation.
void _moveMarker() {
// The starting position of the marker
final LatLng startPosition = _marker.position;
// The next position from our list
_locationIndex = (_locationIndex + 1) % _locations.length;
final LatLng endPosition = _locations[_locationIndex];
// Remove any existing listeners
_animation.removeListener(_updateMarkerPosition);
// Reset the controller to ensure it starts from the beginning
_animationController.reset();
// Redefine the animation to go from 0.0 to 1.0
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
// Add a listener to update the marker's position
void _updateMarkerPosition() {
final newPosition = _lerp(startPosition, endPosition, _animation.value);
setState(() {
_marker = _marker.copyWith(positionParam: newPosition);
});
}
_animation.addListener(_updateMarkerPosition);
// Start the animation
_animationController.forward();
}
DartWe call _marker.copyWith()
to create a new Marker
instance with the updated position, which is necessary for the GoogleMap
widget to detect the change and redraw.
Step 5: Building the UI
Finally, let’s put it all together in the build
method. We’ll have a GoogleMap
to display the marker and a FloatingActionButton
to trigger the movement.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Smooth Marker Movement'),
),
body: GoogleMap(
initialCameraPosition: CameraPosition(
target: _markerPosition,
zoom: 14,
),
onMapCreated: (controller) {
_mapController = controller;
},
markers: {_marker},
),
floatingActionButton: FloatingActionButton(
onPressed: _moveMarker,
child: const Icon(Icons.location_on),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
DartNow, when you run the app and tap the button, you’ll see the marker glide smoothly from one point to the next!
Conclusion
By leveraging Flutter’s AnimationController
and some simple interpolation math, you can transform a jarring location jump into a fluid, professional-looking animation. This technique is fundamental for any app that involves real-time location tracking and greatly enhances the overall user experience.