iOS 7 添加了转场动画,让我们更加方便的切换控制器。iOS 7 SDK 支持两种自定义过渡:

  • 自定义视图控制器过渡
  • 交互式视图控制器过渡

怎么来区分自定义视图控制器过渡和交互式试图控制器过渡呢?

当过渡是时间的函数式,它通常是一个自定义视图控制器过渡

如果他是一个手势识别器或者其他类似事件的函数,它通常是一个交互式视图控制器过渡

比如:UINavigationController 过渡使我们最常见的自定义视图控制器过渡。UIPageViewController 过渡不是随着时间进行,它需要跟随我们的手指滑动,所以它是一种交互式视图控制器过渡。

自定义视图控制器过渡

实现自定义视图控制器过渡有两种方式:

  • 使用 Storyboard 实现
  • 代码方式实现

使用 Storyboard 来实现自定义视图控制器过渡

我们可以使用连线( segue )的方式,来实现自定义视图控制器过渡,segue 有好几种类型,
选择某个 segue,我们可以看到下图:

0.png

我们选择自定义这种方式可以看到:

3.png

我们可以指定 segue Class 它是一个 UIStoryboardSegue 子类,我们可以创建一个 UIStoryboardSegue 子类 IYMCustomSegue,填写在这里。

UIStoryboardSegue 里有个 perform 方法,我们在 IYMCustomSegue 重写一下:

1
2
3
4
- (void)perfrom{
//这里实现动画
...
}

下面实现一个自定义 push 动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)perform {
UIViewController *sourceViewController = self.sourceViewController;
UIViewController *desinationViewController = self.destinationViewController;
CGRect sourceRect = sourceViewController.view.frame;
CGRect desinationViewFromRect = CGRectMake(sourceRect.origin.x + sourceRect.size.width, sourceRect.origin.y, sourceRect.size.width, sourceRect.size.height);
desinationViewController.view.frame = desinationViewFromRect;
desinationViewController.view.alpha = 0;
[sourceViewController.view.superview addSubview:desinationViewController.view];
[UIView animateWithDuration:0.3 animations:^{
sourceViewController.view.alpha = 0.5;
desinationViewController.view.alpha = 1;
desinationViewController.view.frame = sourceRect;
} completion:^(BOOL finished) {
sourceViewController.view.alpha = 1;
[desinationViewController.view removeFromSuperview];
[sourceViewController.navigationController pushViewController:desinationViewController animated:NO];
}];
}

代码方式实现

自定义试图控制器过渡是由两个协议实现的,即UIViewControllerTransitioningDelegateUIViewControllerAnimatedTransitioning

我们从 A 控制器到 B 控制器,通常有两种方式:

  • presentViewController 方式
  • pushViewController 方式

使用 presentViewController 方式:从 A 控制器 present 到 B 控制器

  1. 设置 B 控制器的 transitionDelegate 为 A 控制器
  2. 在 A 控制器中实现 UIViewControllerTransitioningDelegate 协议
  3. 实现 UIViewControllerAnimatedTransitioning

实现 UIViewControllerTransitioningDelegate 中的方法:

1
2
3
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

这两个方法返回的是一个实现了 UIViewControllerAnimatedTransitioning 协议的类实例。通过方法名字我们就可以看到它们适用于 presentViewController 方式。如果自定义过渡效果只有一个视图控制器使用,视图控制器本身就可以实现UIViewControllerAnimatedTransitioning 协议,否则,就需要一个单独的继承自NSObject 一个单独的类来实现 UIViewControllerAnimatedTransitioning 协议。

这里我们就实现一个 B 视图控制器过渡动画,就直接在 A 控制器中实现UIViewControllerAnimatedTransitioning

实现 UIViewControllerTransitioningDelegate 中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// This is used for percent driven interactive transitions, as well as for container controllers that have companion animations that might need to
// synchronize with the main animation.
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
//返回动画时间
return 1.0f;
}
// This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
//动画代码在这里写
...
[transitionContext completeTransition:YES];//必须实现这个方法
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext中的 transitionContext ,我们可以获得:

源控制器即来自哪个视图控制器:

1
UIViewController *src = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

目标控制器,这里是B控制器:

1
UIViewController *dest = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

容器视图:

1
UIView *containerView = [transitionContext containerView];

