自定义视图控制器的转换动画在2013年的一次WWDC session: Custom Transitions Using View Controllers就已经被提出了。不论是系统的presentation、导航控制器的push||pop以及UITabBarController在转换控制器的时候,都可以自定义转换动画。这个好处是,Apple只是向开发者开放了自定义控制器转换时候的动画的接口,而不会破坏转换之后的控制器层级关系;因而,开发者可以只是关注如何实现一个自定义的动画。

计划用三篇博文来分别示例介绍如何自定义系统的presentation、导航控制器的push||pop以及UITabBarController的点击转换动画。实际上,Apple给的自定义流程对于这三种都是大同小异。从transition的开始到transition的结束,是一次view controller heirachy(视图控制器层级)view heirachy(视图层级)从稳定的初始状态变化到动画中间态,然后再次回到另外的一种稳定状态的过程。

Custom Transitions Using View Controllers有详细的剖析这个过程。具体有如下图片:

视图控制器转换开始和结束状态

图中是当前正在显示的视图控制器A的视图层级被Transition出来的视图控制器B的视图层级替换的始终过程;结束时,视图控制器A的所有视图和子视图将不再出现在当前的window上。如果来研究下面的中间状态的话,有一个很重要的状态发生地:containerView,是自定义动画发生的地方。Apple的设计十分巧妙,给开发者开放一个只在transition过程中才存在的动画容器视图可以让动画和transition过程中的其他视图完全没有耦合;另外,containerView只是在transition过程中才存在,而当开发者在完成自己的自定义动画之后,会调用completeTransition:(待会儿解释这个方法)告知系统动画完成,系统会拆除containerView,将视图层级回归到新的consistent的状态。

视图控制器中间状态&&containerView

实现步骤

以presentation为例:也就是对系统方法- (void)presentViewController:animated:completion: NS_AVAILABLE_IOS(5_0)的弹出动画如何做自定义。

示例代码

效果如下:

效果图

实现转换代理(The Transitioning Delegate)

实现转换代理(The Transitioning Delegate)。transitioning delegate这个名字就揭示它的作用是作为被弹出的视图控制器的转换代理;被弹出的控制器可以在transition的不同时机询问必要的信息来动画地展示(presentation)或者移除(dismissal)自己。这些必要信息包括:

  • 动画对象(Animator objects)。动画对象是遵从于UIViewControllerAnimatedTransitioning协议的对象(该协议规定了转换动画必须的实现的方法,因而遵从该协议的对象被称为动画对象)。动画对象是后面示例代码的重点,它负责将视图以动画的方式去呈现presentation或者去掉dismissal
  • 交互动画对象。用来和手势结合,实现随手势进行的过程而渐变的动画,类似于导航控制器的edge pan来pop掉处于顶部的控制器。
  • 展示控制器(Presentation controller)。presentation controller是用来控制presentation style的,也就是控制了当被弹出的控制器在屏幕上时是以什么style来呈现的。

展示控制器(Presentation controller)将不再这里做介绍;在这篇博文中,也不对交互动画对象(Intractive Animator)做给出示例,而将在下一篇中介绍它。

谁作为被present出的控制器的transitioning delegate会比较合适呢?这个问题需要根据App本身的逻辑来确定,可以将transitioning delegate独立出来,也可以将这个责任给presenting view controller(也就是调用- (void)presentViewController:animated:completion:方法的控制器)。在示例代码中,使用了后者机制(JWVCPViewController.m 100 - 115)。

//JWVCPViewController.m 100 - 115
- (void)collectionView:(UICollectionView *)collectionView
didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.item >= 0 && indexPath.item < self.pictureNames.count) {
        self.selectedCell = [collectionView cellForItemAtIndexPath:indexPath];
        NSString *bookName = self.pictureNames[indexPath.item];
        UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
        NSString *detailVCId = NSStringFromClass([JWVCPBookDetailViewController class]);
        JWVCPBookDetailViewController *bookDetailVC =
        [mainStoryboard instantiateViewControllerWithIdentifier:detailVCId];
        bookDetailVC.bookName = bookName;
        bookDetailVC.transitioningDelegate = self;
        [self presentViewController:bookDetailVC
                           animated:YES
                         completion:nil];
    }
}

