From 567242b2ab07c5351589b5595c041341660d9fa1 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Tue, 10 Dec 2024 13:04:15 +0100 Subject: [PATCH 1/6] Restore behaviour of StackAnimationNone - PoC (there is still some header animation) --- ios/RNSScreenStackAnimator.mm | 74 ++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/ios/RNSScreenStackAnimator.mm b/ios/RNSScreenStackAnimator.mm index 65a99f284..2a7f8c9b3 100644 --- a/ios/RNSScreenStackAnimator.mm +++ b/ios/RNSScreenStackAnimator.mm @@ -59,7 +59,7 @@ - (NSTimeInterval)transitionDuration:(id)t } if (screen != nil && screen.stackAnimation == RNSScreenStackAnimationNone) { - return 0; + return 0.0; } if (screen != nil && screen.transitionDuration != nil && [screen.transitionDuration floatValue] >= 0) { @@ -489,6 +489,34 @@ - (void)animateWithNoAnimation:(id)transit } } +- (void)animateNoneWithTransitionContext:(id)transitionContext + toVC:(UIViewController *)toViewController + fromVC:(UIViewController *)fromViewController +{ + if (_operation == UINavigationControllerOperationPush) { + [[transitionContext containerView] addSubview:toViewController.view]; + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:^{ + toViewController.view.alpha = 1.0; + } + completion:^(BOOL finished) { + toViewController.view.alpha = 1.0; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + } else if (_operation == UINavigationControllerOperationPop) { + [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:^{ + fromViewController.view.alpha = 0.0; + } + completion:^(BOOL finished) { + fromViewController.view.alpha = 1.0; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + } +} + #pragma mark - Public API - (nullable id)timingParamsForAnimationCompletion @@ -509,24 +537,34 @@ - (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation toVC:(UIViewController *)toVC fromVC:(UIViewController *)fromVC { - if (animation == RNSScreenStackAnimationSimplePush) { - [self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC]; - return; - } else if (animation == RNSScreenStackAnimationSlideFromLeft) { - [self animateSlideFromLeftWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; - return; - } else if (animation == RNSScreenStackAnimationFade || animation == RNSScreenStackAnimationNone) { - [self animateFadeWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; - return; - } else if (animation == RNSScreenStackAnimationSlideFromBottom) { - [self animateSlideFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; - return; - } else if (animation == RNSScreenStackAnimationFadeFromBottom) { - [self animateFadeFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; - return; + switch (animation) { + case RNSScreenStackAnimationSimplePush: + [self animateSimplePushWithShadowEnabled:shadowEnabled + transitionContext:transitionContext + toVC:toVC + fromVC:fromVC]; + return; + case RNSScreenStackAnimationSlideFromLeft: + [self animateSlideFromLeftWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; + return; + case RNSScreenStackAnimationFade: + [self animateFadeWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; + return; + case RNSScreenStackAnimationSlideFromBottom: + [self animateSlideFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; + return; + case RNSScreenStackAnimationFadeFromBottom: + [self animateFadeFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; + return; + case RNSScreenStackAnimationNone: + [self animateNoneWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; + default: + // simple_push is the default custom animation + [self animateSimplePushWithShadowEnabled:shadowEnabled + transitionContext:transitionContext + toVC:toVC + fromVC:fromVC]; } - // simple_push is the default custom animation - [self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC]; } + (UISpringTimingParameters *)defaultSpringTimingParametersApprox From 4ecda0d30d06b9878f39f2588f08517c99673b3c Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Fri, 13 Dec 2024 12:23:42 +0100 Subject: [PATCH 2/6] Add "old" implementation for reference & comparison --- ios/RNSScreenStack.mm | 6 +- ios/RNSScreenStackAnimator.h | 25 ++ ios/RNSScreenStackAnimator.mm | 437 +++++++++++++++++++++++++++++++++- 3 files changed, 465 insertions(+), 3 deletions(-) diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm index bfeeb002c..3c5addbef 100644 --- a/ios/RNSScreenStack.mm +++ b/ios/RNSScreenStack.mm @@ -750,7 +750,11 @@ - (void)dismissOnReload // otherwise the screen will be just popped immediately due to no animation ((operation == UINavigationControllerOperationPop && shouldCancelDismiss) || _isFullWidthSwiping || [RNSScreenStackAnimator isCustomAnimation:screen.stackAnimation] || _customAnimation)) { - return [[RNSScreenStackAnimator alloc] initWithOperation:operation]; + if (rns::kUsesNewAnimatorImpl) { + return [[RNSScreenStackAnimator alloc] initWithOperation:operation]; + } else { + return [[RNSScreenStackAnimatorLegacy alloc] initWithOperation:operation]; + } } return nil; } diff --git a/ios/RNSScreenStackAnimator.h b/ios/RNSScreenStackAnimator.h index d9c4c67ec..f83a6cd46 100644 --- a/ios/RNSScreenStackAnimator.h +++ b/ios/RNSScreenStackAnimator.h @@ -1,5 +1,11 @@ +#pragma once + #import "RNSScreen.h" +namespace rns { +constexpr bool kUsesNewAnimatorImpl = false; +} + @interface RNSScreenStackAnimator : NSObject /// This property is filled whenever there is an ongoing animation and cleared on animation end. @@ -18,3 +24,22 @@ + (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation; @end + +@interface RNSScreenStackAnimatorLegacy : NSObject + +/// This property is filled whenever there is an ongoing animation and cleared on animation end. +@property (nonatomic, strong, nullable, readonly) UIViewPropertyAnimator *inFlightAnimator; + +- (nonnull instancetype)initWithOperation:(UINavigationControllerOperation)operation; + +/// In case of interactive / interruptible transition (e.g. swipe back gesture) this method should return +/// timing parameters expected by animator to be used for animation completion (e.g. when user's +/// gesture had ended). +/// +/// @return timing curve provider expected to be used for animation completion or nil, +/// when there is no interactive transition running. +- (nullable id)timingParamsForAnimationCompletion; + ++ (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation; + +@end diff --git a/ios/RNSScreenStackAnimator.mm b/ios/RNSScreenStackAnimator.mm index 2a7f8c9b3..9c4931fdd 100644 --- a/ios/RNSScreenStackAnimator.mm +++ b/ios/RNSScreenStackAnimator.mm @@ -495,7 +495,7 @@ - (void)animateNoneWithTransitionContext:(id)transitionContext +{ + RNSScreenView *screen; + if (_operation == UINavigationControllerOperationPush) { + UIViewController *toViewController = + [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; + screen = ((RNSScreen *)toViewController).screenView; + } else if (_operation == UINavigationControllerOperationPop) { + UIViewController *fromViewController = + [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; + screen = ((RNSScreen *)fromViewController).screenView; + } + + if (screen != nil && screen.stackAnimation == RNSScreenStackAnimationNone) { + return 0; + } + + if (screen != nil && screen.transitionDuration != nil && [screen.transitionDuration floatValue] >= 0) { + float durationInSeconds = [screen.transitionDuration floatValue] / 1000.0; + return durationInSeconds; + } + + return _transitionDuration; +} + +- (void)animateTransition:(id)transitionContext +{ + UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; + UIViewController *fromViewController = + [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; + toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController]; + + RNSScreenView *screen; + if (_operation == UINavigationControllerOperationPush) { + screen = ((RNSScreen *)toViewController).screenView; + } else if (_operation == UINavigationControllerOperationPop) { + screen = ((RNSScreen *)fromViewController).screenView; + } + + if (screen != nil) { + if ([screen.reactSuperview isKindOfClass:[RNSScreenStackView class]] && + ((RNSScreenStackView *)(screen.reactSuperview)).customAnimation) { + [self animateWithNoAnimation:transitionContext toVC:toViewController fromVC:fromViewController]; + } else if (screen.fullScreenSwipeEnabled && transitionContext.isInteractive) { + // we are swiping with full width gesture + if (screen.customAnimationOnSwipe) { + [self animateTransitionWithStackAnimation:screen.stackAnimation + shadowEnabled:screen.fullScreenSwipeShadowEnabled + transitionContext:transitionContext + toVC:toViewController + fromVC:fromViewController]; + } else { + // we have to provide an animation when swiping, otherwise the screen will be popped immediately, + // so in case of no custom animation on swipe set, we provide the one closest to the default + [self animateSimplePushWithShadowEnabled:screen.fullScreenSwipeShadowEnabled + transitionContext:transitionContext + toVC:toViewController + fromVC:fromViewController]; + } + } else { + // we are going forward or provided custom animation on swipe or clicked native header back button + [self animateTransitionWithStackAnimation:screen.stackAnimation + shadowEnabled:screen.fullScreenSwipeShadowEnabled + transitionContext:transitionContext + toVC:toViewController + fromVC:fromViewController]; + } + } +} + +- (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled + transitionContext:(id)transitionContext + toVC:(UIViewController *)toViewController + fromVC:(UIViewController *)fromViewController +{ + float containerWidth = transitionContext.containerView.bounds.size.width; + float belowViewWidth = containerWidth * 0.3; + + CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0); + CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0); + + if (toViewController.navigationController.view.semanticContentAttribute == + UISemanticContentAttributeForceRightToLeft) { + rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0); + leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0); + } + + UIView *shadowView; + if (shadowEnabled) { + shadowView = [[UIView alloc] initWithFrame:fromViewController.view.frame]; + shadowView.backgroundColor = [UIColor blackColor]; + } + + if (_operation == UINavigationControllerOperationPush) { + toViewController.view.transform = rightTransform; + [[transitionContext containerView] addSubview:toViewController.view]; + if (shadowView) { + [[transitionContext containerView] insertSubview:shadowView belowSubview:toViewController.view]; + shadowView.alpha = 0.0; + } + + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:^{ + fromViewController.view.transform = leftTransform; + toViewController.view.transform = CGAffineTransformIdentity; + if (shadowView) { + shadowView.alpha = RNSShadowViewMaxAlpha; + } + } + completion:^(BOOL finished) { + if (shadowView) { + [shadowView removeFromSuperview]; + } + fromViewController.view.transform = CGAffineTransformIdentity; + toViewController.view.transform = CGAffineTransformIdentity; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + } else if (_operation == UINavigationControllerOperationPop) { + toViewController.view.transform = leftTransform; + [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; + if (shadowView) { + [[transitionContext containerView] insertSubview:shadowView belowSubview:fromViewController.view]; + shadowView.alpha = RNSShadowViewMaxAlpha; + } + + void (^animationBlock)(void) = ^{ + toViewController.view.transform = CGAffineTransformIdentity; + fromViewController.view.transform = rightTransform; + if (shadowView) { + shadowView.alpha = 0.0; + } + }; + void (^completionBlock)(BOOL) = ^(BOOL finished) { + if (shadowView) { + [shadowView removeFromSuperview]; + } + fromViewController.view.transform = CGAffineTransformIdentity; + toViewController.view.transform = CGAffineTransformIdentity; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }; + + if (!transitionContext.isInteractive) { + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:animationBlock + completion:completionBlock]; + } else { + // we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option + [UIView animateWithDuration:[self transitionDuration:transitionContext] + delay:0.0 + options:UIViewAnimationOptionCurveLinear + animations:animationBlock + completion:completionBlock]; + } + } +} + +- (void)animateSlideFromLeftWithTransitionContext:(id)transitionContext + toVC:(UIViewController *)toViewController + fromVC:(UIViewController *)fromViewController +{ + float containerWidth = transitionContext.containerView.bounds.size.width; + float belowViewWidth = containerWidth * 0.3; + + CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0); + CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0); + + if (toViewController.navigationController.view.semanticContentAttribute == + UISemanticContentAttributeForceRightToLeft) { + rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0); + leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0); + } + + if (_operation == UINavigationControllerOperationPush) { + toViewController.view.transform = rightTransform; + [[transitionContext containerView] addSubview:toViewController.view]; + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:^{ + fromViewController.view.transform = leftTransform; + toViewController.view.transform = CGAffineTransformIdentity; + } + completion:^(BOOL finished) { + fromViewController.view.transform = CGAffineTransformIdentity; + toViewController.view.transform = CGAffineTransformIdentity; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + } else if (_operation == UINavigationControllerOperationPop) { + toViewController.view.transform = leftTransform; + [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; + + void (^animationBlock)(void) = ^{ + toViewController.view.transform = CGAffineTransformIdentity; + fromViewController.view.transform = rightTransform; + }; + void (^completionBlock)(BOOL) = ^(BOOL finished) { + fromViewController.view.transform = CGAffineTransformIdentity; + toViewController.view.transform = CGAffineTransformIdentity; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }; + + if (!transitionContext.isInteractive) { + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:animationBlock + completion:completionBlock]; + } else { + // we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option + [UIView animateWithDuration:[self transitionDuration:transitionContext] + delay:0.0 + options:UIViewAnimationOptionCurveLinear + animations:animationBlock + completion:completionBlock]; + } + } +} + +- (void)animateFadeWithTransitionContext:(id)transitionContext + toVC:(UIViewController *)toViewController + fromVC:(UIViewController *)fromViewController +{ + toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController]; + + if (_operation == UINavigationControllerOperationPush) { + [[transitionContext containerView] addSubview:toViewController.view]; + toViewController.view.alpha = 0.0; + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:^{ + toViewController.view.alpha = 1.0; + } + completion:^(BOOL finished) { + toViewController.view.alpha = 1.0; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + } else if (_operation == UINavigationControllerOperationPop) { + [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:^{ + fromViewController.view.alpha = 0.0; + } + completion:^(BOOL finished) { + fromViewController.view.alpha = 1.0; + + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + } +} + +- (void)animateSlideFromBottomWithTransitionContext:(id)transitionContext + toVC:(UIViewController *)toViewController + fromVC:(UIViewController *)fromViewController +{ + CGAffineTransform topBottomTransform = + CGAffineTransformMakeTranslation(0, transitionContext.containerView.bounds.size.height); + + if (_operation == UINavigationControllerOperationPush) { + toViewController.view.transform = topBottomTransform; + [[transitionContext containerView] addSubview:toViewController.view]; + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:^{ + fromViewController.view.transform = CGAffineTransformIdentity; + toViewController.view.transform = CGAffineTransformIdentity; + } + completion:^(BOOL finished) { + fromViewController.view.transform = CGAffineTransformIdentity; + toViewController.view.transform = CGAffineTransformIdentity; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + } else if (_operation == UINavigationControllerOperationPop) { + toViewController.view.transform = CGAffineTransformIdentity; + [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; + + void (^animationBlock)(void) = ^{ + toViewController.view.transform = CGAffineTransformIdentity; + fromViewController.view.transform = topBottomTransform; + }; + void (^completionBlock)(BOOL) = ^(BOOL finished) { + fromViewController.view.transform = CGAffineTransformIdentity; + toViewController.view.transform = CGAffineTransformIdentity; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }; + + if (!transitionContext.isInteractive) { + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:animationBlock + completion:completionBlock]; + } else { + // we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option + [UIView animateWithDuration:[self transitionDuration:transitionContext] + delay:0.0 + options:UIViewAnimationOptionCurveLinear + animations:animationBlock + completion:completionBlock]; + } + } +} + +- (void)animateFadeFromBottomWithTransitionContext:(id)transitionContext + toVC:(UIViewController *)toViewController + fromVC:(UIViewController *)fromViewController +{ + CGAffineTransform topBottomTransform = + CGAffineTransformMakeTranslation(0, 0.08 * transitionContext.containerView.bounds.size.height); + + const float transitionDuration = [self transitionDuration:transitionContext]; + + if (_operation == UINavigationControllerOperationPush) { + toViewController.view.transform = topBottomTransform; + toViewController.view.alpha = 0.0; + [[transitionContext containerView] addSubview:toViewController.view]; + + // Android Nougat open animation + // http://aosp.opersys.com/xref/android-7.1.2_r37/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml + [UIView animateWithDuration:transitionDuration * RNSSlideOpenTransitionDurationProportion // defaults to 0.35 s + delay:0 + options:UIViewAnimationOptionCurveEaseOut + animations:^{ + fromViewController.view.transform = CGAffineTransformIdentity; + toViewController.view.transform = CGAffineTransformIdentity; + } + completion:^(BOOL finished) { + fromViewController.view.transform = CGAffineTransformIdentity; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + [UIView animateWithDuration:transitionDuration * RNSFadeOpenTransitionDurationProportion // defaults to 0.2 s + delay:0 + options:UIViewAnimationOptionCurveEaseOut + animations:^{ + toViewController.view.alpha = 1.0; + } + completion:nil]; + + } else if (_operation == UINavigationControllerOperationPop) { + toViewController.view.transform = CGAffineTransformIdentity; + [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; + + // Android Nougat exit animation + // http://aosp.opersys.com/xref/android-7.1.2_r37/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml + [UIView animateWithDuration:transitionDuration * RNSSlideCloseTransitionDurationProportion // defaults to 0.25 s + delay:0 + options:UIViewAnimationOptionCurveEaseIn + animations:^{ + toViewController.view.transform = CGAffineTransformIdentity; + fromViewController.view.transform = topBottomTransform; + } + completion:^(BOOL finished) { + fromViewController.view.transform = CGAffineTransformIdentity; + toViewController.view.transform = CGAffineTransformIdentity; + fromViewController.view.alpha = 1.0; + toViewController.view.alpha = 1.0; + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + [UIView animateWithDuration:transitionDuration * RNSFadeCloseTransitionDurationProportion // defaults to 0.15 s + delay:transitionDuration * RNSFadeCloseDelayTransitionDurationProportion // defaults to 0.1 s + options:UIViewAnimationOptionCurveLinear + animations:^{ + fromViewController.view.alpha = 0.0; + } + completion:nil]; + } +} + +- (void)animateWithNoAnimation:(id)transitionContext + toVC:(UIViewController *)toViewController + fromVC:(UIViewController *)fromViewController +{ + if (_operation == UINavigationControllerOperationPush) { + [[transitionContext containerView] addSubview:toViewController.view]; + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:^{ + } + completion:^(BOOL finished) { + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + } else if (_operation == UINavigationControllerOperationPop) { + [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] + animations:^{ + } + completion:^(BOOL finished) { + [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; + }]; + } +} + ++ (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation +{ + return (animation != RNSScreenStackAnimationFlip && animation != RNSScreenStackAnimationDefault); +} + +- (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation + shadowEnabled:(BOOL)shadowEnabled + transitionContext:(id)transitionContext + toVC:(UIViewController *)toVC + fromVC:(UIViewController *)fromVC +{ + if (animation == RNSScreenStackAnimationSimplePush) { + [self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC]; + return; + } else if (animation == RNSScreenStackAnimationSlideFromLeft) { + [self animateSlideFromLeftWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; + return; + } else if (animation == RNSScreenStackAnimationFade || animation == RNSScreenStackAnimationNone) { + [self animateFadeWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; + return; + } else if (animation == RNSScreenStackAnimationSlideFromBottom) { + [self animateSlideFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; + return; + } else if (animation == RNSScreenStackAnimationFadeFromBottom) { + [self animateFadeFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; + return; + } + // simple_push is the default custom animation + [self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC]; +} + +@end From 93e1bef597c2bdc4a91016b1c7dfa5c87f728a8c Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Fri, 13 Dec 2024 12:26:36 +0100 Subject: [PATCH 3/6] align animation duration --- ios/RNSScreenStackAnimator.h | 3 --- ios/RNSScreenStackAnimator.mm | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ios/RNSScreenStackAnimator.h b/ios/RNSScreenStackAnimator.h index f83a6cd46..16f41d896 100644 --- a/ios/RNSScreenStackAnimator.h +++ b/ios/RNSScreenStackAnimator.h @@ -27,9 +27,6 @@ constexpr bool kUsesNewAnimatorImpl = false; @interface RNSScreenStackAnimatorLegacy : NSObject -/// This property is filled whenever there is an ongoing animation and cleared on animation end. -@property (nonatomic, strong, nullable, readonly) UIViewPropertyAnimator *inFlightAnimator; - - (nonnull instancetype)initWithOperation:(UINavigationControllerOperation)operation; /// In case of interactive / interruptible transition (e.g. swipe back gesture) this method should return diff --git a/ios/RNSScreenStackAnimator.mm b/ios/RNSScreenStackAnimator.mm index 9c4931fdd..4f0cb29bd 100644 --- a/ios/RNSScreenStackAnimator.mm +++ b/ios/RNSScreenStackAnimator.mm @@ -596,7 +596,7 @@ - (instancetype)initWithOperation:(UINavigationControllerOperation)operation { if (self = [super init]) { _operation = operation; - _transitionDuration = 0.35; // default duration in seconds + _transitionDuration = RNSDefaultTransitionDuration; // default duration in seconds } return self; } From 692376908a3f89dbfc1e341dc380d152c5bc8447 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Mon, 16 Dec 2024 13:03:30 +0100 Subject: [PATCH 4/6] Remove legacy impl, use only the new --- ios/RNSScreenStack.mm | 6 +- ios/RNSScreenStackAnimator.h | 4 - ios/RNSScreenStackAnimator.mm | 448 +--------------------------------- 3 files changed, 8 insertions(+), 450 deletions(-) diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm index 3c5addbef..bfeeb002c 100644 --- a/ios/RNSScreenStack.mm +++ b/ios/RNSScreenStack.mm @@ -750,11 +750,7 @@ - (void)dismissOnReload // otherwise the screen will be just popped immediately due to no animation ((operation == UINavigationControllerOperationPop && shouldCancelDismiss) || _isFullWidthSwiping || [RNSScreenStackAnimator isCustomAnimation:screen.stackAnimation] || _customAnimation)) { - if (rns::kUsesNewAnimatorImpl) { - return [[RNSScreenStackAnimator alloc] initWithOperation:operation]; - } else { - return [[RNSScreenStackAnimatorLegacy alloc] initWithOperation:operation]; - } + return [[RNSScreenStackAnimator alloc] initWithOperation:operation]; } return nil; } diff --git a/ios/RNSScreenStackAnimator.h b/ios/RNSScreenStackAnimator.h index 16f41d896..a9b0e6b9f 100644 --- a/ios/RNSScreenStackAnimator.h +++ b/ios/RNSScreenStackAnimator.h @@ -2,10 +2,6 @@ #import "RNSScreen.h" -namespace rns { -constexpr bool kUsesNewAnimatorImpl = false; -} - @interface RNSScreenStackAnimator : NSObject /// This property is filled whenever there is an ongoing animation and cleared on animation end. diff --git a/ios/RNSScreenStackAnimator.mm b/ios/RNSScreenStackAnimator.mm index 4f0cb29bd..89caf8d00 100644 --- a/ios/RNSScreenStackAnimator.mm +++ b/ios/RNSScreenStackAnimator.mm @@ -70,12 +70,6 @@ - (NSTimeInterval)transitionDuration:(id)t return _transitionDuration; } -//- (id)interruptibleAnimatorForTransition: -// (id)transitionContext -//{ -// return _inFlightAnimator; -//} - - (void)animateTransition:(id)transitionContext { UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; @@ -493,9 +487,12 @@ - (void)animateNoneWithTransitionContext:(id)transitionContext -{ - RNSScreenView *screen; - if (_operation == UINavigationControllerOperationPush) { - UIViewController *toViewController = - [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; - screen = ((RNSScreen *)toViewController).screenView; - } else if (_operation == UINavigationControllerOperationPop) { - UIViewController *fromViewController = - [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; - screen = ((RNSScreen *)fromViewController).screenView; - } - - if (screen != nil && screen.stackAnimation == RNSScreenStackAnimationNone) { - return 0; - } - - if (screen != nil && screen.transitionDuration != nil && [screen.transitionDuration floatValue] >= 0) { - float durationInSeconds = [screen.transitionDuration floatValue] / 1000.0; - return durationInSeconds; - } - - return _transitionDuration; -} - -- (void)animateTransition:(id)transitionContext -{ - UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; - UIViewController *fromViewController = - [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; - toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController]; - - RNSScreenView *screen; - if (_operation == UINavigationControllerOperationPush) { - screen = ((RNSScreen *)toViewController).screenView; - } else if (_operation == UINavigationControllerOperationPop) { - screen = ((RNSScreen *)fromViewController).screenView; - } - - if (screen != nil) { - if ([screen.reactSuperview isKindOfClass:[RNSScreenStackView class]] && - ((RNSScreenStackView *)(screen.reactSuperview)).customAnimation) { - [self animateWithNoAnimation:transitionContext toVC:toViewController fromVC:fromViewController]; - } else if (screen.fullScreenSwipeEnabled && transitionContext.isInteractive) { - // we are swiping with full width gesture - if (screen.customAnimationOnSwipe) { - [self animateTransitionWithStackAnimation:screen.stackAnimation - shadowEnabled:screen.fullScreenSwipeShadowEnabled - transitionContext:transitionContext - toVC:toViewController - fromVC:fromViewController]; - } else { - // we have to provide an animation when swiping, otherwise the screen will be popped immediately, - // so in case of no custom animation on swipe set, we provide the one closest to the default - [self animateSimplePushWithShadowEnabled:screen.fullScreenSwipeShadowEnabled - transitionContext:transitionContext - toVC:toViewController - fromVC:fromViewController]; - } - } else { - // we are going forward or provided custom animation on swipe or clicked native header back button - [self animateTransitionWithStackAnimation:screen.stackAnimation - shadowEnabled:screen.fullScreenSwipeShadowEnabled - transitionContext:transitionContext - toVC:toViewController - fromVC:fromViewController]; - } - } -} - -- (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled - transitionContext:(id)transitionContext - toVC:(UIViewController *)toViewController - fromVC:(UIViewController *)fromViewController -{ - float containerWidth = transitionContext.containerView.bounds.size.width; - float belowViewWidth = containerWidth * 0.3; - - CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0); - CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0); - - if (toViewController.navigationController.view.semanticContentAttribute == - UISemanticContentAttributeForceRightToLeft) { - rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0); - leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0); - } - - UIView *shadowView; - if (shadowEnabled) { - shadowView = [[UIView alloc] initWithFrame:fromViewController.view.frame]; - shadowView.backgroundColor = [UIColor blackColor]; - } - - if (_operation == UINavigationControllerOperationPush) { - toViewController.view.transform = rightTransform; - [[transitionContext containerView] addSubview:toViewController.view]; - if (shadowView) { - [[transitionContext containerView] insertSubview:shadowView belowSubview:toViewController.view]; - shadowView.alpha = 0.0; - } - - [UIView animateWithDuration:[self transitionDuration:transitionContext] - animations:^{ - fromViewController.view.transform = leftTransform; - toViewController.view.transform = CGAffineTransformIdentity; - if (shadowView) { - shadowView.alpha = RNSShadowViewMaxAlpha; - } - } - completion:^(BOOL finished) { - if (shadowView) { - [shadowView removeFromSuperview]; - } - fromViewController.view.transform = CGAffineTransformIdentity; - toViewController.view.transform = CGAffineTransformIdentity; - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }]; - } else if (_operation == UINavigationControllerOperationPop) { - toViewController.view.transform = leftTransform; - [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; - if (shadowView) { - [[transitionContext containerView] insertSubview:shadowView belowSubview:fromViewController.view]; - shadowView.alpha = RNSShadowViewMaxAlpha; - } - - void (^animationBlock)(void) = ^{ - toViewController.view.transform = CGAffineTransformIdentity; - fromViewController.view.transform = rightTransform; - if (shadowView) { - shadowView.alpha = 0.0; - } - }; - void (^completionBlock)(BOOL) = ^(BOOL finished) { - if (shadowView) { - [shadowView removeFromSuperview]; - } - fromViewController.view.transform = CGAffineTransformIdentity; - toViewController.view.transform = CGAffineTransformIdentity; - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }; - - if (!transitionContext.isInteractive) { - [UIView animateWithDuration:[self transitionDuration:transitionContext] - animations:animationBlock - completion:completionBlock]; - } else { - // we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option - [UIView animateWithDuration:[self transitionDuration:transitionContext] - delay:0.0 - options:UIViewAnimationOptionCurveLinear - animations:animationBlock - completion:completionBlock]; - } - } -} - -- (void)animateSlideFromLeftWithTransitionContext:(id)transitionContext - toVC:(UIViewController *)toViewController - fromVC:(UIViewController *)fromViewController -{ - float containerWidth = transitionContext.containerView.bounds.size.width; - float belowViewWidth = containerWidth * 0.3; - - CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0); - CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0); - - if (toViewController.navigationController.view.semanticContentAttribute == - UISemanticContentAttributeForceRightToLeft) { - rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0); - leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0); - } - - if (_operation == UINavigationControllerOperationPush) { - toViewController.view.transform = rightTransform; - [[transitionContext containerView] addSubview:toViewController.view]; - [UIView animateWithDuration:[self transitionDuration:transitionContext] - animations:^{ - fromViewController.view.transform = leftTransform; - toViewController.view.transform = CGAffineTransformIdentity; - } - completion:^(BOOL finished) { - fromViewController.view.transform = CGAffineTransformIdentity; - toViewController.view.transform = CGAffineTransformIdentity; - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }]; - } else if (_operation == UINavigationControllerOperationPop) { - toViewController.view.transform = leftTransform; - [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; - - void (^animationBlock)(void) = ^{ - toViewController.view.transform = CGAffineTransformIdentity; - fromViewController.view.transform = rightTransform; - }; - void (^completionBlock)(BOOL) = ^(BOOL finished) { - fromViewController.view.transform = CGAffineTransformIdentity; - toViewController.view.transform = CGAffineTransformIdentity; - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }; - - if (!transitionContext.isInteractive) { - [UIView animateWithDuration:[self transitionDuration:transitionContext] - animations:animationBlock - completion:completionBlock]; - } else { - // we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option - [UIView animateWithDuration:[self transitionDuration:transitionContext] - delay:0.0 - options:UIViewAnimationOptionCurveLinear - animations:animationBlock - completion:completionBlock]; - } - } -} - -- (void)animateFadeWithTransitionContext:(id)transitionContext - toVC:(UIViewController *)toViewController - fromVC:(UIViewController *)fromViewController -{ - toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController]; - - if (_operation == UINavigationControllerOperationPush) { - [[transitionContext containerView] addSubview:toViewController.view]; - toViewController.view.alpha = 0.0; - [UIView animateWithDuration:[self transitionDuration:transitionContext] - animations:^{ - toViewController.view.alpha = 1.0; - } - completion:^(BOOL finished) { - toViewController.view.alpha = 1.0; - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }]; - } else if (_operation == UINavigationControllerOperationPop) { - [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; - - [UIView animateWithDuration:[self transitionDuration:transitionContext] - animations:^{ - fromViewController.view.alpha = 0.0; - } - completion:^(BOOL finished) { - fromViewController.view.alpha = 1.0; - - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }]; - } -} - -- (void)animateSlideFromBottomWithTransitionContext:(id)transitionContext - toVC:(UIViewController *)toViewController - fromVC:(UIViewController *)fromViewController -{ - CGAffineTransform topBottomTransform = - CGAffineTransformMakeTranslation(0, transitionContext.containerView.bounds.size.height); - - if (_operation == UINavigationControllerOperationPush) { - toViewController.view.transform = topBottomTransform; - [[transitionContext containerView] addSubview:toViewController.view]; - [UIView animateWithDuration:[self transitionDuration:transitionContext] - animations:^{ - fromViewController.view.transform = CGAffineTransformIdentity; - toViewController.view.transform = CGAffineTransformIdentity; - } - completion:^(BOOL finished) { - fromViewController.view.transform = CGAffineTransformIdentity; - toViewController.view.transform = CGAffineTransformIdentity; - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }]; - } else if (_operation == UINavigationControllerOperationPop) { - toViewController.view.transform = CGAffineTransformIdentity; - [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; - - void (^animationBlock)(void) = ^{ - toViewController.view.transform = CGAffineTransformIdentity; - fromViewController.view.transform = topBottomTransform; - }; - void (^completionBlock)(BOOL) = ^(BOOL finished) { - fromViewController.view.transform = CGAffineTransformIdentity; - toViewController.view.transform = CGAffineTransformIdentity; - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }; - - if (!transitionContext.isInteractive) { - [UIView animateWithDuration:[self transitionDuration:transitionContext] - animations:animationBlock - completion:completionBlock]; - } else { - // we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option - [UIView animateWithDuration:[self transitionDuration:transitionContext] - delay:0.0 - options:UIViewAnimationOptionCurveLinear - animations:animationBlock - completion:completionBlock]; - } - } -} - -- (void)animateFadeFromBottomWithTransitionContext:(id)transitionContext - toVC:(UIViewController *)toViewController - fromVC:(UIViewController *)fromViewController -{ - CGAffineTransform topBottomTransform = - CGAffineTransformMakeTranslation(0, 0.08 * transitionContext.containerView.bounds.size.height); - - const float transitionDuration = [self transitionDuration:transitionContext]; - - if (_operation == UINavigationControllerOperationPush) { - toViewController.view.transform = topBottomTransform; - toViewController.view.alpha = 0.0; - [[transitionContext containerView] addSubview:toViewController.view]; - - // Android Nougat open animation - // http://aosp.opersys.com/xref/android-7.1.2_r37/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml - [UIView animateWithDuration:transitionDuration * RNSSlideOpenTransitionDurationProportion // defaults to 0.35 s - delay:0 - options:UIViewAnimationOptionCurveEaseOut - animations:^{ - fromViewController.view.transform = CGAffineTransformIdentity; - toViewController.view.transform = CGAffineTransformIdentity; - } - completion:^(BOOL finished) { - fromViewController.view.transform = CGAffineTransformIdentity; - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }]; - [UIView animateWithDuration:transitionDuration * RNSFadeOpenTransitionDurationProportion // defaults to 0.2 s - delay:0 - options:UIViewAnimationOptionCurveEaseOut - animations:^{ - toViewController.view.alpha = 1.0; - } - completion:nil]; - - } else if (_operation == UINavigationControllerOperationPop) { - toViewController.view.transform = CGAffineTransformIdentity; - [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; - - // Android Nougat exit animation - // http://aosp.opersys.com/xref/android-7.1.2_r37/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml - [UIView animateWithDuration:transitionDuration * RNSSlideCloseTransitionDurationProportion // defaults to 0.25 s - delay:0 - options:UIViewAnimationOptionCurveEaseIn - animations:^{ - toViewController.view.transform = CGAffineTransformIdentity; - fromViewController.view.transform = topBottomTransform; - } - completion:^(BOOL finished) { - fromViewController.view.transform = CGAffineTransformIdentity; - toViewController.view.transform = CGAffineTransformIdentity; - fromViewController.view.alpha = 1.0; - toViewController.view.alpha = 1.0; - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }]; - [UIView animateWithDuration:transitionDuration * RNSFadeCloseTransitionDurationProportion // defaults to 0.15 s - delay:transitionDuration * RNSFadeCloseDelayTransitionDurationProportion // defaults to 0.1 s - options:UIViewAnimationOptionCurveLinear - animations:^{ - fromViewController.view.alpha = 0.0; - } - completion:nil]; - } -} - -- (void)animateWithNoAnimation:(id)transitionContext - toVC:(UIViewController *)toViewController - fromVC:(UIViewController *)fromViewController -{ - if (_operation == UINavigationControllerOperationPush) { - [[transitionContext containerView] addSubview:toViewController.view]; - [UIView animateWithDuration:[self transitionDuration:transitionContext] - animations:^{ - } - completion:^(BOOL finished) { - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }]; - } else if (_operation == UINavigationControllerOperationPop) { - [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; - - [UIView animateWithDuration:[self transitionDuration:transitionContext] - animations:^{ - } - completion:^(BOOL finished) { - [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; - }]; - } -} - -+ (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation -{ - return (animation != RNSScreenStackAnimationFlip && animation != RNSScreenStackAnimationDefault); -} - -- (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation - shadowEnabled:(BOOL)shadowEnabled - transitionContext:(id)transitionContext - toVC:(UIViewController *)toVC - fromVC:(UIViewController *)fromVC -{ - if (animation == RNSScreenStackAnimationSimplePush) { - [self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC]; - return; - } else if (animation == RNSScreenStackAnimationSlideFromLeft) { - [self animateSlideFromLeftWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; - return; - } else if (animation == RNSScreenStackAnimationFade || animation == RNSScreenStackAnimationNone) { - [self animateFadeWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; - return; - } else if (animation == RNSScreenStackAnimationSlideFromBottom) { - [self animateSlideFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; - return; - } else if (animation == RNSScreenStackAnimationFadeFromBottom) { - [self animateFadeFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC]; - return; - } - // simple_push is the default custom animation - [self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC]; -} - -@end From 9d206a590d4b95424936ac023f661aea3a278428 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Mon, 16 Dec 2024 13:03:56 +0100 Subject: [PATCH 5/6] Add headerright to test case --- apps/src/tests/TestAnimation.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/src/tests/TestAnimation.tsx b/apps/src/tests/TestAnimation.tsx index e4058c5df..21595ae3c 100644 --- a/apps/src/tests/TestAnimation.tsx +++ b/apps/src/tests/TestAnimation.tsx @@ -77,13 +77,18 @@ function Fifth({ navigation }: RoutePropBase<'Fifth'>): React.ReactNode { ); } +function HeaderRight() { + return ( + + ); +} + export default function App() { return ( Date: Mon, 16 Dec 2024 13:24:32 +0100 Subject: [PATCH 6/6] Remove declaration of legacy animator --- ios/RNSScreenStackAnimator.h | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/ios/RNSScreenStackAnimator.h b/ios/RNSScreenStackAnimator.h index a9b0e6b9f..0870f8d1e 100644 --- a/ios/RNSScreenStackAnimator.h +++ b/ios/RNSScreenStackAnimator.h @@ -20,19 +20,3 @@ + (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation; @end - -@interface RNSScreenStackAnimatorLegacy : NSObject - -- (nonnull instancetype)initWithOperation:(UINavigationControllerOperation)operation; - -/// In case of interactive / interruptible transition (e.g. swipe back gesture) this method should return -/// timing parameters expected by animator to be used for animation completion (e.g. when user's -/// gesture had ended). -/// -/// @return timing curve provider expected to be used for animation completion or nil, -/// when there is no interactive transition running. -- (nullable id)timingParamsForAnimationCompletion; - -+ (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation; - -@end