鍍金池/ 教程/ iOS/ UICollectionView + UIKit 力學(xué)
與四軸無人機(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 上捕獲視頻
四軸無人機(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é)無止境
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ī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

UICollectionView + UIKit 力學(xué)

UIKit Dynamics 是 iOS 7 中基于物理動(dòng)畫引擎的一個(gè)新功能--它被特別設(shè)計(jì)使其能很好地與 collection views 配合工作,而后者是在 iOS 6 中才被引入的新特性。接下來,我們要好好看看如何將這兩個(gè)特性結(jié)合在一起。

這篇文章將討論兩個(gè)結(jié)合使用 UIkit Dynamics 和 collection view 的例子。第一個(gè)例子展示了如何去實(shí)現(xiàn)像 iOS 7 里信息 app 中的消息泡泡的彈簧動(dòng)效,然后再進(jìn)一步結(jié)合平鋪機(jī)制來實(shí)現(xiàn)布局的可伸縮性。第二個(gè)例子展現(xiàn)了如何用 UIKit Dynamics 來模擬牛頓擺,這個(gè)例子中物體可以一個(gè)個(gè)地加入到 collection view 中,并和其他物體發(fā)生相互作用。

在我們開始之前,我假定你們對(duì) UICollectionView 是如何工作是有基本的了解——查看這篇 objc.io 文章會(huì)有你想要的所有細(xì)節(jié)。我也假定你已經(jīng)理解了 UIKit Dynamics 的工作原理--閱讀這篇博客,可以了解更多 UIKit Dynamics 的知識(shí)。

編者注 如果您閱讀本篇文章感覺有點(diǎn)吃力的話,可以先來看看 @onevcat《UICollectionView 入門》《UIKit Dynamics 入門》這兩篇入門文章,幫助您快速補(bǔ)充相關(guān)知識(shí)。

文章中的兩個(gè)例子項(xiàng)目都已經(jīng)在 GitHub 中:

關(guān)于 UIDynamicAnimator

支持 UICollectionView 實(shí)現(xiàn) UIKit Dynamics 的最關(guān)鍵部分就是 UIDynamicAnimator。要實(shí)現(xiàn)這樣的 UIKit Dynamics 的效果,我們需要自己自定義一個(gè)繼承于 UICollectionViewFlowLayout 的子類,并且在這個(gè)子類對(duì)象里面持有一個(gè) UIDynamicAnimator 的對(duì)象。

當(dāng)我們創(chuàng)建自定義的 dynamic animator 時(shí),我們不會(huì)使用常用的初始化方法 -initWithReferenceView: ,因?yàn)槲覀儾恍枰堰@個(gè) dynamic animator 關(guān)聯(lián)一個(gè) view ,而是給它關(guān)聯(lián)一個(gè) collection view layout。所以我們使用 -initWithCollectionViewLayout: 這個(gè)初始化方法,并把 collection view layout 作為參數(shù)傳入。這很關(guān)鍵,當(dāng)?shù)?animator 的 behavior item 的屬性應(yīng)該被更新的時(shí)候,它必須能夠確保 collection view 的 layout 失效。換句話說,dynamic animator 將會(huì)經(jīng)常使舊的 layout 失效。

我們很快就能看到這些事情是怎么連接起來的,但是在概念上理解 collection view 如何與 dynamic animator 相互作用是很重要的。

Collection view layout 將會(huì)為 collection view 中的每個(gè) UICollectionViewLayoutAttributes 添加 behavior(稍后我們會(huì)討論平鋪它們)。在將這些 behaviors 添加到 dynamic animator 之后,UIKit 將會(huì)向 collection view layout 詢問 atrribute 的狀態(tài)。我們此時(shí)可以直接將由 dynamic animator 所提供的 items 返回,而不需要自己做任何計(jì)算。Animator 將在模擬時(shí)禁用 layout。這會(huì)導(dǎo)致 UIKit 再次查詢 layout,這個(gè)過程會(huì)一直持續(xù)到模擬滿足設(shè)定條件而結(jié)束。

所以重申一下,layout 創(chuàng)建了 dynamic animator,并且為其中每個(gè) item 的 layout attribute 添加對(duì)應(yīng)的 behaviors。當(dāng) collection view 需要 layout 信息時(shí),由 dynamic animator 來提供需要的信息。

繼承 UICollectionViewFlowLayout

我們將要?jiǎng)?chuàng)建一個(gè)簡(jiǎn)單的例子來展示如何使用一個(gè)帶 UIkit Dynamic 的 collection view layout。當(dāng)然,我們需要做的第一件事就是,創(chuàng)建一個(gè)數(shù)據(jù)源去驅(qū)動(dòng)我們的 collection view。我知道以你的能力完全可以獨(dú)立實(shí)現(xiàn)一個(gè)數(shù)據(jù)源,但是為了完整性,我還是提供了一個(gè)給你:

@implementation ASHCollectionViewController

static NSString * CellIdentifier = @"CellIdentifier";

-(void)viewDidLoad 
{
    [super viewDidLoad];
    [self.collectionView registerClass:[UICollectionViewCell class] 
            forCellWithReuseIdentifier:CellIdentifier];
}

-(UIStatusBarStyle)preferredStatusBarStyle 
{
    return UIStatusBarStyleLightContent;
}

-(void)viewDidAppear:(BOOL)animated 
{
    [super viewDidAppear:animated];
    [self.collectionViewLayout invalidateLayout];
}

#pragma mark - UICollectionView Methods

-(NSInteger)collectionView:(UICollectionView *)collectionView 
    numberOfItemsInSection:(NSInteger)section 
{
    return 120;
}

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView 
                 cellForItemAtIndexPath:(NSIndexPath *)indexPath 
{
    UICollectionViewCell *cell = [collectionView 
        dequeueReusableCellWithReuseIdentifier:CellIdentifier 
                                  forIndexPath:indexPath];

    cell.backgroundColor = [UIColor orangeColor];
    return cell;
}

@end

我們注意到當(dāng) view 第一次出現(xiàn)的時(shí)候,這個(gè) layout 是被無效的。這是因?yàn)闆]有用 Storyboard 的結(jié)果(使用或不使用 Storyboard,調(diào)用 prepareLayout 方法的時(shí)機(jī)是不同的,蘋果在 WWDC 的視頻中并沒有告訴我們這一點(diǎn))。所以,當(dāng)這些視圖一出現(xiàn)我們就需要手動(dòng)使這個(gè) collection view layout 無效。當(dāng)我們用平鋪(后面會(huì)詳細(xì)介紹)的時(shí)候,就不需要這樣。

現(xiàn)在來創(chuàng)建自定義的 collection view layout 吧,我們需要強(qiáng)引用一個(gè) dynamic animator,并且使用它來驅(qū)動(dòng)我們的 collcetion view layout 的 attribute。我們?cè)趯?shí)現(xiàn)文件里定義了一個(gè)私有屬性:

@interface ASHSpringyCollectionViewFlowLayout ()

@property (nonatomic, strong) UIDynamicAnimator *dynamicAnimator;

@end

我們將在 layout 的初始化方法中初始化我們的 dynamic animator。還要設(shè)置一些屬于父類 UICollectionViewFlowLayout 中的屬性:

- (id)init 
{
    if (!(self = [super init])) return nil;

    self.minimumInteritemSpacing = 10;
    self.minimumLineSpacing = 10;
    self.itemSize = CGSizeMake(44, 44);
    self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

    self.dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];

    return self;
}

我們將實(shí)現(xiàn)的下一個(gè)方法是 prepareLayout。我們首先需要調(diào)用父類的方法。因?yàn)槲覀兪抢^承 UICollectionViewFlowLayout 類,所以在調(diào)用父類的 prepareLayout 方法時(shí),可以使 collection view layout 的各個(gè) attribute 都放置在合適的位置。我們可以依靠父類的這個(gè)方法來提供一個(gè)默認(rèn)的排布,并且能夠使用 [super layoutAttributesForElementsInRect:visibleRect]; 方法得到指定 rect 內(nèi)的所有 item 的 layout attributes。

[super prepareLayout];

CGSize contentSize = self.collectionView.contentSize;
NSArray *items = [super layoutAttributesForElementsInRect:
    CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height)];

真的是效率低下的代碼。因?yàn)槲覀兊?collection view 中可能會(huì)有成千上萬個(gè) cell,一次性加載所有的 cell 是一個(gè)可能會(huì)產(chǎn)生難以置信的內(nèi)存緊張的操作。我們要在一段時(shí)間內(nèi)遍歷所有的元素,這也成為耗時(shí)的操作。這真的是效率的雙重打擊!別擔(dān)心——我們是負(fù)責(zé)任的開發(fā)者,所以我們會(huì)很快解決這個(gè)問題的。我們先暫時(shí)繼續(xù)使用簡(jiǎn)單、粗暴的實(shí)現(xiàn)方式。

當(dāng)加載完我們所有的 collection view layout attribute 之后,我們需要檢查他們是否都已經(jīng)被加載到我們的 animator 里了。如果一個(gè) behavior 已經(jīng)在 animator 中存在,那么我們就不能重新添加,否則就會(huì)得到一個(gè)非常難懂的運(yùn)行異常提示:

<UIDynamicAnimator: 0xa5ba280> (0.004987s) in 
<ASHSpringyCollectionViewFlowLayout: 0xa5b9e60> \{\{0, 0}, \{0, 0\}\}: 
body <PKPhysicsBody> type:<Rectangle> representedObject:
[<UICollectionViewLayoutAttributes: 0xa281880> 
index path: (<NSIndexPath: 0xa281850> {length = 2, path = 0 - 0}); 
frame = (10 10; 300 44); ] 0xa2877c0  
PO:(159.999985,32.000000) AN:(0.000000) VE:(0.000000,0.000000) AV:(0.000000) 
dy:(1) cc:(0) ar:(1) rs:(0) fr:(0.200000) re:(0.200000) de:(1.054650) gr:(0) 
without representedObject for item <UICollectionViewLayoutAttributes: 0xa3833e0> 
index path: (<NSIndexPath: 0xa382410> {length = 2, path = 0 - 0}); 
frame = (10 10; 300 44);

如果看到了這個(gè)錯(cuò)誤,那么這基本表明你添加了兩個(gè) behavior 給同一個(gè) UICollectionViewLayoutAttribute,這使得系統(tǒng)不知道該怎么處理。

無論如何,一旦我們已經(jīng)檢查好我們是否已經(jīng)將 behavior 添加到 dynamic animator 之后,我們就需要遍歷每個(gè) collection view layout attribute 來創(chuàng)建和添加新的 dynamic animator:

if (self.dynamicAnimator.behaviors.count == 0) {
    [items enumerateObjectsUsingBlock:^(id<UIDynamicItem> obj, NSUInteger idx, BOOL *stop) {
        UIAttachmentBehavior *behaviour = [[UIAttachmentBehavior alloc] initWithItem:obj 
                                                                    attachedToAnchor:[obj center]];

        behaviour.length = 0.0f;
        behaviour.damping = 0.8f;
        behaviour.frequency = 1.0f;

        [self.dynamicAnimator addBehavior:behaviour];
    }];
}

這段代碼非常簡(jiǎn)單。我們?yōu)槊總€(gè) item 創(chuàng)建了一個(gè)以物體的中心為附著點(diǎn)的 UIAttachmentBehavior 對(duì)象。然后又設(shè)置了我們的 attachment behavior 的 length 為 0 以便約束這個(gè) cell 能一直以 behavior 的附著點(diǎn)為中心。然后又給 dampingfrequency 這兩個(gè)參數(shù)設(shè)置一個(gè)比較合適的值。

