UIKit Dynamics 是 iOS 7 中基于物理動畫引擎的一個新功能--它被特別設計使其能很好地與 collection views 配合工作,而后者是在 iOS 6 中才被引入的新特性。接下來,我們要好好看看如何將這兩個特性結(jié)合在一起。
這篇文章將討論兩個結(jié)合使用 UIkit Dynamics 和 collection view 的例子。第一個例子展示了如何去實現(xiàn)像 iOS 7 里信息 app 中的消息泡泡的彈簧動效,然后再進一步結(jié)合平鋪機制來實現(xiàn)布局的可伸縮性。第二個例子展現(xiàn)了如何用 UIKit Dynamics 來模擬牛頓擺,這個例子中物體可以一個個地加入到 collection view 中,并和其他物體發(fā)生相互作用。
在我們開始之前,我假定你們對 UICollectionView 是如何工作是有基本的了解——查看這篇 objc.io 文章會有你想要的所有細節(jié)。我也假定你已經(jīng)理解了 UIKit Dynamics 的工作原理--閱讀這篇博客,可以了解更多 UIKit Dynamics 的知識。
編者注 如果您閱讀本篇文章感覺有點吃力的話,可以先來看看 @onevcat 的《UICollectionView 入門》 和《UIKit Dynamics 入門》這兩篇入門文章,幫助您快速補充相關(guān)知識。
文章中的兩個例子項目都已經(jīng)在 GitHub 中:
支持 UICollectionView 實現(xiàn) UIKit Dynamics 的最關(guān)鍵部分就是 UIDynamicAnimator。要實現(xiàn)這樣的 UIKit Dynamics 的效果,我們需要自己自定義一個繼承于 UICollectionViewFlowLayout 的子類,并且在這個子類對象里面持有一個 UIDynamicAnimator 的對象。
當我們創(chuàng)建自定義的 dynamic animator 時,我們不會使用常用的初始化方法 -initWithReferenceView: ,因為我們不需要把這個 dynamic animator 關(guān)聯(lián)一個 view ,而是給它關(guān)聯(lián)一個 collection view layout。所以我們使用 -initWithCollectionViewLayout: 這個初始化方法,并把 collection view layout 作為參數(shù)傳入。這很關(guān)鍵,當?shù)?animator 的 behavior item 的屬性應該被更新的時候,它必須能夠確保 collection view 的 layout 失效。換句話說,dynamic animator 將會經(jīng)常使舊的 layout 失效。
我們很快就能看到這些事情是怎么連接起來的,但是在概念上理解 collection view 如何與 dynamic animator 相互作用是很重要的。
Collection view layout 將會為 collection view 中的每個 UICollectionViewLayoutAttributes 添加 behavior(稍后我們會討論平鋪它們)。在將這些 behaviors 添加到 dynamic animator 之后,UIKit 將會向 collection view layout 詢問 atrribute 的狀態(tài)。我們此時可以直接將由 dynamic animator 所提供的 items 返回,而不需要自己做任何計算。Animator 將在模擬時禁用 layout。這會導致 UIKit 再次查詢 layout,這個過程會一直持續(xù)到模擬滿足設定條件而結(jié)束。
所以重申一下,layout 創(chuàng)建了 dynamic animator,并且為其中每個 item 的 layout attribute 添加對應的 behaviors。當 collection view 需要 layout 信息時,由 dynamic animator 來提供需要的信息。
我們將要創(chuàng)建一個簡單的例子來展示如何使用一個帶 UIkit Dynamic 的 collection view layout。當然,我們需要做的第一件事就是,創(chuàng)建一個數(shù)據(jù)源去驅(qū)動我們的 collection view。我知道以你的能力完全可以獨立實現(xiàn)一個數(shù)據(jù)源,但是為了完整性,我還是提供了一個給你:
@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
我們注意到當 view 第一次出現(xiàn)的時候,這個 layout 是被無效的。這是因為沒有用 Storyboard 的結(jié)果(使用或不使用 Storyboard,調(diào)用 prepareLayout 方法的時機是不同的,蘋果在 WWDC 的視頻中并沒有告訴我們這一點)。所以,當這些視圖一出現(xiàn)我們就需要手動使這個 collection view layout 無效。當我們用平鋪(后面會詳細介紹)的時候,就不需要這樣。
現(xiàn)在來創(chuàng)建自定義的 collection view layout 吧,我們需要強引用一個 dynamic animator,并且使用它來驅(qū)動我們的 collcetion view layout 的 attribute。我們在實現(xiàn)文件里定義了一個私有屬性:
@interface ASHSpringyCollectionViewFlowLayout ()
@property (nonatomic, strong) UIDynamicAnimator *dynamicAnimator;
@end
我們將在 layout 的初始化方法中初始化我們的 dynamic animator。還要設置一些屬于父類 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;
}
我們將實現(xiàn)的下一個方法是 prepareLayout。我們首先需要調(diào)用父類的方法。因為我們是繼承 UICollectionViewFlowLayout 類,所以在調(diào)用父類的 prepareLayout 方法時,可以使 collection view layout 的各個 attribute 都放置在合適的位置。我們可以依靠父類的這個方法來提供一個默認的排布,并且能夠使用 [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)];
這真的是效率低下的代碼。因為我們的 collection view 中可能會有成千上萬個 cell,一次性加載所有的 cell 是一個可能會產(chǎn)生難以置信的內(nèi)存緊張的操作。我們要在一段時間內(nèi)遍歷所有的元素,這也成為耗時的操作。這真的是效率的雙重打擊!別擔心——我們是負責任的開發(fā)者,所以我們會很快解決這個問題的。我們先暫時繼續(xù)使用簡單、粗暴的實現(xiàn)方式。
當加載完我們所有的 collection view layout attribute 之后,我們需要檢查他們是否都已經(jīng)被加載到我們的 animator 里了。如果一個 behavior 已經(jīng)在 animator 中存在,那么我們就不能重新添加,否則就會得到一個非常難懂的運行異常提示:
<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);
如果看到了這個錯誤,那么這基本表明你添加了兩個 behavior 給同一個 UICollectionViewLayoutAttribute,這使得系統(tǒng)不知道該怎么處理。
無論如何,一旦我們已經(jīng)檢查好我們是否已經(jīng)將 behavior 添加到 dynamic animator 之后,我們就需要遍歷每個 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];
}];
}
這段代碼非常簡單。我們?yōu)槊總€ item 創(chuàng)建了一個以物體的中心為附著點的 UIAttachmentBehavior 對象。然后又設置了我們的 attachment behavior 的 length 為 0 以便約束這個 cell 能一直以 behavior 的附著點為中心。然后又給 damping 和 frequency 這兩個參數(shù)設置一個比較合適的值。
這就是 prepareLayout。我們現(xiàn)在需要實現(xiàn) layoutAttributesForElementsInRect: 和 layoutAttributesForItemAtIndexPath: 這兩個方法,UIKit 會調(diào)用它們來詢問 collection view 每一個 item 的布局信息。我們寫的代碼會把這些查詢交給專門做這些事的 dynamic animator:
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
return [self.dynamicAnimator itemsInRect:rect];
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [self.dynamicAnimator layoutAttributesForCellAtIndexPath:indexPath];
}
我們目前實現(xiàn)的代碼給我們展示的只是一個在正?;瑒酉轮挥徐o態(tài)感覺的 UICollectionView,運行起來沒什么特別的??瓷先ズ芎茫皇钦娴?em>動態(tài),不是么?
為了使它表現(xiàn)地動態(tài)點,我們需要 layout 和 dynamic animator 能夠?qū)?collection view 中滑動位置的變化做出反應。幸好這里有個非常適合這個要求的方法 shouldInvalidateLayoutForBoundsChange:。這個方法會在 collection view 的 bound 發(fā)生改變的時候被調(diào)用,根據(jù)最新的 content offset 調(diào)整我們的 dynamic animator 中的 behaviors 的參數(shù)。在重新調(diào)整這些 behavior 的 item 之后,我們在這個方法中返回 NO;因為 dynamic animator 會關(guān)心 layout 的無效問題,所以在這種情況下,它不需要去主動使其無效:
-(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;
}
讓我們仔細查看這個代碼的細節(jié)。首先我們得到了這個 scroll view(就是我們的 collection view ),然后計算它的 content offset 中 y 的變化(在這個例子中,我們的 collection view 是垂直滑動的)。一旦我們得到這個增量,我們需要得到用戶接觸的位置。這是非常重要的,因為我們希望離接觸位置比較近的那些物體能移動地更迅速些,而離接觸位置比較遠的那些物體則應該滯后些。
對于 dynamic animator 中的每個 behavior,我們將接觸點到該 behavior 物體的 x 和 y 的距離之和除以 1500,1500 是我根據(jù)經(jīng)驗設的。分母越小,這個 collection view 的的交互就越有彈簧的感覺。一旦我們拿到了這個“滑動阻力”的值,我們就可以用它的增量乘上 scrollResistance 這個變量來指定這個 behavior 物體的中心點的 y 值。最后,我們在滑動阻力大于增量的情況下對增量和滑動阻力的結(jié)果進行了選擇(這意味著物體開始往錯誤的方向移動了)。在本例我們用了這么大的分母,那么這種情況是不可能的,但是在一些更具彈性的 collection view layout 中還是需要注意的。
就是這么一回事。以我的經(jīng)驗,這個方法對多達幾百個物體的 collection view 來說也是是適用的。超過這個數(shù)量的話,一次性加載所有物體到內(nèi)存中就會變成很大的負擔,并且在滑動的時候就會開始卡頓了。
http://wiki.jikexueyuan.com/project/objc/images/5-9.gif" alt="" />
當你的 collection view 中只有幾百個 cell 的時候,他運行的很好,但當數(shù)據(jù)源超過這個范圍的時候會發(fā)生什么呢?或者在運行的時你不能預測你的數(shù)據(jù)源有多大呢?我們的簡單粗暴的方法就不管用了。
除了在 prepareLayout 中加載所有的物體,如果我們能更聰明地知道哪些物體會加載那該多好啊。是的,就是僅加載顯示的和即將顯示的物體。這正是我們要采取的辦法。
我們需要做的第一件事就是是跟蹤 dynamic animator 中的所有 behavior 物體的 index path。我在 collection view 中添加一個屬性來做這件事:
@property (nonatomic, strong) NSMutableSet *visibleIndexPathsSet;
我們用 set 是因為它具有常數(shù)復雜度的查找效率,并且我們經(jīng)常地查找 visibleIndexPathsSet 中是否已經(jīng)包含了某個 index path。
在我們實現(xiàn)全新的 prepareLayout 方法之前——有一個問題就是什么是平鋪 behavior —— 理解平鋪的意思是非常重要的。當我們平鋪behavior 的時候,我們會在這些 item 離開 collection view 的可視范圍的時候刪除對應的 behavior,在這些 item 進入可視范圍的時候又添加對應的 behavior。這是一個大麻煩:我們需要在滾動中創(chuàng)建新的 behavior。這就意味著讓人覺得創(chuàng)建它們就好像它們本來就已經(jīng)在 dynamic animator 里了一樣,并且它們是在 shouldInvalidateLayoutForBoundsChange: 方法被修改的。
因為我們是在滾動中創(chuàng)建這些新的 behavior,所以我們需要維持現(xiàn)在 collection view 的一些狀態(tài)。尤其我們需要跟蹤最近一次我們 bound 變化的增量。我們會在滾動時用這個狀態(tài)去創(chuàng)建我們的 behavior:
@property (nonatomic, assign) CGFloat latestDelta;
添加完這個 property 后,我們將要在 shouldInvalidateLayoutForBoundsChange: 方法中添加下面這行代碼:
self.latestDelta = delta;
這就是我們需要修改我們的方法來響應滾動事件。我們的這兩個方法是為了將 collection view 中 items 的 layout 信息傳給 dynamic animator,這種方式?jīng)]有變化。事實上,當你的 collection view 實現(xiàn)了 dynamic animator 的大部分情況下,都需要實現(xiàn)我們上面提到的兩個方法 layoutAttributesForElementsInRect: 和 layoutAttributesForItemAtIndexPath:。
這里最難懂的部分就是平鋪機制。我們將要完全重寫我們的 prepareLayout。
這個方法的第一步是將那些物體的 index path 已經(jīng)不在屏幕上顯示的 behavior 從 dynamic animator 上刪除。第二步是添加那些即將顯示的物體的 behavior。
讓我們先看一下第一步。
像以前一樣,我們要調(diào)用 super prepareLayout,這樣我們就能依賴父類 UICollectionViewFlowLayout 提供的默認排布。還像以前一樣,我們通過父類獲取一個矩形內(nèi)的所有元素的 layout attribute。不同的是我們不是獲取整個 collection view 中的元素屬性,而只是獲取顯示范圍內(nèi)的。
所以我們需要計算這個顯示矩形。但是別著急!有件事要記住。我們的用戶可能會非??斓鼗瑒?collection view,導致了 dynamic animator 不能跟上,所以我們需要稍微擴大顯示范圍,這樣就能包含到那些將要顯示的物體了。否則,在滑動很快的時候就會出現(xiàn)頻閃現(xiàn)象了。讓我們計算一下顯示范圍:
CGRect originalRect = (CGRect){.origin = self.collectionView.bounds.origin, .size = self.collectionView.frame.size};
CGRect visibleRect = CGRectInset(originalRect, -100, -100);
我確信在實際顯示矩形上的每個方向都擴大100個像素對我的 demo 來說是可行的。仔細查看這些值是否適合你們的 collection view,尤其是當你們的 cell 很小的情況下。
接下來我們就需要收集在顯示范圍內(nèi)的 collection view layout attributes。還有它們的 index paths:
NSArray *itemsInVisibleRectArray = [super layoutAttributesForElementsInRect:visibleRect];
NSSet *itemsIndexPathsInVisibleRectSet = [NSSet setWithArray:[itemsInVisibleRectArray valueForKey:@"indexPath"]];
注意我們是在用一個 NSSet。這是因為它具有常數(shù)復雜度的查找效率,并且我們經(jīng)常的查找 visibleIndexPathsSet 是否已經(jīng)包含了某個 index path:
接下來我們要做的就是遍歷 dynamic animator 的 behaviors,過濾掉那些已經(jīng)在 itemsIndexPathsInVisibleRectSet 中的 item。因為我們已經(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 的話,那就表示這個用戶沒有在滑動 collection view,這時我就假定我們不需要在滾動時創(chuàng)建新的 behavior 了:
CGPoint touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];
這是一個潛藏危險的假定。如果用戶很快地滑動了 collection view 之后釋放了他的手指呢?這個 collection view 就會一直滾動,但是我們的方法就不會在滾動時創(chuàng)建新的 behavior 了。但幸運的是,那也就意味這時 scroll view 滾動太快很難被注意到!好哇!但是,對于那些擁有大型 cell 的 collection view 來說,這仍然是個問題。那么在這種情況下,就需要增加你的可視范圍的 bounds 來加載更多物體以解決這個問題。
現(xiàn)在我們需要枚舉我們剛顯示的 item,為他們創(chuàng)建 behavior,再將他們的 index path 添加到 visibleIndexPathsSet。我們還需要在滾動時做些數(shù)學運算來創(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];
}];
大部分代碼看起來還是挺熟悉的。大概有一半是來自沒有實現(xiàn)平鋪的 prepareLayout。另一半是來自 shouldInvalidateLayoutForBoundsChange: 這個方法。我們用 latestDelta 這個屬性來表示 bound 變化的增量,適當?shù)卣{(diào)整 UICollectionViewLayoutAttributes 使這些 cell 表現(xiàn)地就像被 attachment behavior “拉”著一樣。
就這樣就完成了,真的!我已經(jīng)在真機上測試過顯示上千個 cell 的情況了,它運行地非常完美。去試試吧。
一般來說,當我們使用 UICollectionView 的時候,繼承 UICollectionViewFlowLayout 會比直接繼承 UICollectionViewLayout 更容易。這是因為 flow layout 會為我們做很多事。然而,瀑布流布局是嚴格基于它們的尺寸一個接一個的展現(xiàn)出來。如果你有一個布局不能適應這個標準怎么辦?好的,如果你已經(jīng)嘗試用 UICollectionViewFlowLayout 來適應,而且你很確定它不能很好運行,那么就應該拋棄 UICollectionViewFlowLayout 這個定制性比較弱的子類,而應該直接在 UICollectionViewLayout 這個基類上進行定制。
這個原則在處理 UIKit Dynamic 時也是適用的。
讓我們先創(chuàng)建 UICollectionViewLayout 的子類。當繼承 UICollectionViewLayout 的時候需要實現(xiàn) collectionViewContentSize 方法,這點非常重要。否則這個 collection view 就不知道如果去顯示自己,也不會有顯示任何東西。因為我們想要 collection view 不能滾動,所以這里要返回 collection view 的 frame 的 size,減去它的 contentInset.top:
-(CGSize)collectionViewContentSize
{
return CGSizeMake(self.collectionView.frame.size.width,
self.collectionView.frame.size.height - self.collectionView.contentInset.top);
}
在這個(有點教學式)的例子中,我們的 collection view 總是會以零個cell開始,物體通過 performBatchUpdates: 這個方法添加。這就意味著我們必須使用 -[UICollectionViewLayout prepareForCollectionViewUpdates:] 這個方法來添加我們的 behavior(即這個 collection view 的數(shù)據(jù)源總是以零開始)。
除了給各個 item 添加 attachment behavior 外,我們還將保留另外兩個 behavior:重力和碰撞。對于添加在這個 collection view 中的每個 item 來說,我們必須把這些 item 添加到我們的碰撞和 attachment behavior 中。最后一步就是設置這些 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="" />
刪除就有點復雜了。我們希望這些物體有“掉落”的效果而不是簡單的消失。這就不僅僅是從 collection view 中刪除個 cell 這么簡單了,因為我們希望在它離開了屏幕之前還是保留它。我已經(jīng)在代碼中實現(xiàn)了這樣的效果,但是做法有點取巧。
基本上我們要做的是在 layout 中提供一個方法,在它刪除 attachment behavior 兩秒之后,將這個 cell 從 collection view 中刪除。我們希望在這段時間里,這個 cell 能掉出屏幕,但是這不一定會發(fā)生。如果沒有發(fā)生,也沒關(guān)系。只要淡出就行了。然而,我們必須保證在這兩秒內(nèi)既沒有新的 cell 被添加,也沒有舊的 cell 被刪除。(我說了有點取巧。)
歡迎提交 pull request。
這個方法是有局限性的。我將 cell 數(shù)量的上限設為 10,但是即使這樣,在像 iPad2 這樣比較舊的設備中,動畫就會運行地很慢。當然,這個例子只是為了展示如何模擬有趣的動力學的一個方法——它并不是一個可以解決任何問題的萬金油。你個人在實踐中如何來進行模擬,包括性能等各個方面,都取決于你自己了。