鍍金池/ 教程/ iOS/ Collection View 動(dòng)畫
與四軸無(wú)人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無(wú)人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無(wú)止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫(kù)支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語(yǔ)言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過(guò)程

Collection View 動(dòng)畫

UICollectionView 和相關(guān)類的設(shè)置非常靈活和強(qiáng)大。但是靈活性一旦增強(qiáng),某種程度上也增加了其復(fù)雜性: UICollectionView 比老式的 UITableView 更有深度,適用性也更強(qiáng)。

Collection View 深入太多了,事實(shí)上,Ole BegemanAsh Furrow 之前曾在 objc.io 上發(fā)表過(guò) 自定義 Collection View 布局UICollectionView + UIKit 力學(xué),但是我依然有一些他們沒有提及的內(nèi)容可以寫。在這篇文章中,我假設(shè)你已經(jīng)非常熟悉 UICollectionView 的基本布局,并且至少閱讀了蘋果精彩的編程指南以及 Ole 之前的文章

本文的第一部分將集中討論并舉例說(shuō)明如何用不同的類和方法來(lái)共同幫助實(shí)現(xiàn)一些常見的 UICollectionView 動(dòng)畫。在第二部分,我們將看一下帶有 collection views 的 view controller 轉(zhuǎn)場(chǎng)動(dòng)畫以及在 useLayoutToLayoutNavigationTransitions 可用時(shí)使用其進(jìn)行轉(zhuǎn)場(chǎng),如果不可用時(shí),我們會(huì)實(shí)現(xiàn)一個(gè)自定義轉(zhuǎn)場(chǎng)動(dòng)畫。

你可以在 GitHub 中找到本文提到的兩個(gè)示例工程:

Collection View 布局動(dòng)畫

標(biāo)準(zhǔn) UICollectionViewFlowLayout 除了動(dòng)畫是非常容易自定義的,蘋果選擇了一種安全的途徑去實(shí)現(xiàn)一個(gè)簡(jiǎn)單的淡入淡出動(dòng)畫作為所有布局的默認(rèn)動(dòng)畫。如果你想實(shí)現(xiàn)自定義動(dòng)畫,最好的辦法是子類化 UICollectionViewFlowLayout 并且在適當(dāng)?shù)牡胤綄?shí)現(xiàn)你的動(dòng)畫。讓我們通過(guò)一些例子來(lái)了解 UICollectionViewFlowLayout 子類中的一些方法如何協(xié)助完成自定義動(dòng)畫。

插入刪除元素

一般來(lái)說(shuō),我們對(duì)布局屬性從初始狀態(tài)到結(jié)束狀態(tài)進(jìn)行線性插值來(lái)計(jì)算 collection view 的動(dòng)畫參數(shù)。然而,新插入或者刪除的元素并沒有最初或最終狀態(tài)來(lái)進(jìn)行插值。要計(jì)算這樣的 cells 的動(dòng)畫,collection view 將通過(guò) initialLayoutAttributesForAppearingItemAtIndexPath: 以及 finalLayoutAttributesForAppearingItemAtIndexPath: 方法來(lái)詢問(wèn)其布局對(duì)象,以獲取最初的和最后的屬性。蘋果默認(rèn)的實(shí)現(xiàn)中,對(duì)于特定的某個(gè) indexPath,返回的是它的通常的位置,但 alpha 值為 0.0,這就產(chǎn)生了一個(gè)淡入或淡出動(dòng)畫。如果你想要更漂亮的效果,比如你的新的 cells 從屏幕底部發(fā)射并且旋轉(zhuǎn)飛到對(duì)應(yīng)位置,你可以如下實(shí)現(xiàn)這樣的布局子類:

- (UICollectionViewLayoutAttributes*)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:itemIndexPath];

    attr.transform = CGAffineTransformRotate(CGAffineTransformMakeScale(0.2, 0.2), M_PI);
    attr.center = CGPointMake(CGRectGetMidX(self.collectionView.bounds), CGRectGetMaxY(self.collectionView.bounds));

    return attr;
}

結(jié)果如下:

http://wiki.jikexueyuan.com/project/objc/images/12-24.gif" alt="" />

對(duì)應(yīng)的 finalLayoutAttributesForAppearingItemAtIndexPath: 方法中,除了設(shè)定了不同的 transform 以外,其他都很相似。

響應(yīng)設(shè)備旋轉(zhuǎn)

設(shè)備方向變化通常會(huì)導(dǎo)致 collection view 的 bounds 變化。如果通過(guò) shouldInvalidateLayoutForBoundsChange: 判定為布局需要被無(wú)效化并重新計(jì)算的時(shí)候,布局對(duì)象會(huì)被詢問(wèn)以提供新的布局。UICollectionViewFlowLayout 的默認(rèn)實(shí)現(xiàn)正確地處理了這個(gè)情況,但是如果你子類化 UICollectionViewLayout 的話,你需要在邊界變化時(shí)返回 YES

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    CGRect oldBounds = self.collectionView.bounds;
    if (!CGSizeEqualToSize(oldBounds.size, newBounds.size)) {
        return YES;
    }
    return NO;
}

在 bounds 變化的動(dòng)畫中,collection view 表現(xiàn)得像當(dāng)前顯示的元素被移除然后又在新的 bounds 中被被重新插入,這會(huì)對(duì)每個(gè) IndexPath 產(chǎn)生一系列的 finalLayoutAttributesForAppearingItemAtIndexPath:initialLayoutAttributesForAppearingItemAtIndexPath: 的調(diào)用。

如果你在插入和刪除的時(shí)候加入了非常炫的動(dòng)畫,現(xiàn)在你應(yīng)該看看為何蘋果明智的使用簡(jiǎn)單的淡入淡出動(dòng)畫作為默認(rèn)效果:

http://wiki.jikexueyuan.com/project/objc/images/12-25.gif" alt="" />

啊哦...

為了防止這種不想要的動(dòng)畫,初始化位置 -> 刪除動(dòng)畫 -> 插入動(dòng)畫 -> 最終位置的順序必須完全匹配 collection view 的每一項(xiàng),以便最終呈現(xiàn)出一個(gè)平滑動(dòng)畫。換句話說(shuō),finalLayoutAttributesForAppearingItemAtIndexPath: 以及 initialLayoutAttributesForAppearingItemAtIndexPath: 應(yīng)該針對(duì)元素到底是真的在顯示或者消失,還是 collection view 正在經(jīng)歷的邊界改變動(dòng)畫的不同情況,做出不同反應(yīng),并返回不同的布局屬性。

幸運(yùn)的是,collection view 會(huì)告知布局對(duì)象哪一種動(dòng)畫將被執(zhí)行。它分別通過(guò)調(diào)用 prepareForAnimatedBoundsChange:prepareForCollectionViewUpdates: 來(lái)對(duì)應(yīng) bounds 變化以及元素更新。出于本實(shí)例的說(shuō)明目的,我們可以使用 prepareForCollectionViewUpdates: 來(lái)跟蹤更新對(duì)象:

- (void)prepareForCollectionViewUpdates:(NSArray *)updateItems
{
    [super prepareForCollectionViewUpdates:updateItems];
    NSMutableArray *indexPaths = [NSMutableArray array];
    for (UICollectionViewUpdateItem *updateItem in updateItems) {
        switch (updateItem.updateAction) {
            case UICollectionUpdateActionInsert:
                [indexPaths addObject:updateItem.indexPathAfterUpdate];
                break;
            case UICollectionUpdateActionDelete:
                [indexPaths addObject:updateItem.indexPathBeforeUpdate];
                break;
            case UICollectionUpdateActionMove:
                [indexPaths addObject:updateItem.indexPathBeforeUpdate];
                [indexPaths addObject:updateItem.indexPathAfterUpdate];
                break;
            default:
                NSLog(@"unhandled case: %@", updateItem);
                break;
        }
    }  
    self.indexPathsToAnimate = indexPaths;
}

以及修改我們?cè)氐牟迦雱?dòng)畫,讓元素只在其正在被插入 collection view 時(shí)進(jìn)行發(fā)射:

- (UICollectionViewLayoutAttributes*)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:itemIndexPath];

    if ([_indexPathsToAnimate containsObject:itemIndexPath]) {
        attr.transform = CGAffineTransformRotate(CGAffineTransformMakeScale(0.2, 0.2), M_PI);
        attr.center = CGPointMake(CGRectGetMidX(self.collectionView.bounds), CGRectGetMaxY(self.collectionView.bounds));
        [_indexPathsToAnimate removeObject:itemIndexPath];
    }

    return attr;
}

如果這個(gè)元素沒有正在被插入,那么將通過(guò) layoutAttributesForItemAtIndexPath 來(lái)返回一個(gè)普通的屬性,以此取消特殊的外觀動(dòng)畫。結(jié)合 finalLayoutAttributesForAppearingItemAtIndexPath: 中相應(yīng)的邏輯,最終將會(huì)使元素能夠在 bounds 變化時(shí),從初始位置到最終位置以很流暢的動(dòng)畫形式實(shí)現(xiàn),從而建立一個(gè)簡(jiǎn)單但很酷的動(dòng)畫效果:

http://wiki.jikexueyuan.com/project/objc/images/12-26.gif" alt="" />

交互式布局動(dòng)畫

Collection views 讓用戶通過(guò)手勢(shì)實(shí)現(xiàn)與布局交互這件事變得很容易。如蘋果建議的那樣,為 collection view 布局添加交互的途徑一般會(huì)遵循以下步驟:

  1. 創(chuàng)建手勢(shì)識(shí)別
  2. 將手勢(shì)識(shí)別添加給 collection view
  3. 通過(guò)手勢(shì)來(lái)驅(qū)動(dòng)布局動(dòng)畫

讓我們來(lái)看看我們?nèi)绾慰梢越⒁恍┯脩艨煽s放捏合的元素,以及一旦用戶釋放他們的捏合手勢(shì)元素返回到原始大小。

我們的處理方式可能會(huì)是這樣:

- (void)handlePinch:(UIPinchGestureRecognizer *)sender {
    if ([sender numberOfTouches] != 2)
        return;

    if (sender.state == UIGestureRecognizerStateBegan ||
        sender.state == UIGestureRecognizerStateChanged) {
        // 獲取捏合的點(diǎn)
        CGPoint p1 = [sender locationOfTouch:0 inView:[self collectionView]];
        CGPoint p2 = [sender locationOfTouch:1 inView:[self collectionView]];

        // 計(jì)算擴(kuò)展距離
        CGFloat xd = p1.x - p2.x;
        CGFloat yd = p1.y - p2.y;
        CGFloat distance = sqrt(xd*xd + yd*yd);

        // 更新自定義布局參數(shù)以及無(wú)效化
        FJAnimatedFlowLayout* layout = (FJAnimatedFlowLayout*)[[self collectionView] collectionViewLayout];

        NSIndexPath *pinchedItem = [self.collectionView indexPathForItemAtPoint:CGPointMake(0.5*(p1.x+p2.x), 0.5*(p1.y+p2.y))];
        [layout resizeItemAtIndexPath:pinchedItem withPinchDistance:distance];
        [layout invalidateLayout];

    }
    else if (sender.state == UIGestureRecognizerStateCancelled ||
             sender.state == UIGestureRecognizerStateEnded){
        FJAnimatedFlowLayout* layout = (FJAnimatedFlowLayout*)[[self collectionView] collectionViewLayout];
        [self.collectionView
         performBatchUpdates:^{
            [layout resetPinchedItem];
         }
         completion:nil];
    }
}

這個(gè)捏合操作需要計(jì)算捏合距離并找出被捏合的元素,并且在用戶捏合的時(shí)候通知布局以實(shí)現(xiàn)自身更新。當(dāng)捏合手勢(shì)結(jié)束的時(shí)候,布局會(huì)做一個(gè)批量更新動(dòng)畫返回原始尺寸。

另一方面,我們的布局始終在跟蹤捏合的元素以及期望尺寸,并在需要的時(shí)候提供正確的屬性:

- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray *attrs = [super layoutAttributesForElementsInRect:rect];

    if (_pinchedItem) {
        UICollectionViewLayoutAttributes *attr = [[attrs filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"indexPath == %@", _pinchedItem]] firstObject];

        attr.size = _pinchedItemSize;
        attr.zIndex = 100;
    }
    return attrs;
}

小結(jié)

我們通過(guò)一些例子來(lái)說(shuō)明了如何在 collection view 布局中創(chuàng)建自定義動(dòng)畫。雖然 UICollectionViewFlowLayout 并不直接允許定制動(dòng)畫,但是蘋果工程師提供了清晰的架構(gòu)讓你可以子類化并實(shí)現(xiàn)各種自定義行為。從本質(zhì)來(lái)說(shuō),在你的 UICollectionViewLayout 子類中正確地響應(yīng)以下信號(hào),并對(duì)那些要求返回 UICollectionViewLayoutAttributes 的方法返回合適的屬性,那么實(shí)現(xiàn)自定義布局和動(dòng)畫的唯一約束就是你的想象力:

  • prepareLayout
  • prepareForCollectionViewUpdates:
  • finalizeCollectionViewUpdates
  • prepareForAnimatedBoundsChange:
  • finalizeAnimatedBoundsChange
  • shouldInvalidateLayoutForBoundsChange:

更引人入勝的動(dòng)畫可以結(jié)合像在 objc.io 話題 #5 中 UIKit 力學(xué)這樣的技術(shù)來(lái)實(shí)現(xiàn)。

帶有 Collection views 的 View controller 轉(zhuǎn)場(chǎng)

就如 Chris 之前在 objc.io 的文章中所說(shuō)的那樣,iOS 7 中的一個(gè)重大更新是自定義 view controller 轉(zhuǎn)場(chǎng)動(dòng)畫。與自定義轉(zhuǎn)場(chǎng)動(dòng)畫相呼應(yīng),蘋果也在 UICollectionViewController 添加了 useLayoutToLayoutNavigationTransitions 標(biāo)記來(lái)在可復(fù)用的單個(gè) collection view 間啟用導(dǎo)航轉(zhuǎn)場(chǎng)。蘋果自己的照片和日歷應(yīng)用就是這類轉(zhuǎn)場(chǎng)動(dòng)畫的非常好的代表作。

UICollectionViewController 實(shí)例之間的轉(zhuǎn)場(chǎng)動(dòng)畫

讓我們來(lái)看看我們?nèi)绾文軌蚶蒙弦还?jié)相同的示例項(xiàng)目達(dá)到類似的效果:

http://wiki.jikexueyuan.com/project/objc/images/12-27.gif" alt="" />

為了使布局到布局的轉(zhuǎn)場(chǎng)動(dòng)畫工作,navigation controller 的 root view controller 必須是一個(gè) useLayoutToLayoutNavigationTransitions 設(shè)置為 NO 的 collection view controller。當(dāng)另一個(gè) useLayoutToLayoutNavigationTransitions 設(shè)置為 YESUICollectionViewController 實(shí)例被 push 到根視圖控制器之上時(shí),navigation controller 會(huì)用布局轉(zhuǎn)場(chǎng)動(dòng)畫來(lái)代替標(biāo)準(zhǔn)的 push 轉(zhuǎn)場(chǎng)動(dòng)畫。這里要注意一個(gè)重要的細(xì)節(jié),根視圖控制器的 collection view 實(shí)例被回收用于在導(dǎo)航棧上 push 進(jìn)來(lái)的 collection 控制器中,如果你試圖在 viewDidLoad 之類的方法中中設(shè)置 collection view 屬性, 它們將不會(huì)有任何反應(yīng),你也不會(huì)收到任何警告。

這個(gè)行為可能最常見的陷阱是期望回收的 collection view 根據(jù)頂層的 collection 視圖控制器來(lái)更新數(shù)據(jù)源和委托。它當(dāng)然不會(huì)這樣:根 collection 視圖控制器會(huì)保持?jǐn)?shù)據(jù)源和委托,除非我們做點(diǎn)什么。

解決此問(wèn)題的方法是實(shí)現(xiàn) navigation controller 的委托方法,并根據(jù)導(dǎo)航堆棧頂部的當(dāng)前視圖控制器的需要正確設(shè)置 collection view 的數(shù)據(jù)源和委托。在我們簡(jiǎn)單的例子中,這可以通過(guò)以下方式實(shí)現(xiàn):

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if ([viewController isKindOfClass:[FJDetailViewController class]]) {
        FJDetailViewController *dvc = (FJDetailViewController*)viewController;
        dvc.collectionView.dataSource = dvc;
        dvc.collectionView.delegate = dvc;
        [dvc.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:_selectedItem inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:NO];
    }
    else if (viewController == self){
        self.collectionView.dataSource = self;
        self.collectionView.delegate = self;
    }
}

當(dāng)詳細(xì)頁(yè)面的 collection view 被推入導(dǎo)航棧時(shí),我們重新設(shè)置 collection view 的數(shù)據(jù)源到詳細(xì)視圖控制器,確保只有被選擇的 cell 顏色顯示在詳細(xì)頁(yè)面的 collection view 中。如果我們不打算這樣做,布局依然可以正確過(guò)渡,但是collection 將顯示所有的 cells。在實(shí)際應(yīng)用中,detail 的數(shù)據(jù)源通常負(fù)責(zé)在轉(zhuǎn)場(chǎng)動(dòng)畫過(guò)程中顯示更詳細(xì)的數(shù)據(jù)。

用于常規(guī)轉(zhuǎn)換的 Collection View 布局動(dòng)畫

使用了 useLayoutToLayoutNavigationTransitions 的布局和布局間導(dǎo)航轉(zhuǎn)換是很有用的,但卻局限于僅在 兩個(gè) view controller 都是 UICollectionViewController 的實(shí)例,并且轉(zhuǎn)場(chǎng)的必須發(fā)生在頂級(jí) collection views 之間。為了達(dá)到在任意視圖控制器的任意 collection view 之間都能實(shí)現(xiàn)相似的過(guò)渡,我們需要自定義一個(gè) view collection 的轉(zhuǎn)場(chǎng)動(dòng)畫。

http://wiki.jikexueyuan.com/project/objc/images/12-28.gif" alt="" />

針對(duì)此類自定義過(guò)渡的動(dòng)畫控制器,需要遵循以下步驟進(jìn)行設(shè)計(jì):

  1. 對(duì)初始的 collection view 中的所有可見元素制作截圖
  2. 將截圖添加到轉(zhuǎn)場(chǎng)上下文的 container view 中
  3. 運(yùn)用目標(biāo) collection view 的布局計(jì)算最終位置
  4. 制作動(dòng)畫使快照到正確的位置
  5. 當(dāng)目標(biāo) collection view 可見時(shí)刪除截圖

一個(gè)這樣的動(dòng)畫設(shè)計(jì)有兩重缺陷:它只能對(duì)初始的 collection view 的可見元素制作動(dòng)畫,因?yàn)?a rel="nofollow" >快照 APIs 只能工作于屏幕上可見的 view,另外,依賴于可見的元素?cái)?shù)量,可能會(huì)有很多的 views 需要進(jìn)行正確的跟蹤并為其制作動(dòng)畫。但另一方面,這種設(shè)計(jì)又具有一個(gè)明顯的優(yōu)勢(shì),那就是它可以為所有類型的 UICollectionViewLayout 組合所使用。這樣一個(gè)系統(tǒng)的實(shí)現(xiàn)就留給讀者們?nèi)ミM(jìn)行練習(xí)吧。

在附帶的演示項(xiàng)目中我們用另一種途徑進(jìn)行了實(shí)現(xiàn),它依賴于一些 UICollectionViewFlowLayout 的巧合。

基本的想法是,因?yàn)樵?collection view 和目標(biāo) collection view 都擁有有效的 flow layouts,因此源 layout 的布局屬性正好可以用作目標(biāo) collection view 的布局中的初始布局屬性,以此驅(qū)動(dòng)轉(zhuǎn)場(chǎng)動(dòng)畫。一旦正確建立,就算對(duì)于那些一開始在屏幕上不可見的元素,collection view 的機(jī)制都將為我們追蹤它們并進(jìn)行動(dòng)畫。下面是我們的動(dòng)畫控制器中的 animateTransition: 的核心代碼:

    CGRect initialRect = [inView.window convertRect:_fromCollectionView.frame fromView:_fromCollectionView.superview];
    CGRect finalRect   = [transitionContext finalFrameForViewController:toVC];

    UICollectionViewFlowLayout *toLayout = (UICollectionViewFlowLayout*) _toCollectionView.collectionViewLayout;

    UICollectionViewFlowLayout *currentLayout = (UICollectionViewFlowLayout*) _fromCollectionView.collectionViewLayout;

    //制作原來(lái)布局的拷貝
    UICollectionViewFlowLayout *currentLayoutCopy = [[UICollectionViewFlowLayout alloc] init];

    currentLayoutCopy.itemSize = currentLayout.itemSize;
    currentLayoutCopy.sectionInset = currentLayout.sectionInset;
    currentLayoutCopy.minimumLineSpacing = currentLayout.minimumLineSpacing;
    currentLayoutCopy.minimumInteritemSpacing = currentLayout.minimumInteritemSpacing;
    currentLayoutCopy.scrollDirection = currentLayout.scrollDirection;

    //將拷貝賦值給源 collection view
    [self.fromCollectionView setCollectionViewLayout:currentLayoutCopy animated:NO];

    UIEdgeInsets contentInset = _toCollectionView.contentInset;

    CGFloat oldBottomInset = contentInset.bottom;

    //強(qiáng)制在目標(biāo) collection view 中設(shè)定一個(gè)很大的 bottom inset
    contentInset.bottom = CGRectGetHeight(finalRect)-(toLayout.itemSize.height+toLayout.sectionInset.bottom+toLayout.sectionInset.top);
    self.toCollectionView.contentInset = contentInset;

    //將源布局設(shè)置給目標(biāo) collection view
    [self.toCollectionView setCollectionViewLayout:currentLayout animated:NO];

    toView.frame = initialRect;

    [inView insertSubview:toView aboveSubview:fromView];

    [UIView
     animateWithDuration:[self transitionDuration:transitionContext]
     delay:0
     options:UIViewAnimationOptionBeginFromCurrentState
     animations:^{
       //使用最終 frame 制作動(dòng)畫
         toView.frame = finalRect;
         //在 performUpdates 中設(shè)定最終的布局
         [_toCollectionView
          performBatchUpdates:^{
              [_toCollectionView setCollectionViewLayout:toLayout animated:NO];
          }
          completion:^(BOOL finished) {
              _toCollectionView.contentInset = UIEdgeInsetsMake(contentInset.top,
                                                                contentInset.left,
                                                                oldBottomInset,
                                                                contentInset.right);
          }];

     } completion:^(BOOL finished) {
         [transitionContext completeTransition:YES];
     }];

首先,動(dòng)畫控制器確保目標(biāo) collection view 以與原來(lái)的 collection view 完全相同的框架和布局作為開始。接著,它將源 collection view 的布局設(shè)定給目標(biāo) collection view,以確保其不會(huì)失效。與此同時(shí),該布局已經(jīng)復(fù)制到另一個(gè)新的布局對(duì)象中,而這個(gè)布局對(duì)象則是為防止在導(dǎo)航回原始視圖控制器時(shí)出現(xiàn)奇怪的布局 bug。我們還會(huì)強(qiáng)制在目標(biāo) collection view 的底部設(shè)定一個(gè)很大的 content inset,來(lái)確保布局在動(dòng)畫的初始位置時(shí)保持在一行上。觀察日志的話,你會(huì)發(fā)現(xiàn)由于元素的尺寸加上 inset 的尺寸會(huì)比 collection view 的非滾動(dòng)維度要大,因此 collection view 會(huì)在控制臺(tái)警告。在這樣的情況下,collection view 的行為是沒有定義的,我們也只是使用這樣一個(gè)不穩(wěn)定的狀態(tài)來(lái)作為我們轉(zhuǎn)換動(dòng)畫的初始狀態(tài)。最后,復(fù)雜的動(dòng)畫 block 將展現(xiàn)它的魅力,首先將目標(biāo) collection view 的框架設(shè)定到最終位置,然后在 performBatchUpdates:completion: 的 update block 中執(zhí)行一個(gè)無(wú)動(dòng)畫的布局來(lái)改變至最終布局,緊隨其后便是在 completion block 中將 content insets 重置為原始值。

小結(jié)

我們討論了兩種可以在 collection view 之間實(shí)現(xiàn)布局轉(zhuǎn)場(chǎng)的途徑。一種使用了內(nèi)置的 useLayoutToLayoutNavigationTransitions,看起來(lái)令人印象深刻并且極其容易實(shí)現(xiàn),缺點(diǎn)就是可以使用的范圍較為局限。由于 useLayoutToLayoutNavigationTransitions 在一些案例中不能使用,想驅(qū)動(dòng)自定義的過(guò)渡動(dòng)畫的話,就需要一個(gè)自定義的 animator。這篇文章中,我們看到了如何實(shí)現(xiàn)這樣一個(gè) animator,然而,由于你的應(yīng)用程序大概肯定會(huì)需要在兩個(gè)和本例完全不同的 view 結(jié)構(gòu)中實(shí)現(xiàn)完全不同的動(dòng)畫,所以正如此例中做的那樣,不要吝于嘗試不同的方法來(lái)探究其是否能夠工作。