Skip to content

Instantly share code, notes, and snippets.

@avioli
Last active February 28, 2023 19:18
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save avioli/a0b800d6a5ed053871ab4eec8f57c2da to your computer and use it in GitHub Desktop.
Save avioli/a0b800d6a5ed053871ab4eec8f57c2da to your computer and use it in GitHub Desktop.
A helper to animate a flutter_map's MapController
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart' show MapController;
import 'package:latlong/latlong.dart';
// ////////////////////////////////////////////////////////////////////////////
class AnimatedMapController {
/// Creates an animated MapController
AnimatedMapController({
@required this.mapController,
@required TickerProvider tickerProvider,
this.duration = const Duration(milliseconds: 2500),
this.curve = Curves.fastOutSlowIn,
}) {
_animationController = AnimationController(
vsync: tickerProvider,
)..addListener(onAnimationTick);
}
/// Holds a reference to the [MapController]
final MapController mapController;
/// Holds the default [Duration] for the animations
final Duration duration;
/// Holds the easing [Curve] for the animation
final Curve curve;
AnimationController _animationController;
/// Provides the [AnimationController]
AnimationController get animationController => _animationController;
Animation<LatLng> _centerAnimation;
Animation<double> _zoomAnimation;
bool _willUpdateProp = false;
/// Signifies if a prop is being updated
bool get willUpdateProp => _willUpdateProp;
bool _needsAnimation = false;
/// Signifies if this instance needs to animate
bool get needsAnimation => _needsAnimation;
/// Animates a mapController.move to a given center (and zoom)
TickerFuture move(LatLng center, [double zoom]) {
assert(center != null);
return animate(() {
this.center = center;
this.zoom = zoom ?? mapController.zoom;
});
}
/// Release the resources used by this object. The object is no longer usable
/// after this method is called.
void dispose() {
if (animationController.isAnimating) {
animationController.stop();
}
animationController.dispose();
}
/// Animates the changed properties
///
/// Changes can be made prior calling the method or within the [animator]
/// callback.
TickerFuture animate([Function animator]) => animateIn(duration, animator);
/// Animates the changed properties, overriding the default [Duration]
///
/// Changes can be made prior calling the method or within the [animator]
/// callback.
///
/// NOTE: Calling this method will stop any ongoing animation.
/// Its [orCancel] future will be (silently) rejected.
TickerFuture animateIn(Duration duration, [Function animator]) {
assert(duration != null);
if (animator != null) {
animator();
}
if (needsAnimation) {
_needsAnimation = false;
return animationController.animateTo(
animationController.upperBound,
duration: duration,
);
}
return TickerFuture.complete();
}
/// A convenience getter to get the map's center
LatLng get center => mapController.center;
/// Sets the map's center
///
/// Must be set within an [animate] or [animateIn] callback, or the desired
/// method must be called after the set to run the animation.
set center(LatLng value) {
assert(value != null);
updateProp(() {
_centerAnimation = animationFor(LatLngTween(
begin: center,
end: value,
))
..onEnd(() => _centerAnimation = null);
_needsAnimation = true;
});
}
/// A convenience getter to get the map's zoom level
double get zoom => mapController.zoom;
/// Sets the map's zoom level
///
/// Must be set within an [animate] or [animateIn] callback, or the desired
/// method must be called after the set to run the animation.
set zoom(double value) {
assert(value != null);
updateProp(() {
_zoomAnimation = animationFor(Tween<double>(
begin: zoom,
end: value,
))
..onEnd(() => _zoomAnimation = null);
_needsAnimation = true;
});
}
/// Returns an [Animation] by chaining the [tween] with the curve and
/// attaching to the [AnimationController]
@protected
Animation<T> animationFor<T>(Tween<T> tween) {
final curveTween = CurveTween(curve: curve);
return tween.chain(curveTween).animate(animationController);
}
/// Runs on every animation tick to adjust the map's center and/or zoom
@protected
void onAnimationTick() {
if (willUpdateProp) {
// NOTE: ignore the value reset
return;
}
if (_centerAnimation == null && _zoomAnimation == null) {
return;
}
mapController.move(
_centerAnimation?.value ?? mapController.center,
_zoomAnimation?.value ?? mapController.zoom,
);
}
/// Resets the [AnimationController], if needed
///
/// It ensures the [AnimationController] is reset, but retain
/// the [MapController]'s center and zoom.
@protected
void updateProp(Function callback) {
_willUpdateProp = true;
if (animationController.value != 0.0 || animationController.isAnimating) {
animationController.value = 0.0;
}
try {
callback();
} finally {
_willUpdateProp = false;
}
}
}
// ////////////////////////////////////////////////////////////////////////////
/// An interpolation between two LatLng instances.
///
/// See [Tween] for a discussion on how to use interpolation objects.
class LatLngTween extends Tween<LatLng> {
/// Creates a [LatLng] tween.
///
/// The [begin] and [end] properties may be null; the null value
/// is treated as an empty LatLng.
LatLngTween({LatLng begin, LatLng end}) : super(begin: begin, end: end);
/// Returns the value this variable has at the given animation clock value.
@override
LatLng lerp(double t) {
assert(t != null);
if (begin == null && end == null) return null;
double lat, lng;
if (begin == null) {
lat = end.latitude * t;
lng = end.longitude * t;
} else if (end == null) {
lat = begin.latitude * (1.0 - t);
lng = begin.longitude * (1.0 - t);
} else {
lat = lerpDouble(begin.latitude, end.latitude, t);
lng = lerpDouble(begin.longitude, end.longitude, t);
}
return LatLng(lat, lng);
}
@protected
double lerpDouble(double a, double b, double t) {
if (a == null && b == null) return null;
a ??= 0.0;
b ??= 0.0;
return a + (b - a) * t;
}
}
// ////////////////////////////////////////////////////////////////////////////
extension EndListener<T> on Animation<T> {
/// Adds a one-off `completed/dismissed` listener that is automatically
/// removed
Function onEnd(Function callback) {
AnimationStatusListener wrapper;
wrapper = (AnimationStatus status) {
if (status == AnimationStatus.completed ||
status == AnimationStatus.dismissed) {
removeStatusListener(wrapper);
callback();
}
};
addStatusListener(wrapper);
return () {
removeStatusListener(wrapper);
};
}
}
@avioli
Copy link
Author

