向 UINavigationController 的傳統(tǒng)動畫說”再見” — 自定義過場動畫(一)

題外話
看了一眼最近寫的一篇文章, 發(fā)現(xiàn)居然已是兩個多月之前的了, 猛然間警覺到自己近期的產(chǎn)能下降幅度很大啊!(話外音: 咳咳, 話說, 之前也只不過是寫了3篇文章而已, 裝什么職業(yè)寫手>_<) 于是乎, 我決定”改過自新, 重新做人”, 再次執(zhí)起擱置已久的筆, 分享自己的心得!!! 好了, 吐槽到此結(jié)束, 進入正式話題!
我作為純正的半路出家的 iOS 開發(fā)者, 特別希望能夠給同樣處境的朋友們獻上一些自己的所知所學(xué), 同時也作為自己對知識的一種沉淀和總結(jié). 于是乎, 我異想天開的決定開一個超大的坑, 那就是不定期以實戰(zhàn)的形式(所謂實戰(zhàn), 就是以實際開發(fā)項目的形式, 當然, 這里的所謂”項目”都是一些很簡單的項目)來分享一些自己在工作和學(xué)習(xí)中了解到的知識, 可能分享的東西談不上”高端”, 更多的是為了知識的傳播.
在走上 iOS 開發(fā)的道路上后, 前前后后也讀了不少相關(guān)的書籍, 看過很多大神和”所謂的”大神的博客, 其中印象最深的就是 https://www.raywenderlich.com, 這里幾乎以一種手把手的形式去教授你各種 iOS 開發(fā)中能夠用到的知識點. 于是, 我也決定嘗試著以這種形式來寫一寫, 如果大家覺得讀了我的文章之后有那么一丁點兒的收獲, 我的付出就算值得了.
由于這類文章本質(zhì)上是通過一個小 demo 去分享某個知識點, 那么我不希望大家完全從一張白紙開始. 我會為大家提供了相應(yīng)的工程起始文件, 這里面會有已經(jīng)寫好的一些代碼和相關(guān)的素材, 大家可以直接下載來使用.
文章使用的環(huán)境為 Xcode 7.3.1, 語言為 Swift 2.2. 好了, 閑話不多說了, 下面正式開始!


項目準備

想必大家對 UINavigationController 的過場動畫再熟悉不過了. 沒錯! 就是那萬年不變的 push 和 pop 動畫: 在屏幕右側(cè)以從右至左的姿態(tài)滑入, 再從右側(cè)以從左至右的姿態(tài)消失于黑暗中… 當然, 我對此并沒有任何貶義和不滿, 畢竟 Apple 延續(xù)下來的東西是有必然的道理在里面的, 而且我們手機上系統(tǒng)自帶的有導(dǎo)航欄的應(yīng)用都延續(xù)了這種風(fēng)格, 因此這也算是 Apple 血液里的一種基因了吧.
好了, 為了再次一睹這種動畫的風(fēng)采, 我準備好了一份項目的起始文件, 可以從 https://github.com/magiclee203/NavAnimator 下載. 這里我制作了一個非常簡易的圖片瀏覽器, 是通過 UINavigationController 的 push 和 pop 來實現(xiàn)瀏覽功能的.


嗯, 沒錯, 對于上面的動畫, 總結(jié)一下就是: ”沒有任何亮點”… 非常傳統(tǒng)的過場動畫, that’s it.
當當當當, 今天的主題終于來了!!! 少年啊, 你不會天真滴以為 UINavigationController 的過場動畫僅能如此而已吧! 如果你確實這么認為, 那么很抱歉, Apple 令你失望了! (話外音: 咦? 怎么莫名滴感覺這種”失望”反而是件好事兒O)
是的, Apple 賦予了我們強大的自定義能力來重新改寫過場動畫. 那么一定會有小伙伴問了: 我可以自定義到什么程度呢? 我可以將過場動畫制作成多么炫酷呢? 答案就是: 能制約你的過場動畫炫酷程度的因素, 只有你的想象力而已!
So, 小伙伴們, 放飛你們的想象力, 讓自定義來的更猛烈一些吧!!!

項目目標

由于本文的目的在于向大家介紹如何自定義過場動畫, 因此并沒有制作復(fù)雜和華麗的過場動畫, 僅僅是將系統(tǒng)原生的 push 和 pop 效果進行了改動, 最終效果如下:


雖然改動之后的過場動畫完全談不上”驚艷”, 但是我們確實改變了系統(tǒng)原生的東西. 那么, 我們就開始嘍~~

基本概念

既然要自定義過場動畫, 那么首先就要清楚到底何時會出現(xiàn)過場動畫. 能夠出現(xiàn)過場動畫的場合有如下 3 種:

  1. 本文要講到的 navigation controller 在 push 和 pop 其內(nèi)部的 view controller 時, 會有過場動畫.
  2. tabbar controller 在切換其內(nèi)部的 view controller 時, 會有過場動畫. What!!! 意想不到吧, 當你在 tabbar 上點來點去選擇 view controller 時, 其實是有過場動畫的! 只不過... 額... 系統(tǒng)原生的效果也能叫”動畫”? 還是算了吧...
  3. 當你 present 和 dismiss 一個 view controller 時, 會有過場動畫. 這個就很明顯了吧, 系統(tǒng)原生的效果是: present 時從屏幕下方跳出來一個 view controller, dismiss 時這個 view controller 再從下方退出.

以上 3 種情況下出現(xiàn)的過場動畫都是可以自定義的.
那么過場動畫又有幾種類型呢? 有 2 種 (注意, 這里所謂的動畫”類型”與實現(xiàn)出來的動畫”效果”沒有任何聯(lián)系!!)

  1. 無交互效果的過場動畫. 顧名思義, 這種過場動畫就是你無法控制的. 回想一下系統(tǒng) navigation controller 在 push 時, 你什么都不能做, 只能等待這個過場動畫結(jié)束, 然后才能操作 push 出的頁面.
  2. 有交互效果的過場動畫. 再次回想系統(tǒng)原生的 navigation controller. 我想大家都應(yīng)該知道 pop 一個頁面的方法不只有點擊導(dǎo)航欄左上角的返回按鈕吧, 當你按住屏幕的左側(cè), 然后向右滑動時, 這個頁面依然會被 pop, 而且整個過程完全在你的掌控之下, 想滑到哪里就可以滑到哪里. (什么? 莫非有人還不知道這件事兒? 那趕緊打開設(shè)備去試一下吧!) 這就是有交互效果的過場動畫.

有交互效果的過場動畫在某種程度上是依賴于無交互效果的過場動畫的(這種說法可能不太嚴謹, 目前可以這么認為), 因此本文先從無交互效果的過場動畫說起, 我個人覺得也更好理解一些.

無交互效果過場動畫的具體實現(xiàn)

要實現(xiàn)無交互效果的過場動畫, 只需要做 3 件事兒!

  1. 你需要讓 view controller 知道接下來要進行過場動畫了. 以本文為例, 你需要讓 navigation controller 知道 push 或 pop 即將發(fā)生. 因此, 你首先需要為對應(yīng)的 view controller 設(shè)置代理來感知這件事兒.
  2. 當代理知道過場動畫即將開始時, 它會去尋找一個動畫控制器. 這個動畫控制器就是一個遵守了 UIViewControllerAnimatedTransitioning 協(xié)議的東西, 因此, 你可以令任何東西擔負起變?yōu)閯赢嬁刂破鞯穆氊? 只要其遵循 UIViewControllerAnimatedTransitioning 協(xié)議即可. 一旦代理找到了動畫控制器, 那么就執(zhí)行動畫控制器定義的過場動畫, 反之如果沒有找到動畫控制器, 那么系統(tǒng)默認的過場動畫就會被執(zhí)行.
  3. 去動畫控制器中實現(xiàn)具體的動畫. 由于動畫控制器遵守了 UIViewControllerAnimatedTransitioning 協(xié)議, 那么就需要實現(xiàn)該協(xié)議中的兩個 required 方法, 分別為:
    (1) transitionDuration: 方法. 這個方法返回了自定義動畫的執(zhí)行時間.
    (2) animateTransition: 方法. 整個自定義動畫的核心, 到底要執(zhí)行什么樣的動畫均在該方法中定義.

好了, 了解了自定義過場動畫的整體步驟后, 我們就直接擼代碼吧!


