你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 自定義 push 和 pop 實現有趣的相冊翻開效果(上)

自定義 push 和 pop 實現有趣的相冊翻開效果(上)

編輯:IOS開發基礎

本文是投稿文章,作者:seedante(簡書)


效果預覽:

27.gif

蘋果自家應用 Photos 裡點擊相冊後的動畫是非常精妙的,而且是可交互的。我有類似的動畫需求,上面是我自己的設計效果。本指南分上下兩篇,分別探討非交互和交互動畫的實現,從入門到深入,並搜集了實現過程中遇到的一些陷阱,對於想深入的人我想說這兩篇文章不會浪費你的時間。

本文是將三個月前的 Demo 重構後重新寫的,重構後,這個效果可以方便地在你的工程中使用,僅需添加幾行代碼和幾個簡單的設置。效果適用場景:兩個UICollectionViewController類之間的 push 和 pop 操作。Demo 是個小型的相冊浏覽器, 這完全是基於我的需求來做的,因此在初期並沒有考慮做成一個手把手教你實現這個效果的教程,不過前面說了,僅需添加幾行代碼就可在你的工程裡使用,花上幾分鐘搭建一個場景照著做下來也是沒問題的。另外,部分細節比較繁瑣,都放進文章裡就太長了,想了解的話看源代碼,遇到這部分我會提示的。

Demo 地址:SDECollectionViewAlbumTransition。

動畫分析

我把 iOS 裡的動畫分為兩種:趣味動畫和邏輯動畫,前者比如一些加載場景的動畫,用來消磨時間,怎麼炫酷都可以,後者是符合場景變化的動畫,符合邏輯最重要,如果還能很有趣那就更好了。我實現的效果算得上符合邏輯,離有趣或者酷還有點距離。

如上所示,我希望呈現出打開相簿後照片飛出來的效果,這個設計是行為上的擬物,最好翻開封面時還能發出金光,NO,NO,太浮誇了,簡直跟中華小當家或者國產奇幻劇開寶箱似的。當然,主要是我不知道怎麼做,會做的話我就會做出來給大家看的,不過,我是不會把這種效果放在正常的產品裡的,在游戲界這種效果比較常見,比如爐石裡新卡牌點開時就帶這種聖光效果。

從技術上講,這個動畫本質上就是個 View Controller Transition 加上多個元素協作進行動畫的過程。總的來說,動畫分為兩個部分,首先是自定義 push 和 pop,其次是各種元素的協作。現在先攻克第一個難點,下面進入科普時間。

View Controller Transition 視圖控制器轉換

對於這個話題,我推薦:1. WWDC13 上的 Custom Transitions Using View Controllers,2.Custom Transitions on iOS,3. Objc.io 的自定義 ViewController 容器轉場。以及一個自定義 transition 效果的庫:VCTransitionsLibrary,可以讀讀代碼看看這些效果怎麼實現的。

自定義 transition 類型    

View Controller Transition 是什麼?其實平時你就一直能看到,在切換或是添加新的視圖控制器來顯示視圖的時候發生的過程就是 ViewController Transition,比如 push 或 pop 一個 View Controller,在 TabBarController 中切換到其他 View Controller,以模態方式顯示另外一個 View Controller。只不過,在 iOS 7 之前我們無法干涉這個過程,從 iOS 7 開始支持自定義 View Controller Transition,目前僅支持以下四種自定義類型:

011.png

iOS 支持的的自定義視圖轉換類型 from WWDC13 #218

除了最後一個是布局轉換,前三種基本囊括了 iOS 中顯示切換視圖的全部方式:

1.Modal 視圖的顯示和消失;

2.TabBar Controller 在子視圖中切換;

3.Navigation Controller 推入和推出視圖。

其中 presentations and dismissals 只支持 UIModalPresentationFullScreen 和 UIModalPresentationCustom 這兩種 Modal 視圖的顯示和消失。在 iOS 8 中推出了 UIPresentationController 類對 Modal 視圖的顯示和消失進行了增強,增加了對自定義 Modal 視圖尺寸的支持,自定義 Modal 視圖尺寸這在以往是很難做到的(反正我還沒有找到好的方法)。

文章開頭的效果是第三種,需要實現自定義 push 和 pop。

Transition Protocol

iOS 提供了幾套 protocol 來滿足自定義 transition 的需求。

012.png

WWDC13#218-Custom Transition 的構成

對以上 protocol 的解釋節選自 Objc.io 的自定義 ViewController 容器轉場:

iOS 7 自定義視圖控制器轉場的 API 基本上都是以協議的方式提供的,這也使其可以非常靈活的使用,因為你可以很簡單地將它們插入到你的類中。最主要的五個組件如下:

1.動畫控制器 (Animation Controllers) 遵從 UIViewControllerAnimatedTransitioning 協議,並且負責實際執行動畫。

2.交互控制器 (Interaction Controllers) 通過遵從 UIViewControllerInteractiveTransitioning 協議來控制可交互式的轉場。

3.轉場代理 (Transitioning Delegates) 根據不同的轉場類型方便的提供需要的動畫控制器和交互控制器。

4.轉場上下文 (Transitioning Contexts) 定義了轉場時需要的元數據,比如在轉場過程中所參與的視圖控制器和視圖的相關屬性。 轉場上下文對象遵從 UIViewControllerContextTransitioning 協議,並且這是由系統負責生成和提供的。

5.轉場協調器(Transition Coordinators) 可以在運行轉場動畫時,並行的運行其他動畫。 轉場協調器遵從 UIViewControllerTransitionCoordinator 協議。

看暈了?沒關系。這五個組件並不是全部都需要你提供,實現一個最簡單的非交互的自定義 transition,只需要實現1和3即可,其實還會用到4,不過大部分情況下這個組件由系統提供給我們,我們只需要實現組件1和3就可以了。

實戰

准備工作

這篇不涉及交互過程,因此我單獨做了個分支:No-Interaction-Transition,是本篇內容的最終版本;或者你還是想自己動手,使用純色塊的 Cell 就好了,幾分鐘就能搞定,又或者不怕再麻煩一點,提取這個分支裡面 Example 文件夾裡的文件替換到你的工程好了。到這裡還是很簡單的,如果覺得不簡單,那就看看好了,把本文加入待讀列表過一個月後再來學習。

Demo 裡有三個分支,默認分支是能夠自動添加 pinch 手勢支持 pop 操作,還是就是這篇文章的分支 No-Interaction-Transition,還有一種就是同時支持 push 和 pop 操作的 pinch 手勢的分支 Pinch-Push-Pop-Transition。

下面需要你配置這樣的一個場景,在此基礎上逐步改造成最終的效果:在 storyboard 裡放置一個UINavigationController和兩個UICollectionViewController,如果你不用 storyboard,相信你也能自己搞定設置。

37334-99e758632d5a09f3.png

使用場景

下面使用 fromVC 和 toVC 分別代表 push 和 pop 過程涉及的源和目標UICollectionViewController,animationController 代表動畫控制器,它執行真正的動畫。實現一個最基本的非自定義 push,在你的 fromVC 裡實現以下代理方法:

override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
    if let toVC = self.storyboard?.instantiateViewControllerWithIdentifier("XXX") {
        /*對 toVC 做一些設置,然後 push*/
        ......
        self.navigationController?.pushViewController(toVC, animated: true)
    }
}

現在,一個最簡單的場景就搭建完成了。此時,push 和 pop 都是系統替我們完成,運行程序,動畫效果是 Slide。接下來,我們就把這個動畫換成我設計的。

如果你在 storyboard 裡通過拉 segue 來完成跳轉,那你去- prepareForSegue:sender:裡做一些調整了,先別這麼干,按照我的節奏來。

接手系統 transition

第一步,為UINavigationController提供遵守UINavigationControllerDelegate協議的對象(組件3)作為代理 delegate,在 push 和 pop 時系統會要求這個 delegate 來提供動畫控制器和交互控制器;沒有提供這個代理時,比如上面的情況裡,系統將會使用默認的 Slide 動畫。該協議的方法名很直白,其中前者必須實現,用於提供組件1來執行實際的動畫,後者提供組件2實現交互動畫,是可選的。

- navigationController:animationControllerForOperation:fromViewController:toViewController:
- navigationController:interactionControllerForAnimationController:

fromVC 也可以作為代理來提供這些方法,但這樣一來不方便其他類使用該效果,這裡單獨提供一個對象來作為代理,俗稱解耦。新建SDENavigationControllerDelegate類,聲明如下:

014.png

在 storyboard 裡拖一個 NSObject 下面圖中這一塊區域,然後將其類設置為SDENavigationControllerDelegate。你沒看錯,就是拖一個 NSObject,在你經常拖控件的地方輸入 object 就能看到。如果你還不知道,恭喜,現在你又學到新知識了。

