blob: 90ee3cea97c7b84a81b777657e027ed7c835eacc [file] [log] [blame] [edit]
/*
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 "MDMMotionAnimator.h"
#import <UIKit/UIKit.h>
#import "CATransaction+MotionAnimator.h"
#import "private/CABasicAnimation+MotionAnimator.h"
#import "private/MDMAnimationRegistrar.h"
#import "private/MDMUIKitValueCoercion.h"
#import "private/MDMBlockAnimations.h"
#import "private/MDMDragCoefficient.h"
@implementation MDMMotionAnimator {
NSMutableArray *_tracers;
MDMAnimationRegistrar *_registrar;
}
- (instancetype)init {
self = [super init];
if (self) {
_registrar = [[MDMAnimationRegistrar alloc] init];
_timeScaleFactor = 1;
_additive = true;
}
return self;
}
- (void)animateWithTraits:(MDMAnimationTraits *)traits
between:(NSArray *)values
layer:(CALayer *)layer
keyPath:(MDMAnimatableKeyPath)keyPath {
[self animateWithTraits:traits between:values layer:layer keyPath:keyPath completion:nil];
}
- (void)animateWithTraits:(MDMAnimationTraits *)traits
between:(NSArray *)values
layer:(CALayer *)layer
keyPath:(MDMAnimatableKeyPath)keyPath
completion:(void(^)(BOOL))completion {
NSAssert([values count] == 2, @"The values array must contain exactly two values.");
if (_shouldReverseValues) {
values = [[values reverseObjectEnumerator] allObjects];
}
values = MDMCoerceUIKitValuesToCoreAnimationValues(values);
void (^commitToModelLayer)(void) = ^{
[CATransaction begin];
[CATransaction setDisableActions:YES];
[layer setValue:[values lastObject] forKeyPath:keyPath];
[CATransaction commit];
};
void (^exitEarly)(void) = ^{
commitToModelLayer();
if (completion) {
completion(YES);
}
};
CGFloat timeScaleFactor = [self computedTimeScaleFactor];
if (timeScaleFactor == 0) {
exitEarly();
return;
}
CABasicAnimation *animation = MDMAnimationFromTraits(traits, timeScaleFactor);
if (animation == nil) {
exitEarly();
return;
}
BOOL beginFromCurrentState = self.beginFromCurrentState;
[self addAnimation:animation
toLayer:layer
withKeyPath:keyPath
traits:traits
timeScaleFactor:timeScaleFactor
destination:[values lastObject]
initialValue:^(BOOL wantsPresentationValue) {
if (beginFromCurrentState) {
if (wantsPresentationValue && [layer presentationLayer]) {
return [[layer presentationLayer] valueForKeyPath:keyPath];
} else {
return [layer valueForKeyPath:keyPath];
}
} else {
return [values firstObject];
}
} completion:completion];
commitToModelLayer();
for (void (^tracer)(CALayer *, CAAnimation *) in _tracers) {
tracer(layer, animation);
}
}
- (void)animateWithTraits:(MDMAnimationTraits *)traits animations:(void (^)(void))animations {
[self animateWithTraits:traits animations:animations completion:nil];
}
- (void)animateWithTraits:(MDMAnimationTraits *)traits
animations:(void (^)(void))animations
completion:(void(^)(BOOL))completion {
NSArray<MDMImplicitAction *> *actions = MDMAnimateImplicitly(animations);
void (^exitEarly)(void) = ^{
[CATransaction begin];
[CATransaction setDisableActions:YES];
animations();
[CATransaction commit];
if (completion) {
completion(YES);
}
};
CGFloat timeScaleFactor = [self computedTimeScaleFactor];
if (timeScaleFactor == 0) {
exitEarly();
return; // No need to animate anything.
}
// We'll reuse this animation template for each action.
CABasicAnimation *animationTemplate = MDMAnimationFromTraits(traits, timeScaleFactor);
if (animationTemplate == nil) {
exitEarly();
return;
}
[CATransaction begin];
if (completion) {
[CATransaction setCompletionBlock:^{
completion(YES);
}];
}
for (MDMImplicitAction *action in actions) {
CABasicAnimation *animation = [animationTemplate copy];
[self addAnimation:animation
toLayer:action.layer
withKeyPath:action.keyPath
traits:traits
timeScaleFactor:timeScaleFactor
destination:[action.layer valueForKeyPath:action.keyPath]
initialValue:^(BOOL wantsPresentationValue) {
if (wantsPresentationValue && action.hadPresentationLayer) {
return action.initialPresentationValue;
} else {
// Additive animations always animate from the initial model layer value.
return action.initialModelValue;
}
} completion:nil];
for (void (^tracer)(CALayer *, CAAnimation *) in _tracers) {
tracer(action.layer, animation);
}
}
[CATransaction commit];
}
- (void)addCoreAnimationTracer:(void (^)(CALayer *, CAAnimation *))tracer {
if (!_tracers) {
_tracers = [NSMutableArray array];
}
[_tracers addObject:[tracer copy]];
}
- (void)removeAllAnimations {
[_registrar removeAllAnimations];
}
- (void)stopAllAnimations {
[_registrar commitCurrentAnimationValuesToAllLayers];
[_registrar removeAllAnimations];
}
#pragma mark - UIKit equivalency
+ (void)animateWithDuration:(NSTimeInterval)duration
animations:(void (^ __nonnull)(void))animations {
[self animateWithDuration:duration animations:animations completion:nil];
}
+ (void)animateWithDuration:(NSTimeInterval)duration
animations:(void (^)(void))animations
completion:(void (^)(BOOL))completion {
[self animateWithDuration:duration
delay:UIViewAnimationOptionCurveEaseInOut
options:0
animations:animations
completion:completion];
}
+ (void)animateWithDuration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
options:(UIViewAnimationOptions)options
animations:(void (^ __nonnull)(void))animations
completion:(void (^ __nullable)(BOOL finished))completion {
MDMMotionAnimator *animator = [[[self class] alloc] init];
MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:duration];
traits.delay = delay;
if ((options & UIViewAnimationOptionCurveEaseIn) == UIViewAnimationOptionCurveEaseIn) {
traits.timingCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
} else if ((options & UIViewAnimationOptionCurveEaseOut) == UIViewAnimationOptionCurveEaseOut) {
traits.timingCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
} else if ((options & UIViewAnimationOptionCurveLinear) == UIViewAnimationOptionCurveLinear) {
traits.timingCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
}
[animator animateWithTraits:traits animations:animations completion:completion];
}
+ (void)animateWithDuration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
usingSpringWithDamping:(CGFloat)dampingRatio
initialSpringVelocity:(CGFloat)velocity
options:(UIViewAnimationOptions)options
animations:(void (^)(void))animations
completion:(void (^)(BOOL))completion {
MDMMotionAnimator *animator = [[[self class] alloc] init];
MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:duration];
traits.delay = delay;
traits.timingCurve =
[[MDMSpringTimingCurveGenerator alloc] initWithDuration:duration
dampingRatio:dampingRatio
initialVelocity:velocity].springTimingCurve;
[animator animateWithTraits:traits animations:animations completion:completion];
}
#pragma mark - Legacy
- (void)animateWithTiming:(MDMMotionTiming)timing animations:(void (^)(void))animations {
MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithMotionTiming:timing];
[self animateWithTraits:traits animations:animations];
}
- (void)animateWithTiming:(MDMMotionTiming)timing
animations:(void (^)(void))animations
completion:(void (^)(void))completion {
MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithMotionTiming:timing];
[self animateWithTraits:traits animations:animations completion:^(BOOL didComplete) {
if (completion) {
completion();
}
}];
}
- (void)animateWithTiming:(MDMMotionTiming)timing
toLayer:(CALayer *)layer
withValues:(NSArray *)values
keyPath:(MDMAnimatableKeyPath)keyPath {
MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithMotionTiming:timing];
[self animateWithTraits:traits between:values layer:layer keyPath:keyPath];
}
- (void)animateWithTiming:(MDMMotionTiming)timing
toLayer:(CALayer *)layer
withValues:(NSArray *)values
keyPath:(MDMAnimatableKeyPath)keyPath
completion:(void (^)(void))completion {
MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithMotionTiming:timing];
[self animateWithTraits:traits
between:values
layer:layer
keyPath:keyPath
completion:^(BOOL didComplete) {
if (completion) {
completion();
}
}];
}
#pragma mark - Private
- (CGFloat)computedTimeScaleFactor {
CGFloat timeScaleFactor;
id transactionTimeScaleFactor = [CATransaction mdm_timeScaleFactor];
if (transactionTimeScaleFactor != nil) {
#if CGFLOAT_IS_DOUBLE
timeScaleFactor = [transactionTimeScaleFactor doubleValue];
#else
timeScaleFactor = [transactionTimeScaleFactor floatValue];
#endif
} else {
timeScaleFactor = _timeScaleFactor;
}
return MDMSimulatorAnimationDragCoefficient() * timeScaleFactor;
}
- (void)addAnimation:(CABasicAnimation *)animation
toLayer:(CALayer *)layer
withKeyPath:(NSString *)keyPath
traits:(MDMAnimationTraits *)traits
timeScaleFactor:(CGFloat)timeScaleFactor
destination:(id)destination
initialValue:(id(^)(BOOL wantsPresentationValue))initialValueBlock
completion:(void(^)(BOOL))completion {
// Must configure the keyPath and toValue before we can identify whether the animation supports
// being additive.
animation.keyPath = keyPath;
animation.toValue = destination;
animation.additive = self.additive && MDMCanAnimationBeAdditive(keyPath, animation.toValue);
// Additive animations always read from the model layer's value so that the new displacement
// reflects the change in destination and momentum appears to be conserved across multiple
// animations.
//
// Non-additive animations should try to read from the presentation layer's current value
// because we'll be interrupting whatever animation previously existed and immediately moving
// toward the new destination.
BOOL wantsPresentationValue = self.beginFromCurrentState && !animation.additive;
animation.fromValue = initialValueBlock(wantsPresentationValue);
NSString *key = animation.additive ? nil : keyPath;
MDMConfigureAnimation(animation, traits);
if (traits.delay != 0) {
animation.beginTime = ([layer convertTime:CACurrentMediaTime() fromLayer:nil]
+ traits.delay * timeScaleFactor);
animation.fillMode = kCAFillModeBackwards;
} else if (@available(iOS 14, *)) {
// iOS 14 introduced a behavioral change for animations such that they no longer appear to
// immediately be committed to the render server, potentially resulting in a brief flicker to
// the model layer's value before the animation takes effect. This could reasonably be
// considered a bug in iOS.
// To work around this, we can explicitly enforce the contract that this animation is expected
// to start "now" in terms of render server timing. This does mean we may lose some microseconds
// of animation timing at the beginning of the animation, so we only apply this on iOS 14+ where
// it's needed. If and when iOS fixes this bug we can remove the following line and lean on the
// render server choosing the appropriate start time once the animation is flushed to the render
// server.
animation.beginTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
}
[_registrar addAnimation:animation toLayer:layer forKey:key completion:completion];
}
@end