先大致說明一下工程起始文件的結(jié)構(gòu).(我是個偏執(zhí)的代碼黨, 所以幾乎不使用 storyboard 和 xib, 望大家諒解>_<)
> 由于要自定義 navigation controller 的過場動畫, 后續(xù)對其會進行一些操作, 所以沒有直接使用 UINavigationController, 而是自定義了一個 DTNavController, 繼承自UINavigationController.
> DTViewController 就是實際展示圖片的 view controller.


Step 1

首先要讓 DTNavController 知道要執(zhí)行過場動畫了, 因此, 我們需要為其設(shè)置代理. 讓 DTNavController 自己通知自己最好不過了, 所以我們將其自身設(shè)置為代理. 別忘了在設(shè)置代理前, 要先遵守 UINavigationControllerDelegate 協(xié)議.
我們直接在 DTNavController 的 viewDidLoad 方法中來操作.

class DTNavController: UINavigationController, UINavigationControllerDelegate {
      override func viewDidLoad() {
          super.viewDidLoad()
          self.delegate = self
      }
}

Step 2

設(shè)置了代理后, DTNavController 的代理會在即將進行過場動畫時去尋找動畫控制器, 因此我們要提供一個動畫控制器.
一旦 DTNavController 要執(zhí)行過場動畫, 它的代理(目前就是其自身)就會收到如下消息:
navigationController:animationControllerForOperation:fromViewController:toViewController:.
可以看到, 這個消息有返回值, 并且返回值是一個遵守了 UIViewControllerAnimatedTransitioning協(xié)議 的東西, 這正是我們需要的動畫控制器.
因此在 DTNavController 類中實現(xiàn) UINavigationControllerDelegate 協(xié)議 里的方法, 并返回一個動畫控制器.

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
     // 這里要 return 一個遵守了 UIViewControllerAnimatedTransitioning 協(xié)議的東西
}

好了, 我們接下來要做的就是去制造一個動畫控制器.

Step 3

動畫控制器是任何遵循了 UIViewControllerAnimatedTransitioning協(xié)議 的東西, 所以我們完全可以讓 DTNavController 自己來做這件事兒. 但考慮到代碼結(jié)構(gòu)的合理性, 單獨創(chuàng)建一個動畫控制器類來做這件事兒是更合理的.
于是乎, 我們的動畫控制器 DTAnimationController 就這樣誕生了, 而且不要忘了實現(xiàn) 2 個重要的方法

class DTAnimationController: NSObject, UIViewControllerAnimatedTransitioning {     
      func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
       // 1
         return 0.4
      }
 
      func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
       // 2
      }
 } 
  1. 返回過場動畫的持續(xù)時間
  2. 具體執(zhí)行的過場動畫

最終 BOSS 戰(zhàn) — 過場動畫的具體實現(xiàn)

首先要介紹一個非常重要的小伙伴. 大家可能已經(jīng)看到了, transitionDuration:animateTransition: 這兩個方法都有一個參數(shù) transitionContext, 這是個遵守了 UIViewControllerContextTransitioning 協(xié)議 的東西(如果一定要翻譯過來的話應(yīng)該是叫”過場上下文”, 總感覺好拗口, 下文中就直接用英文名稱 transitionContext 了).
transitionContext 這個東西非常給力, 它能提供給你本次過場動畫涉及到的方方面面的東西, 包括:

  1. 一個容器 view(containerView). 你可以把這個容器 view 想象成一張大大的畫板, 你將要執(zhí)行的過場動畫就是在這張畫板上展現(xiàn)出來的.
  2. 要消失的 view controller 和要顯現(xiàn)的 view controller.
  3. 要消失的 view 和要顯現(xiàn)的 view. 通常情況下, 這兩個 view 就是對應(yīng)的 view controller 的 view, 但以防萬一, transitionContext 直接將這兩個 view 提供給了我們, 多么貼心啊!
  4. 要消失的 view 的起始 frame 和要顯現(xiàn)的 view 的終止 frame.
  5. 要消失的 view 已經(jīng)被添加到容器 view 上了.

怎么樣? 是不是有點兒蒙圈了, 這都什么跟什么啊… >_<
來, 希望通過下面的一系列圖示為你理清上述內(nèi)容的關(guān)系. 就以實現(xiàn)這個過場動畫的效果為例:



對這個過場動畫而言, 要消失的 view 和 view controller 是路飛(本質(zhì)上來說他們并沒有消失, 還是在原地, 只不過被覆蓋住了, 因此所謂的”要消失”是指視覺上的看不到了), 要顯現(xiàn)的 view 和 view controller 是索隆.