015.png

在 storyboard 裡為 navigation controller 設置 delegate

小坑預警:如果你想在代碼裡設置UINavigationController的 delegate,那麼viewDidLoad()並不是一個合適的地方,因為此時 ViewController 尚未被推入UINavigationController的viewControllers棧裡,通過UIViewController.navigationController得到的只是 nil。哪兒合適,在viewDidAppear()後調用的方法都可以,這麼說這有點......作為一個UICollectionViewController,push 時在 didSelectCell 那個方法裡最合適了。

本文將只實現非交互的動畫,可交互的動畫在系列下篇討論。在SDENavigationControllerDelegate類裡實現以下方法提供動畫控制器:

private (set) animationController: UIViewControllerAnimatedTransitioning!//設置為只讀屬性

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    //需要通過是 push 還是 pop 操作來執行不同的動畫,因此自定義了一個需要用操作類型來初始化的動畫控制器
    animationController = SDEPushAndPopAnimationController(operation: operation)控制器
    return animationController
}

第二步,實現上面提供的動畫控制器類SDEPushAndPopAnimationController,該類遵守 UIViewControllerAnimatedTransitioning協議,需要實現以下方法:

- transitionDuration: //提供 transition animation 的持續時間
- animateTransition:  //執行動畫的地方,最重要的方法
- animationEnded:     //可選方法,動畫完畢後調用,大部分時候用不上

SDEPushAndPopAnimationController類的實現:

import UIKit
class SDEPushAndPopAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
//通過變量來保存操作類型
private var operation: UINavigationControllerOperation

init(operation: UINavigationControllerOperation){
    self.operation = operation
    super.init()
}

//返回動畫執行時間,實際上 navigationBar 的動畫時間也由該方法返回的時間決定。
//所有自定義的 navigationbar transition 的動畫效果都是 cross fade。
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
    return 1.0
}
//執行動畫的地方
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    switch operation{
    case .Push:
    /*do some thing*/
    case .Pop:
    /*do some thing also*/
    default:
        /*do nothing*/
        print("Do Nothing")
    }
}

WT...恩,暫時先這麼處理吧。接下來,再次進入科普時間。

來看看 WWDC13 Session 218 中對 NavigationController push transition 的解釋:

016.png

NavigationController Push Transition 圖解

NavigationController 維持的 ViewController 的結構和我們想象的一樣,是個棧,但其對應的 View 的結構卻不是這樣。在 transition 結束時,fromView 被從 containerView 中被移除,如果我們沒有這麼做,系統會替我們完成的。這麼看來,containerView 裡只保留棧頂 ViewController 的視圖,也就是屏幕上我們看到的那個視圖。

圖中的兩個狀態之間的變化就發生在- animateTransition:裡,不過動畫的執行不限於這裡,viewWillXXX, viewDidXXX等這些方法裡都可以執行你想要的動畫,但是,出於解耦的目的,將所有的動畫都放在- animateTransition:裡執行,這樣就能夠也適用於其他UICollectionViewController類了,而如果你需要保證動畫執行的順序,那麼這些方法並不是一個好的選擇,在 WWDC13 Session 218 裡蘋果的工程師提到了不能保證viewDidXXX一定在對應的viewWillXXX後面執行,雖然我在三個月前的實現裡是依賴這些方法而且沒有發現這個問題,那麼,可以繼續這樣做嗎?答案是否,使用- animateTransition:可以從根源上杜絕此類問題;不過,所有動畫放在這裡執行還有一個最最最最最重要的目的,先放結論:你想納入交互化控制過程的動畫必須在- animateTransition:裡執行,而且,必須使用 UIView Animation 來實現,不要使用 Core Animation,在系列下篇裡實現交互動畫時會詳細討論有關細節。科普結束,返回實現過程。

- animateTransition:

該方法原型為:

func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)

該函數的參數由系統提供給我們,同時該參數就是組件4,它提供了 transition 過程中我們需要的絕大部分信息,包括參與 transition 過程的控制器以及 transition 過程的狀態,最後還要將 transition 的執行結果通知給系統。

在很多文章裡,會給你演示一些簡單的動畫,不過,在這裡我需要你明白,此時的環境是怎樣的以及你能夠做什麼。整理下現在的局面,現在屏幕的內容由當前視圖提供,無論你以何種方式 push 或是 pop,最終會切換到下一屏的畫面,系統會詢問當前 NavigationController 的 delegate 要求提供動畫控制器和交互控制器,如果我們沒有提供動畫控制器,那麼系統就用 Slide 動畫來展示當前畫面和下一屏的畫面的切換。不過,現在我們提供了動畫控制器,系統問我們的動畫控制器怎麼處理這個切換過程。這時候,我們有當前視圖 fromView,當前視圖的容器視圖 containerView,還有下一個屏幕的內容視圖 toView,需要我們做的是將 toView 添加到 containerView 裡用於顯示下一屏的內容,而在 push 或 pop 結束時,fromView 會被從 containerView 裡移除,如果我們沒有這麼做,系統會自動替我們在結束時移除,如果你想干預這個過程也是可以的,在 push 或 pop 結束之前,我們可以對當前視圖 fromView 和下一屏視圖 toView 做任何你想做的事,這就是我們即將要實現的動畫。

VCTransitionsLibrary 這個庫囊括了大部分對 view 整體之間進行切換的效果,而當 transition 涉及 view 上的元素的話,就需要你針對元素進行定制了,這個庫就不適用這種情況了。比如神奇移動,就是將 fromView 上的元素移動到 toView 上,實現思路有兩種:一是,toView 出現時,將目標元素移動到源元素的位置進行遮擋,然後移動到預定位置,比較簡單;二是將 fromView 和 toView 中相同元素都隱藏,對源元素截圖並加入 toView 中作為偽裝,然後將偽裝的源元素移動到 toView 上的指定位置,最後移除偽裝的元素然後將目標元素恢復顯示。這兩個方法中很重要的一點就是無論是偽裝的元素還是目標元素在開始和結束移動時的位置和大小都要吻合,不然就露餡了。

說教完畢,那麼來實現開頭的效果吧。

動畫技術點

認真看下開頭的效果,以 push 為例:圖片像一本相冊的封面一樣翻開,這是一個可用 transform 實現的 flip 動畫;下一層級的視圖裡的元素也就是相冊裡的照片在封面後出現,這個效果需要縮小照片並按一定規則排列好;封面繼續往左翻動,而照片則移動到預定位置並在這個過程中恢復到原大小。

上面提到,實現交互動畫,一定要使用 UIView Animation 而不是 Core Animation。而且這裡的動畫還涉及多個元素的配合,不同元素的動畫的開始時間與持續時間都不一樣,使用 UIView Animation 是沒法滿足這個要求的,因為常規的延遲執行手段在交互動畫裡沒有作用,只有一個解決辦法:UIView key frame animation,這裡 push 和 pop 過程中的動畫都是采用這種方式實現的。

在回到-animateTransition:執行動畫之前,還有一個問題,從技術角度講,pop 結束後要恢復被隱藏的封面,但這和 push 有什麼關系呢?有關系,大有關系,事前做好准備才不怕事後找麻煩嘛。我們需要在 push 前保留這個被點擊的封面的 indexpath 以便在 pop 結束時能夠將之恢復。但又不想在UICollectionViewController添加屬性,因為你讓別人在自己的工程中為這個類添加這個屬性還是挺麻煩的,有辦法:extensition + associated object,這個技巧是從這個庫學來的。為UICollectionViewController添加一個 extension,新建UICollectionViewControllerExtension.swift文件,為所有的UICollectionViewController類添加下面兩個屬性:

import UIKit

private var selectedIndexPathAssociationKey: UInt8 = 0
private var coverRectInSuperviewKey: UInt8 = 1

