O’s World

努力!奋斗!

iOS 7 交互式过渡

| 评论

本文转载自:NONOMORI

iOS 7 新加入了一个介于 ViewController 之间的过渡的实现方法。本文将介绍如何利用自定义的过渡实现如图所示效果。

在这个例子中,我们将在两个 ViewConrtoller 的转换过程中加入一个自定义的过渡。DSLFirstViewController 是我们的第一个 viewController,其包含一个 CollectionView,每一个 Cell 都包含一张图片和一个标签。DSLSecondViewController 是我们的第二个 viewController,其上有一张图和一个标签。我们希望,当用户点击 DSLFirstViewController 的 Cell 后能平滑过渡到 DSLSecondViewController 中去。

这个例子源代码已发布在 GitHub

实现自定义过渡

过渡是由使用了 UIViewControllerAnimatedTransitioning 协议的对象来实现的。我们现在新建一个继承自 NSObject 的类,取名 DSLTransitionFromFirstToSecond。将上面提到的协议加入该类,然后就可以使用他来实现我们的两个类的过渡效果了。

在这个对象中,有两个方法需要实现:animateTransition:transitionDuration:。后者相当直观,就是这个过渡的持续时间,我们只要简单返回一个 NSTimeInterval 值就行。

1
2
3
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.3;
}

animateTransition: 方法是定义两个 ViewController 之间过渡效果的地方。这个方法会传递给我们一个参数,该参数可以让我们访问一些实现过渡所必须的对象。

  • viewControllerForKey:我们可以通过他访问过渡的两个 ViewController。
  • containerView:两个 ViewController 的 containerView。
  • initialFrameForViewController 和 finalFrameForViewController 是过渡开始和结束时每个 ViewController 的 frame。

现在我们开始这个方法的具体实现。首先我们需要得到过渡前后两个 ViewController 以及他们的 containerView 的指针。

1
2
3
4
5
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    DSLFirstViewController *fromViewController = (DSLFirstViewController*)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    DSLSecondViewController *toViewController = (DSLSecondViewController*)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    UIView *containerView = [transitionContext containerView];

接下来,获得我们需要过渡的 Cell,并且对它上面的 imageView 截图。这个截图就会用在我们的过渡效果中。同时,我们将这个 imageView 本身隐藏,从而让用户以为是 imageView 在移动的。

1
2
3
4
5
// 获得cell上imageView的截图
DSLThingCell *cell = (DSLThingCell*)[fromViewController.collectionView cellForItemAtIndexPath:[[fromViewController.collectionView indexPathsForSelectedItems] firstObject]];
UIView *cellImageSnapshot = [cell.imageView snapshotView];
cellImageSnapshot.frame = [containerView convertRect:cell.imageView.frame fromView:cell.imageView.superview];
cell.imageView.hidden = YES;

然后,我们对第二个 viewController 进行设置,将它的放到过渡后的位置,但让他完全透明,我们会在过渡时给它一个淡入的效果。

1
2
3
4
5
6
7
// 初始化一开始的状态
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
toViewController.view.alpha = 0;
toViewController.imageView.hidden = YES;

[containerView addSubview:toViewController.view];
[containerView addSubview:cellImageSnapshot];

现在来做 view 的动画,移动之前生成的 imageView 的截图,淡入第二个 viewController 的 view。在动画结束后,移除 imageView 的截图,让第二个 view 完全呈现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[UIView animateWithDuration:duration animations:^{
    // 淡入第二个viewController的view
    toViewController.view.alpha = 1.0;

    // 将截图放到第二个viewController的imageView上
    CGRect frame = [containerView convertRect:toViewController.imageView.frame fromView:toViewController.view];
    cellImageSnapshot.frame = frame;
} completion:^(BOOL finished) {
    // Clean up
    toViewController.imageView.hidden = NO;
    cell.hidden = NO;
    [cellImageSnapshot removeFromSuperview];

    // 声明过渡结束
    [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
}

记住,一定别忘了在过渡结束时调用 completeTransition: 这个方法。

使用自定义过渡

到目前为止,我们实现了自定义过渡对象,不过我们并没有告知 UINavigationController 去使用它。接下来,将介绍我们如何做到这一点。

当一个新的 viewController 被推入或者弹出它的导航堆,它将询问它的代理,是否有一个使用了 UIViewCOntrollerAnimatedTransitioning 协议的对象,我们现在要做的,就是提供这个对象使得过渡能够展现。

首先是把 UINavigationControllerDelegate 协议加入到 DSLFirstViewController 中去。

1
@interface DSLFirstViewController ()<UINavigationControllerDelegate>

我们还需要给 navigationController 的 delegate 赋值。一个比较理想的地方是在 viewDidAppear:

1
2
3
4
5
6
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    // 让我自己变成navigationController的delegate
    self.navigationController.delegate = self;
}

别忘了在 view 消失时,把 navigationController 的 delegate 去除。

1
2
3
4
5
6
7
8
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    // 我不再是 navigationController 的代理啦
    if (self.navigationController.delegate == self) {
        self.navigationController.delegate = nil;
    }
}