所以, 所謂的自定義過場動畫, 就是由我們來填補這兩個狀態(tài)之間的空白, 僅此而已!
如何填補呢? 做下面兩件事兒就足夠了:
  1. 將要顯現(xiàn)的 view 添加到容器 view 的正確起始位置上
  2. 對要顯現(xiàn)的 view 做動畫. 當然了, 如果你想對要消失的 view 做動畫也是完全可以的.

對于我們的這個效果而言, 只要對要顯現(xiàn)的 view (索隆)做動畫就行了, 要消失的 view 可以不動, 見下圖:



沒錯, 就是這樣, 看似很厲害的自定義過場動畫就這么搞定了!! 其實并沒有你想象的那么難! 來, 讓我們歡快滴擼一會兒代碼吧!

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    // 1
    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
    let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!

    // 2     
    let toViewEndFrame = transitionContext.finalFrameForViewController(toViewController)
    var toViewStartFrame = toViewEndFrame
    toViewStartFrame.origin.y -= toViewEndFrame.size.height
     
    let containerView = transitionContext.containerView()!
    containerView.addSubview(toView)
    toView.frame = toViewStartFrame
   
    // 3  
    UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: {
        toView.frame = toViewEndFrame
    }, completion: { _ in
       // 4
        transitionContext.completeTransition(true)
    })
}
  1. 首先通過 transitionContext 拿到了要顯現(xiàn)的 view controller 和 view.
  2. 通過要顯現(xiàn)的 view controller 拿到要顯現(xiàn)的 view 的終止位置, 進而計算出要顯現(xiàn)的 view 的起始位置, 并將要顯現(xiàn)的 view 加到了容器 view 上.
  3. 執(zhí)行動畫, 動畫的效果就是使要顯現(xiàn)的 view 出現(xiàn)在其終止位置上.

咦? 第 4 步是怎么回事兒? 細心的你應(yīng)該發(fā)現(xiàn)了, 在動畫結(jié)束的時候還做了一步事情. 千萬注意!!! 這一步非常非常非常的重要!!! 即是沒有說三遍, 這件事兒也是相當重要的!!!
當自定義的過場動畫結(jié)束后, 你一定不要忘記通知 transitionContext, 告訴它過場動畫已經(jīng)執(zhí)行完了, 向其發(fā)送 completeTransition: 消息即可.
最后一步, 我們只要回到 DTNavController 中, 將上述我們制造的動畫控制器作為其代理方法的返回值即可.

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
     let animationController = DTAnimationController()
     return animationController
}

至此, 終極 BOSS 已經(jīng)被我們征服. 恭喜你勇士, 你又 get 了一個新技能 O 趕快來看看我們自定義的過場動畫吧.


程序的完善

你應(yīng)該發(fā)現(xiàn)了, 盡管上述程序已經(jīng)可以實現(xiàn)自定義的過場動畫, 但還是有些缺陷. 當我們點擊導(dǎo)航欄上左上角的返回按鈕時, pop 的過場動畫居然也是”從天而降”的, 顯然不太合理. 既然 push 的方式是從天而降, 那么 pop 應(yīng)該是”一飛沖天”的方式才對!
為此, 我們需要修改一部分代碼.

1. navigation controller 的代理方法

navigationController:animationControllerForOperation:fromViewController:toViewController: 有一個參數(shù) operation, 這是個枚舉值, 它可以告訴你當前要執(zhí)行的是 push 操作還是 pop 操作. 因此, 我們可以通過這個值來執(zhí)行不同的過場動畫.
你可能想到再寫一個 pop 操作的動畫控制器, 將這個動畫控制器和之前我們完成的 DTAnimationController 區(qū)分開. 這完全可以, 但只要稍加處理, 我們還是可以靠一個 DTAnimationController 來同時完成 push 和 pop 的動畫的.

2. 為 DTAnimationController 添加屬性 operation

這個屬性對應(yīng)著 navigation controller 的 push 和 pop 操作.
var operation: UINavigationControllerOperation = .None
回到 DTNavController 中, 修改代理方法如下:

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
     let animationController = DTAnimationController()
     animationController.operation = operation
     return animationController
}

這樣, 我們的動畫控制器就知道該執(zhí)行何種動畫效果了.

3. 修改 DTAnimationController 的動畫效果

