Skip to content

Commit

Permalink
EaseInOut transition for header
Browse files Browse the repository at this point in the history
  • Loading branch information
kkafar committed Nov 12, 2024
1 parent b38c590 commit 7846bdd
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 70 deletions.
5 changes: 3 additions & 2 deletions ios/RNSPercentDrivenInteractiveTransition.mm
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@ - (void)finalizeInteractiveTransitionWithAnimationWasCancelled:(BOOL)cancelled
return;
}

UIViewPropertyAnimator *_Nullable animator = _animationController.inFlightAnimator;
id<UIViewImplicitlyAnimating> _Nullable animator = _animationController.inFlightAnimator;
if (animator == nil) {
return;
}

BOOL shouldReverseAnimation = cancelled;

id<UITimingCurveProvider> timingParams = [_animationController timingParamsForAnimationCompletion];
// Nil params mean that the transition should be completed using originally set timing params.
id<UITimingCurveProvider> _Nullable timingParams = [_animationController timingParamsForAnimationCompletion];

[animator pauseAnimation];
[animator setReversed:shouldReverseAnimation];
Expand Down
2 changes: 1 addition & 1 deletion ios/RNSScreenStackAnimator.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
@interface RNSScreenStackAnimator : NSObject <UIViewControllerAnimatedTransitioning>

/// 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<UIViewImplicitlyAnimating> inFlightAnimator;

- (nonnull instancetype)initWithOperation:(UINavigationControllerOperation)operation;

Expand Down
170 changes: 103 additions & 67 deletions ios/RNSScreenStackAnimator.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "RNSScreenStackAnimator.h"
#import "RNSScreenStack.h"
#import "RNSViewPropertyAnimatorCompositor.h"

#import "RNSScreen.h"

Expand Down Expand Up @@ -28,17 +29,18 @@
static constexpr float RNSShadowViewMaxAlpha = 0.1;

@implementation RNSScreenStackAnimator {
@private
UINavigationControllerOperation _operation;
NSTimeInterval _transitionDuration;
UIViewPropertyAnimator *_Nullable _inFlightAnimator;
RNSViewPropertyAnimatorCompositor *_Nullable _animatorCompositor;
}

- (instancetype)initWithOperation:(UINavigationControllerOperation)operation
{
if (self = [super init]) {
_operation = operation;
_transitionDuration = RNSDefaultTransitionDuration; // default duration in seconds
_inFlightAnimator = nil;
_animatorCompositor = nil;
}
return self;
}
Expand Down Expand Up @@ -70,12 +72,6 @@ - (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)t
return _transitionDuration;
}

- (id<UIViewImplicitlyAnimating>)interruptibleAnimatorForTransition:
(id<UIViewControllerContextTransitioning>)transitionContext
{
return _inFlightAnimator;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
Expand Down Expand Up @@ -121,9 +117,15 @@ - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionCo
}
}

- (id<UIViewImplicitlyAnimating>)interruptibleAnimatorForTransition:
(id<UIViewControllerContextTransitioning>)transitionContext
{
return [_animatorCompositor animatorForImplicitAnimations];
}

- (void)animationEnded:(BOOL)transitionCompleted
{
_inFlightAnimator = nil;
_animatorCompositor = nil;
}

