If you've ever built a ride-sharing or delivery tracking app in Flutter using flutter_map or google_maps_flutter, you've likely encountered the "jumping marker" problem. When a new GPS coordinate arrives every 2-3 seconds, simply updating the marker's state causes it to teleport instantly to the new location. It looks glitchy and unprofessional.
To achieve Uber-like smoothness, you need to interpolate the marker's position between the old coordinate and the new coordinate at 60 FPS. Here is the exact solution we implemented in BhuMitra for real-time surveyor tracking.
1. The Problem: Discrete Data vs Continuous UI
GPS sensors emit data discretely (e.g., once every 2000ms). Your screen refreshes continuously (60 times a second, or every 16ms). You need to bridge this gap using Linear Interpolation (Lerp).
2. Creating a Custom LatLngTween
Flutter has built-in tweens for colors, doubles, and offsets, but not for Map coordinates. We need to create a custom Tween that interpolates between two LatLng objects.
import 'package:flutter/animation.dart';
import 'package:latlong2/latlong.dart';
class LatLngTween extends Tween<LatLng> {
LatLngTween({LatLng? begin, LatLng? end}) : super(begin: begin, end: end);
@override
LatLng lerp(double t) {
if (begin == null || end == null) return end ?? begin ?? LatLng(0, 0);
// Linear interpolation formula: start + (end - start) * t
final lat = begin!.latitude + (end!.latitude - begin!.latitude) * t;
final lng = begin!.longitude + (end!.longitude - begin!.longitude) * t;
return LatLng(lat, lng);
}
}
3. Using TweenAnimationBuilder for Implicit Animation
Instead of manually managing an AnimationController, we can use Flutter's TweenAnimationBuilder. Whenever you pass it a new end value, it automatically animates from its current value to the new value over the specified duration.
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
class AnimatedMarkerLayer extends StatelessWidget {
final LatLng currentLocation; // The latest GPS coordinate from your stream
const AnimatedMarkerLayer({Key? key, required this.currentLocation}) : super(key: key);
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder<LatLng>(
// Provide the new location. When this changes, the animation triggers.
tween: LatLngTween(begin: currentLocation, end: currentLocation),
// Duration should match your GPS polling interval roughly (e.g., 1-2 seconds)
duration: const Duration(milliseconds: 1500),
// Use linear or easeInOut depending on the physics you want
curve: Curves.linear,
builder: (context, animatedLatLng, child) {
return MarkerLayer(
markers: [
Marker(
point: animatedLatLng,
width: 40,
height: 40,
builder: (ctx) => const Icon(
Icons.navigation,
color: Colors.blue,
size: 40,
),
),
],
);
},
);
}
}
4. Handling Marker Rotation (Bearing)
Moving the marker smoothly is only half the battle. If a user is turning a corner, the marker icon (usually an arrow) needs to rotate smoothly as well. You can apply the exact same logic using a standard Tween<double> for the bearing.
Pro tip: Be careful with bearing interpolation when transitioning from 359° to 1°. A simple lerp will spin the marker all the way backwards! You must calculate the shortest angular distance.
Conclusion
With less than 50 lines of code, TweenAnimationBuilder completely transforms the feel of your map interface. It turns chunky, teleporting dots into a premium, smooth tracking experience.
— Ankit Kumar