自从 iOS 7 以后,就可以自定义跳转动画了,苹果开放了相关的 API ,在这里做一个基本的总结。
原理 UIViewControllerTransitioningDelegate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @protocol UIViewControllerTransitioningDelegate <NSObject> @optional - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source; - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed; - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator; - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator; - (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0); @end
这个协议中的方法,会在 UIViewController 转场的时候被调用,用来控制是否需要自定义的转场动画。
UIViewControllerAnimatedTransitioning
1 2 3 - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext; // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition. - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
这个协议就是控制具体的转场动画的实现,包括动画的执行时间,动画的具体执行。
转场动画主要实现 UIViewController 的这两个协议。
实现 场景:A present B 。 假如现在需要自定义 B 出现和消失时的动画,代码主要写在 B 里面。
在 B 中将 self.transitioningDelegate 设置成 self; 1 2 self.transitioningDelegate = self; self.modalTransitionStyle = UIModalPresentationCustom;
在 B 中实现协议中的代理方法: 1 2 3 4 5 6 7 8 9 - (nullable id - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return [[PresentTransition alloc] initWithTransitionType:XWPresentOneTransitionTypePresent]; } - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return [[PresentTransition alloc] initWithTransitionType:XWPresentOneTransitionTypeDismiss]; }
创建 PresentTransition 类,并实现 * UIViewControllerAnimatedTransitioning* 协议 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 PresentTransition.h #import <Foundation/Foundation.h> typedef NS_ENUM(NSUInteger, XWPresentOneTransitionType) { XWPresentOneTransitionTypePresent = 0,//管理present动画 XWPresentOneTransitionTypeDismiss//管理dismiss动画 }; @interface PresentTransition : NSObject <UIViewControllerAnimatedTransitioning> + (instancetype)transitionWithTransitionType:(XWPresentOneTransitionType)type; - (instancetype)initWithTransitionType:(XWPresentOneTransitionType)type; @end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 PresentTransition.m #import "PresentTransition.h" @interface PresentTransition () @property (nonatomic ,assign) XWPresentOneTransitionType type; @end @implementation PresentTransition + (instancetype)transitionWithTransitionType:(XWPresentOneTransitionType)type { PresentTransition *transition = [[PresentTransition alloc] initWithTransitionType:type]; return transition; } - (instancetype)initWithTransitionType:(XWPresentOneTransitionType)type { if (self = [super init]) { _type = type; } return self; } - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext { return 0.7; } - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { switch (_type) { case XWPresentOneTransitionTypePresent: [self presentAnimation:transitionContext]; break; case XWPresentOneTransitionTypeDismiss: [self dismissAnimation:transitionContext]; break; } } //实现present动画逻辑代码 - (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContext { //获取 VC B UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; //获取 VC A UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; //snapshotViewAfterScreenUpdates可以对某个视图截图,我们采用对这个截图做动画代替直接对vc1做动画,因为在手势过渡中直接使用vc1动画会和手势有冲突, 如果不需要实现手势的话,就可以不是用截图视图了,大家可以自行尝试一下 UIView *tempView = [fromVC.view snapshotViewAfterScreenUpdates:NO]; tempView.frame = fromVC.view.frame; //因为对截图做动画,vc1就可以隐藏了 fromVC.view.hidden = YES; //这里有个重要的概念containerView,如果要对视图做转场动画,视图就必须要加入containerView中才能进行,可以理解containerView管理着所有做转场动画的视图 UIView *containerView = [transitionContext containerView]; //将截图视图和vc2的view都加入ContainerView中 [containerView addSubview:tempView]; [containerView addSubview:toVC.view]; //拿到两个视图的 view ,就可以进行具体动画了 /*动画代码*/ } //实现dismiss动画逻辑代码 - (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{ /*与 present 动画代码相似,只不过 fromVC 和 toVC 变换了*/ }
封装 实现以后功能,该如何将其封装成一个工具库呢,首先设想一下这个工具库,最简化的版本也应该提供自定义动画的功能。这是我最终的库的结构: 其中 UIViewController+HJTransition 中代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // // UIViewController+HJTransition.h // VoucherCollection // // Created by eassy on 17/2/21. // Copyright © 2017年 eassy. All rights reserved. // #import <UIKit/UIKit.h> #import "HJTransitionUtilities.h" #import "HJTransitionAnimator.h" @interface UIViewController (HJTransition) - (void)hj_presentViewController:(UIViewController *)controller animation:(BOOL)animation animator:(HJTransitionAnimator *)animator; - (void)hj_dismissViewController:(UIViewController *)controller animation:(BOOL)animation animator:(HJTransitionAnimator *)animator; @end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // // UIViewController+HJTransition.m // VoucherCollection // // Created by eassy on 17/2/21. // Copyright © 2017年 eassy. All rights reserved. // #import "UIViewController+HJTransition.h" @implementation UIViewController (HJTransition) - (void)hj_presentViewController:(UIViewController *)controller animation:(BOOL)animation animator:(HJTransitionAnimator *)animator { controller.transitioningDelegate = animator; [self presentViewController:controller animated:animation completion:nil]; } - (void)hj_dismissViewController:(UIViewController *)controller animation:(BOOL)animation animator:(HJTransitionAnimator *)animator { controller.transitioningDelegate = animator; [self presentViewController:controller animated:animation completion:nil]; } @end
在进行跳转的时候,可以传入自定义的 animator 对象,这个 animator 需要继承自 HJTransitionAnimator 类,其代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // // HJTransitionAnimator.h // VoucherCollection // // Created by eassy on 17/2/21. // Copyright © 2017年 eassy. All rights reserved. // #import <Foundation/Foundation.h> #import "HJTransitionUtilities.h" @interface HJTransitionAnimator : NSObject<UIViewControllerTransitioningDelegate,UIViewControllerAnimatedTransitioning> @property (nonatomic ,assign) NSTimeInterval duration; - (instancetype)init; + (instancetype)animator; - (void)animatePresent:(id <UIViewControllerContextTransitioning>)transitionContext; - (void)animateDismiss:(id <UIViewControllerContextTransitioning>)transitionContext; @end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 // // HJTransitionAnimator.m // VoucherCollection // // Created by eassy on 17/2/21. // Copyright © 2017年 eassy. All rights reserved. // #import "HJTransitionAnimator.h" @interface HJTransitionAnimator () @property (nonatomic ,assign) HJAnimationType type; @end @implementation HJTransitionAnimator - (instancetype)init { if (self = [super init]) { _duration = 0.7; } return self; } + (instancetype)animator { return [[[self class] alloc] init]; } //子类需重写 - (void)animatePresent:(id <UIViewControllerContextTransitioning>)transitionContext { } //子类需重写 - (void)animateDismiss:(id <UIViewControllerContextTransitioning>)transitionContext { } #pragma mark - UIViewControllerTransitioningDelegate - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { self.type = HJAnimationTypePresent; return self; } - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { self.type = HJAnimationTypeDismiss; return self; } #pragma mark - UIViewControllerAnimatedTransitioning - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext { return self.duration; } // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition. - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { if (self.type == HJAnimationTypePresent) { [self animatePresent:transitionContext]; } else{ [self animateDismiss:transitionContext]; } } @end
通过继承一个基类,将转场动画中可能出现的细小差别代码分离了出来。下面是自定义的子类:
1 2 3 4 5 6 7 8 9 10 11 12 13 // // DirectionTransitionAnimator.h // VoucherCollection // // Created by eassy on 17/2/21. // Copyright © 2017年 eassy. All rights reserved. // #import "HJTransitionAnimator.h" @interface DirectionTransitionAnimator : HJTransitionAnimator @end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 // // DirectionTransitionAnimator.m // VoucherCollection // // Created by eassy on 17/2/21. // Copyright © 2017年 eassy. All rights reserved. // #import "DirectionTransitionAnimator.h" @implementation DirectionTransitionAnimator - (void)animateDismiss:(id<UIViewControllerContextTransitioning>)transitionContext { //注意在dismiss的时候fromVC就是vc2了,toVC才是VC1了,注意这个关系 UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; //参照present动画的逻辑,present成功后,containerView的最后一个子视图就是截图视图,我们将其取出准备动画 UIView *tempView = [transitionContext containerView].subviews[0]; //动画吧 [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ //因为present的时候都是使用的transform,这里的动画只需要将transform恢复就可以了 fromVC.view.transform = CGAffineTransformIdentity; tempView.transform = CGAffineTransformIdentity; } completion:^(BOOL finished) { if ([transitionContext transitionWasCancelled]) { //失败了标记失败 [transitionContext completeTransition:NO]; }else{ //如果成功了,我们需要标记成功,同时让vc1显示出来,然后移除截图视图, [transitionContext completeTransition:YES]; toVC.view.hidden = NO; [tempView removeFromSuperview]; } }]; } - (void)animatePresent:(id<UIViewControllerContextTransitioning>)transitionContext { UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; //snapshotViewAfterScreenUpdates可以对某个视图截图,我们采用对这个截图做动画代替直接对vc1做动画,因为在手势过渡中直接使用vc1动画会和手势有冲突, 如果不需要实现手势的话,就可以不是用截图视图了,大家可以自行尝试一下 UIView *tempView = [fromVC.view snapshotViewAfterScreenUpdates:NO]; tempView.frame = fromVC.view.frame; //因为对截图做动画,vc1就可以隐藏了 fromVC.view.hidden = YES; //这里有个重要的概念containerView,如果要对视图做转场动画,视图就必须要加入containerView中才能进行,可以理解containerView管理着所有做转场动画的视图 UIView *containerView = [transitionContext containerView]; //将截图视图和vc2的view都加入ContainerView中 [containerView addSubview:tempView]; [containerView addSubview:toVC.view]; //设置vc2的frame,因为这里vc2present出来不是全屏,且初始的时候在底部,如果不设置frame的话默认就是整个屏幕咯,这里containerView的frame就是整个屏幕 toVC.view.frame = CGRectMake(-containerView.frame.size.width, 0, containerView.frame.size.width, containerView.frame.size.height); //开始动画吧,使用产生弹簧效果的动画API [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:1.0 / 0.55 options:0 animations:^{ //首先我们让vc2向上移动 toVC.view.transform = CGAffineTransformMakeTranslation(containerView.frame.size.width, 0); //然后让截图视图缩小一点即可 tempView.transform = CGAffineTransformMakeScale(0.85, 0.85); } completion:^(BOOL finished) { //使用如下代码标记整个转场过程是否正常完成[transitionContext transitionWasCancelled]代表手势是否取消了,如果取消了就传NO表示转场失败,反之亦然,如果不用手势present的话直接传YES也是可以的,但是无论如何我们都必须标记转场的状态,系统才知道处理转场后的操作,否者认为你一直还在转场中,会出现无法交互的情况,切记! [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; //转场失败后的处理 if ([transitionContext transitionWasCancelled]) { //失败后,我们要把vc1显示出来 fromVC.view.hidden = NO; //然后移除截图视图,因为下次触发present会重新截图 [tempView removeFromSuperview]; } }]; } @end
使用的时候
1 [self hj_presentViewController:voucherListVC animation:YES animator:[DirectionTransitionAnimator animator]];
通过继承基类,满足了这个库的通用性,同时可以满足自定义。