這就是 prepareLayout。我們現(xiàn)在需要實(shí)現(xiàn) layoutAttributesForElementsInRect:layoutAttributesForItemAtIndexPath: 這兩個(gè)方法,UIKit 會(huì)調(diào)用它們來詢問 collection view 每一個(gè) item 的布局信息。我們寫的代碼會(huì)把這些查詢交給專門做這些事的 dynamic animator:

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 
{
    return [self.dynamicAnimator itemsInRect:rect];
}

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath 
{
    return [self.dynamicAnimator layoutAttributesForCellAtIndexPath:indexPath];
}

響應(yīng)滾動(dòng)事件

我們目前實(shí)現(xiàn)的代碼給我們展示的只是一個(gè)在正常滑動(dòng)下只有靜態(tài)感覺的 UICollectionView,運(yùn)行起來沒什么特別的??瓷先ズ芎?,但不是真的動(dòng)態(tài),不是么?

為了使它表現(xiàn)地動(dòng)態(tài)點(diǎn),我們需要 layout 和 dynamic animator 能夠?qū)?collection view 中滑動(dòng)位置的變化做出反應(yīng)。幸好這里有個(gè)非常適合這個(gè)要求的方法 shouldInvalidateLayoutForBoundsChange:。這個(gè)方法會(huì)在 collection view 的 bound 發(fā)生改變的時(shí)候被調(diào)用,根據(jù)最新的 content offset 調(diào)整我們的 dynamic animator 中的 behaviors 的參數(shù)。在重新調(diào)整這些 behavior 的 item 之后,我們?cè)谶@個(gè)方法中返回 NO;因?yàn)?dynamic animator 會(huì)關(guān)心 layout 的無效問題,所以在這種情況下,它不需要去主動(dòng)使其無效:

-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 
{
    UIScrollView *scrollView = self.collectionView;
    CGFloat delta = newBounds.origin.y - scrollView.bounds.origin.y;

    CGPoint touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];

    [self.dynamicAnimator.behaviors enumerateObjectsUsingBlock:^(UIAttachmentBehavior *springBehaviour, NSUInteger idx, BOOL *stop) {
        CGFloat yDistanceFromTouch = fabsf(touchLocation.y - springBehaviour.anchorPoint.y);
        CGFloat xDistanceFromTouch = fabsf(touchLocation.x - springBehaviour.anchorPoint.x);
        CGFloat scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0f;

        UICollectionViewLayoutAttributes *item = springBehaviour.items.firstObject;
        CGPoint center = item.center;
        if (delta < 0) {
            center.y += MAX(delta, delta*scrollResistance);
        }
        else {
            center.y += MIN(delta, delta*scrollResistance);
        }
        item.center = center;

        [self.dynamicAnimator updateItemUsingCurrentState:item];
    }];

    return NO;
}

讓我們仔細(xì)查看這個(gè)代碼的細(xì)節(jié)。首先我們得到了這個(gè) scroll view(就是我們的 collection view ),然后計(jì)算它的 content offset 中 y 的變化(在這個(gè)例子中,我們的 collection view 是垂直滑動(dòng)的)。一旦我們得到這個(gè)增量,我們需要得到用戶接觸的位置。這是非常重要的,因?yàn)槲覀兿Mx接觸位置比較近的那些物體能移動(dòng)地更迅速些,而離接觸位置比較遠(yuǎn)的那些物體則應(yīng)該滯后些。

對(duì)于 dynamic animator 中的每個(gè) behavior,我們將接觸點(diǎn)到該 behavior 物體的 x 和 y 的距離之和除以 1500,1500 是我根據(jù)經(jīng)驗(yàn)設(shè)的。分母越小,這個(gè) collection view 的的交互就越有彈簧的感覺。一旦我們拿到了這個(gè)“滑動(dòng)阻力”的值,我們就可以用它的增量乘上 scrollResistance 這個(gè)變量來指定這個(gè) behavior 物體的中心點(diǎn)的 y 值。最后,我們?cè)诨瑒?dòng)阻力大于增量的情況下對(duì)增量和滑動(dòng)阻力的結(jié)果進(jìn)行了選擇(這意味著物體開始往錯(cuò)誤的方向移動(dòng)了)。在本例我們用了這么大的分母,那么這種情況是不可能的,但是在一些更具彈性的 collection view layout 中還是需要注意的。

