iOS开发-转场动画切换界面(类似系统动画)

176 篇文章 1 订阅
104 篇文章 1 订阅

iOS开发-转场动画切换界面(类似系统动画)

在开发中,无论我们使用 push 还是 present 推出新的 viewcontroller 时,系统为了提高用户体验都会为我们默认加上一些过渡动画。但是开发中需要自定义过度动画效果。这里就需要用到了转场动画了。

如图所示

在这里插入图片描述

在这里插入图片描述

一、转场动画相关协议

自定义转场动画时需要使用的协议:

  • UIViewControllerAnimatedTransitioning: 实现此协议的实例控制转场动画效果。
  • UIViewControllerInteractiveTransitioning: 实现此协议的实例控制着利用手势过渡时的进度处理。

二、实现转场动画代码

2.1、实现UIViewControllerAnimatedTransitioning协议

实现转场动画时候,实现UIViewControllerAnimatedTransitioning协议

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext;

在区分Push与Pop操作时候,需要用到以下的

UIView *containerView = [transitionContext containerView];

    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIView *fromView = fromVC.view;
    
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *toView = toVC.view;

注意:在转场过程中,转场过渡的容器,如果是PUSH时候,A->B是,fromVC是A,toVC是B;如果是POP的时候,fromVC是B,toVC是A。

动画实现
在转场过程中更改fromView与toView的toFrame,动画结束后调用transitionContext的completeTransition方法。

具体代码如下

INPushPopTranstioning.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "INPercentDrivenInteractiveTransition.h"

@interface INPushPopTranstioning : NSObject<UIViewControllerAnimatedTransitioning, UINavigationControllerDelegate>

// 是否是Push
@property (nonatomic, assign) NSTimeInterval transitionDuration;

@property (nonatomic, strong) INPercentDrivenInteractiveTransition *interactiveTransition;

+ (instancetype)sharedInstance;

@end

INPushPopTranstioning.m

#import "INPushPopTranstioning.h"

#define kINScreenWidth [[UIScreen mainScreen] bounds].size.width

static INPushPopTranstioning *_sharedInstance = nil;

@interface INPushPopTranstioning ()

// 是否是Push
@property (nonatomic, assign) BOOL isPush;

@end

@implementation INPushPopTranstioning

+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedInstance = [[INPushPopTranstioning alloc] init];
        _sharedInstance.transitionDuration = 0.35;
        _sharedInstance.interactiveTransition = [[INPercentDrivenInteractiveTransition alloc] init];
    });
    
    return _sharedInstance;
}

- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                                   interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController {
    return self.interactiveTransition.isInteractive?self.interactiveTransition:nil;
}

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
    return self.transitionDuration;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    
    // 转场过渡的容器 如果是PUSH时候,A->B是,fromVC是A,toVC是B;如果是POP的时候,fromVC是B,toVC是A。
    // Transition container
    UIView *containerView = [transitionContext containerView];

    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIView *fromView = fromVC.view;
    
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *toView = toVC.view;

    UIImage *fromImage = [self screenShotImage:fromView];
    
    UIImageView *backView = [[UIImageView alloc] initWithFrame:CGRectZero];
    //backView.image = fromImage;
    backView.backgroundColor = [UIColor blackColor];
    backView.frame = containerView.bounds;
    
    UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
    effectView.frame = backView.bounds;
    //[backView addSubview:effectView];

    UIImageView *viewMaskView = [[UIImageView alloc] initWithFrame:CGRectZero];
    viewMaskView.backgroundColor = [UIColor blackColor];
    viewMaskView.frame = containerView.bounds;
    
    // 判断是 push 还是 pop 操作
    // Determine if it is a push or pop operation
    if (self.isPush) {
        //[containerView addSubview:backView];
        [containerView addSubview:fromView];
        [containerView addSubview:viewMaskView];
        [containerView addSubview:toView];

    } else {
        //[containerView addSubview:backView];
        [containerView addSubview:toView];
        [containerView addSubview:viewMaskView];
        [containerView addSubview:fromView];
    }
    
    
    CGRect fromFrame;
    CGRect toFrame;

    CGFloat beginAlpha = 0.0;
    if (self.isPush) {
        fromFrame = containerView.bounds;
        fromView.frame = fromFrame;
        
        toFrame = CGRectMake(containerView.bounds.size.width, 0, containerView.bounds.size.width, containerView.bounds.size.height);
        toView.frame = toFrame;        
        beginAlpha = 0.0;
    } else {
        fromFrame = containerView.bounds;
        fromView.frame = fromFrame;
        
        toFrame = CGRectMake(containerView.bounds.size.width, 0, containerView.bounds.size.width, containerView.bounds.size.height);
        
        toView.frame = fromFrame;
        
        beginAlpha = 0.3;
    }

    viewMaskView.alpha = beginAlpha;
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        if (self.isPush) {
            //fromView.transform = CGAffineTransformMakeScale(0.9, 0.9);
            toView.frame = fromFrame;
            viewMaskView.alpha = 0.3;
        } else {
            fromView.frame = toFrame;
            //toView.transform = CGAffineTransformMakeScale(1.0, 1.0);
            viewMaskView.alpha = 0.0;
        }
    } completion:^(BOOL finished) {
        BOOL cancelled = [transitionContext transitionWasCancelled];
        
        fromView.transform = CGAffineTransformIdentity;
        toView.transform = CGAffineTransformIdentity;

        // 删除遮罩
        [backView removeFromSuperview];
        [viewMaskView removeFromSuperview];
        
        // 设置 transitionContext 通知系统动画执行完毕
        [transitionContext completeTransition:!cancelled];
    }];
}

- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC {
    if (operation == UINavigationControllerOperationPush) {
        self.isPush = YES;
    } else {
        self.isPush = NO;
    }
    return self;
}

- (UIImage *)screenShotImage:(UIView *)view {
    // 第一个参数表示区域大小。第二个参数表示是否是非透明的。如果需要显示半透明效果,需要传NO,否则传YES。第三个参数就是屏幕密度了,设置为[UIScreen mainScreen].scale可以保证转成的图片不失真。
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO,[UIScreen mainScreen].scale);
    if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
        [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];
    } else {
        [view.layer renderInContext:UIGraphicsGetCurrentContext()];
    }
    UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
    
    UIGraphicsEndImageContext();
    
    return viewImage;
}

// Called when the navigation controller shows a new top view controller via a push, pop or setting of the view controller stack.
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    NSLog(@"willShowViewController:%@",viewController);
}

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    NSLog(@"didShowViewController:%@",viewController);
}

- (CATransform3D)transformRotation:(CGFloat)angle {
    CATransform3D rotation3DIdentity = CATransform3DIdentity;
    rotation3DIdentity.m34 = 0.3/500.0;
    CATransform3D rotateTransform = CATransform3DRotate(rotation3DIdentity, angle, 0, 1, 0);
    CATransform3D transform = CATransform3DMakeTranslation(0, 0, kINScreenWidth/4.0);
    return CATransform3DConcat(rotateTransform, transform);
}

- (void)resetAnChorPoint:(UIView *)view anchorPoint:(CGPoint)anchorPoint {
    CGPoint oldAnchorPoint = view.layer.anchorPoint;
    view.layer.anchorPoint = anchorPoint;
    [view.layer setPosition:CGPointMake(view.layer.position.x + view.layer.bounds.size.width * (view.layer.anchorPoint.x - oldAnchorPoint.x), view.layer.position.y + view.layer.bounds.size.height * (view.layer.anchorPoint.y - oldAnchorPoint.y))];
}

@end

2.2、处理手势过渡

在Pop可能需要手势滑动返回,我这里使用继承UIPercentDrivenInteractiveTransition的子类INPercentDrivenInteractiveTransition来处理

实现拖拽手势UIPanGestureRecognizer

代码如下

INPercentDrivenInteractiveTransition.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface INPercentDrivenInteractiveTransition : UIPercentDrivenInteractiveTransition

@property (nonatomic, assign, readonly) BOOL isInteractive;

@end

NS_ASSUME_NONNULL_END

INPercentDrivenInteractiveTransition.m

#import "INPercentDrivenInteractiveTransition.h"

// 屏幕宽与高
#define kScreenWidth  ([UIScreen mainScreen].bounds.size.width)
#define kScreenHeight ([UIScreen mainScreen].bounds.size.height)

@interface INPercentDrivenInteractiveTransition()

@property (nonatomic, assign, readwrite) BOOL isInteractive;

@end

@implementation INPercentDrivenInteractiveTransition

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.isInteractive = NO;
    }
    return self;
}

