iOS7中彈簧式列表的製作

UIScrollView可以說是UIKit中最重要的類之一了包括UITableView和UICollectionView等重要的數據容器類都是UIScrollView的子類。在歷年的WWDC上UIScrollView和相關的API都有專門的主題進行介紹也可以看出這個類的使用和變化之快。今年也不例外因爲iOS7完全重新定義了UI這使得UIScrollView裏原來不太會使用的一些用法和實現的效果在新的系統中得到了很好的表現。另外由於引入了UIKit Dynamics我們還可以結合ScrollView做出一些以前不太可能或者需要花費很大力氣來實現的效果包括帶有重力的swipe或者是類似新的信息app中的帶有彈簧效果聊天泡泡等。如果您還不太瞭解iOS7中信息app的效果這裏有一張gif圖可以幫您大概瞭解一下

142003129.gif

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
//ViewController.m
                                                                                        
@interface ViewController ()<UICollectionViewDataSource, UICollectionViewDelegate>
@property (nonatomic, strong) VVSpringCollectionViewFlowLayout *layout;
@end
                                                                                        
static NSString *reuseId = @"collectionViewCellReuseId";
                                                                                        
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
  // Do any additional setup after loading the view, typically from a nib.
                                                                                        
  self.layout = [[VVSpringCollectionViewFlowLayout alloc] init];
    self.layout.itemSize = CGSizeMake(self.view.frame.size.width, 44);
    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:self.layout];
                                                                                        
    collectionView.backgroundColor = [UIColor clearColor];
                                                                                        
    [collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseId];
                                                                                        
    collectionView.dataSource = self;
    [self.view insertSubview:collectionView atIndex:0];
}
                                                                                        
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 50;
}
                                                                                        
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseId forIndexPath:indexPath];
                                                                                        
    //Just give a random color to the cell. See https://gist.github.com/kylefox/1689973
    cell.contentView.backgroundColor = [UIColor randomColor];
    return cell;
}
@end
這部分沒什麼可以多說的現在我們有一個標準的FlowLayout的UICollectionView了。通過使用UICollectionViewFlowLayout的子類來作爲開始的layout我們可以節省下所有的初始cell位置計算的代碼在上面代碼的情況下這個collectionView的表現和一個普通的tableView並沒有太大不同。接下來我們着重來看看要如何實現彈性的layout。對於彈性效果我們需要的是連接一個item和一個錨點間彈性連接的UIAttachmentBehavior並能在滾動時設置新的錨點位置。我們在scroll的時候只要使用UIKit Dynamics的計算結果替代掉原來的位置更新計算其實就是簡單的scrollView的contentOffset的改變就可以模擬出彈性的效果了。
首先在-prepareLayout中爲cell添加UIAttachmentBehavior。
//VVSpringCollectionViewFlowLayout.m
@interface VVSpringCollectionViewFlowLayout()
@property (nonatomic, strong) UIDynamicAnimator *animator;
@end
                                                                                    
@implementation VVSpringCollectionViewFlowLayout
//...
                                                                                    
-(void)prepareLayout {
    [super prepareLayout];
                                                                                    
    if (!_animator) {
        _animator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];
        CGSize contentSize = [self collectionViewContentSize];
        NSArray *items = [super layoutAttributesForElementsInRect:CGRectMake(0, 0, contentSize.width, contentSize.height)];
                                                                                    
        for (UICollectionViewLayoutAttributes *item in items) {
            UIAttachmentBehavior *spring = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
                                                                                    
            spring.length = 0;
            spring.damping = 0.5;
            spring.frequency = 0.8;
                                                                                    
            [_animator addspring];
         }
    }
}
@end

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

@interface UIDynamicAnimator (UICollectionViewAdditions)
                                                                               
// When you initialize a dynamic animator with this method, you should only associate collection view layout attributes with your behaviors.
// The animator will employ thecollection view layout’s content size coordinate system.
- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout*)layout;
                                                                               
// The three convenience methods returning layout attributes (if associated to behaviors in the animator) if the animator was configured with collection view layout
- (UICollectionViewLayoutAttributes*)layoutAttributesForCellAtIndexPath:(NSIndexPath*)indexPath;
- (UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
- (UICollectionViewLayoutAttributes*)layoutAttributesForDecorationViewOfKind:(NSString*)decorationViewKind atIndexPath:(NSIndexPath *)indexPath;
                                                   @end
//於是通過-initWithCollectionViewLayout:進行初始化後這個UIDynamicAnimator實例便和我們的layout進行了綁定之後這個layout對應的attributes都應該由綁定的UIDynamicAnimator的實例給出。就像下面這樣
               
               
//VVSpringCollectionViewFlowLayout.m
@implementation VVSpringCollectionViewFlowLayout
//...
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    return [_animator itemsInRect:rect];
}
                                                                               
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    return [_animator layoutAttributesForCellAtIndexPath:indexPath];
}
@end
讓我們回到-prepareLayout方法中在創建了UIDynamicAnimator實例後我們對於這個layout中的每個attributes對應的點都創建並添加一個添加一個UIAttachmentBehavior在iOS7 SDK中UICollectionViewLayoutAttributes已經實現了UIDynamicItem接口可以直接參與UIKit Dynamic的計算中去。創建時我們希望collectionView的每個cell就保持在原位因此我們設定了錨點爲當前attribute本身的center。
接下來我們考慮滑動時的彈性效果的實現。在系統的信息app中我們可以看到彈性效果有兩個特點
隨着滑動的速度增大初始的拉伸和壓縮的幅度將變大
隨着cell距離屏幕觸摸位置越遠拉伸和壓縮的幅度
對於考慮到這兩方面的特點我們所期望的滑動時的各cell錨點的變化應該是類似這樣的

142420954.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的數據了。
也許你還沒有緩過神來但是我們確實已經做完了讓我們來看看實際的效果吧

142136796.gif

帶有彈性效果的collecitonView
當然通過調節dampingfrequency和scrollResistance的係數等參數可以得到彈性不同的效果比如更多的震盪或者更大的幅度等等。
這個layout實現起來非常簡單我順便封裝了一下放到了Github上大家有需要的話可以點擊這裏下載並直接使用。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章