有了寫 push 動畫的經(jīng)驗, 再寫一個 pop 動畫應(yīng)該不在話下了吧.
借此回顧一下: 只要我們將要顯現(xiàn)的 view 加到容器 view 的正確起始位置上, 然后再根據(jù)實際的需求對要顯現(xiàn)的 view 和/或 要消失的 view 執(zhí)行動畫就可以了.
根據(jù)我們的需求, 寫 pop 動畫時, 要顯現(xiàn)的 view 和 要消失的 view 要執(zhí)行的事情如下:

  1. 要顯現(xiàn)的 view 擺放在終止位置不動, 對要消失的 view 做”一飛沖天”的動畫即可.
  2. 由于要消失的 view 已經(jīng)在容器 view 上了, 那么在容器 view 上添加了要顯現(xiàn)的 view 之后, 要顯現(xiàn)的 view 就會覆蓋在要消失的 view 上面, 這種情況下, 你是看不到要消失的 view 在執(zhí)行 pop 動畫. 為了解決這個問題, 在執(zhí)行動畫之前, 需要調(diào)整要顯現(xiàn)的 view 和 要消失的 view 在容器 view 中的層級關(guān)系, 即應(yīng)該將要顯現(xiàn)的 view 放到要消失的 view 的下面.

不多說了, 擼段代碼瞧瞧就知道了.

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
     // 1
     let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
     let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
     let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
     let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
     
     let containerView = transitionContext.containerView()!
     containerView.addSubview(toView)
     
     let fromViewStartFrame = transitionContext.initialFrameForViewController(fromViewController)
     let toViewEndFrame = transitionContext.finalFrameForViewController(toViewController)
     var fromViewEndFrame = fromViewStartFrame
     var toViewStartFrame = toViewEndFrame
 
     // 2   
     if operation == .Push {
         toViewStartFrame.origin.y -= toViewEndFrame.size.height
     } else if operation == .Pop {
         fromViewEndFrame.origin.y -= fromViewStartFrame.size.height
         containerView.sendSubviewToBack(toView)
     }
     
     fromView.frame = fromViewStartFrame
     toView.frame = toViewStartFrame
    
     // 3 
     UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: {
         fromView.frame = fromViewEndFrame
         toView.frame = toViewEndFrame
     }, completion: { _ in
        // 4
         transitionContext.completeTransition(true)
     })
}
  1. 還是老套路, 將需要的參數(shù)先提取出來. 不過這一次因為要應(yīng)對 push 和 pop 的兩種情況, 因此要將要顯現(xiàn)的 view 和要消失的 view 的信息全部取出.
  2. 根據(jù) push 和 pop 來配置相應(yīng) view 的 frame. 要注意, 在執(zhí)行 pop 動畫操作前, 不要忘記將要顯現(xiàn)的 view 放到底層, 否則就會覆蓋在要消失的 view 上面.
  3. 執(zhí)行動畫.
  4. 千萬別忘了通知 transitionContext, 過場動畫已經(jīng)執(zhí)行完畢!!

好了, 至此, 徹底的大功告成!!!
如果對代碼部分有疑惑, 可以去 https://github.com/magiclee203/NavAnimator 下載工程結(jié)束時的代碼.

小作業(yè)

如果你有興趣, 你可以嘗試如何為 tabbar controller 添加能明顯看到的過場動畫. 當你在不同的 view controller 之間切換時, 展現(xiàn)一些酷炫的視覺效果吧!

下期預(yù)告

不要大意, 這里只是一小步! 我們實現(xiàn)了無交互效果的自定義過場動畫, 動畫執(zhí)行的整個過程我們都無法參與. 如果你想依靠手勢來實現(xiàn)可交互的過場動畫, 就像系統(tǒng)原生的 navigation controller 可以依靠拖拽來實現(xiàn) pop 效果那樣, 那么敬請期待下一期吧!!!

(話外音: 喂喂! 下一期到底什么時候來啊?)
額... 盡量不太晚吧…
(話外音: 真是個靠不住的作者啊…)
(話外音2: 有收獲嗎? 有收獲就打個賞吧, 哈哈哈哈!!!)

開玩笑啦, 只要你們有收獲, 就是對我最大的支持. 不過由于作者還有班要上, 所以更新時間的問題嘛, 大家就不要太苛刻了, 吼吼吼吼!!!!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容