现在我们可以开始实现这个长长名字的 UINavigationControllerDelegate 的方法。

1
2
3
4
5
6
7
8
9
10
11
12
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                  animationControllerForOperation:(UINavigationControllerOperation)operation
                                               fromViewController:(UIViewController *)fromVC
                                                 toViewController:(UIViewController *)toVC {
    // 检查一下是不是过渡到DSLSecondViewController
    if (fromVC == self && [toVC isKindOfClass:[DSLSecondViewController class]]) {
        return [[DSLTransitionFromFirstToSecond alloc] init];
    }
    else {
        return nil;
    }
}

That’s it. 当第二个 viewController 被推入进来时,navigationController 将使用我们自定义的过渡。

要实现弹回时的过渡效果,还是一样的方法,实现一个新的 DSLTransitionFromSecondToFirst 类用来过渡即可。

让过渡变得可以交互

现在我们有自定义过渡了,是时候加入交互了。我们希望让这个过渡在用户手指从屏幕左边边缘划入时产生互动。为了做到这点,我们将使用一个 iOS 7 新加入的手势识别器, UIScreenEdgePanGestureRecognizer

我们在第二个 viewController 的 viewDidLoad 方法中,创建这个手势识别器。

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad {
    [super viewDidLoad];

    ...

    UIScreenEdgePanGestureRecognizer *popRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopRecognizer:)];
    popRecognizer.edges = UIRectEdgeLeft;
    [self.view addGestureRecognizer:popRecognizer];
}

现在我们可以识别该手势了,我们用它来设置并更新一个 iOS 7 新加入的类的对象。 UIPercentDrivenInteractiveTransition。这个类的对象会根据我们的手势,来决定我们的自定义过渡的完成度。我们把这些都放到手势识别器的 action 方法中去,具体就是:

当手势刚刚开始,我们创建一个 UIPercentDrivenInteractiveTransition 对象,然后让 navigationController 去把当前这个 viewController 弹出。

当手慢慢划入时,我们把总体手势划入的进度告诉 UIPercentDrivenInteractiveTransition 对象。

当手势结束,我们根据用户的手势进度来判断过渡是应该完成还是取消并相应的调用 finishInteractiveTransition 或者 cancelInteractiveTransition 方法.

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
- (void)handlePopRecognizer:(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) {
        // 创建过渡对象,弹出viewController
        self.interactivePopTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
        [self.navigationController popViewControllerAnimated:YES];
    }
    else if (recognizer.state == UIGestureRecognizerStateChanged) {
        // 更新 interactive transition 的进度
        [self.interactivePopTransition updateInteractiveTransition:progress];
    }
    else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) {
        // 完成或者取消过渡
        if (progress > 0.5) {
            [self.interactivePopTransition finishInteractiveTransition];
        }
        else {
            [self.interactivePopTransition cancelInteractiveTransition];
        }

        self.interactivePopTransition = nil;
    }
}

现在我们可以创建并更新 UIPercentDrivenInteractiveTransition 对象了,我们需要告诉 navigationController 去用它。为此,我们需要实现另一个 UInavigationControllerDelegate 的方法。

1
2
3
4
5
6
7
8
9
10
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                         interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
    // 检查是否是我们的自定义过渡
    if ([animationController isKindOfClass:[DSLTransitionFromSecondToFirst class]]) {
        return self.interactivePopTransition;
    }
    else {
        return nil;
    }
}

至此,我们第二个 viewController 回到第一个 viewController 的过渡就可以交互了。

尾声

希望这篇文章能帮你理解如何创建你自己的自定义过渡及其交互。文中例子的工程文件已上传至 GitHub