logo头像

学如逆水行舟

玩转iOS转场动画

玩转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
//这个函数用来设置当执行present方法时 进行的转场动画
/*
presented为要弹出的Controller
presenting为当前的Controller
source为源Contrller 对于present动作 presenting与source是一样的
*/
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
//这个函数用来设置当执行dismiss方法时 进行的转场动画
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

//这个函数用来设置当执行present方法时 进行可交互的转场动画
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
//这个函数用来设置当执行dismiss方法时 进行可交互的转场动画
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
//iOS8后提供的新接口 返回UIPresentationController处理转场
- (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; // This indicates whether the transition is currently 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;
//获取转场中的两个视图控制器
/*
UITransitionContextViewControllerKey的定义
UITransitionContextFromViewControllerKey //原视图控制器
UITransitionContextToViewControllerKey //跳转的视图控制器
*/
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
//直接获取转场中的视图
/*
UITransitionContextFromViewKey //原视图
UITransitionContextToViewKey //转场的视图
*/
- (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{
//遵守了UIViewControllerInteractiveTransitioning协议的对象
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

//这个函数用来保存transitionContext
-(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];
// Do any additional setup after loading the view.
}


-(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:{
//手势过程中,通过updateInteractiveTransition设置pop过程进行的百分比
[obj updateAniProgress:persent];
break;
}
case UIGestureRecognizerStateEnded:{
//手势完成后结束标记并且判断移动距离是否过半,过则finishInteractiveTransition完成转场操作,否者取消转场操作
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
//设置转场的动画不论是push或pop 返回nil 则使用系统默认的导航转场动画
- (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, //push操作
UINavigationControllerOperationPop, //pop操作
};

实现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{

}

这两个函数的应用和导航自定义动画基本是一致的,这里就不再列举代码,简单的效果见下图: