1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ios/chrome/browser/ui/reversed_animation.h"
7 #import <QuartzCore/QuartzCore.h>
11 #include "base/logging.h"
12 #include "base/mac/objc_property_releaser.h"
14 @protocol ReversedAnimationProtocol;
15 typedef CAAnimation<ReversedAnimationProtocol> ReversedAnimation;
18 // Enum type used to denote the direction of a reversed animation relative to
19 // the original animation's direction.
21 ANIMATION_DIRECTION_NORMAL,
22 ANIMATION_DIRECTION_REVERSE
24 // Returns the AnimationDirection opposite of |direction|.
25 AnimationDirection AnimationDirectionOpposite(AnimationDirection direction) {
26 return direction == ANIMATION_DIRECTION_NORMAL ? ANIMATION_DIRECTION_REVERSE
27 : ANIMATION_DIRECTION_NORMAL;
31 // Returns an animation that reverses |animation| when added to |layer|, given
32 // that |animation| is in |parent|'s timespace, which begins at
34 CAAnimation* CAAnimationMakeReverse(CAAnimation* animation,
36 CAAnimationGroup* parent,
37 CFTimeInterval parentBeginTime);
38 // Updates |reversedAnimation|'s properties for |animationToReverse|, given that
39 // |animationToReverse| is in |parent|'s timespace, which begins at
41 void UpdateReversedAnimation(ReversedAnimation* reversedAnimation,
42 CAAnimation* animationToReverse,
44 CAAnimationGroup* parent,
45 CFTimeInterval parentBeginTime);
47 #pragma mark - ReversedAnimation protocol
49 @protocol ReversedAnimationProtocol<NSObject>
51 // The original animation that's being played in reverse.
52 @property(nonatomic, retain) CAAnimation* originalAnimation;
53 // The current direction for the animation.
54 @property(nonatomic, assign) AnimationDirection animationDirection;
55 // The offset into the original animation's duration at the begining of the
57 @property(nonatomic, assign) CFTimeInterval animationTimeOffset;
61 #pragma mark - ReversedBasicAnimation
63 @interface ReversedBasicAnimation
64 : CABasicAnimation<ReversedAnimationProtocol> {
65 base::mac::ObjCPropertyReleaser _propertyReleaser_ReversedBasicAnimation;
68 // Returns an animation that performs |animation| in reverse when added to
69 // |layer|. |parentBeginTime| should be set to the beginTime in absolute time
70 // of |animation|'s parent in the timing hierarchy.
71 + (instancetype)reversedAnimationForAnimation:(CABasicAnimation*)animation
72 forLayer:(CALayer*)layer
73 parent:(CAAnimationGroup*)parent
74 parentBeginTime:(CFTimeInterval)parentBeginTime;
78 @implementation ReversedBasicAnimation
80 @synthesize originalAnimation = _originalAnimation;
81 @synthesize animationDirection = _animationDirection;
82 @synthesize animationTimeOffset = _animationTimeOffset;
84 - (instancetype)init {
87 _propertyReleaser_ReversedBasicAnimation.Init(
88 self, [ReversedBasicAnimation class]);
93 - (instancetype)copyWithZone:(NSZone*)zone {
94 ReversedBasicAnimation* copy = [super copyWithZone:zone];
95 copy.originalAnimation = self.originalAnimation;
96 copy.animationDirection = self.animationDirection;
97 copy.animationTimeOffset = self.animationTimeOffset;
101 + (instancetype)reversedAnimationForAnimation:(CABasicAnimation*)animation
102 forLayer:(CALayer*)layer
103 parent:(CAAnimationGroup*)parent
104 parentBeginTime:(CFTimeInterval)parentBeginTime {
105 // Create new animation and copy properties. Note that we can't use |-copy|
106 // because we need the new animation to be the correct class.
107 NSString* keyPath = animation.keyPath;
109 [layer convertTime:CACurrentMediaTime() fromLayer:nil] - parentBeginTime;
110 ReversedBasicAnimation* reversedAnimation =
111 [ReversedBasicAnimation animationWithKeyPath:keyPath];
112 UpdateReversedAnimation(reversedAnimation, animation, layer, parent,
115 // Update from and to values.
117 reversedAnimation.animationDirection == ANIMATION_DIRECTION_REVERSE;
118 CABasicAnimation* originalBasicAnimation =
119 static_cast<CABasicAnimation*>(reversedAnimation.originalAnimation);
120 reversedAnimation.toValue = isReversed ? originalBasicAnimation.fromValue
121 : originalBasicAnimation.toValue;
122 if (now > animation.beginTime &&
123 now < animation.beginTime + animation.duration) {
124 // Use the presentation layer's current value for reversals that occur mid-
126 reversedAnimation.fromValue =
127 [[layer presentationLayer] valueForKeyPath:keyPath];
129 reversedAnimation.fromValue = isReversed ? originalBasicAnimation.toValue
130 : originalBasicAnimation.fromValue;
132 return reversedAnimation;
137 #pragma mark - ReversedAnimationGroup
139 @interface ReversedAnimationGroup
140 : CAAnimationGroup<ReversedAnimationProtocol> {
141 base::mac::ObjCPropertyReleaser _propertyReleaser_ReversedAnimationGroup;
144 // Returns an animation that performs |animation| in reverse when added to
145 // |layer|. |parentBeginTime| should be set to the beginTime in absolute time
146 // of the animation group to which |animation| belongs.
147 + (instancetype)reversedAnimationGroupForGroup:(CAAnimationGroup*)group
148 forLayer:(CALayer*)layer
149 parent:(CAAnimationGroup*)parent
150 parentBeginTime:(CFTimeInterval)parentBeginTime;
154 @implementation ReversedAnimationGroup
156 @synthesize originalAnimation = _originalAnimation;
157 @synthesize animationDirection = _animationDirection;
158 @synthesize animationTimeOffset = _animationTimeOffset;
160 - (instancetype)init {
163 _propertyReleaser_ReversedAnimationGroup.Init(
164 self, [ReversedAnimationGroup class]);
169 - (instancetype)copyWithZone:(NSZone*)zone {
170 ReversedAnimationGroup* copy = [super copyWithZone:zone];
171 copy.originalAnimation = self.originalAnimation;
172 copy.animationDirection = self.animationDirection;
173 copy.animationTimeOffset = self.animationTimeOffset;
177 + (instancetype)reversedAnimationGroupForGroup:(CAAnimationGroup*)group
178 forLayer:(CALayer*)layer
179 parent:(CAAnimationGroup*)parent
180 parentBeginTime:(CFTimeInterval)parentBeginTime {
181 // Create new animation and copy properties. Note that we can't use |-copy|
182 // because we need the new animation to be the correct class.
183 ReversedAnimationGroup* reversedGroup = [ReversedAnimationGroup animation];
184 UpdateReversedAnimation(reversedGroup, group, layer, parent, parentBeginTime);
186 // Reverse the animations of the original group.
187 NSMutableArray* reversedAnimations = [NSMutableArray array];
188 for (CAAnimation* animation in group.animations) {
189 CAAnimation* reversedAnimation = CAAnimationMakeReverse(
190 animation, layer, group, group.beginTime + parentBeginTime);
191 [reversedAnimations addObject:reversedAnimation];
193 reversedGroup.animations = reversedAnimations;
195 return reversedGroup;
200 #pragma mark - animation_util functions
202 CAAnimation* CAAnimationMakeReverse(CAAnimation* animation, CALayer* layer) {
203 return CAAnimationMakeReverse(animation, layer, nil, layer.beginTime);
206 CAAnimation* CAAnimationMakeReverse(CAAnimation* animation,
208 CAAnimationGroup* parent,
209 CFTimeInterval parentBeginTime) {
210 CAAnimation* reversedAnimation = nil;
211 if ([animation isKindOfClass:[CABasicAnimation class]]) {
212 CABasicAnimation* basicAnimation =
213 static_cast<CABasicAnimation*>(animation);
215 [ReversedBasicAnimation reversedAnimationForAnimation:basicAnimation
218 parentBeginTime:parentBeginTime];
219 } else if ([animation isKindOfClass:[CAAnimationGroup class]]) {
220 CAAnimationGroup* animationGroup =
221 static_cast<CAAnimationGroup*>(animation);
223 [ReversedAnimationGroup reversedAnimationGroupForGroup:animationGroup
226 parentBeginTime:parentBeginTime];
228 // TODO(kkhorimoto): Investigate possible general-case reversals. It may
229 // be possible to implement this by manipulating the CAMediaTiming
232 return reversedAnimation;
235 void UpdateReversedAnimation(ReversedAnimation* reversedAnimation,
236 CAAnimation* animationToReverse,
238 CAAnimationGroup* parent,
239 CFTimeInterval parentBeginTime) {
242 [layer convertTime:CACurrentMediaTime() fromLayer:nil] - parentBeginTime;
243 reversedAnimation.fillMode = animationToReverse.fillMode;
244 reversedAnimation.removedOnCompletion =
245 animationToReverse.removedOnCompletion;
246 reversedAnimation.timingFunction = animationToReverse.timingFunction;
248 // Extract the previous reversal if it exists.
249 ReversedAnimation* previousReversedAnimation = nil;
250 Protocol* reversedAnimationProtocol = @protocol(ReversedAnimationProtocol);
251 if ([animationToReverse conformsToProtocol:reversedAnimationProtocol]) {
252 previousReversedAnimation =
253 static_cast<ReversedAnimation*>(animationToReverse);
254 animationToReverse = previousReversedAnimation.originalAnimation;
256 reversedAnimation.originalAnimation = animationToReverse;
257 reversedAnimation.animationDirection =
258 previousReversedAnimation
259 ? AnimationDirectionOpposite(
260 previousReversedAnimation.animationDirection)
261 : ANIMATION_DIRECTION_REVERSE;
263 CAAnimation* previousAnimation = previousReversedAnimation
264 ? previousReversedAnimation
265 : animationToReverse;
267 reversedAnimation.animationDirection == ANIMATION_DIRECTION_REVERSE;
268 if (now < previousAnimation.beginTime) {
269 // Reversal occurs before previous animation begins.
270 reversedAnimation.beginTime = 2.0 * now - previousAnimation.beginTime -
271 reversedAnimation.originalAnimation.duration;
272 reversedAnimation.duration = reversedAnimation.originalAnimation.duration;
273 reversedAnimation.animationTimeOffset =
274 isReversed ? 0 : reversedAnimation.originalAnimation.duration;
275 } else if (now < previousAnimation.beginTime + previousAnimation.duration) {
276 // Reversal occurs while the previous animation is occurring.
277 reversedAnimation.beginTime = 0;
278 CFTimeInterval timeDelta = now - previousAnimation.beginTime;
279 reversedAnimation.animationTimeOffset =
280 previousReversedAnimation.animationTimeOffset +
281 (isReversed ? 1.0 : -1.0) * timeDelta;
282 reversedAnimation.duration =
283 isReversed ? reversedAnimation.animationTimeOffset
284 : reversedAnimation.originalAnimation.duration -
285 reversedAnimation.animationTimeOffset;
287 // Reversal occurs after the previous animation has ended.
288 if (!parentBeginTime) {
289 // If the parent's begin time is zero, the animation is using absolute
290 // time as its beginTime.
291 reversedAnimation.beginTime =
292 2.0 * now - previousAnimation.beginTime - previousAnimation.duration;
294 CFTimeInterval previousEndTime =
295 previousAnimation.beginTime + previousAnimation.duration;
296 if (now > parent.duration) {
297 // The animation's parent has already ended, so use the difference
298 // between the parent's ending and the previous animation's end.
299 reversedAnimation.beginTime = parent.duration - previousEndTime;
301 // The parent hasn't ended, so use the difference between the current
302 // time and the previous animation's end.
303 reversedAnimation.beginTime = now - previousEndTime;
306 reversedAnimation.duration = reversedAnimation.originalAnimation.duration;
307 reversedAnimation.animationTimeOffset =
308 isReversed ? reversedAnimation.originalAnimation.duration : 0;
312 void ReverseAnimationsForKeyForLayers(NSString* key, NSArray* layers) {
313 for (CALayer* layer in layers) {
314 CAAnimation* reversedAnimation =
315 CAAnimationMakeReverse([layer animationForKey:key], layer);
316 [layer removeAnimationForKey:key];
317 [layer addAnimation:reversedAnimation forKey:key];