iOS 控制器转场iOS开发基础知识移动前端-iOS

自定义UINavigationController内的转场动画

2015-12-10  本文已影响6032人  卖萌凉

之前略微尝试了自定义view controller间的转场动画,然后发现,其实UINavigationController也可以自定义push和pop的转场动画,便也写了个demo实验了一下。

代码放在这里->github


自定义push和pop动画



还是以最老土的zoom效果来举例好了(⊙ω⊙)

首先我们定义了XSQMasterViewControllerXSQDetailViewController这两个视图控制器,它们在同一个导航栈中,当点击XSQMasterViewController中的一个按钮时,XSQDetailViewController就会被push到导航栈中展现出来。

为了自定义这一转场动画,需要给XSQNavigationController对象设定一个delegate。这个delegate对象需要实现UINavigationControllerDelegate接口,其中有两个方法和转场动画有关,分别是:

- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC

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

第一个方法可以用来自定义一个不带用户交互的转场动画,而第二个方法可以为这个动画添加用户交互。

然后,我们需要创建一个转场动画对象,来作为第一个方法的返回值。如果给第一个方法返回nil,则UIKit会使用默认的转场动画效果。

创建一个类,我把它命名为XSQExpandAnimatorObject,它需要实现UIViewControllerAnimatedTransitioning协议。这个类中,定义了XSQDetailViewControllerXSQMasterViewController上展开的动画。

我这样实现了在XSQExpandAnimatorObject中这样实现了UIViewControllerAnimatedTransitioning中的两个方法:

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
    return 1.0;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    
    CGRect thumbFrame = [[transitionContext containerView] convertRect:self.thumbView.bounds fromView:self.thumbView];
    [toView setFrame:thumbFrame];

    [[transitionContext containerView] addSubview:toView];
     
    CGRect toViewFinalFrame = [transitionContext finalFrameForViewController:toVC];
    [UIView animateWithDuration:[self transitionDuration:transitionContext]
                     animations:^{
                         [toView setFrame:toViewFinalFrame];
                     }
                     completion:^(BOOL finished) {
                         if (![transitionContext transitionWasCancelled]) {
                             [fromView removeFromSuperview];
                             [transitionContext completeTransition:YES];
                         }
                         else {
                             [toView removeFromSuperview];
                             [transitionContext completeTransition:NO];
                         }
                     }];
}

然后将一个XSQExpandAnimatorObject的对象作为UINavigationControllerDelegate第一个方法的返回值返回:

- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC {
    
    if (operation == UINavigationControllerOperationPush && [fromVC isKindOfClass:[XSQMasterViewController class]] && [toVC isKindOfClass:[XSQDetailViewController class]]) {
        XSQMasterViewController *masterViewController = (XSQMasterViewController *)fromVC;
        return [[XSQExpandAnimatorObject alloc] initWithThumbView:masterViewController.thumbView];
    }
    return nil;
}

这样,当一个XSQDetailViewController被push到XSQMasterViewController之上时,便会使用我们自定义的zoom效果。

反向的pop动画的实现方式也类似。


用UIPercentDrivenInteractiveTransition为转场动画添加交互



在完成了没有交互的自定义转场动画后,我尝试了为转场动画添加简单的交互。最简单的方式应该就是利用UIKit提供的UIPercentDrivenInteractiveTransition类了,这个类已经实现了UIViewControllerInteractiveTransitioning协议,第三方程序员可以通过这个类的对象指定转场动画的完成百分比。

比如我们可以在XSQDetailViewController中添加一个手势,当用户下拉时执行pop操作,并且转场动画随着用户下拉的幅度运动:

- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer {
    
    UIWindow *window = [[UIApplication sharedApplication] keyWindow];
    
    static CGFloat beginY;
    CGFloat currentY = [gestureRecognizer translationInView:window].y;
    CGFloat percent = (currentY - beginY) / CGRectGetHeight(window.bounds);
    
    switch (gestureRecognizer.state) {
        case UIGestureRecognizerStateBegan:
            beginY = [gestureRecognizer translationInView:window].y;
            [self.navigationController popViewControllerAnimated:YES];
            break;
        case UIGestureRecognizerStateChanged:
            [self.interactiveTransition updateInteractiveTransition:percent];
            break;
        case UIGestureRecognizerStateEnded:
            if (percent > 0.5) {
                [self.interactiveTransition finishInteractiveTransition];
            }
            else {
                [self.interactiveTransition cancelInteractiveTransition];
            }
            break;
        default:
            break;
    }
}