#pragma mark - Animation implementations
Expand Down Expand Up @@ -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];
Expand All @@ -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;
}
}
}
Expand Down Expand Up @@ -262,8 +260,8 @@ - (void)animateSlideFromLeftWithTransitionContext:(id<UIViewControllerContextTra
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];
Expand All @@ -278,24 +276,20 @@ - (void)animateSlideFromLeftWithTransitionContext:(id<UIViewControllerContextTra
[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 setUserInteractionEnabled:YES];
_inFlightAnimator = animator;
[animator setScrubsLinearly:YES];
}
}
}
Expand All @@ -318,8 +312,8 @@ - (void)animateFadeWithTransitionContext:(id<UIViewControllerContextTransitionin
toViewController.view.alpha = 1.0;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
_animatorCompositor = [self animatorCompositorWithAnimators:@[ animator ]];
[_animatorCompositor startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
Expand All @@ -331,8 +325,13 @@ - (void)animateFadeWithTransitionContext:(id<UIViewControllerContextTransitionin
fromViewController.view.alpha = 1.0;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
_animatorCompositor = [self animatorCompositorWithAnimators:@[ animator ]];

if (!transitionContext.isInteractive) {
[_animatorCompositor startAnimation];
} else {
[animator setScrubsLinearly:YES];
}
}
}

Expand All @@ -359,8 +358,8 @@ - (void)animateSlideFromBottomWithTransitionContext:(id<UIViewControllerContextT
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 = CGAffineTransformIdentity;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
Expand All @@ -375,20 +374,16 @@ - (void)animateSlideFromBottomWithTransitionContext:(id<UIViewControllerContextT
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
};

auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveEaseInOut
animations:animationBlock];
[animator addCompletion:completionBlock];
_animatorCompositor = [self animatorCompositorWithAnimators:@[ animator ]];

if (!transitionContext.isInteractive) {
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveEaseInOut
animations: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
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveLinear
animations:animationBlock];
[animator addCompletion:completionBlock];
_inFlightAnimator = animator;
[animator setScrubsLinearly:YES];
}
}
}
Expand Down Expand Up @@ -428,9 +423,8 @@ - (void)animateFadeFromBottomWithTransitionContext:(id<UIViewControllerContextTr
toViewController.view.alpha = 1.0;
}];

_inFlightAnimator = slideAnimator;
[slideAnimator startAnimation];
[fadeAnimator startAnimation];
_animatorCompositor = [self animatorCompositorWithAnimators:@[ slideAnimator, fadeAnimator ]];
[_animatorCompositor startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = CGAffineTransformIdentity;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
Expand Down Expand Up @@ -459,9 +453,16 @@ - (void)animateFadeFromBottomWithTransitionContext:(id<UIViewControllerContextTr
fromViewController.view.alpha = 0.0;
}];

_inFlightAnimator = slideAnimator;
[slideAnimator startAnimation];
[fadeAnimator startAnimationAfterDelay:baseTransitionDuration * RNSFadeCloseDelayTransitionDurationProportion];
_animatorCompositor = [self animatorCompositorWithAnimators:@[ slideAnimator, fadeAnimator ]];

if (!transitionContext.isInteractive) {
[slideAnimator startAnimation];
[fadeAnimator startAnimationAfterDelay:baseTransitionDuration * RNSFadeCloseDelayTransitionDurationProportion];
[[_animatorCompositor animatorForImplicitAnimations] startAnimation];
} else {
slideAnimator.scrubsLinearly = YES;
fadeAnimator.scrubsLinearly = YES;
}
}
}

Expand Down Expand Up @@ -491,9 +492,17 @@ - (void)animateWithNoAnimation:(id<UIViewControllerContextTransitioning>)transit

#pragma mark - Public API

- (nullable id<UIViewImplicitlyAnimating>)inFlightAnimator
{
return _animatorCompositor;
}

- (nullable id<UITimingCurveProvider>)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
Expand Down Expand Up @@ -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<UIViewPropertyAnimator *> *)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
Expand All @@ -545,4 +576,9 @@ + (UISpringTimingParameters *)defaultSpringTimingParametersApprox
return [[UISpringTimingParameters alloc] initWithDampingRatio:4.56];
}

+ (id<UITimingCurveProvider>)defaultTimingCurveProviderForHeader
{
return [[UICubicTimingParameters alloc] initWithAnimationCurve:UIViewAnimationCurveEaseInOut];
}

@end
30 changes: 30 additions & 0 deletions ios/RNSViewPropertyAnimatorCompositor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#import <React/RCTAssert.h>
#import <UIKit/UIKit.h>

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 <UIViewImplicitlyAnimating>

@property (nonnull, strong, nonatomic, readonly) NSArray<id<UIViewImplicitlyAnimating>> *animators;
@property (nullable, strong, nonatomic) id<UIViewImplicitlyAnimating> 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<id<UIViewImplicitlyAnimating>> *)animators
implicitAnimator:(nullable id<UIViewImplicitlyAnimating>)implicitAnimator
NS_DESIGNATED_INITIALIZER;

- (nonnull id<UIViewImplicitlyAnimating>)animatorForImplicitAnimations;

@end

NS_ASSUME_NONNULL_END
Loading

0 comments on commit 7846bdd

Please sign in to comment.