diff --git a/ios/RNSPercentDrivenInteractiveTransition.mm b/ios/RNSPercentDrivenInteractiveTransition.mm index 459aa289d0..876d2dd726 100644 --- a/ios/RNSPercentDrivenInteractiveTransition.mm +++ b/ios/RNSPercentDrivenInteractiveTransition.mm @@ -49,14 +49,15 @@ - (void)finalizeInteractiveTransitionWithAnimationWasCancelled:(BOOL)cancelled return; } - UIViewPropertyAnimator *_Nullable animator = _animationController.inFlightAnimator; + id _Nullable animator = _animationController.inFlightAnimator; if (animator == nil) { return; } BOOL shouldReverseAnimation = cancelled; - id timingParams = [_animationController timingParamsForAnimationCompletion]; + // Nil params mean that the transition should be completed using originally set timing params. + id _Nullable timingParams = [_animationController timingParamsForAnimationCompletion]; [animator pauseAnimation]; [animator setReversed:shouldReverseAnimation]; diff --git a/ios/RNSScreenStackAnimator.h b/ios/RNSScreenStackAnimator.h index d9c4c67ec9..dbd764a66e 100644 --- a/ios/RNSScreenStackAnimator.h +++ b/ios/RNSScreenStackAnimator.h @@ -3,7 +3,7 @@ @interface RNSScreenStackAnimator : NSObject /// This property is filled whenever there is an ongoing animation and cleared on animation end. -@property (nonatomic, strong, nullable, readonly) UIViewPropertyAnimator *inFlightAnimator; +@property (nonatomic, strong, nullable, readonly) id inFlightAnimator; - (nonnull instancetype)initWithOperation:(UINavigationControllerOperation)operation; diff --git a/ios/RNSScreenStackAnimator.mm b/ios/RNSScreenStackAnimator.mm index 410a461285..ac38b9fe78 100644 --- a/ios/RNSScreenStackAnimator.mm +++ b/ios/RNSScreenStackAnimator.mm @@ -1,5 +1,6 @@ #import "RNSScreenStackAnimator.h" #import "RNSScreenStack.h" +#import "RNSViewPropertyAnimatorCompositor.h" #import "RNSScreen.h" @@ -28,9 +29,10 @@ static constexpr float RNSShadowViewMaxAlpha = 0.1; @implementation RNSScreenStackAnimator { + @private UINavigationControllerOperation _operation; NSTimeInterval _transitionDuration; - UIViewPropertyAnimator *_Nullable _inFlightAnimator; + RNSViewPropertyAnimatorCompositor *_Nullable _animatorCompositor; } - (instancetype)initWithOperation:(UINavigationControllerOperation)operation @@ -38,7 +40,7 @@ - (instancetype)initWithOperation:(UINavigationControllerOperation)operation if (self = [super init]) { _operation = operation; _transitionDuration = RNSDefaultTransitionDuration; // default duration in seconds - _inFlightAnimator = nil; + _animatorCompositor = nil; } return self; } @@ -70,12 +72,6 @@ - (NSTimeInterval)transitionDuration:(id)t return _transitionDuration; } -- (id)interruptibleAnimatorForTransition: - (id)transitionContext -{ - return _inFlightAnimator; -} - - (void)animateTransition:(id)transitionContext { UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; @@ -121,9 +117,15 @@ - (void)animateTransition:(id)transitionCo } } +- (id)interruptibleAnimatorForTransition: + (id)transitionContext +{ + return [_animatorCompositor animatorForImplicitAnimations]; +} + - (void)animationEnded:(BOOL)transitionCompleted { - _inFlightAnimator = nil; + _animatorCompositor = nil; } #pragma mark - Animation implementations @@ -179,8 +181,9 @@ - (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled toViewController.view.transform = CGAffineTransformIdentity; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }]; - _inFlightAnimator = animator; - [animator startAnimation]; + + _animatorCompositor = [self animatorCompositorWithAnimators:@[ animator ]]; + [_animatorCompositor startAnimation]; } else if (_operation == UINavigationControllerOperationPop) { toViewController.view.transform = leftTransform; [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view]; @@ -206,25 +209,20 @@ - (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }; + UIViewPropertyAnimator *animator = + [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext] + timingParameters:[RNSScreenStackAnimator defaultSpringTimingParametersApprox]]; + + [animator addAnimations:animationBlock]; + [animator addCompletion:completionBlock]; + _animatorCompositor = [self animatorCompositorWithAnimators:@[ animator ]]; + if (!transitionContext.isInteractive) { - UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc] - initWithDuration:[self transitionDuration:transitionContext] - timingParameters:[RNSScreenStackAnimator defaultSpringTimingParametersApprox]]; - - [animator addAnimations:animationBlock]; - [animator addCompletion:completionBlock]; - _inFlightAnimator = animator; - [animator startAnimation]; + [_animatorCompositor startAnimation]; } else { // we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option - UIViewPropertyAnimator *animator = - [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext] - curve:UIViewAnimationCurveLinear - animations:animationBlock]; - - [animator addCompletion:completionBlock]; + [animator setScrubsLinearly:YES]; [animator setUserInteractionEnabled:YES]; - _inFlightAnimator = animator; } } } @@ -262,8 +260,8 @@ - (void)animateSlideFromLeftWithTransitionContext:(id)transit #pragma mark - Public API +- (nullable id)inFlightAnimator +{ + return _animatorCompositor; +} + - (nullable id)timingParamsForAnimationCompletion { - return [RNSScreenStackAnimator defaultSpringTimingParametersApprox]; + // Returning null causes animation to complete with initial timing params. + // TODO: maybe use this to expose possibility of customizing completion curve. + // return [RNSScreenStackAnimator defaultSpringTimingParametersApprox]; + return nil; } + (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation @@ -529,6 +538,28 @@ - (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation [self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC]; } +- (nonnull UIViewPropertyAnimator *)defaultHeaderAnimatorWithDuration:(NSTimeInterval)duration +{ + return [[UIViewPropertyAnimator alloc] initWithDuration:duration + timingParameters:[RNSScreenStackAnimator defaultTimingCurveProviderForHeader]]; +} + +- (RNSViewPropertyAnimatorCompositor *)animatorCompositorWithAnimators:(NSArray *)animators +{ + assert(animators.count > 0); + + NSTimeInterval maxDuration = 0; + for (UIViewPropertyAnimator *animator in animators) { + if (animator.duration > maxDuration) { + maxDuration = animator.duration; + } + } + + return [[RNSViewPropertyAnimatorCompositor alloc] + initWithAnimators:animators + implicitAnimator:[self defaultHeaderAnimatorWithDuration:maxDuration]]; +} + + (UISpringTimingParameters *)defaultSpringTimingParametersApprox { // Default curve provider is as defined below, however spring timing defined this way @@ -545,4 +576,9 @@ + (UISpringTimingParameters *)defaultSpringTimingParametersApprox return [[UISpringTimingParameters alloc] initWithDampingRatio:4.56]; } ++ (id)defaultTimingCurveProviderForHeader +{ + return [[UICubicTimingParameters alloc] initWithAnimationCurve:UIViewAnimationCurveEaseInOut]; +} + @end diff --git a/ios/RNSViewPropertyAnimatorCompositor.h b/ios/RNSViewPropertyAnimatorCompositor.h new file mode 100644 index 0000000000..79ca7ded10 --- /dev/null +++ b/ios/RNSViewPropertyAnimatorCompositor.h @@ -0,0 +1,30 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Retains collection of animators and itself implements `UIViewImplicitlyAnimating`. Object of this class +/// fans out all call to methods of `UIViewAnimating` and `continueAnimationWithTimingParameters:durationFactor` to all +/// the animators. The remaining methods of `UIViewImplicitlyAnimating`, are forwareded to the +/// `animatorForImplicitAnimations`. +/// +/// This allows to pass instance of this class as the interruptible animator and have the `animators` be interruptible +/// with timing curve of the implicit animator. This is useful for navigation item animation. +/// We also get possibility to drive all the animators simultaneously with gesture (interactive animation). +@interface RNSViewPropertyAnimatorCompositor : NSObject + +@property (nonnull, strong, nonatomic, readonly) NSArray> *animators; +@property (nullable, strong, nonatomic) id implicitAnimator; + +/// @param animators - nonnull, nonempty animator list +/// @param implicitAnimator - designated aniamator to return from `- animatorForImplicitAnimations` +/// @return nonnull instance only in case above invariants are not violated +- (nullable instancetype)initWithAnimators:(nonnull NSArray> *)animators + implicitAnimator:(nullable id)implicitAnimator + NS_DESIGNATED_INITIALIZER; + +- (nonnull id)animatorForImplicitAnimations; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/RNSViewPropertyAnimatorCompositor.mm b/ios/RNSViewPropertyAnimatorCompositor.mm new file mode 100644 index 0000000000..fefcf82f8b --- /dev/null +++ b/ios/RNSViewPropertyAnimatorCompositor.mm @@ -0,0 +1,136 @@ +#import "RNSViewPropertyAnimatorCompositor.h" +#import + +@implementation RNSViewPropertyAnimatorCompositor + +RCT_NOT_IMPLEMENTED(-(instancetype)init) + +- (instancetype)initWithAnimators:(nonnull NSArray> *)animators + implicitAnimator:(nullable id)implicitAnimator +{ + if (animators == nil || animators.count == 0) { + assert((false) && "[RNScreens] Expected non-empty animator list"); + return nil; + } + + if (self = [super init]) { + _animators = animators ?: @[]; + _implicitAnimator = implicitAnimator; + } + return self; +} + +- (id)animatorForImplicitAnimations +{ + if (_implicitAnimator != nil) { + return _implicitAnimator; + } + assert(_animators.count > 0 && "[RNScreens] Animator list must not be empty"); + return _animators.firstObject; +} + +#pragma mark - UIViewImplicityAnimating optional methods + +- (void)addAnimations:(void (^)())animation +{ + [self.animatorForImplicitAnimations addAnimations:animation]; +} + +- (void)addAnimations:(void (^)())animation delayFactor:(CGFloat)delayFactor +{ + [self.animatorForImplicitAnimations addAnimations:animation delayFactor:delayFactor]; +} + +- (void)addCompletion:(void (^)(UIViewAnimatingPosition))completion +{ + [self.animatorForImplicitAnimations addCompletion:completion]; +} + +- (void)continueAnimationWithTimingParameters:(id)parameters + durationFactor:(CGFloat)durationFactor +{ + for (id animator in _animators) { + [animator continueAnimationWithTimingParameters:parameters durationFactor:durationFactor]; + } + [self.implicitAnimator continueAnimationWithTimingParameters:parameters durationFactor:durationFactor]; +} + +#pragma mark - UIViewAnimating + +- (UIViewAnimatingState)state +{ + return self.animatorForImplicitAnimations.state; +} + +- (BOOL)isRunning +{ + return self.animatorForImplicitAnimations.isRunning; +} + +- (BOOL)isReversed +{ + return self.animatorForImplicitAnimations.isReversed; +} + +- (void)setReversed:(BOOL)reversed +{ + for (id animator in _animators) { + animator.reversed = reversed; + } + [self.implicitAnimator setReversed:reversed]; +} + +- (void)setFractionComplete:(CGFloat)fractionComplete +{ + for (id animator in _animators) { + animator.fractionComplete = fractionComplete; + } + [self.implicitAnimator setFractionComplete:fractionComplete]; +} + +- (CGFloat)fractionComplete +{ + return self.animatorForImplicitAnimations.fractionComplete; +} + +- (void)startAnimation +{ + for (id animator in _animators) { + [animator startAnimation]; + } + [self.implicitAnimator startAnimation]; +} + +- (void)startAnimationAfterDelay:(NSTimeInterval)delay +{ + for (id animator in _animators) { + [animator startAnimationAfterDelay:delay]; + } + [self.implicitAnimator startAnimationAfterDelay:delay]; +} + +- (void)pauseAnimation +{ + for (id animator in _animators) { + [animator pauseAnimation]; + } + [self.implicitAnimator pauseAnimation]; +} + +- (void)stopAnimation:(BOOL)withoutFinishing +{ + for (id animator in _animators) { + [animator stopAnimation:withoutFinishing]; + } + [self.implicitAnimator stopAnimation:withoutFinishing]; +} + +- (void)finishAnimationAtPosition:(UIViewAnimatingPosition)finalPosition +{ + for (id animator in _animators) { + [animator finishAnimationAtPosition:finalPosition]; + } + [self.implicitAnimator finishAnimationAtPosition:finalPosition]; +} + +@end