avioli commented Apr 20, 2020

LICENSE: Public domain

Because of the extension it requires Dart ≥ 2.6

@mmcc007
Copy link

mmcc007 commented Aug 20, 2020

Been looking for a LatLng tween. Might give it a try. Thanks a bunch!

With my own attempts to interpolate a position along a straight line on a map, I have run into rendering artifacts on screen (the line flickers in a random way). Spent hours trying to figure it out with no luck. I notice you are using the latlng package instead of google map's LatLng. Is this because of an issue with the coordinate system (Mercator projection)?

Are you seeing any funky artifacts from the interperolated LatLngs?

Anyway, thanks again for the great looking animation controller.

@avioli
Copy link
Author

avioli commented Aug 20, 2020

@mmcc007 I used latlng package, because flutter_map uses it. No funky artefacts. Feel free to use whatever works for you.

Animation and AnimationController use Tween and you can read their source code files to find more examples. That’s one of the best parts of open source - you can explore it. Programming is about constant exploration, which involves lots of hours with no luck, until you figure it out.

Good luck.

@mmcc007
Copy link

mmcc007 commented Aug 22, 2020

Thanks for response.

FYI: I'm trying to animate the route, traveling along the route and changing the color as I step thru each straight line segment along the route and step thru corners. I have the animation running. It moves smoothly along the route based on the numbers generated by the animator. No problem there.

Ordinarily everything would be fine at this point. But I noticed that there is a jitter on the screen along the green route as though google maps is trying to re-render parts of the green route that has already being drawn, while the remainder of the green route is being rendered. So maybe google maps needs something I have missed, or there is a bug in the rendering or somewhere in the calculation is incorrect or their is some kind of rounding error. I captured the points generated by a regular Animator() in Flutter (that I am using) and the subsequent LatLngs that I generate and plotted them on a map. You can see the errors below. Tiny but apparently enough to cause jitter on the mobile screen.
Screen Shot 2020-08-21 at 5 58 01 PM
The original route (from Google's Drive API?) is in purple. The points generated by animator (and my calculations) are in green. I even tried your lerp formula a + (b - a) * t. Also tried a 'snap to line' approach and rounding. All attempts result in a tiny error as shown.

Maybe I'll try it on another mobile device.

Let me know if you run into something like this in the future and figure out how to solve it.

While it's up you can see the full map at https://maps.co/map/5f3fef8911764419538679e9be34

Anyway, fun times :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment