Skip to content

Instantly share code, notes, and snippets.

@tomtaylor
Created August 2, 2015 04:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomtaylor/91d29f47f281b9d96fcd to your computer and use it in GitHub Desktop.
Save tomtaylor/91d29f47f281b9d96fcd to your computer and use it in GitHub Desktop.
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