就是這么一回事。以我的經(jīng)驗(yàn),這個(gè)方法對(duì)多達(dá)幾百個(gè)物體的 collection view 來說也是是適用的。超過這個(gè)數(shù)量的話,一次性加載所有物體到內(nèi)存中就會(huì)變成很大的負(fù)擔(dān),并且在滑動(dòng)的時(shí)候就會(huì)開始卡頓了。

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

平鋪(Tiling)你的 Dynamic Behaviors 來優(yōu)化性能

當(dāng)你的 collection view 中只有幾百個(gè) cell 的時(shí)候,他運(yùn)行的很好,但當(dāng)數(shù)據(jù)源超過這個(gè)范圍的時(shí)候會(huì)發(fā)生什么呢?或者在運(yùn)行的時(shí)你不能預(yù)測(cè)你的數(shù)據(jù)源有多大呢?我們的簡(jiǎn)單粗暴的方法就不管用了。

除了在 prepareLayout 中加載所有的物體,如果我們能更聰明地知道哪些物體會(huì)加載那該多好啊。是的,就是僅加載顯示的和即將顯示的物體。這正是我們要采取的辦法。

我們需要做的第一件事就是是跟蹤 dynamic animator 中的所有 behavior 物體的 index path。我在 collection view 中添加一個(gè)屬性來做這件事:

@property (nonatomic, strong) NSMutableSet *visibleIndexPathsSet;

我們用 set 是因?yàn)樗哂谐?shù)復(fù)雜度的查找效率,并且我們經(jīng)常地查找 visibleIndexPathsSet 中是否已經(jīng)包含了某個(gè) index path。

在我們實(shí)現(xiàn)全新的 prepareLayout 方法之前——有一個(gè)問題就是什么是平鋪 behavior —— 理解平鋪的意思是非常重要的。當(dāng)我們平鋪behavior 的時(shí)候,我們會(huì)在這些 item 離開 collection view 的可視范圍的時(shí)候刪除對(duì)應(yīng)的 behavior,在這些 item 進(jìn)入可視范圍的時(shí)候又添加對(duì)應(yīng)的 behavior。這是一個(gè)大麻煩:我們需要在滾動(dòng)中創(chuàng)建新的 behavior。這就意味著讓人覺得創(chuàng)建它們就好像它們本來就已經(jīng)在 dynamic animator 里了一樣,并且它們是在 shouldInvalidateLayoutForBoundsChange: 方法被修改的。

因?yàn)槲覀兪窃跐L動(dòng)中創(chuàng)建這些新的 behavior,所以我們需要維持現(xiàn)在 collection view 的一些狀態(tài)。尤其我們需要跟蹤最近一次我們 bound 變化的增量。我們會(huì)在滾動(dòng)時(shí)用這個(gè)狀態(tài)去創(chuàng)建我們的 behavior:

@property (nonatomic, assign) CGFloat latestDelta;

添加完這個(gè) property 后,我們將要在 shouldInvalidateLayoutForBoundsChange: 方法中添加下面這行代碼:

self.latestDelta = delta;

這就是我們需要修改我們的方法來響應(yīng)滾動(dòng)事件。我們的這兩個(gè)方法是為了將 collection view 中 items 的 layout 信息傳給 dynamic animator,這種方式?jīng)]有變化。事實(shí)上,當(dāng)你的 collection view 實(shí)現(xiàn)了 dynamic animator 的大部分情況下,都需要實(shí)現(xiàn)我們上面提到的兩個(gè)方法 layoutAttributesForElementsInRect:layoutAttributesForItemAtIndexPath:。

這里最難懂的部分就是平鋪機(jī)制。我們將要完全重寫我們的 prepareLayout。

這個(gè)方法的第一步是將那些物體的 index path 已經(jīng)不在屏幕上顯示的 behavior 從 dynamic animator 上刪除。第二步是添加那些即將顯示的物體的 behavior。

讓我們先看一下第一步。

像以前一樣,我們要調(diào)用 super prepareLayout,這樣我們就能依賴父類 UICollectionViewFlowLayout 提供的默認(rèn)排布。還像以前一樣,我們通過父類獲取一個(gè)矩形內(nèi)的所有元素的 layout attribute。不同的是我們不是獲取整個(gè) collection view 中的元素屬性,而只是獲取顯示范圍內(nèi)的。

所以我們需要計(jì)算這個(gè)顯示矩形。但是別著急!有件事要記住。我們的用戶可能會(huì)非常快地滑動(dòng) collection view,導(dǎo)致了 dynamic animator 不能跟上,所以我們需要稍微擴(kuò)大顯示范圍,這樣就能包含到那些將要顯示的物體了。否則,在滑動(dòng)很快的時(shí)候就會(huì)出現(xiàn)頻閃現(xiàn)象了。讓我們計(jì)算一下顯示范圍:

CGRect originalRect = (CGRect){.origin = self.collectionView.bounds.origin, .size = self.collectionView.frame.size};
CGRect visibleRect = CGRectInset(originalRect, -100, -100);

我確信在實(shí)際顯示矩形上的每個(gè)方向都擴(kuò)大100個(gè)像素對(duì)我的 demo 來說是可行的。仔細(xì)查看這些值是否適合你們的 collection view,尤其是當(dāng)你們的 cell 很小的情況下。

接下來我們就需要收集在顯示范圍內(nèi)的 collection view layout attributes。還有它們的 index paths:

NSArray *itemsInVisibleRectArray = [super layoutAttributesForElementsInRect:visibleRect];

NSSet *itemsIndexPathsInVisibleRectSet = [NSSet setWithArray:[itemsInVisibleRectArray valueForKey:@"indexPath"]];

注意我們是在用一個(gè) NSSet。這是因?yàn)樗哂谐?shù)復(fù)雜度的查找效率,并且我們經(jīng)常的查找 visibleIndexPathsSet 是否已經(jīng)包含了某個(gè) index path:

接下來我們要做的就是遍歷 dynamic animator 的 behaviors,過濾掉那些已經(jīng)在 itemsIndexPathsInVisibleRectSet 中的 item。因?yàn)槲覀円呀?jīng)過濾掉我們的 behavior,所以我們將要遍歷的這些 item 都是不在顯示范圍里的,我們就可以將這些 item 從 animator 中刪除掉(連同 visibleIndexPathsSet 屬性中的 index path):

NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIAttachmentBehavior *behaviour, NSDictionary *bindings) {
    BOOL currentlyVisible = [itemsIndexPathsInVisibleRectSet member:[[[behaviour items] firstObject] indexPath]] != nil;
    return !currentlyVisible;
}]
NSArray *noLongerVisibleBehaviours = [self.dynamicAnimator.behaviors filteredArrayUsingPredicate:predicate];

[noLongerVisibleBehaviours enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
    [self.dynamicAnimator removeBehavior:obj];
    [self.visibleIndexPathsSet removeObject:[[[obj items] firstObject] indexPath]];
}];

下一步就是要得到出現(xiàn) item 的 UICollectionViewLayoutAttributes 數(shù)組——那些 item 的 index path 在 itemsIndexPathsInVisibleRectSet 而不在 visibleIndexPathsSet

NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes *item, NSDictionary *bindings) {
    BOOL currentlyVisible = [self.visibleIndexPathsSet member:item.indexPath] != nil;
    return !currentlyVisible;
}];
NSArray *newlyVisibleItems = [itemsInVisibleRectArray filteredArrayUsingPredicate:predicate];

一旦我們有新的 layout attribute 出現(xiàn),我就可以遍歷他們來創(chuàng)建新的 behavior,并且將他們的 index path 添加到 visibleIndexPathsSet 中。首先,無論如何,我都需要獲取到用戶手指觸碰的位置。如果它是 CGPointZero 的話,那就表示這個(gè)用戶沒有在滑動(dòng) collection view,這時(shí)我就假定我們不需要在滾動(dòng)時(shí)創(chuàng)建新的 behavior 了:

CGPoint touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];

這是一個(gè)潛藏危險(xiǎn)的假定。如果用戶很快地滑動(dòng)了 collection view 之后釋放了他的手指呢?這個(gè) collection view 就會(huì)一直滾動(dòng),但是我們的方法就不會(huì)在滾動(dòng)時(shí)創(chuàng)建新的 behavior 了。但幸運(yùn)的是,那也就意味這時(shí) scroll view 滾動(dòng)太快很難被注意到!好哇!但是,對(duì)于那些擁有大型 cell 的 collection view 來說,這仍然是個(gè)問題。那么在這種情況下,就需要增加你的可視范圍的 bounds 來加載更多物體以解決這個(gè)問題。

現(xiàn)在我們需要枚舉我們剛顯示的 item,為他們創(chuàng)建 behavior,再將他們的 index path 添加到 visibleIndexPathsSet。我們還需要在滾動(dòng)時(shí)做些數(shù)學(xué)運(yùn)算來創(chuàng)建 behavior:

[newlyVisibleItems enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *item, NSUInteger idx, BOOL *stop) {
    CGPoint center = item.center;
    UIAttachmentBehavior *springBehaviour = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:center];

    springBehaviour.length = 0.0f;
    springBehaviour.damping = 0.8f;
    springBehaviour.frequency = 1.0f;

    if (!CGPointEqualToPoint(CGPointZero, touchLocation)) {
        CGFloat yDistanceFromTouch = fabsf(touchLocation.y - springBehaviour.anchorPoint.y);
        CGFloat xDistanceFromTouch = fabsf(touchLocation.x - springBehaviour.anchorPoint.x);
        CGFloat scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0f;

        if (self.latestDelta < 0) {
            center.y += MAX(self.latestDelta, self.latestDelta*scrollResistance);
        }
        else {
            center.y += MIN(self.latestDelta, self.latestDelta*scrollResistance);
        }
        item.center = center;
    }

    [self.dynamicAnimator addBehavior:springBehaviour];
    [self.visibleIndexPathsSet addObject:item.indexPath];
}];

大部分代碼看起來還是挺熟悉的。大概有一半是來自沒有實(shí)現(xiàn)平鋪的 prepareLayout。另一半是來自 shouldInvalidateLayoutForBoundsChange: 這個(gè)方法。我們用 latestDelta 這個(gè)屬性來表示 bound 變化的增量,適當(dāng)?shù)卣{(diào)整 UICollectionViewLayoutAttributes 使這些 cell 表現(xiàn)地就像被 attachment behavior “拉”著一樣。

就這樣就完成了,真的!我已經(jīng)在真機(jī)上測(cè)試過顯示上千個(gè) cell 的情況了,它運(yùn)行地非常完美。去試試吧。

超越瀑布流布局

一般來說,當(dāng)我們使用 UICollectionView 的時(shí)候,繼承 UICollectionViewFlowLayout 會(huì)比直接繼承 UICollectionViewLayout 更容易。這是因?yàn)?flow layout 會(huì)為我們做很多事。然而,瀑布流布局是嚴(yán)格基于它們的尺寸一個(gè)接一個(gè)的展現(xiàn)出來。如果你有一個(gè)布局不能適應(yīng)這個(gè)標(biāo)準(zhǔn)怎么辦?好的,如果你已經(jīng)嘗試用 UICollectionViewFlowLayout 來適應(yīng),而且你很確定它不能很好運(yùn)行,那么就應(yīng)該拋棄 UICollectionViewFlowLayout 這個(gè)定制性比較弱的子類,而應(yīng)該直接在 UICollectionViewLayout 這個(gè)基類上進(jìn)行定制。

這個(gè)原則在處理 UIKit Dynamic 時(shí)也是適用的。

讓我們先創(chuàng)建 UICollectionViewLayout 的子類。當(dāng)繼承 UICollectionViewLayout 的時(shí)候需要實(shí)現(xiàn) collectionViewContentSize 方法,這點(diǎn)非常重要。否則這個(gè) collection view 就不知道如果去顯示自己,也不會(huì)有顯示任何東西。因?yàn)槲覀兿胍?collection view 不能滾動(dòng),所以這里要返回 collection view 的 frame 的 size,減去它的 contentInset.top

