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 中:
支持 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 來提供需要的信息。
我們將要?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)為中心。然后又給 damping
和 frequency
這兩個(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];
}
我們目前實(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)槲覀兿Mx接觸位置比較近的那些物體能移動(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="" />
當(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è)方面,都取決于你自己了。