Created
June 8, 2017 20:47
-
-
Save jverkoey/2f0628392a24cdcabc390d3f54252dfe to your computer and use it in GitHub Desktop.
This file contains hidden or 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
/* | |
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 |
This file contains hidden or 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
/* | |
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