玩转iOS转场动画
一、引言
关于动画在iOS开发中的应用,曾经整理过一系列的博客进行总结。包括简单的UIView层的动画,CALayer层的动画,Autolayout自动布局动画以及CoreAnimation核心动画框架等。本篇博客主要深入讨论视图控制器、导航控制器来进行界面跳转时的专场动画相关内容。之前的动画相关博客列举如下:
iOS动画开发之一——UIViewAnimation动画的使用:https://my.oschina.net/u/2340880/blog/484457
iOS动画开发之二——UIView动画执行的另一种方式:https://my.oschina.net/u/2340880/blog/484538
iOS动画开发之三——UIView的转场切换:https://my.oschina.net/u/2340880/blog/484669
iOS动画开发之四——核心动画编程(CoreAnimation):https://my.oschina.net/u/2340880/blog/484793
iOS动画开发之五——炫酷的粒子效果:https://my.oschina.net/u/2340880/blog/485095
iOS开发CoreAnimation解读之一——初识CoreAnimation核心动画编程:https://my.oschina.net/u/2340880/blog/535235
iOS开发CoreAnimation解读之二——对CALayer的分析:https://my.oschina.net/u/2340880/blog/536048
iOS开发CoreAnimation解读之三——几种常用Layer的使用解析:https://my.oschina.net/u/2340880/blog/538024
iOS开发CoreAnimation解读之四——Layer层动画内容:https://my.oschina.net/u/2340880/blog/539599
iOS开发CoreAnimation解读之五——高级动画技巧:https://my.oschina.net/u/2340880/blog/539827
iOS开发CoreAnimation解读之五——CATransform3D变换的应用:https://my.oschina.net/u/2340880/blog/539878
iOS中播放gif动态图的方式探讨:https://my.oschina.net/u/2340880/blog/608560
iOS界面布局之三——纯代码的autoLayout及布局动画:https://my.oschina.net/u/2340880/blog/524089
开始本篇博客前,先上一张图,如果你觉得不好理解,没关系,看完后面的内容再回来看这张图,就一目了然了。
二、UIViewController进行模态跳转的转场
首先,使用CoreAnimation框架中的CATransition类也可以实现视图控制器的转场动画,前面的博客有过讨论,这里不再重复。presentViewController这个函数使用率可谓是非常高的,默认的转场动画为新的视图控制器从下向上弹出,dismissViewControllerAnimated函数的返回动画则是弹出动画的逆序播放。其实,系统提供了4种转场动画供开发者选择,通过设置将要弹出的UIViewController实例的如下属性:
1
| @property(nonatomic,assign) UIModalTransitionStyle modalTransitionStyle;
|
UIModalTransitionStyle是一个枚举,如下:
1 2 3 4 5 6
| typedef NS_ENUM(NSInteger, UIModalTransitionStyle) { UIModalTransitionStyleCoverVertical = 0, UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED, UIModalTransitionStyleCrossDissolve, UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED, };
|
很多时候,上面4种枚举的转场动画样式并不能满足我们的需求,我们可以使用UIViewControllerTransitioningDelegate协议来完全自定义想要的转场动画效果。
首先创建一个类,使其遵守UIViewControllerTransitioningDelegate协议,比如我这里将类名去做TransDelegate,继承自NSObject。在界面跳转时,将要弹出的视图控制器设置如下:
1 2 3 4
| ViewController2 * v2 = [ViewController2 new]; self.transDelegate = [[TransDelegate alloc]init]; v2.transitioningDelegate = self.transDelegate; [self presentViewController:v2 animated:YES completion:nil];
|
我们先来看UIViewControllerTransitioningDelegate协议中的如下几个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
- (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);
|
我们先来看上面的前两个函数,这两个函数都要返回一个实现了UIViewControllerAnimatedTransitioning协议的对象,UIViewControllerAnimatedTransitioning则用来负责具体的动画展示,例如我们在创建一个命名为AniObject的类,继承自NSObject,使其实现UIViewControllerAnimatedTransitioning协议,在TransDelegate类中实现如下:
1 2 3
| - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{ return [AniObject new]; }
|
下面我们来实现AniObject类来具体的处理动画效果:
UIViewControllerAnimatedTransitioning协议中有两个函数是必须实现的,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{ return 2; }
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{ UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; CGRect finalRect = [transitionContext finalFrameForViewController:toVC]; toVC.view.frame = CGRectOffset(finalRect, [[UIScreen mainScreen]bounds].size.width, 0); [[transitionContext containerView]addSubview:toVC.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ toVC.view.frame = finalRect; } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; }
|
上面我们实现了一个简单的自定义转场动画,将present动画修改成了从右侧滑入,但是dismiss动画依然是默认的从下方划出。效果如下:
下面我们来分析下transitionContext这个对象,这个对象实际上是一个转场上下文,使用它来进行动画的定义和执行:
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
| @property(nonatomic, readonly) UIView *containerView;
@property(nonatomic, readonly, getter=isAnimated) BOOL animated;
@property(nonatomic, readonly, getter=isInteractive) BOOL interactive;
@property(nonatomic, readonly) BOOL transitionWasCancelled;
@property(nonatomic, readonly) UIModalPresentationStyle presentationStyle;
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
- (void)finishInteractiveTransition;
- (void)cancelInteractiveTransition;
- (void)pauseInteractiveTransition;
- (void)completeTransition:(BOOL)didComplete;
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key;
- (CGRect)initialFrameForViewController:(UIViewController *)vc;
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
|
通过上面的介绍,我们可以使用UIViewControllerContextTransitioning随心所欲的定制转场动画,但是还有一个困难我们无法克服,那就是可以交互的转场动画。我们在使用系统的导航控制器时,右划返回效果对用户体验十分友好,我们下面就来试着将视图控制器的模态跳转设计成类似导航可交互的。
首先我们需要实现TransDelegate类中的如下两个函数:
1 2 3 4 5 6 7
| - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{ return [AniObject new]; } - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator{ return self.object; }
|
UIViewControllerInteractiveTransitioning协议用来处理可交互的转场动画的具体表现,需要注意,因为使用的是可交互的转场动画,UIViewControllerAnimatedTransitioning协议中的animateTransition:方法可以空实现。下面我们再创建一个遵守UIViewControllerInteractiveTransitioning协议的类,比如命名为IntObject,上面代码中的self.object即是这个类的示例,IntObject.h文件如下:
1 2 3 4 5 6 7 8 9
| @interface IntObject : NSObject<UIViewControllerInteractiveTransitioning>
-(void)updateAniProgress:(CGFloat)progress;
-(void)finish;
-(void)cancel;
@end
|
IntObject.m文件实现如下:
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
| @interface IntObject()
@property(nonatomic,strong)id<UIViewControllerContextTransitioning> context;
@end
@implementation IntObject
-(void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext{ self.context = transitionContext; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController * fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; toVC.view.frame = [transitionContext finalFrameForViewController:toVC]; [[transitionContext containerView]insertSubview:toVC.view belowSubview:fromVC.view]; }
-(void)updateAniProgress:(CGFloat)progress{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; CGRect fR = CGRectMake( [UIScreen mainScreen].bounds.size.width*progress, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); frameVC.frame = fR; }
-(void)finish{ [UIView animateWithDuration:0.2 animations:^{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; frameVC.frame = CGRectMake([UIScreen mainScreen].bounds.size.width, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); } completion:^(BOOL finished) { [self.context completeTransition:YES]; }]; }
-(void)cancel{ [UIView animateWithDuration:0.2 animations:^{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; frameVC.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); } completion:^(BOOL finished) { [self.context cancelInteractiveTransition]; }]; }
@end
|
下面我们来添加手势,在ViewController2类中添加如下代码:
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
| @interface ViewController2 () @property(nonatomic,strong)UIPanGestureRecognizer * pan; @end
@implementation ViewController2
- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor redColor]; [self.view addGestureRecognizer:self.pan]; }
-(void)pan:(UIPanGestureRecognizer *)pan{
CGPoint translatedPoint = [pan translationInView:self.view]; CGFloat persent = translatedPoint.x / [[UIScreen mainScreen]bounds].size.width; if (persent<0) { return; } persent = fabs(persent); IntObject * obj = [(TransDelegate *)self.transitioningDelegate object]; switch (pan.state) { case UIGestureRecognizerStateBegan:{ [self dismissViewControllerAnimated:YES completion:nil]; break; } case UIGestureRecognizerStateChanged:{ [obj updateAniProgress:persent]; break; } case UIGestureRecognizerStateEnded:{ if (persent > 0.5) { [obj finish]; }else{ [obj cancel]; } break; } default: break; } } -(UIPanGestureRecognizer *)pan{ if (!_pan) { _pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(pan:)]; } return _pan; } @end
|
手势效果如下:
其实,上面演示的是我们自己创建了一个类来实现UIViewControllerInteractiveTransitioning协议,其实系统也为我们提供一个类:UIPercentDrivenInteractiveTransition类,我们可以直接调用这个类的如下3个函数而不需要我们自己重写了,但是必须实现UIViewControllerAnimatedTransitioning协议中的transitionContext函数来实现动画效果。
1 2 3
| - (void)updateInteractiveTransition:(CGFloat)percentComplete; - (void)cancelInteractiveTransition; - (void)finishInteractiveTransition;
|
其实现原理与我们上面进行完全的自定义是一样的。
三、导航转场动画的自定义
导航转场动画的原理与模态跳转转场动画的原理基本是一致的,不同的我们需要设置UINavigationController实例的delegate为遵守UINavigationControllerDelegate协议的类对象。之后实现如下两个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| - (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { NSLog(@"sss"); return nil; }
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController{ NSLog(@"aaa"); return nil; }
|
可以看到 animationControllerForOperation:函数依然需要返回一个遵守了UIViewControllerAnimatedTransitioning协议的对象,使用方式和前面所介绍的模态跳转自定义转场一模一样。UINavigationControllerOperation这个枚举将告知开发者导航所做的操作,如下:
1 2 3 4 5
| typedef NS_ENUM(NSInteger, UINavigationControllerOperation) { UINavigationControllerOperationNone, UINavigationControllerOperationPush, UINavigationControllerOperationPop, };
|
实现UIViewControllerInteractiveTransitioning协议如下:
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
| @interface IntObject()
@property(nonatomic,strong)id<UIViewControllerContextTransitioning> context;
@end
@implementation IntObject
-(void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext{ self.context = transitionContext; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController * fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; toVC.view.frame = [transitionContext finalFrameForViewController:toVC]; [[transitionContext containerView]insertSubview:toVC.view belowSubview:fromVC.view]; }
-(void)updateAniProgress:(CGFloat)progress{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; CGRect fR = CGRectMake( [UIScreen mainScreen].bounds.size.width*progress, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); frameVC.frame = fR; [self.context updateInteractiveTransition:progress]; }
-(void)finish{ [UIView animateWithDuration:0.2 animations:^{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; frameVC.frame = CGRectMake([UIScreen mainScreen].bounds.size.width, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); } completion:^(BOOL finished) { [self.context finishInteractiveTransition]; [self.context completeTransition:YES]; }]; }
-(void)cancel{ [UIView animateWithDuration:0.2 animations:^{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; frameVC.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); } completion:^(BOOL finished) { [self.context cancelInteractiveTransition]; [self.context completeTransition:NO]; }]; }
@end
|
如此即可以轻松实现可交互的自定义导航动画。
四、UITabBarController的转场动画
UITabbar也可以进行转场动画的自定义,需要设置UITabBarController的delegate并实现协议中的如下两个函数:
1 2 3 4 5 6 7 8 9 10
| - (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController animationControllerForTransitionFromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { }
- (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController{ }
|
这两个函数的应用和导航自定义动画基本是一致的,这里就不再列举代码,简单的效果见下图: