自定義 push 和 pop 實(shí)現(xiàn)相冊(cè)翻開(kāi)效果(上)

效果預(yù)覽:

AlbumTransition.gif

前言

蘋(píng)果自家應(yīng)用 Photos 里點(diǎn)擊相冊(cè)后的動(dòng)畫(huà)是非常精妙的,而且是可交互的。我有類(lèi)似的動(dòng)畫(huà)需求,上面是我自己的設(shè)計(jì)效果。本指南分上下兩篇,分別探討非交互和交互動(dòng)畫(huà)的實(shí)現(xiàn)。

本文是將三個(gè)月前的 Demo 重構(gòu)后重新寫(xiě)的,重構(gòu)后,這個(gè)效果可以方便地在你的工程中使用,僅需添加幾行代碼和幾個(gè)簡(jiǎn)單的設(shè)置。效果適用場(chǎng)景:兩個(gè)UICollectionViewController類(lèi)之間的 push 和 pop 操作。Demo 是個(gè)小型的相冊(cè)瀏覽器, 這完全是基于我的需求來(lái)做的,因此在初期并沒(méi)有考慮做成一個(gè)手把手教你實(shí)現(xiàn)這個(gè)效果的教程,不過(guò)前面說(shuō)了,僅需添加幾行代碼就可在你的工程里使用,花上幾分鐘搭建一個(gè)場(chǎng)景照著做下來(lái)也是沒(méi)問(wèn)題的。另外,部分細(xì)節(jié)比較繁瑣,都放進(jìn)文章里就太長(zhǎng)了,想了解的話看源代碼,遇到這部分我會(huì)提示的。

Demo 地址:SDECollectionViewAlbumTransition

我把 iOS 里的動(dòng)畫(huà)分為兩種:趣味動(dòng)畫(huà)和邏輯動(dòng)畫(huà),前者比如一些加載場(chǎng)景的動(dòng)畫(huà),用來(lái)消磨時(shí)間,怎么炫酷都可以,后者是符合場(chǎng)景變化的動(dòng)畫(huà),符合邏輯最重要,如果還能很有趣那就更好了。我實(shí)現(xiàn)的效果算得上符合邏輯,離有趣或者酷還有點(diǎn)距離。

如上所示,我希望呈現(xiàn)出打開(kāi)相簿后照片飛出來(lái)的效果,這個(gè)設(shè)計(jì)是行為上的擬物,最好翻開(kāi)封面時(shí)還能發(fā)出金光,NO,NO,太浮夸了,簡(jiǎn)直跟中華小當(dāng)家或者國(guó)產(chǎn)奇幻劇開(kāi)寶箱似的。當(dāng)然,主要是我不知道怎么做,會(huì)做的話我就會(huì)做出來(lái)給大家看的,不過(guò),我是不會(huì)把這種效果放在正常的產(chǎn)品里的,在游戲界這種效果比較常見(jiàn),比如爐石里新卡牌點(diǎn)開(kāi)時(shí)就帶這種圣光效果。

從技術(shù)上講,以 push 為例:圖片像一本相冊(cè)的封面一樣翻開(kāi),這是一個(gè)可用 transform 實(shí)現(xiàn)的 翻轉(zhuǎn)動(dòng)畫(huà);下一層級(jí)的視圖也就是相冊(cè)里的照片在封面后出現(xiàn),這個(gè)效果需要縮小照片并按一定規(guī)則排列好;封面繼續(xù)往左翻動(dòng),而照片則移動(dòng)到預(yù)定位置并在這個(gè)過(guò)程中恢復(fù)到原大小。這個(gè)動(dòng)畫(huà)本質(zhì)上就是個(gè) View Controller Transition 加上多個(gè)元素協(xié)作進(jìn)行動(dòng)畫(huà)的過(guò)程??偟膩?lái)說(shuō),動(dòng)畫(huà)分為兩個(gè)部分,首先是自定義 push 和 pop,其次是各種元素的協(xié)作。現(xiàn)在先攻克第一個(gè)難點(diǎn),下面進(jìn)入科普時(shí)間。

