iOS7中彈簧式列表的製做

本文轉載至 http://www.devdiv.com/forum.php?mod=viewthread&tid=208170&extra=page%3D1%26filter%3Dtypeid%26typeid%3D23%26typeid%3D23php

這是個人WWDC2013系列筆記中的一篇,完整的筆記列表請參看這篇總覽。本文僅做爲我的記錄使用,也歡迎在許可協議範圍內轉載或使用,可是還煩請保留原文連接,謝謝您的理解合做。若是您以爲本站對您能有幫助,您可使用RSS或郵件方式訂閱本站,這樣您將能在第一時間獲取本站信息。
本文涉及到的WWDC2013 Session有
Session 206 Getting Started with UIKit Dynamics
Session 217 Exploring Scroll Views in iOS7
UIScrollView能夠說是UIKit中最重要的類之一了,包括UITableView和UICollectionView等重要的數據容器類都是UIScrollView的子類。在歷年的WWDC上,UIScrollView和相關的API都有專門的主題進行介紹,也能夠看出這個類的使用和變化之快。今年也不例外,由於iOS7徹底從新定義了UI,這使得UIScrollView裏原來不太會使用的一些用法和實現的效果在新的系統中獲得了很好的表現。另外,因爲引入了UIKit Dynamics,咱們還能夠結合ScrollView作出一些之前不太可能或者須要花費很大力氣來實現的效果,包括帶有重力的swipe或者是相似新的信息app中的帶有彈簧效果聊天泡泡等。若是您還不太瞭解iOS7中信息app的效果,這裏有一張gif圖能夠幫您大概瞭解一下:
<ignore_js_op>ios7-message-app-spring.gif 
iOS7中信息app的彈簧效果

此次筆記的內容主要就是實現一個這樣的效果。爲了不重複造輪子,我對這個效果進行了一些簡單的封裝,並連同這篇筆記的demo一塊兒扔在了Github上,有須要的童鞋能夠到這裏自取。
iOS7的SDK中Apple最大的野心實際上是想用SpriteKit來結束iOS平臺遊戲開發(至少是2D遊戲開發)的亂戰,統一遊戲開發的方式並創建良性社區。而UIKit Dynamics,我的猜想Apple在花費力氣爲SpriteKit開發了物理引擎的同時,發如今UIKit中也可使用,並能獲得不錯的效果,因而順便革新了一下設計理念,在UI設計中引入了很多物理的概念。在iOS系統中,最爲典型的應用是鎖屏界面打開相機時中途放棄後的重力下墜+反彈的效果,另外一個就是信息應用中的加入彈性的消息列表了。彈性列表在我本身上手試過之後以爲表現形式確實很生動,能夠消除原來列表那種冷冰冰的感受,是有可能在從此的設計中被大量使用的,所以決定學上一學。
首先咱們須要知道要如何實現這樣一種效果,咱們會用到哪些東西。毋庸置疑,若是不使用UIKit Dynamics的話,本身從頭開始來完成會是一件很是費力的事情,你可能須要實現一套位置計算和物理模擬來使效果看起來真實滑潤。而UIKit Dynamics中已經給咱們提供了現成的彈簧效果,能夠用UIAttachmentBehavior進行實現。另外,在說到彈性效果的時候,咱們實際上是在描述一個列表中的各個cell之間的關係,對於傳統的UITableView來講,描述UITableViewCell之間的關係是比較複雜的(由於Apple已經把絕大多數工做作了,包括計算cell位置和位移等。使用越簡單,定製就會越麻煩在絕大多數狀況下都是真理)。而UICollectionView則經過layout來完成cell之間位置關係的描述,給了開發者較大的空間來實現佈局。另外,UIKit Dynamics爲UICollectionView作了不少方便的Catagory,能夠很容易地「指導」UICollectionView利用加入物理特性計算後的結果,在實現彈性效果的時候,UICollectionView是咱們不二的選擇。
若是您在閱讀這篇筆記的時候遇到困難的話,建議您能夠看看我以前的一些筆記,包括今年的UIKit Dynamics的介紹和去年的UICollectionView介紹
話很少說,咱們開工。首先準備一個UICollectionViewFlowLayout的子類(在這裏叫作VVSpringCollectionViewFlowLayout),而後在ViewController中用這個layout實現一個簡單的collectionView:ios