调用UIPercentDrivenInteractiveTransitionupdateInteractiveTransition:方法可以控制转场动画进行到哪了,当用户的下拉手势完成时,调用finishInteractiveTransition或者cancelInteractiveTransition,UIKit会自动执行剩下的一半动画,或者让动画回到最开始的状态。


对比UINavigationController的默认转场动画



在写这个demo的时候,我还想到了一些问题。嗯,其实更多的时间是花在想这些问题上(⊙ω⊙)。

一. 在push的过程中,这个XSQDetailViewController对象是什么时候进入导航栈的呢?而在pop的过程中,它又是什么时候被移出导航栈的呢?



我曾以为addChildViewController:removeFromParentViewController的操作也是需要第三方程序员在animateTransition:方法中完成,后来发现UIKit已经为我们做好了。

在push的过程中,UINavigationControllerpushViewController:animated:方法引起了对XSQDetailViewControllerwillMoveToParentViewController:方法的调用,而自定义动画完成时的[transitionContext completeTransition:YES];则引起了对XSQDetailViewControllerdidMoveToParentViewController:方法的调用。

willMoveToParentViewController:方法被调用 didMoveToParentViewController:方法被调用

比较神奇的是,XSQNavigationController中的addChildViewController:方法却没有被调用,估计是UIKit直接通过私有方法完成了这个操作。

类似的,在pop的过程中,popViewControllerAnimated:方法引起了对XSQDetailViewControllerwillMoveToParentViewController:方法的调用,自定义动画完成时的[transitionContext completeTransition:YES];则引起了对XSQDetailViewControllerdidMoveToParentViewController:方法的调用。

willMoveToParentViewController:方法被调用 didMoveToParentViewController:方法被调用

以及对称的,XSQDetailViewController中的removeFromParentViewController也没有被调用到。

以上也说明了,在自定义转场动画时,对transitionContext调用completeTransition:是非常重要的。如果没有调用这个方法,UIKit会认为转场动画仍然在进行,导致之后XSQDetailViewController的种种状态都是错误的。

二. 应该在什么时候将XSQMasterViewController的视图从视图层次中移除?



如果不自定义转场动画,而是使用UINavigationController默认的转场动画,会发现当push动画完成后,XSQDetailViewController的视图完全遮盖住了XSQMasterViewController的视图,此时XSQMasterViewController的视图已经不在视图层次结构中了。

XSQMasterViewController的视图是如何从视图层次结构中被移除的呢?重写XSQMasterViewControllerloadView方法,让XSQMasterViewController的根视图使用自定义的XSQView类的对象,然后重写XSQViewremoveFromSuperview方法,会发现,当默认的转场动画结束时,removeFromSuperview方法被调用了:

removeFromSuperview方法被调用

可能苹果是出于性能的考虑,只显示导航栈中栈顶视图控制器的视图。所以在实现自定义转场动画的时候,我也在动画结束时将XSQMasterViewController的视图从视图层次中移除了。

三. viewWillAppear等方法真的和视图什么时候被显示有关么



如果自定义转场动画中,animateTransition:中什么也不做,XSQDetailViewControllerviewWillAppear方法也会被调用。

viewWillAppear方法被调用

说明viewWillAppear方法的调用,和视图到底有没有显示出来似乎并没有什么关系。

四. navigationController属性是什么



苹果的注释是这样写的:

The nearest ancestor in the view controller hierarchy that is a navigation controller. (read-only)
If the view controller or one of its ancestors is a child of a navigation controller, this property contains the owning navigation controller. This property is nil if the view controller is not embedded inside a navigation controller.

说明navigationController会返回距离当前视图控制器最近的、类型为UINavigationController的祖先视图控制器。

五. 自定义一个容器类



已经可以为UINavigationController自定义转场动画,是不是再进一步,我们可以自定义一个容器类呢?
然而稍微查了查,原来自定义一个容器类还有许多工作要做。发现了这篇文章Custom Container View Controller,觉得很厉害(☆_☆)


参考

Custom transitions on iOS 7 & a little bit about UX
UINavigationController Class Reference

上一篇下一篇

猜你喜欢

热点阅读