- (void)handleGesture:(UIPanGestureRecognizer *)panGesture {
    CGFloat transitionX = [panGesture translationInView:panGesture.view].x;
    CGFloat persent = transitionX / panGesture.view.frame.size.width;
    
    switch (panGesture.state) {
        case UIGestureRecognizerStateBegan:
            self.isInteractive = YES;
            [self start];
            break;
        case UIGestureRecognizerStateChanged:
            [self updateInteractiveTransition:persent];
            break;
        case UIGestureRecognizerStateEnded:
            self.isInteractive = NO;
            if (persent > 0.5) {
                [self finishInteractiveTransition];
            }else{
                [self cancelInteractiveTransition];
            }
            break;
        default:
            break;
    }
}

- (void)start {
    UIViewController *controller = [self getCurrentWindowController];
    [controller.navigationController popViewControllerAnimated:YES];
}

/**
 获取当前屏幕显示的controller

 @return controller
 */
- (UIViewController *)getCurrentWindowController {
    UIViewController *result = nil;
    UIWindow * window = [[UIApplication sharedApplication] keyWindow];
    if (window.windowLevel != UIWindowLevelNormal) {
        NSArray *windows = [[UIApplication sharedApplication] windows];
        for(UIWindow * tmpWin in windows) {
            if (tmpWin.windowLevel == UIWindowLevelNormal) {
                window = tmpWin;
                break;
            }
        }
    }
    
    result = window.rootViewController;
    
    while (result.presentingViewController) {
        result = result.presentingViewController;
    }
    
    if ([result isKindOfClass:[UITabBarController class]]) {
        result = [(UITabBarController *)result selectedViewController];
    }
    
    if ([result isKindOfClass:[UINavigationController class]]) {
        result = [(UINavigationController *)result visibleViewController];
    }

    return result;
}

@end

2.3、UINavigationController扩展Category

在Push与Pop时候需要转场动画,那需要替换UINavigationController中的方法

需要使用method_exchangeImplementations

这里暂不详细说明方法替换的逻辑了,详细参考

https://blog.csdn.net/gloryFlow/article/details/131677505

具体代码如下

UINavigationController+Transition.h

#import <UIKit/UIKit.h>
#import "UIViewController+Transition.h"

/**
 处理转场动画
 */
@interface UINavigationController (Transition)


@end

UINavigationController+Transition.m

#import "UINavigationController+Transition.h"
#import <objc/runtime.h>
#import "INPushPopTranstioning.h"

@implementation UINavigationController (Transition)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        method_exchangeImplementations(class_getInstanceMethod(class, @selector(popViewControllerAnimated:)), class_getInstanceMethod(class, @selector(in_popViewControllerAnimated:)));
        method_exchangeImplementations(class_getInstanceMethod(class, @selector(pushViewController:animated:)), class_getInstanceMethod(class, @selector(in_pushViewController:animated:)));
    });
}

- (void)in_pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if (animated && [INPushPopTranstioning sharedInstance].transitionDuration > 0.0) {
        // 跳转到某些ViewController,需要特定的转场动画,需要设置showNavTransitionDelegate。
        if (viewController.showNavTransitionDelegate) {
            self.delegate = viewController.showNavTransitionDelegate;
        } else {
            self.delegate = [INPushPopTranstioning sharedInstance];
        }
    }
    
    [self in_pushViewController:viewController animated:animated];
}

- (nullable UIViewController *)in_popViewControllerAnimated:(BOOL)animated {
    if (animated && [INPushPopTranstioning sharedInstance].transitionDuration > 0.0) {
        // 回到到某些ViewController,需要特定的转场动画,需要设置showNavTransitionDelegate。
        if (self.visibleViewController.showNavTransitionDelegate) {
            self.delegate = self.visibleViewController.showNavTransitionDelegate;
        } else {
            self.delegate = [INPushPopTranstioning sharedInstance];
        }
    }
    return [self in_popViewControllerAnimated:animated];
}

@end

2.4、UIViewController扩展Category

我这里扩展UIViewController添加运行时属性,代码如下

UIViewController+Transition.h

#import <UIKit/UIKit.h>

@interface UIViewController (Transition)

@property (nonatomic, weak) id <UINavigationControllerDelegate> showNavTransitionDelegate;

@end

UIViewController+Transition.m

#import "UIViewController+Transition.h"
#import <objc/runtime.h>

static const void *showTransitionKey = &showTransitionKey;

@implementation UIViewController (Transition)

- (id<UINavigationControllerDelegate>)showNavTransitionDelegate {
    return objc_getAssociatedObject(self, showTransitionKey);
}

- (void)setShowNavTransitionDelegate:(id<UINavigationControllerDelegate>)showNavTransitionDelegate {
    objc_setAssociatedObject(self, showTransitionKey, showNavTransitionDelegate, OBJC_ASSOCIATION_ASSIGN);
}

@end

2.4、UIViewController扩展Category Navigation

UIViewController+Navigation.h

#import <UIKit/UIKit.h>

@interface UIViewController (Navigation)

@property (nonatomic, weak) UINavigationController *in_navigationController;

@end

UIViewController+Navigation.m

#import "UIViewController+Navigation.h"
#import <objc/runtime.h>

static const void *showNavTransitionKey = &showNavTransitionKey;

@implementation UIViewController (Navigation)

- (UINavigationController *)in_navigationController {
    return objc_getAssociatedObject(self, showNavTransitionKey);
}

- (void)setIn_navigationController:(UINavigationController *)in_navigationController {
    objc_setAssociatedObject(self, showNavTransitionKey, in_navigationController, OBJC_ASSOCIATION_ASSIGN);
}

@end

三、使用自定义转场动画

在BaseViewController中添加拖拽手势,用于处理手势滑动返回。

UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:[INPushPopTranstioning sharedInstance].interactiveTransition action:@selector(handleGesture:)];
    pan.edges = UIRectEdgeLeft;
    [self.view addGestureRecognizer:pan];

BaseViewController完整代码如下

INBaseViewController.h

#import <UIKit/UIKit.h>
#import "UIViewController+Navigation.h"

NS_ASSUME_NONNULL_BEGIN

@interface INBaseViewController : UIViewController

@property (nonatomic, assign) BOOL isViewDidAppear;

@end

NS_ASSUME_NONNULL_END

INBaseViewController.m

#import "INBaseViewController.h"
#import "INPushPopTranstioning.h"

@interface INBaseViewController ()

@end

@implementation INBaseViewController

- (id)init {
    self = [super init];
    if (self) {
        self.hidesBottomBarWhenPushed = YES;
        [self setNeedsStatusBarAppearanceUpdate];
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.exclusiveTouch = YES;
    self.view.clipsToBounds = YES;
    
    self.automaticallyAdjustsScrollViewInsets = NO;
    self.extendedLayoutIncludesOpaqueBars = YES;
    
    UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:[INPushPopTranstioning sharedInstance].interactiveTransition action:@selector(handleGesture:)];
    pan.edges = UIRectEdgeLeft;
    [self.view addGestureRecognizer:pan];
}

- (void)loadView {
    [super loadView];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    self.isViewDidAppear = NO;
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    self.isViewDidAppear = NO;
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    self.isViewDidAppear = YES;
    self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    self.isViewDidAppear = NO;
}

- (void)handleGesture:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer {
    // 手势
}

#pragma mark - StatusBar style
- (UIStatusBarStyle)preferredStatusBarStyle {
    return UIStatusBarStyleDefault;
}

- (BOOL)prefersStatusBarHidden {
    return NO;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
    return (toInterfaceOrientation == UIInterfaceOrientationPortrait);
}

- (BOOL)shouldAutorotate {
    return NO;
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return UIInterfaceOrientationPortrait;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)dealloc {
    NSLog(@"DEALLOCBaseViewController");
}

@end

我这里使用的是NavigationController嵌套自定义的TabbarController.

在AppDelegate中didFinishLaunchingWithOptions进行设置

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    [self.window makeKeyAndVisible];
    
    
    INMainTabBarController *tabbar = [[INMainTabBarController alloc] init];
    UINavigationController *mainNav = [[UINavigationController alloc] initWithRootViewController:tabbar];
    mainNav.delegate = [INPushPopTranstioning sharedInstance];
    
    self.window.backgroundColor = [UIColor whiteColor];
    self.window.rootViewController = mainNav;
    
    return YES;
}

三、小结

iOS开发-转场动画切换界面(类似系统动画)。UIViewControllerAnimatedTransitioning与UIViewControllerInteractiveTransitioning。

学习记录,每天不停进步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值