01 //ViewController.m
02  
03 @interface ViewController ()<UICollectionViewDataSource, UICollectionViewDelegate>
04 @property (nonatomic, strong) VVSpringCollectionViewFlowLayout *layout;
05 @end
06  
07 static NSString *reuseId = @"collectionViewCellReuseId";
08  
09 @implementation ViewController
10 - (void)viewDidLoad
11 {
12     [super viewDidLoad];
13   // Do any additional setup after loading the view, typically from a nib.
14  
15   self.layout = [[VVSpringCollectionViewFlowLayout alloc] init];
16     self.layout.itemSize = CGSizeMake(self.view.frame.size.width, 44);
17     UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:self.layout];
18  
19     collectionView.backgroundColor = [UIColor clearColor];
20  
21     [collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseId];
22  
23     collectionView.dataSource = self;
24     [self.view insertSubview:collectionView atIndex:0];
25 }
26  
27 #pragma mark - UICollectionViewDataSource
28 - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
29 {
30     return 50;
31 }
32  
33 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
34 {
35     UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseId forIndexPath:indexPath];
36  
37     //Just give a random color to the cell. See <a href="\"https://gist.github.com/kylefox/1689973\"" target="\"_blank\"">https://gist.github.com/kylefox/1689973</a>
38     cell.contentView.backgroundColor = [UIColor randomColor];
39     return cell;
40 }
41 @end


這部分沒什麼能夠多說的,如今咱們有一個標準的FlowLayout的UICollectionView了。經過使用UICollectionViewFlowLayout的子類來做爲開始的layout,咱們能夠節省下全部的初始cell位置計算的代碼,在上面代碼的狀況下,這個collectionView的表現和一個普通的tableView並無太大不一樣。接下來咱們着重來看看要如何實現彈性的layout。對於彈性效果,咱們須要的是鏈接一個item和一個錨點間彈性鏈接的UIAttachmentBehavior,並能在滾動時設置新的錨點位置。咱們在scroll的時候,只要使用UIKit Dynamics的計算結果,替代掉原來的位置更新計算(其實就是簡單的scrollView的contentOffset的改變),就能夠模擬出彈性的效果了。
首先在-prepareLayout中爲cell添加UIAttachmentBehavior。git

01 //VVSpringCollectionViewFlowLayout.m
02 @interface VVSpringCollectionViewFlowLayout()
03 @property (nonatomic, strong) UIDynamicAnimator *animator;
04 @end
05  
06 @implementation VVSpringCollectionViewFlowLayout
07 //...
08  
09 -(void)prepareLayout {
10     [super prepareLayout];
11  
12     if (!_animator) {
13         _animator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];
14         CGSize contentSize = [self collectionViewContentSize];
15         NSArray *items = [super layoutAttributesForElementsInRect:CGRectMake(0, 0, contentSize.width, contentSize.height)];
16  
17         for (UICollectionViewLayoutAttributes *item in items) {
18             UIAttachmentBehavior *spring = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
19  
20             spring.length = 0;
21             spring.damping = 0.5;
22             spring.frequency = 0.8;
23  
24             [_animator addBehavior:spring];
25         }
26     }
27 }
28 @end


prepareLayout將在CollectionView進行排版的時候被調用。首先固然是call一下super的prepareLayout,你確定不會想要全都要本身進行設置的。接下來,若是是第一次調用這個方法的話,先初始化一個UIDynamicAnimator實例,來負責以後的動畫效果。iOS7 SDK中,UIDynamicAnimator類專門有一個針對UICollectionView的Category,以使UICollectionView可以輕易地利用UIKit Dynamics的結果。在UIDynamicAnimator.h中可以找到這個Category:github

01 @interface UIDynamicAnimator (UICollectionViewAdditions)
02  
03 // When you initialize a dynamic animator with this method, you should only associate collection view layout attributes with your behaviors.
04 // The animator will employ thecollection view layout’s content size coordinate system.
05 - (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout*)layout;
06  
07 // The three convenience methods returning layout attributes (if associated to behaviors in the animator) if the animator was configured with collection view layout
08 - (UICollectionViewLayoutAttributes*)layoutAttributesForCellAtIndexPath:(NSIndexPath*)indexPath;
09 - (UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
10 - (UICollectionViewLayoutAttributes*)layoutAttributesForDecorationViewOfKind:(NSString*)decorationViewKind atIndexPath:(NSIndexPath *)indexPath;
11  
12 @end


因而經過-initWithCollectionViewLayout:進行初始化後,這個UIDynamicAnimator實例便和咱們的layout進行了綁定,以後這個layout對應的attributes都應該由綁定的UIDynamicAnimator的實例給出。就像下面這樣:spring

01 //VVSpringCollectionViewFlowLayout.m
02 @implementation VVSpringCollectionViewFlowLayout
03  
04 //...
05  
06 -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
07     return [_animator itemsInRect:rect];
08 }
09  
10 -(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
11     return [_animator layoutAttributesForCellAtIndexPath:indexPath];
12 }
13 @end