使用 pushViewController 方式:从 A 控制器 push 到 B 控制器

因为是从 A 控制器 push 到 B 控制器,我们不需要实现 UIViewControllerTransitioningDelegate 中的方法,我们需要:

  1. 在 A 控制器设置 UINavigationDelegateself
  2. 实现 UINavigationDelegate
  3. 实现 UIViewControllerAnimatedTransitioning 协议

实现 UINavigationDelegate 中的方法:

1
2
3
4
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC;

返回的是一个实现了 UIViewControllerAnimatedTransitioning 协议的类实例。这个和 presentViewController 方式一样。

交互式视图控制器过渡

要实现一个交互式视图控制器过渡,除了要是上面的动画外,还要告诉交互控制器动画完成了多少,我们只要知道完成了多少百分比,告诉系统,系统就直接给咱们处理了。通过计算手势/动作事件或其他驱动过渡的事件,可以得出百分比。例如通过 UIScreenEdgePanGestureRecognizer 边缘返回手势,我们可以通过平移量来查看返回完成了多少。

视图控制器过渡方式还是要分成 2 种:

  • presentViewController 方式
  • pushViewController 方式

使用 presentViewController 方式

除了上面 presentViewController 方式实现的协议方法外,我们还需要实现 UIViewControllerTransitioningDelegate 中的另外两个方法:

1
2
3
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;

交互控制器实现了 UIViewControllerInteractiveTransitioning 协议。交互式过渡的交互控制器都必须是 UIPercentDrivenInteractiveTransition 实例或者 UIPercentDrivenInteractiveTransition 子类实例。

UIPercentDrivenInteractiveTransition 中以下方法可以完成交互过渡:

1
2
3
- (void)updateInteractiveTransition:(CGFloat)percentComplete;//完成了多少,percentComplete是百分比
- (void)cancelInteractiveTransition;//取消
- (void)finishInteractiveTransition;//完成

例如我们给 B 控制器(A 控制器 present to B控制器),B 控制器通过缩放到达 A 控制器,我们给B控制器视图添加了一个缩放手势 UIPinchGestureRecognizer

上面的 UIViewControllerTransitioningDelegate 我们返回一个 [[UIPercentDrivenInteractiveTransition alloc] init] 实例 percentDrivenInteractiveTransition

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
- (void)pinchGestureAction:(UIPinchGestureRecognizer*) gestureRecognizer {
CGFloat scale = gestureRecognizer.scale;
if(gestureRecognizer.state == UIGestureRecognizerStateBegan) {
self.percentDrivenInteractiveTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
self.startScale = scale;
[self.controller dismissViewControllerAnimated:YES completion:nil];
} else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
CGFloat completePercent = 1.0 - (scale/self.startScale);
[self updateInteractiveTransition:completePercent];//完成了多少百分比
} else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
if(gestureRecognizer.velocity >= 0){
[self cancelInteractiveTransition];//取消
} else {
[self finishInteractiveTransition];//完成
}
self.percentDrivenInteractiveTransition = nil;
}
if(gestureRecognizer.state == UIGestureRecognizerStateCancelled) {
[self cancelInteractiveTransition];//取消
self.percentDrivenInteractiveTransition = nil;
}
}

使用 pushViewController 方式

和 present 方式不同的一点是,我们使用 UINavigationDelegate 中的:

1
2
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController

来代替:

1
2
3
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;

其他的都一样。

一个完整的例子

单击图片展示大图,返回慢慢缩放到小图,效果如下:

1.gif

将用到:

  • UINavigationDelegate
  • 返回手势
  • 交互式过渡效果

新建 2 个控制器

  • 首页:IYMFirstViewController
  • 喵星人:IYMSecondViewController

使用 Storyboard 创建两个视图

2.png

设置 UINavigationDelegate

IYMFirstViewController.m IYMSecondViewController.m 分别重写以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Set outself as the navigation controller's delegate so we're asked for a transitioning object
self.navigationController.delegate = self;
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Stop being the navigation controller's delegate
if (self.navigationController.delegate == self) {
self.navigationController.delegate = nil;
}
}

实现 UINavigationDelegate

IYMFirstViewController.m 中:

1
2
3
4
5
6
7
8
9
10
11
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC{
// Check if we're transitioning from this view controller to a DSLSecondViewController
if (fromVC == self && [toVC isKindOfClass:[IYMSecondViewController class]]) {
return self;
}else {
return nil;
}
}

IYMSecondViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController{
return _percentDrivenInteractiveTransition;
}
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC{
if (fromVC == self && [toVC isKindOfClass:[IYMFirstViewController class]]) {
return self;
} else {
return nil;
}
}

其中 _percentDrivenInteractiveTransition 是一个 UIPercentDrivenInteractiveTransition 实例,在边缘手势中实例化。

实现 UIViewControllerAnimatedTransitioning

IYMFirstViewController.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
- (NSTimeInterval) transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.3f;//动画时间
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
IYMFirstViewController *fromViewContrroller = (IYMFirstViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
IYMSecondViewController *toViewController = (IYMSecondViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
UIView *snapshotView = [fromViewContrroller.bottomImageView snapshotViewAfterScreenUpdates:NO];//截图
CGRect snapshotViewFromRect = [containerView convertRect:fromViewContrroller.bottomImageView.frame fromView:fromViewContrroller.view];//获取在ContainerView中的frame
snapshotView.frame = snapshotViewFromRect;
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
toViewController.view.alpha = 0;
[containerView addSubview:toViewController.view];
[containerView addSubview:snapshotView];
fromViewContrroller.bottomImageView.hidden = YES;
toViewController.topImageView.hidden = YES;
CGRect snapshotViewToRect = [containerView convertRect:toViewController.topImageView.frame fromView:toViewController.view];
[UIView animateWithDuration:0.3 animations:^{
toViewController.view.alpha = 1;
snapshotView.frame = snapshotViewToRect;
} completion:^(BOOL finished) {
if (finished) {
[snapshotView removeFromSuperview];
fromViewContrroller.bottomImageView.hidden = NO;
toViewController.topImageView.hidden = NO;
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}
}];
}

IYMSecondViewController.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
- (NSTimeInterval) transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.3f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
IYMSecondViewController *fromViewController = (IYMSecondViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
IYMFirstViewController *toViewController = (IYMFirstViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
UIView *snapshotView = [fromViewController.topImageView snapshotViewAfterScreenUpdates:NO];
CGRect snapshotViewFromRect = [containerView convertRect:fromViewController.topImageView.frame fromView:fromViewController.view];
snapshotView.frame = snapshotViewFromRect; // Setup the initial view states
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
toViewController.view.alpha = 0;
[containerView insertSubview:toViewController.view belowSubview:fromViewController.view];
[containerView addSubview:snapshotView];
fromViewController.topImageView.hidden = YES;
toViewController.bottomImageView.hidden = YES;
CGRect snapshotViewToRect = [containerView convertRect:toViewController.bottomImageView.frame fromView:toViewController.view];
[UIView animateWithDuration:0.3 animations:^{
toViewController.view.alpha = 1;
snapshotView.frame = snapshotViewToRect;
} completion:^(BOOL finished) {
if (finished) {
[snapshotView removeFromSuperview];
fromViewController.topImageView.hidden = NO;
toViewController.bottomImageView.hidden = NO;
// Declare that we've finished
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}
}];
}

为 IYMSecondViewController 添加边缘返回手势

1
2
3
UIScreenEdgePanGestureRecognizer *screenEdgePanGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleScreenEdgePanGesture:)];
screenEdgePanGestureRecognizer.edges = UIRectEdgeLeft;
[self.view addGestureRecognizer:screenEdgePanGestureRecognizer];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void) handleScreenEdgePanGesture:(UIScreenEdgePanGestureRecognizer *) recognizer
{
CGFloat progress = [recognizer translationInView:self.view].x / (self.view.bounds.size.width * 1.0);
progress = MIN(1.0, MAX(0.0, progress));
if (recognizer.state == UIGestureRecognizerStateBegan) {
_percentDrivenInteractiveTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
[self.navigationController popViewControllerAnimated:YES];
}else if (recognizer.state == UIGestureRecognizerStateChanged) {
[_percentDrivenInteractiveTransition updateInteractiveTransition:progress];
}else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) {
if (progress > 0.5f) {
[_percentDrivenInteractiveTransition finishInteractiveTransition];
}else{
[_percentDrivenInteractiveTransition cancelInteractiveTransition];
}
_percentDrivenInteractiveTransition = nil;
}
}