Created
August 2, 2015 04:53
-
-
Save tomtaylor/91d29f47f281b9d96fcd to your computer and use it in GitHub Desktop.
MGLMapView Spring Animation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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