View Controller Transition 視圖控制器轉(zhuǎn)換

對(duì)于這個(gè)話題,我推薦:1. WWDC13 上的 Custom Transitions Using View Controllers,2.Custom Transitions on iOS,3. Objc.io 的自定義 ViewController 容器轉(zhuǎn)場(chǎng)。以及一個(gè)自定義 transition 效果的庫(kù):VCTransitionsLibrary,可以讀讀代碼看看這些效果怎么實(shí)現(xiàn)的。

自定義 transition 類(lèi)型

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


iOS 支持的的自定義視圖轉(zhuǎn)換類(lèi)型 from WWDC13 #218

除了最后一個(gè)是布局轉(zhuǎn)換,前三種基本囊括了 iOS 中顯示切換視圖的全部方式:
1.Modal 視圖的顯示和消失;
2.TabBar Controller 在子視圖中切換;
3.Navigation Controller 推入和推出視圖。

其中 presentations and dismissals 只支持 UIModalPresentationFullScreen 和 UIModalPresentationCustom 這兩種 Modal 視圖的顯示和消失。

文章開(kāi)頭的效果是第三種,需要實(shí)現(xiàn)自定義 push 和 pop。

Transition Protocol

iOS 提供了幾套 protocol 來(lái)滿足自定義 transition 的需求。

WWDC13#218-Custom Transition 的構(gòu)成

對(duì)以上 protocol 的解釋節(jié)選自 Objc.io 的自定義 ViewController 容器轉(zhuǎn)場(chǎng)

iOS 7 自定義視圖控制器轉(zhuǎn)場(chǎng)的 API 基本上都是以協(xié)議的方式提供的,這也使其可以非常靈活的使用,因?yàn)槟憧梢院芎?jiǎn)單地將它們插入到你的類(lèi)中。最主要的五個(gè)組件如下:
1.動(dòng)畫(huà)控制器 (Animation Controllers) 遵從UIViewControllerAnimatedTransitioning協(xié)議,并且負(fù)責(zé)實(shí)際執(zhí)行動(dòng)畫(huà)。
2.交互控制器 (Interaction Controllers) 通過(guò)遵從UIViewControllerInteractiveTransitioning協(xié)議來(lái)控制可交互式的轉(zhuǎn)場(chǎng)。
3.轉(zhuǎn)場(chǎng)代理 (Transitioning Delegates) 根據(jù)不同的轉(zhuǎn)場(chǎng)類(lèi)型方便的提供需要的動(dòng)畫(huà)控制器和交互控制器。
4.轉(zhuǎn)場(chǎng)上下文 (Transitioning Contexts) 定義了轉(zhuǎn)場(chǎng)時(shí)需要的元數(shù)據(jù),比如在轉(zhuǎn)場(chǎng)過(guò)程中所參與的視圖控制器和視圖的相關(guān)屬性。 轉(zhuǎn)場(chǎng)上下文對(duì)象遵從UIViewControllerContextTransitioning協(xié)議,并且這是由系統(tǒng)負(fù)責(zé)生成和提供的。
5.轉(zhuǎn)場(chǎng)協(xié)調(diào)器(Transition Coordinators) 可以在運(yùn)行轉(zhuǎn)場(chǎng)動(dòng)畫(huà)時(shí),并行的運(yùn)行其他動(dòng)畫(huà)。 轉(zhuǎn)場(chǎng)協(xié)調(diào)器遵從UIViewControllerTransitionCoordinator協(xié)議。

看暈了?沒(méi)關(guān)系。這五個(gè)組件并不是全部都需要你提供,實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的非交互的自定義 transition,只需要實(shí)現(xiàn)1和3即可,其實(shí)還會(huì)用到4,不過(guò)大部分情況下這個(gè)組件由系統(tǒng)提供給我們,我們只需要實(shí)現(xiàn)組件1和3就可以了。

實(shí)戰(zhàn)

準(zhǔn)備工作

這篇不涉及交互過(guò)程,因此我單獨(dú)做了個(gè)分支:No-Interaction-Transition,是本篇內(nèi)容的最終版本;或者你還是想自己動(dòng)手,使用純色塊的 Cell 就好了,幾分鐘就能搞定,又或者不怕再麻煩一點(diǎn),提取這個(gè)分支里面 Example 文件夾里的文件替換到你的工程好了。到這里還是很簡(jiǎn)單的,如果覺(jué)得不簡(jiǎn)單,那就看看好了,把本文加入待讀列表過(guò)一個(gè)月后再來(lái)學(xué)習(xí)。

Demo 里有三個(gè)分支,默認(rèn)分支是能夠自動(dòng)添加 pinch 手勢(shì)支持 pop 操作,還是就是這篇文章的分支 No-Interaction-Transition,還有一種就是同時(shí)支持 push 和 pop 操作的 pinch 手勢(shì)的分支 Pinch-Push-Pop-Transition。

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

使用場(chǎng)景

下面使用 fromVC 和 toVC 分別代表 push 和 pop 過(guò)程涉及的源和目標(biāo)UICollectionViewController,animationController 代表動(dòng)畫(huà)控制器,它執(zhí)行真正的動(dòng)畫(huà)。實(shí)現(xiàn)一個(gè)最基本的非自定義 push,在你的 fromVC 里實(shí)現(xiàn)以下代理方法:

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

現(xiàn)在,一個(gè)最簡(jiǎn)單的場(chǎng)景就搭建完成了。此時(shí),push 和 pop 都是系統(tǒng)替我們完成,運(yùn)行程序,動(dòng)畫(huà)效果是 Slide。接下來(lái),我們就把這個(gè)動(dòng)畫(huà)換成我設(shè)計(jì)的。

如果你是在 storyboard 里通過(guò)拉 segue 來(lái)完成跳轉(zhuǎn),那需要你去- prepareForSegue:sender:里做一些調(diào)整了,但先別這么干,按照我的節(jié)奏來(lái)。

接手系統(tǒng) transition

第一步,為UINavigationController提供遵守UINavigationControllerDelegate協(xié)議的對(duì)象(組件3)作為代理 delegate,在 push 和 pop 時(shí)系統(tǒng)會(huì)要求這個(gè) delegate 來(lái)提供動(dòng)畫(huà)控制器和交互控制器;沒(méi)有提供這個(gè)代理時(shí),比如上面的情況里,系統(tǒng)將會(huì)使用默認(rèn)的 Slide 動(dòng)畫(huà)。該協(xié)議的方法名很直白,其中前者必須實(shí)現(xiàn),用于提供組件1來(lái)執(zhí)行實(shí)際的動(dòng)畫(huà),后者提供組件2實(shí)現(xiàn)交互動(dòng)畫(huà),是可選的。
- navigationController:animationControllerForOperation:fromViewController:toViewController:
- navigationController:interactionControllerForAnimationController:

新建SDENavigationControllerDelegate類(lèi)作為代理,聲明如下:

在 storyboard 里拖一個(gè) NSObject 下面圖中這一塊區(qū)域,然后將其類(lèi)設(shè)置為SDENavigationControllerDelegate。你沒(méi)看錯(cuò),就是拖一個(gè) NSObject,在你經(jīng)常拖控件的地方輸入 object 就能看到。如果你還不知道,恭喜,現(xiàn)在你又學(xué)到新知識(shí)了。

在 storyboard 里為 navigation controller 設(shè)置 delegate

小坑預(yù)警:如果你想在代碼里設(shè)置UINavigationController的 delegate,那么viewDidLoad()并不是一個(gè)合適的地方,因?yàn)榇藭r(shí) ViewController 尚未被推入UINavigationControllerviewControllers棧里,通過(guò)UIViewController.navigationController得到的只是 nil。哪兒合適,在viewDidAppear()后調(diào)用的方法都可以,這么說(shuō)這有點(diǎn)......作為一個(gè)UICollectionViewController,push 時(shí)在 didSelectCell 那個(gè)方法里最合適了。

本文將只實(shí)現(xiàn)非交互的動(dòng)畫(huà),可交互的動(dòng)畫(huà)在系列下篇討論。在SDENavigationControllerDelegate類(lèi)里實(shí)現(xiàn)以下方法提供動(dòng)畫(huà)控制器:

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

第二步,實(shí)現(xiàn)上面提供的動(dòng)畫(huà)控制器類(lèi)SDEPushAndPopAnimationController,該類(lèi)遵守 UIViewControllerAnimatedTransitioning協(xié)議,需要實(shí)現(xiàn)以下方法:

- transitionDuration: //提供 transition animation 的持續(xù)時(shí)間
- animateTransition:  //執(zhí)行動(dòng)畫(huà)的地方,最重要的方法
- animationEnded:     //可選方法,動(dòng)畫(huà)完畢后調(diào)用,大部分時(shí)候用不上

SDEPushAndPopAnimationController類(lèi)的實(shí)現(xiàn):

class SDEPushAndPopAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
//通過(guò)變量來(lái)保存操作類(lèi)型
private var operation: UINavigationControllerOperation

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

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

WT...恩,暫時(shí)先這么處理吧。接下來(lái),再次進(jìn)入科普時(shí)間。

來(lái)看看 WWDC13 Session 218 中對(duì) ViewController Transition 的解釋?zhuān)?/p>

ViewController Transition 圖解

NavigationController 維持的 ViewController 的結(jié)構(gòu)和我們想象的一樣,是個(gè)棧,但其對(duì)應(yīng)的 View 的結(jié)構(gòu)卻不是這樣。在 transition 結(jié)束時(shí),fromView 被從 containerView 中被移除,如果我們沒(méi)有這么做,系統(tǒng)會(huì)替我們完成的。這么看來(lái),containerView 里只保留棧頂 ViewController 的視圖,也就是屏幕上我們看到的那個(gè)視圖。

圖中的兩個(gè)狀態(tài)之間的變化就發(fā)生在動(dòng)畫(huà)控制器的- animateTransition:方法里,不過(guò)動(dòng)畫(huà)的執(zhí)行不限于這里,viewWillXXX, viewDidXXX等這些方法里都可以執(zhí)行你想要的動(dòng)畫(huà)。不過(guò),所有動(dòng)畫(huà)放在這里執(zhí)行還有一個(gè)最最最最最重要的目的,先放結(jié)論:你想納入交互化控制過(guò)程的動(dòng)畫(huà)必須在- animateTransition:里執(zhí)行,而且,必須使用 UIView Animation 來(lái)實(shí)現(xiàn),不要使用 Core Animation,在系列下篇里實(shí)現(xiàn)交互動(dòng)畫(huà)時(shí)會(huì)詳細(xì)討論有關(guān)細(xì)節(jié)??破战Y(jié)束,返回實(shí)現(xiàn)過(guò)程。

定制動(dòng)畫(huà)

animateTransition:方法的原型為:

func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)

該函數(shù)的參數(shù)也就是組件4,由系統(tǒng)提供給我們,它提供了 transition 過(guò)程中我們需要的絕大部分信息,包括參與 transition 過(guò)程的控制器以及 transition 過(guò)程的狀態(tài),最后還要將 transition 的執(zhí)行結(jié)果通知給系統(tǒng)。

animateTransition:方法中要做的事情主要是這樣:

func animateTransition(transitionContext:UIViewControllerContextTransitioning) {
    //由系統(tǒng)提供的 transitionContext 能提供大部分需要的信息,下面的,應(yīng)該很好理解吧。
    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)//這是要求實(shí)現(xiàn)的另外一個(gè)方法,往回看
        
    //containerView 在 transition 過(guò)程中擔(dān)任 fromView 和 toView的父視圖;將 toView 添加到 containerView 中,toView 才能顯示在屏幕上
    containerView?.addSubview(toView!)
    UIView.animateWithDuration(duration, animations: {
        /*添加動(dòng)畫(huà)*/
    }, completion: { _ in
            //結(jié)束 transition 過(guò)程
            let isCancelled = transitionContext.transitionWasCancelled()
            transitionContext.completeTransition(!isCancelled)
    })
}

在很多文章里,會(huì)給你演示一些簡(jiǎn)單的動(dòng)畫(huà),實(shí)際上,我們可以對(duì)當(dāng)前視圖 fromView 和下一屏視圖 toView 做任何動(dòng)畫(huà),僅限于你的想象力以及實(shí)現(xiàn)能力。

VCTransitionsLibrary 這個(gè)庫(kù)包含了十種效果,都是針對(duì)視圖整體實(shí)現(xiàn)的動(dòng)畫(huà),而當(dāng) transition 涉及視圖中的子視圖時(shí),這個(gè)庫(kù)就不適用了。比如神奇移動(dòng),就是將 fromView 上的子視圖移動(dòng)到 toView 上,實(shí)現(xiàn)思路有兩種:一是,toView 出現(xiàn)時(shí),將目標(biāo)元素移動(dòng)到源元素的位置進(jìn)行遮擋,然后移動(dòng)到預(yù)定位置,比較簡(jiǎn)單;二是將 fromView 和 toView 中相同子視圖都隱藏,對(duì)該子視圖截圖并加入 toView 中作為偽裝,然后將偽裝的子視圖移動(dòng)到 toView 上的指定位置,最后移除偽裝的子視圖然后將隱藏的子視圖恢復(fù)顯示。這兩個(gè)方法中很重要的一點(diǎn)就是無(wú)論是偽裝的還是真正的子視圖在開(kāi)始和結(jié)束移動(dòng)時(shí)的位置和大小都要吻合,不然就露餡了。

回到這個(gè)動(dòng)畫(huà),前面提到,實(shí)現(xiàn)交互動(dòng)畫(huà),一定要使用 UIView Animation 而不是 Core Animation。而且這里的動(dòng)畫(huà)還涉及多個(gè)元素的配合,不同元素的動(dòng)畫(huà)的開(kāi)始時(shí)間與持續(xù)時(shí)間都不一樣,使用 UIView Animation 是沒(méi)法滿足這個(gè)要求的,因?yàn)槌R?guī)的延遲執(zhí)行手段在交互動(dòng)畫(huà)里沒(méi)有作用,只有一個(gè)解決辦法:UIView key frame animation,這里 push 和 pop 過(guò)程中的動(dòng)畫(huà)都是采用這種方式實(shí)現(xiàn)的。

UIView.animateKeyframesWithDuration(duration, delay: 0.0, options: options, animations: {
    //添加多步動(dòng)畫(huà)
    self.addkeyFrameAnimationForBackgroundColorInPush(fromVC!, toVC: toVC!)
    self.addKeyFrameAnimationInPushForFakeCoverView(self.fakeCoverView)
    self.addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC!)
}, completion: { finished in
    let isCancelled = transitionContext.transitionWasCancelled()
    transitionContext.completeTransition(!isCancelled)
})

開(kāi)頭的效果拆分成三個(gè)動(dòng)畫(huà)完成:

1.翻開(kāi)封面的動(dòng)畫(huà)。由于 toView 里并沒(méi)有封面這個(gè)元素,需要使用偽裝的封面,push 時(shí)隱藏原封面的同時(shí)在 toView 上添加和原封面內(nèi)容一樣的視圖來(lái)欺騙我們的眼睛,pop 時(shí)則將這個(gè)偽裝封面翻回去,然后恢復(fù)源封面的顯示。封面的第二個(gè)問(wèn)題,如何保證封面在 toView 上依然保持在視覺(jué)正確的位置。這個(gè)也好解決,無(wú)論當(dāng)前 collectionView 怎么移動(dòng),封面相對(duì)于 fromView.superView 和封面相對(duì)于 toView.superView 的位置是一樣的,因?yàn)檫@兩個(gè)位置都是相對(duì)于當(dāng)前屏幕的位置。UIView 有一套"convertXXX"的方法用于屬于同一個(gè) UIWindow 的視圖之間進(jìn)行坐標(biāo)的轉(zhuǎn)換:

//配合好封面上翻轉(zhuǎn)和消失動(dòng)畫(huà)的時(shí)間
func addKeyFrameAnimationInPushForFakeCoverView(coverView: UIView?){
    //封面是最早執(zhí)行動(dòng)畫(huà)的元素,并且在整體動(dòng)畫(huà)的中途完成。
    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
    })
}

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

func addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC: UICollectionViewController){
    let collectionView = toVC.collectionView!
    for cell in collectionView.visibleCells(){
        //不同位置的 cell 的動(dòng)畫(huà)的開(kāi)始時(shí)間和持續(xù)時(shí)間有些許差別,讓離得中心越遠(yuǎn)的元素越早到達(dá)位置,最后的效果非常賞心悅目。這個(gè)是從上面那個(gè)庫(kù)里學(xué)來(lái)的,但目前還有點(diǎn)瑕疵。
        let relativeStartTime = ......
        var relativeDuration =  ......
        //以漸顯的方式出現(xiàn)在封面后,但這個(gè)效果一般
        UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.7, animations: {
            cell.alpha = 1
        })
        //在封面完全翻開(kāi)后才開(kāi)始照片的動(dòng)畫(huà),開(kāi)始時(shí)間各有差異。
        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.調(diào)整視圖背景色。這是個(gè)很不起眼的小地方,但可能會(huì)讓你栽個(gè)大跟頭。如果你設(shè)置了 toVC 的視圖的背景色,動(dòng)畫(huà)開(kāi)始時(shí)屏幕就會(huì)呈現(xiàn)該背景,這時(shí)候 fromView 就立刻不可見(jiàn)了,動(dòng)畫(huà)效果是非常糟糕的;這時(shí)候你或許會(huì)在 storyboard 里將 toVC 的 collectionView 的背景色調(diào)整為透明色來(lái)解決這個(gè)問(wèn)題,可惜在動(dòng)畫(huà)結(jié)束后,背景色突然變黑,這是因?yàn)閯?dòng)畫(huà)結(jié)束后,fromView 被移除出去了, toView 沒(méi)有了背景空無(wú)一物,屏幕背景自然就變成黑色了。解決辦法是,在 storyboard 里將 toVC 的 collectionView 的背景色設(shè)置為透明色,然后在 transition 過(guò)程中使用動(dòng)畫(huà)來(lái)進(jìn)行過(guò)渡到你需要的背景色。

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

animateTransition:執(zhí)行動(dòng)畫(huà)之前,還有一個(gè)問(wèn)題,pop 結(jié)束后要恢復(fù)被隱藏的封面,需要在 push 前保留這個(gè)被點(diǎn)擊的封面的 indexpath 以便在 pop 結(jié)束時(shí)能夠?qū)⒅謴?fù)。但又不想在UICollectionViewController添加屬性,因?yàn)槟阕寗e人在自己的工程中為這個(gè)類(lèi)添加這個(gè)屬性還是挺麻煩的,有辦法:extensition + associated object,這個(gè)技巧是從這個(gè)庫(kù)學(xué)來(lái)的。為UICollectionViewController添加一個(gè) extension,為所有的UICollectionViewController類(lèi)添加下面兩個(gè)屬性:

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)
        }
    }
    //記錄被選中的封面相對(duì)于屏幕的位置,這個(gè)會(huì)被傳遞給 toVC,以便于在 toVC 里調(diào)整 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)
    }
}
實(shí)現(xiàn) Push

一切準(zhǔn)備就緒,回到動(dòng)畫(huà)控制器,補(bǔ)充剩下的部分:

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    ...
    switch operation{
    case .Push:
        //隱藏被選中的封面,同時(shí)添加偽裝的封面到 toView 里
        let selectedCell = fromVC?.collectionView?.cellForItemAtIndexPath(fromVC!.selectedIndexPath)
        selectedCell?.hidden = true
        //計(jì)算偽裝的位置,這個(gè)位置對(duì)于后面添加偽裝的封面和調(diào)整 visibleCells 至關(guān)重要。
        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!)

        //強(qiáng)制刷新 toView,以便能夠在 toVC 的collectionView 被顯示之前能夠獲取 visibleCells。
        toVC?.view.layoutIfNeeded()
        //針對(duì) visibleCells 調(diào)整大小和位置,以便能夠隱藏在封面后面,此處比較繁瑣,想知道具體實(shí)現(xiàn)的話可以看源碼
        setupVisibleCellsBeforePushToVC(toVC!)
        //添加 toView, toView 將會(huì)出現(xiàn)在屏幕上
        containerView?.addSubview(toView!)

        UIView.setAnimationCurve(UIViewAnimationCurve.EaseOut)
        let options: UIViewKeyframeAnimationOptions = [.BeginFromCurrentState, .OverrideInheritedDuration, .CalculationModeCubic, .CalculationModeLinear]
        //key frame animation 里添加的動(dòng)畫(huà)的時(shí)間都是針對(duì) duration 進(jìn)行比例計(jì)算的,開(kāi)始時(shí)間和持續(xù)時(shí)間的值都在0和1之間。
        UIView.animateKeyframesWithDuration(duration, delay: 0.0, options: options, animations: {
            //將上面實(shí)現(xiàn)的多步動(dòng)畫(huà)添加到這里
            self.addkeyFrameAnimationForBackgroundColorInPush(fromVC!, toVC: toVC!)
            self.addKeyFrameAnimationInPushForFakeCoverView(self.fakeCoverView)
            self.addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC!)
            }, completion: { finished in
                let isCancelled = transitionContext.transitionWasCancelled()
                //如果 push 被取消,則將一切恢復(fù)原樣,恢復(fù)原裝封面的顯示
                if isCancelled{
                    selectedCell?.hidden = false
                }
                transitionContext.completeTransition(!isCancelled)
        })
    ...
    }
}
實(shí)現(xiàn) Pop

Pop 過(guò)程中的動(dòng)畫(huà)基本上是對(duì) push 過(guò)程的逆向,唯一需要注意的地方是由于用戶可能會(huì)滑動(dòng) collectionView,那么 pop 時(shí)的 visibleCells 可能和 push 時(shí)的不一樣,這時(shí)候要注意調(diào)整有關(guān)計(jì)算相對(duì)位置的算法,具體可以看代碼。這里有個(gè)問(wèn)題,用戶在滑動(dòng)還沒(méi)有結(jié)束時(shí)點(diǎn)擊返回,此時(shí)的 pop 動(dòng)畫(huà)就露餡了,因?yàn)槲恢檬窍鄬?duì)于返回的那一刻在計(jì)算的,而界面依然在滑動(dòng),封面下面的照片會(huì)超出封面的范圍。

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

這樣就完成了非交互動(dòng)畫(huà),接下來(lái)在這里討論下如何使用 pinch 手勢(shì)來(lái)控制 push 和 pop 過(guò)程。

說(shuō)點(diǎn)什么

這么一口氣看下來(lái),對(duì)剛開(kāi)始接觸的人來(lái)說(shuō)有點(diǎn)困難,對(duì)有過(guò)類(lèi)似經(jīng)驗(yàn)的人來(lái)說(shuō),應(yīng)該也能找到點(diǎn)新的東西。如果你還沒(méi)有試過(guò)將這個(gè)過(guò)程交互化,那么這篇內(nèi)容已經(jīng)規(guī)避了大部分交互動(dòng)畫(huà)的陷阱,正如那些加粗顯示的內(nèi)容提示的那樣,也正因?yàn)槿绱嗽谙缕锊艜?huì)顯得如此輕松。三個(gè)月前的 Demo 也做了和如今大部分都相同的東西,但現(xiàn)在的 Demo 有著更好的解耦性,更方便使用,這也是個(gè)進(jìn)步。

參考資料:
1. WWDC13 Session 218: Custom Transitions Using View Controllers
2.《自定義 ViewController 容器轉(zhuǎn)場(chǎng)》
3.《Custom Transitions on iOS》,此文是我見(jiàn)過(guò)關(guān)于 ViewController Custom Transition 的最好文章,強(qiáng)烈推薦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容