-(CGSize)collectionViewContentSize 
{
    return CGSizeMake(self.collectionView.frame.size.width, 
        self.collectionView.frame.size.height - self.collectionView.contentInset.top);
}

在這個(gè)(有點(diǎn)教學(xué)式)的例子中,我們的 collection view 總是會(huì)以零個(gè)cell開始,物體通過 performBatchUpdates: 這個(gè)方法添加。這就意味著我們必須使用 -[UICollectionViewLayout prepareForCollectionViewUpdates:] 這個(gè)方法來添加我們的 behavior(即這個(gè) collection view 的數(shù)據(jù)源總是以零開始)。

除了給各個(gè) item 添加 attachment behavior 外,我們還將保留另外兩個(gè) behavior:重力和碰撞。對(duì)于添加在這個(gè) collection view 中的每個(gè) item 來說,我們必須把這些 item 添加到我們的碰撞和 attachment behavior 中。最后一步就是設(shè)置這些 item 的初始位置為屏幕外的某些地方,這樣就有被 attachment behavior 拉入到屏幕內(nèi)的效果了:

-(void)prepareForCollectionViewUpdates:(NSArray *)updateItems
{
    [super prepareForCollectionViewUpdates:updateItems];

    [updateItems enumerateObjectsUsingBlock:^(UICollectionViewUpdateItem *updateItem, NSUInteger idx, BOOL *stop) {
        if (updateItem.updateAction == UICollectionUpdateActionInsert) {
            UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes 
                layoutAttributesForCellWithIndexPath:updateItem.indexPathAfterUpdate];

            attributes.frame = CGRectMake(CGRectGetMaxX(self.collectionView.frame) + kItemSize, 300, kItemSize, kItemSize);

            UIAttachmentBehavior *attachmentBehaviour = [[UIAttachmentBehavior alloc] initWithItem:attributes 
                                                                                  attachedToAnchor:attachmentPoint];
            attachmentBehaviour.length = 300.0f;
            attachmentBehaviour.damping = 0.4f;
            attachmentBehaviour.frequency = 1.0f;
            [self.dynamicAnimator addBehavior:attachmentBehaviour];

            [self.gravityBehaviour addItem:attributes];
            [self.collisionBehaviour addItem:attributes];
        }
    }];
}

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

刪除就有點(diǎn)復(fù)雜了。我們希望這些物體有“掉落”的效果而不是簡(jiǎn)單的消失。這就不僅僅是從 collection view 中刪除個(gè) cell 這么簡(jiǎn)單了,因?yàn)槲覀兿M谒x開了屏幕之前還是保留它。我已經(jīng)在代碼中實(shí)現(xiàn)了這樣的效果,但是做法有點(diǎn)取巧。

基本上我們要做的是在 layout 中提供一個(gè)方法,在它刪除 attachment behavior 兩秒之后,將這個(gè) cell 從 collection view 中刪除。我們希望在這段時(shí)間里,這個(gè) cell 能掉出屏幕,但是這不一定會(huì)發(fā)生。如果沒有發(fā)生,也沒關(guān)系。只要淡出就行了。然而,我們必須保證在這兩秒內(nèi)既沒有新的 cell 被添加,也沒有舊的 cell 被刪除。(我說了有點(diǎn)取巧。)

歡迎提交 pull request。

這個(gè)方法是有局限性的。我將 cell 數(shù)量的上限設(shè)為 10,但是即使這樣,在像 iPad2 這樣比較舊的設(shè)備中,動(dòng)畫就會(huì)運(yùn)行地很慢。當(dāng)然,這個(gè)例子只是為了展示如何模擬有趣的動(dòng)力學(xué)的一個(gè)方法——它并不是一個(gè)可以解決任何問題的萬金油。你個(gè)人在實(shí)踐中如何來進(jìn)行模擬,包括性能等各個(gè)方面,都取決于你自己了。

上一篇:Foundation下一篇:子類