自定义UINavigationController内的转场动画

之前略微尝试了自定义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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 自定义转场动画 这张图是自己在翻译官方文档Customizing the Transition Animation...
    丨n水瓶座菜虫灬阅读 1,088评论 0 3
  • 前言的前言 唐巧前辈在微信公众号「iOSDevTips」以及其博客上推送了我的文章后,我的 Github 各项指标...
    VincentHK阅读 5,241评论 4 44
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,614评论 4 59
  • 如果你还在犹豫 那么我会鼓起勇气拥抱你 如果你喜欢吃酸菜鱼 我可以满世界找最正宗的酸菜 做鱼给你吃 如果你不喜欢短...
    礼雪晶阅读 1,946评论 5 17
  • 读书的目的不是陷入书中的情节,也不是简单的娱乐消遣。而是从书中的人物,情节和对话的过程中映射自己的生活、工作。“一...
    朱文轩阅读 433评论 0 0