Skip to content

Commit

Permalink
feat: refactor animations in ios
Browse files Browse the repository at this point in the history
  • Loading branch information
WadhahEssam committed Nov 4, 2023
1 parent 086bb78 commit fd5c6f6
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 136 deletions.
36 changes: 36 additions & 0 deletions AnimationHelper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Animation.h
// Pods
//
// Created by Wadah Esam on 04/11/2023.
//

#ifndef Animation_h
#define Animation_h

#import <UIKit/UIKit.h>
#import "ThemeSwitchAnimationModule.h"

@interface AnimationHelper : NSObject

+ (void)performFadeAnimation:(UIView *)overlayView
duration:(NSInteger)duration
callback:(void (^)(void))callback;

+ (void)performCircularAnimation:(UIView *)overlayView
source:(ThemeSwitchAnimationModule *)source
duration:(NSInteger)duration
cxRatio:(CGFloat)cxRatio
cyRatio:(CGFloat)cyRatio
callback:(void (^)(void))callback;

+ (void)performInvertedCircleAnimation:(UIView *)overlayView
source:(ThemeSwitchAnimationModule *)source
duration:(NSInteger)duration
cxRatio:(CGFloat)cxRatio
cyRatio:(CGFloat)cyRatio
callback:(void (^)(void))callback;

@end

#endif /* Animation_h */
153 changes: 153 additions & 0 deletions AnimationHelper.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// AnimationHelper.m
// react-native-theme-switch-animation
//
// Created by Wadah Esam on 04/11/2023.
//


#import "AnimationHelper.h"

@implementation AnimationHelper

+ (void)performFadeAnimation:(UIView *)overlayView
duration:(NSInteger)duration
callback:(void (^)(void))callback
{
[UIView animateWithDuration: duration / 1000.0
animations:^{
overlayView.alpha = 0.0;
}
completion:^(BOOL finished){
if (callback) {
callback();
}
[overlayView removeFromSuperview];
}];
}

+ (void)performInvertedCircleAnimation:(UIView *)overlayView
source:(ThemeSwitchAnimationModule *)source duration:(NSInteger)duration
cxRatio:(CGFloat)cxRatio
cyRatio:(CGFloat)cyRatio
callback:(void (^__strong)())callback {

CGFloat width = CGRectGetWidth(overlayView.bounds);
CGFloat height = CGRectGetHeight(overlayView.bounds);
CGPoint center = CGPointMake(width * cxRatio, height * cyRatio);
CGFloat startRadius = [self getPointMaxDistanceInsideContainerWithCx:center.x cy:center.y width:width height:height];


UIBezierPath *startPath = [self generateCircule:startRadius center:center];
UIBezierPath *endPath = [self generateCircule:0 center:center];


CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = startPath.CGPath;
overlayView.layer.mask = maskLayer;

CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
maskLayerAnimation.fromValue = (__bridge id)(startPath.CGPath);
maskLayerAnimation.toValue = (__bridge id)(endPath.CGPath);

maskLayerAnimation.duration = duration / 1000.0;
maskLayerAnimation.delegate = source;
maskLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
maskLayerAnimation.fillMode = kCAFillModeForwards;
maskLayerAnimation.removedOnCompletion = NO;


[CATransaction begin];
[CATransaction setCompletionBlock:^{
if (callback) {
callback();
}
maskLayerAnimation.delegate = nil;
}];

[maskLayer addAnimation:maskLayerAnimation forKey:@"path"];
[CATransaction commit];
}

+ (void)performCircularAnimation:(UIView *) overlayView
source:(ThemeSwitchAnimationModule *)source
duration:(NSInteger)duration
cxRatio:(CGFloat)cxRatio
cyRatio:(CGFloat)cyRatio
callback:(void (^)(void))callback
{
double frameDuration = 1.0 / [[UIScreen mainScreen] maximumFramesPerSecond];
int delayedFrames = 5;
double delayInSeconds = delayedFrames * frameDuration;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
overlayView.hidden = YES;
UIImage *capturedImageAfterSwitching = [source captureScreen];
UIImageView *capturedImageViewAfterSwitching = [[UIImageView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
capturedImageViewAfterSwitching.image = capturedImageAfterSwitching;
capturedImageViewAfterSwitching.contentMode = UIViewContentModeScaleAspectFill;
[[UIApplication sharedApplication].keyWindow addSubview:capturedImageViewAfterSwitching];
overlayView.hidden = NO;

CGFloat width = CGRectGetWidth(capturedImageViewAfterSwitching.bounds);
CGFloat height = CGRectGetHeight(capturedImageViewAfterSwitching.bounds);
CGPoint center = CGPointMake(width * cxRatio, height * cyRatio);
CGFloat startRadius = [self getPointMaxDistanceInsideContainerWithCx:center.x cy:center.y width:width height:height];

UIBezierPath *startPath = [self generateCircule:0 center:center];
UIBezierPath *endPath = [self generateCircule:startRadius center:center];

CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = startPath.CGPath;
capturedImageViewAfterSwitching.layer.mask = maskLayer;

CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
maskLayerAnimation.fromValue = (__bridge id)(startPath.CGPath);
maskLayerAnimation.toValue = (__bridge id)(endPath.CGPath);
maskLayerAnimation.duration = duration / 1000.0;
maskLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
maskLayerAnimation.fillMode = kCAFillModeForwards;
maskLayerAnimation.removedOnCompletion = NO;


[CATransaction begin];
[CATransaction setCompletionBlock:^{
if (callback) {
callback();
}
maskLayerAnimation.delegate = nil;
capturedImageViewAfterSwitching.layer.mask = nil;
capturedImageViewAfterSwitching.hidden = YES;
[capturedImageViewAfterSwitching removeFromSuperview];
}];

[maskLayer addAnimation:maskLayerAnimation forKey:@"path"];
[CATransaction commit];
});
}

+ (CGFloat)getPointMaxDistanceInsideContainerWithCx:(CGFloat)cx
cy:(CGFloat)cy
width:(CGFloat)width
height:(CGFloat)height
{
CGFloat topLeftDistance = hypotf(cx, cy);
CGFloat topRightDistance = hypotf(width - cx, cy);
CGFloat bottomLeftDistance = hypotf(cx, height - cy);
CGFloat bottomRightDistance = hypotf(width - cx, height - cy);
return MAX(MAX(topLeftDistance, topRightDistance), MAX(bottomLeftDistance, bottomRightDistance));
}

+ (UIBezierPath*) generateCircule:(CGFloat)radius
center:(CGPoint)center
{
UIBezierPath *circule = [UIBezierPath bezierPathWithArcCenter:center
radius:radius == 0 ? 0.1 : radius // 0 produces weired animation
startAngle:0
endAngle:M_PI * 2
clockwise:YES];

return circule;
}

@end
4 changes: 2 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export default function App() {
setTheme(theme === 'light' ? 'dark' : 'light');
},
animationConfig: {
type: 'inverted-circular',
duration: 1200,
type: 'circular',
duration: 1000,
startingPoint: {
cx: 200,
cy: -1,
Expand Down
4 changes: 4 additions & 0 deletions ios/ThemeSwitchAnimationModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@
@property (nonatomic, strong) UIImageView *overlayView;
@property (nonatomic, assign) BOOL isAnimating;

- (void)captureAndDisplayScreen;
- (void)displayCapturedImageFullScreen:(UIImage *)image;
- (UIImage *)captureScreen;

@end
138 changes: 4 additions & 134 deletions ios/ThemeSwitchAnimationModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#import <UIKit/UIKit.h>

#import "ThemeSwitchAnimationModule.h"
#import "AnimationHelper.h"


@implementation ThemeSwitchAnimationModule
Expand Down Expand Up @@ -43,11 +44,11 @@ - (void)triggerEvent {

dispatch_async(dispatch_get_main_queue(), ^{
if ([type isEqualToString:@"inverted-circular"]) {
[self performInvertedCircleAnimation: self->overlayView duration:duration cxRatio:cxRatio cyRatio:cyRatio callback: completionCallback];
[AnimationHelper performInvertedCircleAnimation:self->overlayView source:self duration:duration cxRatio:cxRatio cyRatio:cyRatio callback:completionCallback];
} else if ([type isEqualToString:@"circular"]) {
[self performCircularAnimation: self->overlayView duration:duration cxRatio:cxRatio cyRatio:cyRatio callback: completionCallback];
[AnimationHelper performCircularAnimation:self->overlayView source:self duration:duration cxRatio:cxRatio cyRatio:cyRatio callback:completionCallback];
} else {
[self performFadeAnimation:duration callback: completionCallback];
[AnimationHelper performFadeAnimation:self->overlayView duration:duration callback:completionCallback];
}
});
}
Expand Down Expand Up @@ -76,136 +77,5 @@ - (UIImage *)captureScreen {
return capturedScreen;
}

- (void)performFadeAnimation: (NSInteger) duration callback: (void (^)(void))callback {
[UIView animateWithDuration: duration / 1000.0
animations:^{
self->overlayView.alpha = 0.0;
}
completion:^(BOOL finished){
if (callback) {
callback();
}
[self->overlayView removeFromSuperview];
}];
}



- (void)performCircularAnimation:(UIView *)overlayView
duration:(NSInteger)duration
cxRatio:(CGFloat)cxRatio
cyRatio:(CGFloat)cyRatio
callback:(void (^)(void))callback {

double frameDuration = 1.0 / [[UIScreen mainScreen] maximumFramesPerSecond];
int delayedFrames = 5;
double delayInSeconds = delayedFrames * frameDuration;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
overlayView.hidden = YES;
UIImage *capturedImageAfterSwitching = [self captureScreen];
UIImageView *capturedImageViewAfterSwitching = [[UIImageView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
capturedImageViewAfterSwitching.image = capturedImageAfterSwitching;
capturedImageViewAfterSwitching.contentMode = UIViewContentModeScaleAspectFill;
[[UIApplication sharedApplication].keyWindow addSubview:capturedImageViewAfterSwitching];
overlayView.hidden = NO;

CGFloat width = CGRectGetWidth(capturedImageViewAfterSwitching.bounds);
CGFloat height = CGRectGetHeight(capturedImageViewAfterSwitching.bounds);
CGPoint center = CGPointMake(width * cxRatio, height * cyRatio);
CGFloat startRadius = [self getPointMaxDistanceInsideContainerWithCx:center.x cy:center.y width:width height:height];

UIBezierPath *startPath = [self generateCircule:0 center:center];
UIBezierPath *endPath = [self generateCircule:startRadius center:center];

CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = startPath.CGPath;
capturedImageViewAfterSwitching.layer.mask = maskLayer;

CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
maskLayerAnimation.fromValue = (__bridge id)(startPath.CGPath);
maskLayerAnimation.toValue = (__bridge id)(endPath.CGPath);
maskLayerAnimation.duration = duration / 1000.0;
maskLayerAnimation.delegate = self;
maskLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
maskLayerAnimation.fillMode = kCAFillModeForwards;
maskLayerAnimation.removedOnCompletion = NO;


[CATransaction begin];
[CATransaction setCompletionBlock:^{
if (callback) {
callback();
}
maskLayerAnimation.delegate = nil;
capturedImageViewAfterSwitching.layer.mask = nil;
capturedImageViewAfterSwitching.hidden = YES;
[capturedImageViewAfterSwitching removeFromSuperview];
}];

[maskLayer addAnimation:maskLayerAnimation forKey:@"path"];
[CATransaction commit];
});
}

- (void)performInvertedCircleAnimation:(UIView *)overlayView
duration:(CFTimeInterval)duration
cxRatio:(CGFloat)cxRatio
cyRatio:(CGFloat)cyRatio
callback:(void (^)(void))callback {
CGFloat width = CGRectGetWidth(overlayView.bounds);
CGFloat height = CGRectGetHeight(overlayView.bounds);
CGPoint center = CGPointMake(width * cxRatio, height * cyRatio);
CGFloat startRadius = [self getPointMaxDistanceInsideContainerWithCx:center.x cy:center.y width:width height:height];


UIBezierPath *startPath = [self generateCircule:startRadius center:center];
UIBezierPath *endPath = [self generateCircule:0 center:center];


CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = startPath.CGPath;
overlayView.layer.mask = maskLayer;

CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
maskLayerAnimation.fromValue = (__bridge id)(startPath.CGPath);
maskLayerAnimation.toValue = (__bridge id)(endPath.CGPath);

maskLayerAnimation.duration = duration / 1000.0;
maskLayerAnimation.delegate = self;
maskLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
maskLayerAnimation.fillMode = kCAFillModeForwards;
maskLayerAnimation.removedOnCompletion = NO;


[CATransaction begin];
[CATransaction setCompletionBlock:^{
if (callback) {
callback();
}
maskLayerAnimation.delegate = nil;
}];

[maskLayer addAnimation:maskLayerAnimation forKey:@"path"];
[CATransaction commit];
}

- (CGFloat)getPointMaxDistanceInsideContainerWithCx:(CGFloat)cx cy:(CGFloat)cy width:(CGFloat)width height:(CGFloat)height {
CGFloat topLeftDistance = hypotf(cx, cy);
CGFloat topRightDistance = hypotf(width - cx, cy);
CGFloat bottomLeftDistance = hypotf(cx, height - cy);
CGFloat bottomRightDistance = hypotf(width - cx, height - cy);
return MAX(MAX(topLeftDistance, topRightDistance), MAX(bottomLeftDistance, bottomRightDistance));
}

- (UIBezierPath*) generateCircule: (CGFloat)radius center:(CGPoint)center {
UIBezierPath *circule = [UIBezierPath bezierPathWithArcCenter:center
radius:radius == 0 ? 0.1 : radius // 0 produces weired animation
startAngle:0
endAngle:M_PI * 2
clockwise:YES];

return circule;
}

@end

0 comments on commit fd5c6f6

Please sign in to comment.