Skip to content

Instantly share code, notes, and snippets.

@jverkoey
Created June 8, 2017 20:47
Show Gist options
  • Save jverkoey/2f0628392a24cdcabc390d3f54252dfe to your computer and use it in GitHub Desktop.
Save jverkoey/2f0628392a24cdcabc390d3f54252dfe to your computer and use it in GitHub Desktop.
/*
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MotionInterchange/MotionInterchange.h>
/**
An animator adds Core Animation animations to a layer based on a provided motion timing.
*/
NS_SWIFT_NAME(CoreAnimationAnimator)
@interface MDMCoreAnimationAnimator : NSObject
/**
If enabled, all animations will be added with their values reversed.
Disabled by default.
*/
@property(nonatomic, assign) BOOL shouldReverseValues;
/**
If enabled, all animations will start from their current presentation value.
If disabled, animations will start from the first value in the values array.
Disabled by default.
*/
@property(nonatomic, assign) BOOL beginFromCurrentState;
/**
If enabled, animations will calculate their values in relation to their destination value.
Additive animations can be stacked. This is most commonly used to change the destination of an
animation mid-way through in such a way that momentum appears to be conserved.
Enabled by default.
*/
@property(nonatomic, assign) BOOL additive;
/**
Adds a single animation to the layer with the given timing structure.
@param timing The timing to be used for the animation.
@param layer The layer to be animated.
@param values The values to be used in the animation. Must contain exactly two values. Supported
UIKit types will be coerced to their Core Animation equivalent. Supported UIKit values include
UIColor and UIBezierPath.
@param keyPath The key path of the property to be animated.
*/
- (void)addAnimationWithTiming:(MDMMotionTiming)timing
toLayer:(nonnull CALayer *)layer
withValues:(nonnull NSArray *)values
keyPath:(nonnull NSString *)keyPath;
/**
Adds a single animation to the layer with the given timing structure.
@param timing The timing to be used for the animation.
@param layer The layer to be animated.
@param values The values to be used in the animation. Must contain exactly two values. Supported
UIKit types will be coerced to their Core Animation equivalent. Supported UIKit values include
UIColor and UIBezierPath.
@param keyPath The key path of the property to be animated.
@param completion The completion handler will be executed once this animation has come to rest.
*/
- (void)addAnimationWithTiming:(MDMMotionTiming)timing
toLayer:(nonnull CALayer *)layer
withValues:(nonnull NSArray *)values
keyPath:(nonnull NSString *)keyPath
completion:(nullable void(^)())completion;
/**
Adds a block that will be invoked each time an animation is added to a layer.
*/
- (void)addAnimationTracer:(nonnull void (^)(CALayer * _Nonnull, CAAnimation * _Nonnull))tracer;
@end
/*
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MDMCoreAnimationAnimator.h"
#if TARGET_IPHONE_SIMULATOR
UIKIT_EXTERN float UIAnimationDragCoefficient(void); // UIKit private drag coefficient.
#endif
static CGFloat simulatorAnimationDragCoefficient(void) {
#if TARGET_IPHONE_SIMULATOR
return UIAnimationDragCoefficient();
#else
return 1.0;
#endif
}
static CAMediaTimingFunction* timingFunctionWithControlPoints(CGFloat controlPoints[4]) {
return [CAMediaTimingFunction functionWithControlPoints:(float)controlPoints[0]
:(float)controlPoints[1]
:(float)controlPoints[2]
:(float)controlPoints[3]];
}
static NSArray* coerceUIKitValuesToCoreAnimationValues(NSArray *values) {
if ([[values firstObject] isKindOfClass:[UIColor class]]) {
NSMutableArray *convertedArray = [NSMutableArray arrayWithCapacity:values.count];
for (UIColor *color in values) {
[convertedArray addObject:(id)color.CGColor];
}
values = convertedArray;
} else if ([[values firstObject] isKindOfClass:[UIBezierPath class]]) {
NSMutableArray *convertedArray = [NSMutableArray arrayWithCapacity:values.count];
for (UIBezierPath *bezierPath in values) {
[convertedArray addObject:(id)bezierPath.CGPath];
}
values = convertedArray;
}
return values;
}
static CABasicAnimation *animationFromTiming(MDMMotionTiming timing) {
CABasicAnimation *animation;
switch (timing.curve.type) {
case MDMMotionCurveTypeInstant:
animation = nil;
break;
case MDMMotionCurveTypeDefault:
case MDMMotionCurveTypeBezier:
animation = [CABasicAnimation animation];
animation.timingFunction = timingFunctionWithControlPoints(timing.curve.data);
animation.duration = timing.duration * simulatorAnimationDragCoefficient();
break;
case MDMMotionCurveTypeSpring: {
CASpringAnimation *spring = [CASpringAnimation animation];
spring.mass = timing.curve.data[MDMSpringMotionCurveDataIndexMass];
spring.stiffness = timing.curve.data[MDMSpringMotionCurveDataIndexTension];
spring.damping = timing.curve.data[MDMSpringMotionCurveDataIndexFriction];
spring.duration = spring.settlingDuration;
animation = spring;
break;
}
}
return animation;
}
static void makeAnimationAdditive(CABasicAnimation *animation) {
static NSSet *sizeKeyPaths = nil;
static NSSet *positionKeyPaths = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sizeKeyPaths = [NSSet setWithArray:@[@"bounds.size"]];
positionKeyPaths = [NSSet setWithArray:@[@"position",
@"anchorPoint"]];
});
if ([animation.toValue isKindOfClass:[NSNumber class]]) {
CGFloat currentValue = [animation.fromValue doubleValue];
CGFloat delta = currentValue - [animation.toValue doubleValue];
animation.fromValue = @(delta);
animation.toValue = @0;
animation.additive = true;
} else if ([sizeKeyPaths containsObject:animation.keyPath]) {
CGSize currentValue = [animation.fromValue CGSizeValue];
CGSize destinationValue = [animation.toValue CGSizeValue];
CGSize delta = CGSizeMake(currentValue.width - destinationValue.width,
currentValue.height - destinationValue.height);
animation.fromValue = [NSValue valueWithCGSize:delta];
animation.toValue = [NSValue valueWithCGSize:CGSizeZero];
animation.additive = true;
} else if ([positionKeyPaths containsObject:animation.keyPath]) {
CGPoint currentValue = [animation.fromValue CGPointValue];
CGPoint destinationValue = [animation.toValue CGPointValue];
CGPoint delta = CGPointMake(currentValue.x - destinationValue.x,
currentValue.y - destinationValue.y);
animation.fromValue = [NSValue valueWithCGPoint:delta];
animation.toValue = [NSValue valueWithCGPoint:CGPointZero];
animation.additive = true;
}
}
@implementation MDMCoreAnimationAnimator {
NSMutableArray *_tracers;
}
- (instancetype)init {
self = [super init];
if (self) {
_additive = true;
}
return self;
}
- (void)addAnimationWithTiming:(MDMMotionTiming)timing
toLayer:(CALayer *)layer
withValues:(NSArray *)values
keyPath:(NSString *)keyPath {
[self addAnimationWithTiming:timing toLayer:layer withValues:values keyPath:keyPath completion:nil];
}
- (void)addAnimationWithTiming:(MDMMotionTiming)timing
toLayer:(CALayer *)layer
withValues:(NSArray *)values
keyPath:(NSString *)keyPath
completion:(void(^)())completion {
if (timing.duration == 0) {
return;
}
NSAssert([values count] == 2, @"The values array must contain exactly two values.");
if (_shouldReverseValues) {
values = [[values reverseObjectEnumerator] allObjects];
}
values = coerceUIKitValuesToCoreAnimationValues(values);
CABasicAnimation *animation = animationFromTiming(timing);
if (animation) {
animation.keyPath = keyPath;
id initialValue;
if (_beginFromCurrentState) {
if ([layer presentationLayer]) {
initialValue = [[layer presentationLayer] valueForKeyPath:keyPath];
} else {
initialValue = [layer valueForKeyPath:keyPath];
}
} else {
initialValue = [values firstObject];
}
animation.fromValue = initialValue;
animation.toValue = [values lastObject];
if (![animation.fromValue isEqual:animation.toValue]) {
if (self.additive) {
makeAnimationAdditive(animation);
}
if (timing.delay != 0) {
animation.beginTime = ([layer convertTime:CACurrentMediaTime() fromLayer:nil]
+ timing.delay * simulatorAnimationDragCoefficient());
animation.fillMode = kCAFillModeBackwards;
}
if (completion) {
[CATransaction begin];
[CATransaction setCompletionBlock:completion];
}
// When we use a nil key, Core Animation will ensure that the animation is added with a
// unique key - this enables our additive animations to stack upon one another.
[layer addAnimation:animation forKey:nil];
for (void (^tracer)(CALayer *, CAAnimation *) in _tracers) {
tracer(layer, animation);
}
if (completion) {
[CATransaction commit];
}
}
}
[layer setValue:[values lastObject] forKeyPath:keyPath];
}
- (void)addAnimationTracer:(void (^)(CALayer *, CAAnimation *))tracer {
if (!_tracers) {
_tracers = [NSMutableArray array];
}
[_tracers addObject:tracer];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment