blob: 557c7d8d951ad691d808ad7efc027fcfbc238c2b [file] [log] [blame]
// 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"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@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>
// 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)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>
// 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)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];
}
}