extension UICollectionViewController {
    //保存被選中的封面的索引
    var selectedIndexPath: NSIndexPath! {
        get {
            return objc_getAssociatedObject(self, &selectedIndexPathAssociationKey) as? NSIndexPath
        }
        set(newValue) {
            objc_setAssociatedObject(self, &selectedIndexPathAssociationKey, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }
    //記錄被選中的封面相對於屏幕的位置,這個會被傳遞給 toVC,以便於在 toVC 裡調整 visibleCells 的位置和大小使之能夠隱藏在封面後面
    var coverRectInSuperview: CGRect! {
        get {
            let value = objc_getAssociatedObject(self, &coverRectInSuperviewKey) as? NSValue
            return value?.CGRectValue()
        }
        set(newValue){
            let value = NSValue(CGRect: newValue)
            objc_setAssociatedObject(self, &coverRectInSuperviewKey, value, .OBJC_ASSOCIATION_RETAIN)
        }
    }
}

然後需要在之前的代理方法裡做添加一行代碼:

override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath:NSIndexPath) {
    if let toVC = self.storyboard?.instantiateViewControllerWithIdentifier("XXX") {
        self.selectedIndexPath = indexPath//記錄封面索引位置
        ......
        self.navigationController?.pushViewController(toVC, animated: true)
    }
}

准備工作完成了, 動畫過程中包括這麼幾個步驟,同時也是問題:

問題1:封面旋轉。封面的動畫過程本質上和神奇移動有點像,只不過神奇移動裡元素在移動,而這裡元素位置在原來的位置不動,並且繞左側旋轉。不過,神奇移動之所以為神奇移動在於前後的內容裡有相同的元素,但這裡並不是,但依然可以采用神奇移動的思路來實現這個效果。由於 toView 裡並沒有封面這個元素,需要使用偽裝的封面,push 時隱藏原封面的同時在 toView 上添加和原封面內容一樣的視圖來欺騙我們的眼睛,pop 時則將這個偽裝封面翻回去,然後恢復源封面的顯示。封面的第二個問題,如何保證封面在 toView 上依然保持在視覺正確的位置。這個也好解決,無論當前 collectionView 怎麼移動,封面相對於 fromView.superView 和封面相對於 toView.superView 的位置是一樣的,因為這兩個位置都是相對於當前屏幕的位置。UIView 有一套"convertXXX"的方法用於屬於同一個 UIWindow 的視圖之間進行坐標的轉換:

func createAndSetupFakeCoverView(fromVC: UICollectionViewController, toVC: UICollectionViewController) -> UIView?{
    //對封面進行截圖
    let selectedCell = fromVC.collectionView?.cellForItemAtIndexPath(fromVC.selectedIndexPath)
    let snapshotCellView = selectedCell!.snapshotViewAfterScreenUpdates(false)//偽裝的封面
    snapshotCellView.tag = 10//這裡設置 tag 的目的在於需要解決旋轉時透明背景的問題時便於獲取該視圖
    //將封面內嵌到一個容器視圖裡,為什麼這麼做,這樣可以用來解決翻轉時背景透明這個問題,具體可以卡片動畫這篇博客
    let coverContainerView = UIView(frame: snapshotCellView.frame)
    coverContainerView.backgroundColor = coverViewBackgroundColor
    coverContainerView.addSubview(snapshotCellView)
    coverContainerView.tag = 1000
    //作為偽裝的封面加入 toView 中,
    toVC.view.addSubview(coverContainerView)
    //位置通過上面的方法裡傳遞的 coverRectInSuperview 來獲取
    coverContainerView.frame = toVC.coverRectInSuperview
    //調整 anchorPoint,使封面能夠繞左側旋轉
    let frame = coverContainerView.frame
    coverContainerView.layer.anchorPoint = CGPointMake(0, 0.5)
    coverContainerView.frame = frame
    
    return coverContainerView
}
//配合好封面上翻轉和消失動畫的時間
func addKeyFrameAnimationInPushForFakeCoverView(coverView: UIView?){
    //封面是最早執行動畫的元素,並且在整體動畫的中途完成。
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.5, animations: {
        var flipLeftTransform = CATransform3DIdentity
        flipLeftTransform.m34 = -1.0 / 500.0
        flipLeftTransform = CATransform3DRotate(flipLeftTransform, CGFloat(-M_PI), 0.0, 1.0, 0.0)
        coverView?.layer.transform = flipLeftTransform
    })
    //翻轉快結束時隱藏
    UIView.addKeyframeWithRelativeStartTime(0.45, relativeDuration: 0.05, animations: {
        coverView?.alpha = 0
    })
    
    //這一步用於解決翻轉時背景是透明的這個問題,當內嵌的內容視圖被隱藏時,翻轉過程中就能看到背面了。
    let snapshotView = coverView?.viewWithTag(10)
    UIView.addKeyframeWithRelativeStartTime(0.25, relativeDuration: 0.01, animations: {
        snapshotView?.alpha = 0
    })
}

