Upstreaming browser/ui/uikit_ui_util from iOS.
[chromium-blink-merge.git] / ios / chrome / browser / ui / reversed_animation.mm
bloba46b0dcdd4954209e95340119ebe24a8e658cfcc
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>
8 #include <algorithm>
9 #include <cmath>
11 #include "base/logging.h"
12 #include "base/mac/objc_property_releaser.h"
14 @protocol ReversedAnimationProtocol;
15 typedef CAAnimation<ReversedAnimationProtocol> ReversedAnimation;
17 namespace {
18 // Enum type used to denote the direction of a reversed animation relative to
19 // the original animation's direction.
20 typedef enum {
21   ANIMATION_DIRECTION_NORMAL,
22   ANIMATION_DIRECTION_REVERSE
23 } AnimationDirection;
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;
29 }  // namespace
31 // Returns an animation that reverses |animation| when added to |layer|, given
32 // that |animation| is in |parent|'s timespace, which begins at
33 // |parentBeginTime|.
34 CAAnimation* CAAnimationMakeReverse(CAAnimation* animation,
35                                     CALayer* layer,
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
40 // |parentBeginTime|.
41 void UpdateReversedAnimation(ReversedAnimation* reversedAnimation,
42                              CAAnimation* animationToReverse,
43                              CALayer* layer,
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
56 // reverse animation.
57 @property(nonatomic, assign) CFTimeInterval animationTimeOffset;
59 @end
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;
76 @end
78 @implementation ReversedBasicAnimation
80 @synthesize originalAnimation = _originalAnimation;
81 @synthesize animationDirection = _animationDirection;
82 @synthesize animationTimeOffset = _animationTimeOffset;
84 - (instancetype)init {
85   self = [super init];
86   if (self) {
87     _propertyReleaser_ReversedBasicAnimation.Init(
88         self, [ReversedBasicAnimation class]);
89   }
90   return self;
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;
98   return copy;
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;
108   CFTimeInterval now =
109       [layer convertTime:CACurrentMediaTime() fromLayer:nil] - parentBeginTime;
110   ReversedBasicAnimation* reversedAnimation =
111       [ReversedBasicAnimation animationWithKeyPath:keyPath];
112   UpdateReversedAnimation(reversedAnimation, animation, layer, parent,
113                           parentBeginTime);
115   // Update from and to values.
116   BOOL isReversed =
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-
125     // animation.
126     reversedAnimation.fromValue =
127         [[layer presentationLayer] valueForKeyPath:keyPath];
128   } else {
129     reversedAnimation.fromValue = isReversed ? originalBasicAnimation.toValue
130                                              : originalBasicAnimation.fromValue;
131   }
132   return reversedAnimation;
135 @end
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;
152 @end
154 @implementation ReversedAnimationGroup
156 @synthesize originalAnimation = _originalAnimation;
157 @synthesize animationDirection = _animationDirection;
158 @synthesize animationTimeOffset = _animationTimeOffset;
160 - (instancetype)init {
161   self = [super init];
162   if (self) {
163     _propertyReleaser_ReversedAnimationGroup.Init(
164         self, [ReversedAnimationGroup class]);
165   }
166   return self;
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;
174   return copy;
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];
192   }
193   reversedGroup.animations = reversedAnimations;
195   return reversedGroup;
198 @end
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,
207                                     CALayer* layer,
208                                     CAAnimationGroup* parent,
209                                     CFTimeInterval parentBeginTime) {
210   CAAnimation* reversedAnimation = nil;
211   if ([animation isKindOfClass:[CABasicAnimation class]]) {
212     CABasicAnimation* basicAnimation =
213         static_cast<CABasicAnimation*>(animation);
214     reversedAnimation =
215         [ReversedBasicAnimation reversedAnimationForAnimation:basicAnimation
216                                                      forLayer:layer
217                                                        parent:parent
218                                               parentBeginTime:parentBeginTime];
219   } else if ([animation isKindOfClass:[CAAnimationGroup class]]) {
220     CAAnimationGroup* animationGroup =
221         static_cast<CAAnimationGroup*>(animation);
222     reversedAnimation =
223         [ReversedAnimationGroup reversedAnimationGroupForGroup:animationGroup
224                                                       forLayer:layer
225                                                         parent:parent
226                                                parentBeginTime:parentBeginTime];
227   } else {
228     // TODO(kkhorimoto): Investigate possible general-case reversals.  It may
229     // be possible to implement this by manipulating the CAMediaTiming
230     // properties.
231   }
232   return reversedAnimation;
235 void UpdateReversedAnimation(ReversedAnimation* reversedAnimation,
236                              CAAnimation* animationToReverse,
237                              CALayer* layer,
238                              CAAnimationGroup* parent,
239                              CFTimeInterval parentBeginTime) {
240   // Copy properties.
241   CFTimeInterval now =
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;
255   }
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;
266   BOOL isReversed =
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;
286   } else {
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;
293     } else {
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;
300       } else {
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;
304       }
305     }
306     reversedAnimation.duration = reversedAnimation.originalAnimation.duration;
307     reversedAnimation.animationTimeOffset =
308         isReversed ? reversedAnimation.originalAnimation.duration : 0;
309   }
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];
318   }