讓咱們回到-prepareLayout方法中,在建立了UIDynamicAnimator實例後,咱們對於這個layout中的每一個attributes對應的點,都建立並添加一個添加一個UIAttachmentBehavior(在iOS7 SDK中,UICollectionViewLayoutAttributes已經實現了UIDynamicItem接口,能夠直接參與UIKit Dynamic的計算中去)。建立時咱們但願collectionView的每一個cell就保持在原位,所以咱們設定了錨點爲當前attribute自己的center。
接下來咱們考慮滑動時的彈性效果的實現。在系統的信息app中,咱們能夠看到彈性效果有兩個特色:
隨着滑動的速度增大,初始的拉伸和壓縮的幅度將變大
隨着cell距離屏幕觸摸位置越遠,拉伸和壓縮的幅度
對於考慮到這兩方面的特色,咱們所指望的滑動時的各cell錨點的變化應該是相似這樣的:
<ignore_js_op>spring-list-ios7.png 
向上拖動時的錨點變化示意
如今咱們來實現這個錨點的變化。既然都是滑動,咱們是否是能夠考慮在UIScrollView的–scrollViewDidScroll:委託方法中來設定新的Behavior錨點值呢?理論上來講固然是能夠的,可是若是這樣的話咱們大概就不得不面臨着將剛纔的layout實例設置爲collectionView的delegate這樣一個事實。可是咱們都知道layout應該作的事情是給collectionView提供必要的佈局信息,而不該該負責去處理它的委託事件。處理collectionView的回調更恰當地應該由處於collectionView的controller層級的類來完成,而不該該由一個給collectionView提供數據和信息的類來響應。在UICollectionViewLayout中,咱們有一個叫作-shouldInvalidateLayoutForBoundsChange:的方法,每次layout的bounds發生變化的時候,collectionView都會詢問這個方法是否須要爲這個新的邊界和更新layout。通常狀況下只要layout沒有根據邊界不一樣而發生變化的話,這個方法直接不作處理地返回NO,表示保持如今的layout便可,而每次bounds改變時這個方法都會被調用的特色正好能夠知足咱們更新錨點的需求,所以咱們能夠在這裏面完成錨點的更新。
//VVSpringCollectionViewFlowLayout.m
@implementation VVSpringCollectionViewFlowLayout

//...

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

    //Get the touch point
    CGPoint touchLocation = [scrollView.panGestureRecognizer locationInView:scrollView];

    for (UIAttachmentBehavior *spring in _animator.behaviors) {
        CGPoint anchorPoint = spring.anchorPoint;

        CGFloat distanceFromTouch = fabsf(touchLocation.y - anchorPoint.y);
        CGFloat scrollResistance = distanceFromTouch / 500;

        UICollectionViewLayoutAttributes *item = [spring.items firstObject];
        CGPoint center = item.center;

      //In case the added value bigger than the scrollDelta, which leads an unreasonable effect
        center.y += (scrollDelta > 0) ? MIN(scrollDelta, scrollDelta * scrollResistance)
                                      : MAX(scrollDelta, scrollDelta * scrollResistance);
        item.center = center;

        [_animator updateItemUsingCurrentState:item];
    }
    return NO;
}

@end
首先咱們計算了此次scroll的距離scrollDelta,爲了獲得每一個item與觸摸點的之間的距離,咱們固然還須要知道觸摸點的座標touchLocation。接下來,能夠根據距離對每一個錨點進行設置了:簡單地計算了原來錨點與觸摸點之間的距離distanceFromTouch,並由此計算一個係數。接下來,對於當前的item,咱們獲取其當前錨點位置,而後將其根據scrollDelta的數值和剛纔計算的係數,從新設定錨點的位置。最後咱們須要告訴UIDynamicAnimator咱們已經完成了對冒點的更新,如今能夠開始更新物理計算,並隨時準備collectionView來取LayoutAttributes的數據了。
也許你尚未緩過神來?可是咱們確實已經作完了,讓咱們來看看實際的效果吧:
<ignore_js_op>spring-collection-view-over-ios7.gif 
帶有彈性效果的collecitonView
固然,經過調節damping,frequency和scrollResistance的係數等參數,能夠獲得彈性不一樣的效果,好比更多的震盪或者更大的幅度等等。
附上文件包: <ignore_js_op> VVSpringCollectionViewFlowLayout-master.zip (640.1 KB, 下載次數: 248) app

相關文章
相關標籤/搜索