UNPKG

19.8 kBPlain TextView Raw
1#import "RNSScreenStackAnimator.h"
2#import "RNSScreenStack.h"
3
4#import "RNSScreen.h"
5
6// proportions to default transition duration
7static const float RNSSlideOpenTransitionDurationProportion = 1;
8static const float RNSFadeOpenTransitionDurationProportion = 0.2 / 0.35;
9static const float RNSSlideCloseTransitionDurationProportion = 0.25 / 0.35;
10static const float RNSFadeCloseTransitionDurationProportion = 0.15 / 0.35;
11static const float RNSFadeCloseDelayTransitionDurationProportion = 0.1 / 0.35;
12
13@implementation RNSScreenStackAnimator {
14 UINavigationControllerOperation _operation;
15 NSTimeInterval _transitionDuration;
16}
17
18- (instancetype)initWithOperation:(UINavigationControllerOperation)operation
19{
20 if (self = [super init]) {
21 _operation = operation;
22 _transitionDuration = 0.35; // default duration in seconds
23 }
24 return self;
25}
26
27- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
28{
29 RNSScreenView *screen;
30 if (_operation == UINavigationControllerOperationPush) {
31 UIViewController *toViewController =
32 [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
33 screen = ((RNSScreen *)toViewController).screenView;
34 } else if (_operation == UINavigationControllerOperationPop) {
35 UIViewController *fromViewController =
36 [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
37 screen = ((RNSScreen *)fromViewController).screenView;
38 }
39
40 if (screen != nil && screen.stackAnimation == RNSScreenStackAnimationNone) {
41 return 0;
42 }
43
44 if (screen != nil && screen.transitionDuration != nil && [screen.transitionDuration floatValue] >= 0) {
45 float durationInSeconds = [screen.transitionDuration floatValue] / 1000.0;
46 return durationInSeconds;
47 }
48
49 return _transitionDuration;
50}
51
52- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
53{
54 UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
55 UIViewController *fromViewController =
56 [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
57 toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
58
59 RNSScreenView *screen;
60 if (_operation == UINavigationControllerOperationPush) {
61 screen = ((RNSScreen *)toViewController).screenView;
62 } else if (_operation == UINavigationControllerOperationPop) {
63 screen = ((RNSScreen *)fromViewController).screenView;
64 }
65
66 if (screen != nil) {
67 if ([screen.reactSuperview isKindOfClass:[RNSScreenStackView class]] &&
68 ((RNSScreenStackView *)(screen.reactSuperview)).customAnimation) {
69 [self animateWithNoAnimation:transitionContext toVC:toViewController fromVC:fromViewController];
70 } else if (screen.fullScreenSwipeEnabled && transitionContext.isInteractive) {
71 // we are swiping with full width gesture
72 if (screen.customAnimationOnSwipe) {
73 [self animateTransitionWithStackAnimation:screen.stackAnimation
74 transitionContext:transitionContext
75 toVC:toViewController
76 fromVC:fromViewController];
77 } else {
78 // we have to provide an animation when swiping, otherwise the screen will be popped immediately,
79 // so in case of no custom animation on swipe set, we provide the one closest to the default
80 [self animateSimplePushWithTransitionContext:transitionContext toVC:toViewController fromVC:fromViewController];
81 }
82 } else {
83 // we are going forward or provided custom animation on swipe or clicked native header back button
84 [self animateTransitionWithStackAnimation:screen.stackAnimation
85 transitionContext:transitionContext
86 toVC:toViewController
87 fromVC:fromViewController];
88 }
89 }
90}
91
92- (void)animateSimplePushWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
93 toVC:(UIViewController *)toViewController
94 fromVC:(UIViewController *)fromViewController
95{
96 float containerWidth = transitionContext.containerView.bounds.size.width;
97 float belowViewWidth = containerWidth * 0.3;
98
99 CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0);
100 CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0);
101
102 if (toViewController.navigationController.view.semanticContentAttribute ==
103 UISemanticContentAttributeForceRightToLeft) {
104 rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0);
105 leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0);
106 }
107
108 if (_operation == UINavigationControllerOperationPush) {
109 toViewController.view.transform = rightTransform;
110 [[transitionContext containerView] addSubview:toViewController.view];
111 [UIView animateWithDuration:[self transitionDuration:transitionContext]
112 animations:^{
113 fromViewController.view.transform = leftTransform;
114 toViewController.view.transform = CGAffineTransformIdentity;
115 }
116 completion:^(BOOL finished) {
117 fromViewController.view.transform = CGAffineTransformIdentity;
118 toViewController.view.transform = CGAffineTransformIdentity;
119 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
120 }];
121 } else if (_operation == UINavigationControllerOperationPop) {
122 toViewController.view.transform = leftTransform;
123 [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
124
125 void (^animationBlock)(void) = ^{
126 toViewController.view.transform = CGAffineTransformIdentity;
127 fromViewController.view.transform = rightTransform;
128 };
129 void (^completionBlock)(BOOL) = ^(BOOL finished) {
130 fromViewController.view.transform = CGAffineTransformIdentity;
131 toViewController.view.transform = CGAffineTransformIdentity;
132 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
133 };
134
135 if (!transitionContext.isInteractive) {
136 [UIView animateWithDuration:[self transitionDuration:transitionContext]
137 animations:animationBlock
138 completion:completionBlock];
139 } else {
140 // we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
141 [UIView animateWithDuration:[self transitionDuration:transitionContext]
142 delay:0.0
143 options:UIViewAnimationOptionCurveLinear
144 animations:animationBlock
145 completion:completionBlock];
146 }
147 }
148}
149
150- (void)animateSlideFromLeftWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
151 toVC:(UIViewController *)toViewController
152 fromVC:(UIViewController *)fromViewController
153{
154 float containerWidth = transitionContext.containerView.bounds.size.width;
155 float belowViewWidth = containerWidth * 0.3;
156
157 CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0);
158 CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0);
159
160 if (toViewController.navigationController.view.semanticContentAttribute ==
161 UISemanticContentAttributeForceRightToLeft) {
162 rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0);
163 leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0);
164 }
165
166 if (_operation == UINavigationControllerOperationPush) {
167 toViewController.view.transform = rightTransform;
168 [[transitionContext containerView] addSubview:toViewController.view];
169 [UIView animateWithDuration:[self transitionDuration:transitionContext]
170 animations:^{
171 fromViewController.view.transform = leftTransform;
172 toViewController.view.transform = CGAffineTransformIdentity;
173 }
174 completion:^(BOOL finished) {
175 fromViewController.view.transform = CGAffineTransformIdentity;
176 toViewController.view.transform = CGAffineTransformIdentity;
177 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
178 }];
179 } else if (_operation == UINavigationControllerOperationPop) {
180 toViewController.view.transform = leftTransform;
181 [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
182
183 void (^animationBlock)(void) = ^{
184 toViewController.view.transform = CGAffineTransformIdentity;
185 fromViewController.view.transform = rightTransform;
186 };
187 void (^completionBlock)(BOOL) = ^(BOOL finished) {
188 fromViewController.view.transform = CGAffineTransformIdentity;
189 toViewController.view.transform = CGAffineTransformIdentity;
190 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
191 };
192
193 if (!transitionContext.isInteractive) {
194 [UIView animateWithDuration:[self transitionDuration:transitionContext]
195 animations:animationBlock
196 completion:completionBlock];
197 } else {
198 // we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
199 [UIView animateWithDuration:[self transitionDuration:transitionContext]
200 delay:0.0
201 options:UIViewAnimationOptionCurveLinear
202 animations:animationBlock
203 completion:completionBlock];
204 }
205 }
206}
207
208- (void)animateFadeWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
209 toVC:(UIViewController *)toViewController
210 fromVC:(UIViewController *)fromViewController
211{
212 toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
213
214 if (_operation == UINavigationControllerOperationPush) {
215 [[transitionContext containerView] addSubview:toViewController.view];
216 toViewController.view.alpha = 0.0;
217 [UIView animateWithDuration:[self transitionDuration:transitionContext]
218 animations:^{
219 toViewController.view.alpha = 1.0;
220 }
221 completion:^(BOOL finished) {
222 toViewController.view.alpha = 1.0;
223 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
224 }];
225 } else if (_operation == UINavigationControllerOperationPop) {
226 [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
227
228 [UIView animateWithDuration:[self transitionDuration:transitionContext]
229 animations:^{
230 fromViewController.view.alpha = 0.0;
231 }
232 completion:^(BOOL finished) {
233 fromViewController.view.alpha = 1.0;
234
235 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
236 }];
237 }
238}
239
240- (void)animateSlideFromBottomWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
241 toVC:(UIViewController *)toViewController
242 fromVC:(UIViewController *)fromViewController
243{
244 CGAffineTransform topBottomTransform =
245 CGAffineTransformMakeTranslation(0, transitionContext.containerView.bounds.size.height);
246
247 if (_operation == UINavigationControllerOperationPush) {
248 toViewController.view.transform = topBottomTransform;
249 [[transitionContext containerView] addSubview:toViewController.view];
250 [UIView animateWithDuration:[self transitionDuration:transitionContext]
251 animations:^{
252 fromViewController.view.transform = CGAffineTransformIdentity;
253 toViewController.view.transform = CGAffineTransformIdentity;
254 }
255 completion:^(BOOL finished) {
256 fromViewController.view.transform = CGAffineTransformIdentity;
257 toViewController.view.transform = CGAffineTransformIdentity;
258 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
259 }];
260 } else if (_operation == UINavigationControllerOperationPop) {
261 toViewController.view.transform = CGAffineTransformIdentity;
262 [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
263
264 void (^animationBlock)(void) = ^{
265 toViewController.view.transform = CGAffineTransformIdentity;
266 fromViewController.view.transform = topBottomTransform;
267 };
268 void (^completionBlock)(BOOL) = ^(BOOL finished) {
269 fromViewController.view.transform = CGAffineTransformIdentity;
270 toViewController.view.transform = CGAffineTransformIdentity;
271 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
272 };
273
274 if (!transitionContext.isInteractive) {
275 [UIView animateWithDuration:[self transitionDuration:transitionContext]
276 animations:animationBlock
277 completion:completionBlock];
278 } else {
279 // we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
280 [UIView animateWithDuration:[self transitionDuration:transitionContext]
281 delay:0.0
282 options:UIViewAnimationOptionCurveLinear
283 animations:animationBlock
284 completion:completionBlock];
285 }
286 }
287}
288
289- (void)animateFadeFromBottomWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
290 toVC:(UIViewController *)toViewController
291 fromVC:(UIViewController *)fromViewController
292{
293 CGAffineTransform topBottomTransform =
294 CGAffineTransformMakeTranslation(0, 0.08 * transitionContext.containerView.bounds.size.height);
295
296 const float transitionDuration = [self transitionDuration:transitionContext];
297
298 if (_operation == UINavigationControllerOperationPush) {
299 toViewController.view.transform = topBottomTransform;
300 toViewController.view.alpha = 0.0;
301 [[transitionContext containerView] addSubview:toViewController.view];
302
303 // Android Nougat open animation
304 // http://aosp.opersys.com/xref/android-7.1.2_r37/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
305 [UIView animateWithDuration:transitionDuration * RNSSlideOpenTransitionDurationProportion // defaults to 0.35 s
306 delay:0
307 options:UIViewAnimationOptionCurveEaseOut
308 animations:^{
309 fromViewController.view.transform = CGAffineTransformIdentity;
310 toViewController.view.transform = CGAffineTransformIdentity;
311 }
312 completion:^(BOOL finished) {
313 fromViewController.view.transform = CGAffineTransformIdentity;
314 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
315 }];
316 [UIView animateWithDuration:transitionDuration * RNSFadeOpenTransitionDurationProportion // defaults to 0.2 s
317 delay:0
318 options:UIViewAnimationOptionCurveEaseOut
319 animations:^{
320 toViewController.view.alpha = 1.0;
321 }
322 completion:nil];
323
324 } else if (_operation == UINavigationControllerOperationPop) {
325 toViewController.view.transform = CGAffineTransformIdentity;
326 [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
327
328 // Android Nougat exit animation
329 // http://aosp.opersys.com/xref/android-7.1.2_r37/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml
330 [UIView animateWithDuration:transitionDuration * RNSSlideCloseTransitionDurationProportion // defaults to 0.25 s
331 delay:0
332 options:UIViewAnimationOptionCurveEaseIn
333 animations:^{
334 toViewController.view.transform = CGAffineTransformIdentity;
335 fromViewController.view.transform = topBottomTransform;
336 }
337 completion:^(BOOL finished) {
338 fromViewController.view.transform = CGAffineTransformIdentity;
339 toViewController.view.transform = CGAffineTransformIdentity;
340 fromViewController.view.alpha = 1.0;
341 toViewController.view.alpha = 1.0;
342 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
343 }];
344 [UIView animateWithDuration:transitionDuration * RNSFadeCloseTransitionDurationProportion // defaults to 0.15 s
345 delay:transitionDuration * RNSFadeCloseDelayTransitionDurationProportion // defaults to 0.1 s
346 options:UIViewAnimationOptionCurveLinear
347 animations:^{
348 fromViewController.view.alpha = 0.0;
349 }
350 completion:nil];
351 }
352}
353
354- (void)animateWithNoAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
355 toVC:(UIViewController *)toViewController
356 fromVC:(UIViewController *)fromViewController
357{
358 if (_operation == UINavigationControllerOperationPush) {
359 [[transitionContext containerView] addSubview:toViewController.view];
360 [UIView animateWithDuration:[self transitionDuration:transitionContext]
361 animations:^{
362 }
363 completion:^(BOOL finished) {
364 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
365 }];
366 } else if (_operation == UINavigationControllerOperationPop) {
367 [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
368
369 [UIView animateWithDuration:[self transitionDuration:transitionContext]
370 animations:^{
371 }
372 completion:^(BOOL finished) {
373 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
374 }];
375 }
376}
377
378+ (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation
379{
380 return (animation != RNSScreenStackAnimationFlip && animation != RNSScreenStackAnimationDefault);
381}
382
383- (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation
384 transitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
385 toVC:(UIViewController *)toVC
386 fromVC:(UIViewController *)fromVC
387{
388 if (animation == RNSScreenStackAnimationSimplePush) {
389 [self animateSimplePushWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
390 return;
391 } else if (animation == RNSScreenStackAnimationSlideFromLeft) {
392 [self animateSlideFromLeftWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
393 return;
394 } else if (animation == RNSScreenStackAnimationFade || animation == RNSScreenStackAnimationNone) {
395 [self animateFadeWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
396 return;
397 } else if (animation == RNSScreenStackAnimationSlideFromBottom) {
398 [self animateSlideFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
399 return;
400 } else if (animation == RNSScreenStackAnimationFadeFromBottom) {
401 [self animateFadeFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
402 return;
403 }
404 // simple_push is the default custom animation
405 [self animateSimplePushWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
406}
407
408@end