問題2:調整 visibleCells,這在 pop 時不是問題,但是在 push 時,你會發現在- animateTransition:裡通過 toVC.collectionView?.visibleCells()返回的是空數組,沒法獲取 visibleCells 意味著我們沒法對即將出現的 visibleCells 進行調整,怎麼辦?這個問題在三個月前將我折磨死了,可以從這篇記錄裡看到當時的歷程,由於無法獲取 visibleCells 而苦苦尋求其他辦法最終卻失敗。解決辦法的關鍵是從這篇教程 How to Create an iOS Book Open Animation 裡得知的,使用toVC.view.snapshotViewAfterScreenUpdates(true)能夠強制視圖立即進行刷新,此時可以獲取 visibleCells,事實上可以還有方法也可以:- layoutIfNeeded。具體對於這些 visibleCells 根據自身的 indexPath 來設置大小和位置是一件比較繁瑣的事情,這部分代碼放在setupVisibleCellsBeforePushToVC:裡了,這裡不詳細討論。

func addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC: UICollectionViewController){
    let collectionView = toVC.collectionView!
    for cell in collectionView.visibleCells(){
        //不同位置的 cell 的動畫的開始時間和持續時間有些許差別,讓離得中心越遠的元素越早到達位置,最後的效果非常賞心悅目。這個是從上面那個庫裡學來的,但目前還有點瑕疵。
        let relativeStartTime = ......
        var relativeDuration =  ......
        //以漸顯的方式出現在封面後,但這個效果一般
        UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.7, animations: {
            cell.alpha = 1
        })
        //在封面完全翻開後才開始照片的動畫,開始時間各有差異。
        UIView.addKeyframeWithRelativeStartTime(0.5 + relativeStartTime, relativeDuration: relativeDuration, animations: {
            cell.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1)
        })
        UIView.addKeyframeWithRelativeStartTime(0.5 + relativeStartTime, relativeDuration: relativeDuration, animations: {
            cell.center = layoutAttributes!.center
        })
    }
}

問題3:調整視圖背景色。這是個很不起眼的小地方,但可能會讓你栽個大跟頭。如果你設置了 toVC 的視圖的背景色,動畫開始時屏幕就會呈現該背景,這時候 fromView 就立刻不可見了,動畫效果是非常糟糕的;這時候你或許會在 storyboard 裡將 toVC 的 collectionView 的背景色調整為透明色來解決這個問題,可惜在動畫結束後,背景色突然變黑,這是因為動畫結束後,fromView 被移除出去了, toView 沒有了背景空無一物,屏幕背景自然就變成黑色了。解決辦法是,在 storyboard 裡將 toVC 的 collectionView 的背景色設置為透明色,然後在 transition 過程中使用動畫來進行過渡到你需要的背景色。

func addkeyFrameAnimationForBackgroundColorInPush(fromVC: UICollectionViewController, toVC: UICollectionViewController){
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1.0, animations: {
        let toCollectionViewBackgroundColor = fromVC.collectionView?.backgroundColor
        toVC.collectionView?.backgroundColor = toCollectionViewBackgroundColor
    })
}

實現 Push

在實際的代碼裡我添加了一些屬性由於定制動畫的某些部分,比如設定封面後面的照片的分布區別,布局的間隔,等等,好像用處不大,隨我開心就好。

一切准備就緒,回到動畫控制器,補充剩下的部分:

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    //由系統提供的 transitionContext 能提供大部分需要的信息,下面的,應該很好理解吧。
    let containerView = transitionContext.containerView()
    let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as? UICollectionViewController
    let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as? UICollectionViewController
    let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
    let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
    let duration = transitionDuration(transitionContext)//這是要求實現的另外一個方法,往回看
    
    switch operation{
    case .Push:
        //隱藏被選中的封面,同時添加偽裝的封面到 toView 裡
        let selectedCell = fromVC?.collectionView?.cellForItemAtIndexPath(fromVC!.selectedIndexPath)
        selectedCell?.hidden = true
        //計算偽裝的位置,這個位置對於後面添加偽裝的封面和調整 visibleCells 至關重要。
        let layoutAttributes = fromVC!.collectionView?.layoutAttributesForItemAtIndexPath(fromVC!.selectedIndexPath)
        let areaRect = fromVC!.collectionView?.convertRect(layoutAttributes!.frame, toView: fromVC!.collectionView?.superview)
        toVC!.coverRectInSuperview = areaRect!
        let fakeCoverView = createAndSetupFakeCoverView(fromVC!, toVC: toVC!)
        
        //強制刷新 toView,以便能夠在 toVC 的collectionView 被顯示之前能夠獲取 visibleCells。
        toVC?.view.layoutIfNeeded()
        //針對 visibleCells 調整大小和位置,以便能夠隱藏在封面後面,此處比較繁瑣,想知道具體實現的話可以看源碼
        setupVisibleCellsBeforePushToVC(toVC!)
        //添加 toView, toView 將會出現在屏幕上
        containerView?.addSubview(toView!)
        
        UIView.setAnimationCurve(UIViewAnimationCurve.EaseOut)
        let options: UIViewKeyframeAnimationOptions = [.BeginFromCurrentState, .OverrideInheritedDuration, .CalculationModeCubic, .CalculationModeLinear]
        //key frame animation 裡添加的動畫的時間都是針對 duration 進行比例計算的,開始時間和持續時間的值都在0和1之間。
        UIView.animateKeyframesWithDuration(duration, delay: 0.0, options: options, animations: {
            //將上面實現的多步動畫添加到這裡
            self.addkeyFrameAnimationForBackgroundColorInPush(fromVC!, toVC: toVC!)
            self.addKeyFrameAnimationInPushForFakeCoverView(self.fakeCoverView)
            self.addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC!)
            }, completion: { finished in
                let isCancelled = transitionContext.transitionWasCancelled()
                //如果 push 被取消,則將一切恢復原樣,恢復原裝封面的顯示
                if isCancelled{
                    selectedCell?.hidden = false
                }
                transitionContext.completeTransition(!isCancelled)
        })
    case .Pop:
    /*下節討論*/
    default:
        print("No Operation")
    }
}

實現 Pop

Pop 過程中的動畫基本上是對 push 過程的逆向,唯一需要注意的地方是由於用戶可能會滑動 collectionView,那麼 pop 時的 visibleCells 可能和 push 時的不一樣,這時候要注意調整有關計算相對位置的算法,具體可以看代碼。這裡有個問題,用戶在滑動還沒有結束時點擊返回,此時的 pop 動畫就露餡了,因為位置是相對於返回的那一刻在計算的,而界面依然在滑動,封面下面的照片會超出封面的范圍。

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    ......
    case .Push:
        .....
    case .Pop:
         //fromVC 和 fromView 都是指代當前顯示的視圖控制器和視圖,與操作類型是 push 還是 pop 無關。
        //需要注意的是,此時不能再簡單地使用addSubview:,不然 fromView 會被擋住不可見
        containerView?.insertSubview(toView!, belowSubview: fromView!)
        //根據 tag 來獲取偽裝的封面
        let coverView = fromView?.viewWithTag(1000)
        UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
        UIView.animateKeyframesWithDuration(duration, delay: 1.0, options: UIViewKeyframeAnimationOptions(), animations: {
            //pop 過程的動畫基本上是對 push 過程中動畫的逆向。唯一需要注意的是,push 和 pop 時的 visibleCells 可能會不同,需要做出調整,具體看代碼
            self.addkeyFrameAnimationForBackgroundColorInPop(fromVC!)
            self.addKeyFrameAnimationInPopForFakeCoverView(coverView)
            self.addKeyFrameAnimationOnVisibleCellsInPopFromVC(fromVC!)
            }, completion: { finished in
                let isCancelled = transitionContext.transitionWasCancelled()
                //只有 pop 過程完成了,才能恢復源封面的顯示
                if !isCancelled{
                    let selectedCell = toVC?.collectionView?.cellForItemAtIndexPath(toVC!.selectedIndexPath)
                    selectedCell?.hidden = false
                }
                transitionContext.completeTransition(!isCancelled)
        })
    ......
}

這樣就完成了非交互動畫,接下來在這裡討論下如何使用 pinch 手勢來控制 push 和 pop 過程。

說點什麼

這麼一口氣看下來,對剛開始接觸的人來說有點困難,對有過類似經驗的人來說,應該也能找到點新的東西。如果你還沒有試過將這個過程交互化,那麼這篇內容已經規避了大部分交互動畫的陷阱,正如那些加粗顯示的內容提示的那樣,也正因為如此在下篇裡才會顯得如此輕松。三個月前的 Demo 也做了和如今大部分都相同的東西,但現在的 Demo 有著更好的解耦性,更方便使用,這也是個進步。

參考資料:

1. WWDC13 Session 218: Custom Transitions Using View Controllers

2.《自定義 ViewController 容器轉場》

3.《Custom Transitions on iOS》,此文是我見過關於 ViewController Custom Transition 的最好文章,強烈推薦。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved