| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ios/chrome/browser/ui/reversed_animation.h" |
| |
| #import <QuartzCore/QuartzCore.h> |
| #include <algorithm> |
| #include <cmath> |
| |
| #include "base/logging.h" |
| #include "base/mac/objc_property_releaser.h" |
| |
| @protocol ReversedAnimationProtocol; |
| typedef CAAnimation<ReversedAnimationProtocol> ReversedAnimation; |
| |
| namespace { |
| // Enum type used to denote the direction of a reversed animation relative to |
| // the original animation's direction. |
| typedef enum { |
| ANIMATION_DIRECTION_NORMAL, |
| ANIMATION_DIRECTION_REVERSE |
| } AnimationDirection; |
| // Returns the AnimationDirection opposite of |direction|. |
| AnimationDirection AnimationDirectionOpposite(AnimationDirection direction) { |
| return direction == ANIMATION_DIRECTION_NORMAL ? ANIMATION_DIRECTION_REVERSE |
| : ANIMATION_DIRECTION_NORMAL; |
| } |
| } // namespace |
| |
| // Returns an animation that reverses |animation| when added to |layer|, given |
| // that |animation| is in |parent|'s timespace, which begins at |
| // |parentBeginTime|. |
| CAAnimation* CAAnimationMakeReverse(CAAnimation* animation, |
| CALayer* layer, |
| CAAnimationGroup* parent, |
| CFTimeInterval parentBeginTime); |
| // Updates |reversedAnimation|'s properties for |animationToReverse|, given that |
| // |animationToReverse| is in |parent|'s timespace, which begins at |
| // |parentBeginTime|. |
| void UpdateReversedAnimation(ReversedAnimation* reversedAnimation, |
| CAAnimation* animationToReverse, |
| CALayer* layer, |
| CAAnimationGroup* parent, |
| CFTimeInterval parentBeginTime); |
| |
| #pragma mark - ReversedAnimation protocol |
| |
| @protocol ReversedAnimationProtocol<NSObject> |
| |
| // The original animation that's being played in reverse. |
| @property(nonatomic, retain) CAAnimation* originalAnimation; |
| // The current direction for the animation. |
| @property(nonatomic, assign) AnimationDirection animationDirection; |
| // The offset into the original animation's duration at the begining of the |
| // reverse animation. |
| @property(nonatomic, assign) CFTimeInterval animationTimeOffset; |
| |
| @end |
| |
| #pragma mark - ReversedBasicAnimation |
| |
| @interface ReversedBasicAnimation |
| : CABasicAnimation<ReversedAnimationProtocol> { |
| base::mac::ObjCPropertyReleaser _propertyReleaser_ReversedBasicAnimation; |
| } |
| |
| // Returns an animation that performs |animation| in reverse when added to |
| // |layer|. |parentBeginTime| should be set to the beginTime in absolute time |
| // of |animation|'s parent in the timing hierarchy. |
| + (instancetype)reversedAnimationForAnimation:(CABasicAnimation*)animation |
| forLayer:(CALayer*)layer |
| parent:(CAAnimationGroup*)parent |
| parentBeginTime:(CFTimeInterval)parentBeginTime; |
| |
| @end |
| |
| @implementation ReversedBasicAnimation |
| |
| @synthesize originalAnimation = _originalAnimation; |
| @synthesize animationDirection = _animationDirection; |
| @synthesize animationTimeOffset = _animationTimeOffset; |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| _propertyReleaser_ReversedBasicAnimation.Init( |
| self, [ReversedBasicAnimation class]); |
| } |
| return self; |
| } |
| |
| - (instancetype)copyWithZone:(NSZone*)zone { |
| ReversedBasicAnimation* copy = [super copyWithZone:zone]; |
| copy.originalAnimation = self.originalAnimation; |
| copy.animationDirection = self.animationDirection; |
| copy.animationTimeOffset = self.animationTimeOffset; |
| return copy; |
| } |
| |
| + (instancetype)reversedAnimationForAnimation:(CABasicAnimation*)animation |
| forLayer:(CALayer*)layer |
| parent:(CAAnimationGroup*)parent |
| parentBeginTime:(CFTimeInterval)parentBeginTime { |
| // Create new animation and copy properties. Note that we can't use |-copy| |
| // because we need the new animation to be the correct class. |
| NSString* keyPath = animation.keyPath; |
| CFTimeInterval now = |
| [layer convertTime:CACurrentMediaTime() fromLayer:nil] - parentBeginTime; |
| ReversedBasicAnimation* reversedAnimation = |
| [ReversedBasicAnimation animationWithKeyPath:keyPath]; |
| UpdateReversedAnimation(reversedAnimation, animation, layer, parent, |
| parentBeginTime); |
| |
| // Update from and to values. |
| BOOL isReversed = |
| reversedAnimation.animationDirection == ANIMATION_DIRECTION_REVERSE; |
| CABasicAnimation* originalBasicAnimation = |
| static_cast<CABasicAnimation*>(reversedAnimation.originalAnimation); |
| reversedAnimation.toValue = isReversed ? originalBasicAnimation.fromValue |
| : originalBasicAnimation.toValue; |
| if (now > animation.beginTime && |
| now < animation.beginTime + animation.duration) { |
| // Use the presentation layer's current value for reversals that occur mid- |
| // animation. |
| reversedAnimation.fromValue = |
| [[layer presentationLayer] valueForKeyPath:keyPath]; |
| } else { |
| reversedAnimation.fromValue = isReversed ? originalBasicAnimation.toValue |
| : originalBasicAnimation.fromValue; |
| } |
| return reversedAnimation; |
| } |
| |
| @end |
| |
| #pragma mark - ReversedAnimationGroup |
| |
| @interface ReversedAnimationGroup |
| : CAAnimationGroup<ReversedAnimationProtocol> { |
| base::mac::ObjCPropertyReleaser _propertyReleaser_ReversedAnimationGroup; |
| } |
| |
| // Returns an animation that performs |animation| in reverse when added to |
| // |layer|. |parentBeginTime| should be set to the beginTime in absolute time |
| // of the animation group to which |animation| belongs. |
| + (instancetype)reversedAnimationGroupForGroup:(CAAnimationGroup*)group |
| forLayer:(CALayer*)layer |
| parent:(CAAnimationGroup*)parent |
| parentBeginTime:(CFTimeInterval)parentBeginTime; |
| |
| @end |
| |
| @implementation ReversedAnimationGroup |
| |
| @synthesize originalAnimation = _originalAnimation; |
| @synthesize animationDirection = _animationDirection; |
| @synthesize animationTimeOffset = _animationTimeOffset; |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| _propertyReleaser_ReversedAnimationGroup.Init( |
| self, [ReversedAnimationGroup class]); |
| } |
| return self; |
| } |
| |
| - (instancetype)copyWithZone:(NSZone*)zone { |
| ReversedAnimationGroup* copy = [super copyWithZone:zone]; |
| copy.originalAnimation = self.originalAnimation; |
| copy.animationDirection = self.animationDirection; |
| copy.animationTimeOffset = self.animationTimeOffset; |
| return copy; |
| } |
| |
| + (instancetype)reversedAnimationGroupForGroup:(CAAnimationGroup*)group |
| forLayer:(CALayer*)layer |
| parent:(CAAnimationGroup*)parent |
| parentBeginTime:(CFTimeInterval)parentBeginTime { |
| // Create new animation and copy properties. Note that we can't use |-copy| |
| // because we need the new animation to be the correct class. |
| ReversedAnimationGroup* reversedGroup = [ReversedAnimationGroup animation]; |
| UpdateReversedAnimation(reversedGroup, group, layer, parent, parentBeginTime); |
| |
| // Reverse the animations of the original group. |
| NSMutableArray* reversedAnimations = [NSMutableArray array]; |
| for (CAAnimation* animation in group.animations) { |
| CAAnimation* reversedAnimation = CAAnimationMakeReverse( |
| animation, layer, group, group.beginTime + parentBeginTime); |
| [reversedAnimations addObject:reversedAnimation]; |
| } |
| reversedGroup.animations = reversedAnimations; |
| |
| return reversedGroup; |
| } |
| |
| @end |
| |
| #pragma mark - animation_util functions |
| |
| CAAnimation* CAAnimationMakeReverse(CAAnimation* animation, CALayer* layer) { |
| return CAAnimationMakeReverse(animation, layer, nil, layer.beginTime); |
| } |
| |
| CAAnimation* CAAnimationMakeReverse(CAAnimation* animation, |
| CALayer* layer, |
| CAAnimationGroup* parent, |
| CFTimeInterval parentBeginTime) { |
| CAAnimation* reversedAnimation = nil; |
| if ([animation isKindOfClass:[CABasicAnimation class]]) { |
| CABasicAnimation* basicAnimation = |
| static_cast<CABasicAnimation*>(animation); |
| reversedAnimation = |
| [ReversedBasicAnimation reversedAnimationForAnimation:basicAnimation |
| forLayer:layer |
| parent:parent |
| parentBeginTime:parentBeginTime]; |
| } else if ([animation isKindOfClass:[CAAnimationGroup class]]) { |
| CAAnimationGroup* animationGroup = |
| static_cast<CAAnimationGroup*>(animation); |
| reversedAnimation = |
| [ReversedAnimationGroup reversedAnimationGroupForGroup:animationGroup |
| forLayer:layer |
| parent:parent |
| parentBeginTime:parentBeginTime]; |
| } else { |
| // TODO(crbug.com/546202): Investigate possible general-case reversals. It |
| // may be possible to implement this by manipulating the CAMediaTiming |
| // properties. |
| } |
| return reversedAnimation; |
| } |
| |
| void UpdateReversedAnimation(ReversedAnimation* reversedAnimation, |
| CAAnimation* animationToReverse, |
| CALayer* layer, |
| CAAnimationGroup* parent, |
| CFTimeInterval parentBeginTime) { |
| // Copy properties. |
| CFTimeInterval now = |
| [layer convertTime:CACurrentMediaTime() fromLayer:nil] - parentBeginTime; |
| reversedAnimation.fillMode = animationToReverse.fillMode; |
| reversedAnimation.removedOnCompletion = |
| animationToReverse.removedOnCompletion; |
| reversedAnimation.timingFunction = animationToReverse.timingFunction; |
| |
| // Extract the previous reversal if it exists. |
| ReversedAnimation* previousReversedAnimation = nil; |
| Protocol* reversedAnimationProtocol = @protocol(ReversedAnimationProtocol); |
| if ([animationToReverse conformsToProtocol:reversedAnimationProtocol]) { |
| previousReversedAnimation = |
| static_cast<ReversedAnimation*>(animationToReverse); |
| animationToReverse = previousReversedAnimation.originalAnimation; |
| } |
| reversedAnimation.originalAnimation = animationToReverse; |
| reversedAnimation.animationDirection = |
| previousReversedAnimation |
| ? AnimationDirectionOpposite( |
| previousReversedAnimation.animationDirection) |
| : ANIMATION_DIRECTION_REVERSE; |
| |
| CAAnimation* previousAnimation = previousReversedAnimation |
| ? previousReversedAnimation |
| : animationToReverse; |
| BOOL isReversed = |
| reversedAnimation.animationDirection == ANIMATION_DIRECTION_REVERSE; |
| if (now < previousAnimation.beginTime) { |
| // Reversal occurs before previous animation begins. |
| reversedAnimation.beginTime = 2.0 * now - previousAnimation.beginTime - |
| reversedAnimation.originalAnimation.duration; |
| reversedAnimation.duration = reversedAnimation.originalAnimation.duration; |
| reversedAnimation.animationTimeOffset = |
| isReversed ? 0 : reversedAnimation.originalAnimation.duration; |
| } else if (now < previousAnimation.beginTime + previousAnimation.duration) { |
| // Reversal occurs while the previous animation is occurring. |
| reversedAnimation.beginTime = 0; |
| CFTimeInterval timeDelta = now - previousAnimation.beginTime; |
| reversedAnimation.animationTimeOffset = |
| previousReversedAnimation.animationTimeOffset + |
| (isReversed ? 1.0 : -1.0) * timeDelta; |
| reversedAnimation.duration = |
| isReversed ? reversedAnimation.animationTimeOffset |
| : reversedAnimation.originalAnimation.duration - |
| reversedAnimation.animationTimeOffset; |
| } else { |
| // Reversal occurs after the previous animation has ended. |
| if (!parentBeginTime) { |
| // If the parent's begin time is zero, the animation is using absolute |
| // time as its beginTime. |
| reversedAnimation.beginTime = |
| 2.0 * now - previousAnimation.beginTime - previousAnimation.duration; |
| } else { |
| CFTimeInterval previousEndTime = |
| previousAnimation.beginTime + previousAnimation.duration; |
| if (now > parent.duration) { |
| // The animation's parent has already ended, so use the difference |
| // between the parent's ending and the previous animation's end. |
| reversedAnimation.beginTime = parent.duration - previousEndTime; |
| } else { |
| // The parent hasn't ended, so use the difference between the current |
| // time and the previous animation's end. |
| reversedAnimation.beginTime = now - previousEndTime; |
| } |
| } |
| reversedAnimation.duration = reversedAnimation.originalAnimation.duration; |
| reversedAnimation.animationTimeOffset = |
| isReversed ? reversedAnimation.originalAnimation.duration : 0; |
| } |
| } |
| |
| void ReverseAnimationsForKeyForLayers(NSString* key, NSArray* layers) { |
| for (CALayer* layer in layers) { |
| CAAnimation* reversedAnimation = |
| CAAnimationMakeReverse([layer animationForKey:key], layer); |
| [layer removeAnimationForKey:key]; |
| [layer addAnimation:reversedAnimation forKey:key]; |
| } |
| } |