如果需要某对象作为被present出的控制器的转换代理的话,需要改对象遵从于UIViewControllerTransitioningDelegate协议(这跟成为任何delegate都要遵从于对应的delegate协议一致)。UIViewControllerTransitioningDelegate只有为数不多的几个方法,所有都是optional的,也就是不实现的话,系统将会使用系统自带的presentation效果。

//分别为presentation和dismissal提供动画对象
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

//分别为presentation和dismissal提供交互动画对象
- (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);

在示例代码中,只是对弹出和收回做了自定义的动画非交互动画,因而只需要实现前面两个方法返回动画对象。

//JWVCPViewController.m 117 - 135
#pragma mark - UIViewControllerTransitioningDelegate
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    self.presentationAnimator.presenting = YES;
    CGRect cellViewRect = [self.selectedCell convertRect:self.selectedCell.bounds toView:self.view];
    self.presentationAnimator.originRect = cellViewRect;
    self.presentationAnimator.originCornerRadius = self.selectedCell.layer.cornerRadius;
    self.selectedCell.hidden = YES;
    return self.presentationAnimator;
}

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    self.presentationAnimator.presenting = NO;
    __weak typeof(self) weakSelf = self;
    self.presentationAnimator.aniCompletion = ^{
        __strong typeof(weakSelf) strSelf = weakSelf;
        strSelf.selectedCell.hidden = NO;
    };
    return self.presentationAnimator;
}

自定义Presentaion||Dismissal的系统调用顺序

  • 当我们在调用系统的- (void)presentViewController:animated:completion: - 弹出或者- (void)dismissViewControllerAnimated:completion: - 收起时,系统或先调用transitioning delegate的对应- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:presentingController:sourceController:或者- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:方法来获取动画对象。
  • 调用动画对象的transitionDuration:来获得动画的时长。
  • 执行动画的对象的animateTransition:方法来执行开发者自定义的动画。
  • 系统等待动画对象完成动画后(或者在合适的时机)来调用系统在animateTransition:等方法中传入的context transitioning objectcompleteTransition:方法来告知动画完成。这之后就是前面提到的系统拆除containerView,构建新的稳定的consistent视图控制器和视图层级,回调presentViewController:animated:completion:的completion block。
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    UIView *bookDetailView = (self.isPresenting) ?
    [transitionContext viewForKey:UITransitionContextToViewKey] :
    [transitionContext viewForKey:UITransitionContextFromViewKey];
    CGRect smallOriginViewRect = self.originRect;
    UIView *containerView = transitionContext.containerView;
    CGRect toViewFrame = [transitionContext viewForKey:UITransitionContextToViewKey].frame;

    CGFloat xScale = fabs(smallOriginViewRect.size.width /
                         bookDetailView.bounds.size.width);
    CGFloat yScale = fabs(smallOriginViewRect.size.height/
                         bookDetailView.bounds.size.height);
    CGAffineTransform scaleTrans = CGAffineTransformMakeScale(xScale, yScale);
    if (self.presenting) {
        bookDetailView.center = (CGPoint){CGRectGetMidX(smallOriginViewRect),
            CGRectGetMidY(smallOriginViewRect)};
        bookDetailView.transform = scaleTrans;
        bookDetailView.layer.cornerRadius = self.originCornerRadius/xScale;
        bookDetailView.layer.masksToBounds = YES;
    }

    [containerView addSubview:bookDetailView];
    [containerView insertSubview:[transitionContext viewForKey:UITransitionContextToViewKey]
                    belowSubview:bookDetailView];
    [UIView animateWithDuration:kAnimationDuration
                          delay:0
         usingSpringWithDamping:0.5
          initialSpringVelocity:0
                        options:0
                     animations:
     ^{
         bookDetailView.center = (self.presenting) ?
         (CGPoint){CGRectGetMidX(toViewFrame), CGRectGetMidY(toViewFrame)} :
         (CGPoint){CGRectGetMidX(self.originRect), CGRectGetMidY(self.originRect)};
         bookDetailView.transform = (self.presenting) ?
         CGAffineTransformIdentity : scaleTrans;
         bookDetailView.layer.cornerRadius = (self.presenting) ? 0 :
         self.originCornerRadius/xScale;
     }
                     completion:
     ^(BOOL finished) {
         if (self.aniCompletion) {
             self.aniCompletion();
         }
         [transitionContext completeTransition:YES];
     }];
}

以上是动画对象中animateTransition:方法中的关键代码。

参考资料