自定義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閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容

  • 自定義轉場動畫 這張圖是自己在翻譯官方文檔Customizing the Transition Animation...
    丨n水瓶座菜蟲灬閱讀 1,197評論 0 3
  • 前言的前言 唐巧前輩在微信公眾號「iOSDevTips」以及其博客上推送了我的文章后,我的 Github 各項指標...
    VincentHK閱讀 5,401評論 3 44
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,155評論 4 61
  • 如果你還在猶豫 那么我會鼓起勇氣擁抱你 如果你喜歡吃酸菜魚 我可以滿世界找最正宗的酸菜 做魚給你吃 如果你不喜歡短...
    禮雪晶閱讀 2,058評論 5 17
  • 讀書的目的不是陷入書中的情節,也不是簡單的娛樂消遣。而是從書中的人物,情節和對話的過程中映射自己的生活、工作。“一...
    朱文軒閱讀 479評論 0 0