Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
MGLMapView Spring Animation
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol Animation <NSObject>
- (void)animationTick:(CFTimeInterval)dt finished:(BOOL *)finished;
@end
@interface Animator : NSObject
+ (instancetype)animatorWithScreen:(nullable UIScreen *)screen;
- (void)addAnimation:(id<Animation>)animatable;
- (void)removeAnimation:(id<Animation>)animatable;
@end
@interface UIView (AnimatorAdditions)
@property (NS_NONATOMIC_IOSONLY, readonly, strong) Animator *animator;
@end
NS_ASSUME_NONNULL_END
#import "Animator.h"
#import <objc/runtime.h>
static int ScreenAnimationDriverKey;
@interface Animator ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, strong) NSMutableSet *animations;
@end
@implementation Animator
+ (instancetype)animatorWithScreen:(nullable UIScreen *)screen
{
if (!screen) {
screen = [UIScreen mainScreen];
}
Animator *driver = objc_getAssociatedObject(screen, &ScreenAnimationDriverKey);
if (!driver) {
driver = [[self alloc] initWithScreen:screen];
objc_setAssociatedObject(screen, &ScreenAnimationDriverKey, driver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return driver;
}
- (instancetype)initWithScreen:(UIScreen *)screen
{
self = [super init];
if (self) {
self.displayLink = [screen displayLinkWithTarget:self selector:@selector(animationTick:)];
self.displayLink.paused = YES;
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
self.animations = [NSMutableSet new];
}
return self;
}
- (void)addAnimation:(id<Animation>)animation
{
[self.animations addObject:animation];
if (self.animations.count > 0) {
self.displayLink.paused = NO;
}
}
- (void)removeAnimation:(id <Animation>)animatable
{
if (animatable == nil) return;
[self.animations removeObject:animatable];
if (self.animations.count == 0) {
self.displayLink.paused = YES;
}
}
- (void)animationTick:(CADisplayLink *)displayLink
{
CFTimeInterval dt = displayLink.duration;
for (id<Animation> a in [self.animations copy]) {
BOOL finished = NO;
[a animationTick:dt finished:&finished];
if (finished) {
[self.animations removeObject:a];
}
}
if (self.animations.count == 0) {
self.displayLink.paused = YES;
}
}
@end
@implementation UIView (AnimatorAdditions)
- (Animator *)animator
{
return [Animator animatorWithScreen:self.window.screen];
}
@end
#import <UIKit/UIKit.h>
#import "MapViewProtocol.h"
#import "Animator.h"
NS_ASSUME_NONNULL_BEGIN
@interface MapSpringAnimation : NSObject <Animation>
@property (nonatomic, readonly) CGPoint panVelocity;
@property (nonatomic, readonly) double zoomVelocity;
- (instancetype)initWithMapView:(UIView<MapViewProtocol> *)mapView
targetCoordinate:(CLLocationCoordinate2D)targetCoordinate
targetZoom:(double)targetZoom
panVelocity:(CGPoint)panVelocity
zoomVelocity:(double)zoomVelocity NS_DESIGNATED_INITIALIZER;
@end
NS_ASSUME_NONNULL_END
#import "MapSpringAnimation.h"
#import "GeometryExtras.h"
@interface MapSpringAnimation ()
@property (nonatomic) CGPoint panVelocity;
@property (nonatomic) double zoomVelocity;
@property (nonatomic) CLLocationCoordinate2D targetCoordinate;
@property (nonatomic) double targetZoom;
@property (nonatomic, strong) UIView<MapViewProtocol> *mapView;
@end
@implementation MapSpringAnimation
- (instancetype)initWithMapView:(UIView<MapViewProtocol> *)mapView
targetCoordinate:(CLLocationCoordinate2D)targetCoordinate
targetZoom:(double)targetZoom
panVelocity:(CGPoint)panVelocity
zoomVelocity:(double)zoomVelocity
{
self = [super init];
if (self) {
self.mapView = mapView;
self.targetCoordinate = targetCoordinate;
self.targetZoom = targetZoom;
self.panVelocity = panVelocity;
self.zoomVelocity = zoomVelocity;
}
return self;
}
- (void)animationTick:(CFTimeInterval)dt finished:(BOOL *)finished
{
static const float frictionConstant = 40;
static const float springConstant = 500;
CGFloat time = (CGFloat)dt;
Point3D velocity3D = Point3DMake(self.panVelocity.x, self.panVelocity.y, self.zoomVelocity);
// friction force = velocity * friction constant
Point3D frictionForce = Point3DMultiply(velocity3D, frictionConstant);
// spring force = (target point - current position) * spring constant
Point3D targetPoint = Point3DMake(self.targetCoordinate.latitude, self.targetCoordinate.longitude, self.targetZoom);
CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate;
Point3D centerPoint = Point3DMake(centerCoordinate.latitude, centerCoordinate.longitude, self.mapView.zoomLevel);
Point3D springForce = Point3DMultiply(Point3DSubtract(targetPoint, centerPoint), springConstant);
// force = spring force - friction force
Point3D force = Point3DSubtract(springForce, frictionForce);
// velocity = current velocity + force * time / mass (mass = 1 for simplicity)
velocity3D = Point3DAdd(velocity3D, Point3DMultiply(force, time));
CGPoint panVelocity = CGPointMake(velocity3D.x, velocity3D.y);
self.panVelocity = panVelocity;
self.zoomVelocity = velocity3D.z;
// position = current position + velocity * time
Point3D newCenterPoint = Point3DAdd(centerPoint, Point3DMultiply(velocity3D, time));
CLLocationCoordinate2D newCenterCoordinate = CLLocationCoordinate2DMake(newCenterPoint.x, newCenterPoint.y);
[self.mapView setCenterCoordinate:newCenterCoordinate zoomLevel:newCenterPoint.z animated:NO];
CGPoint targetMapPoint = [self.mapView convertCoordinateToPoint:self.targetCoordinate];
CGPoint centerMapPoint = [self.mapView convertCoordinateToPoint:self.mapView.centerCoordinate];
CGFloat panSpeed = CGPointLength(panVelocity);
CGFloat panDistanceToGoal = CGPointLength(CGPointSubtract(targetMapPoint, centerMapPoint));
CGFloat zoomSpeed = fabs(velocity3D.z);
CGFloat zoomDistanceToGoal = fabs(self.targetZoom - self.mapView.zoomLevel);
if (panSpeed < 0.1 && panDistanceToGoal < 0.1 && zoomSpeed < 0.01 && zoomDistanceToGoal < 0.01) {
[self.mapView setCenterCoordinate:self.targetCoordinate zoomLevel:self.targetZoom animated:NO];
*finished = YES;
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.