自从 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]];

通过继承基类,满足了这个库的通用性,同时可以满足自定义。