這是我的WWDC2013系列筆記中的一篇,完整的筆記列表請(qǐng)參看這篇總覽。本文僅作為個(gè)人記錄使用,也歡迎在許可協(xié)議范圍內(nèi)轉(zhuǎn)載或使用,但是還煩請(qǐng)保留原文鏈接,謝謝您的理解合作。如果您覺得本站對(duì)您能有幫助,您可以使用rss或郵件方式訂閱本站,這樣您將能在第一時(shí)間獲取本站信息。
本文涉及到的WWDC2013 session有
Session 206 Getting Started with UIKit DynamiCSSession 217 Exploring Scroll Views in iOS7UIScrollView可以說是UIKit中最重要的類之一了,包括UITableView和UICollectionView等重要的數(shù)據(jù)容器類都是UIScrollView的子類。在歷年的WWDC上,UIScrollView和相關(guān)的API都有專門的主題進(jìn)行介紹,也可以看出這個(gè)類的使用和變化之快。今年也不例外,因?yàn)閕OS7完全重新定義了UI,這使得UIScrollView里原來不太會(huì)使用的一些用法和實(shí)現(xiàn)的效果在新的系統(tǒng)中得到了很好的表現(xiàn)。另外,由于引入了UIKit Dynamics,我們還可以結(jié)合ScrollView做出一些以前不太可能或者需要花費(fèi)很大力氣來實(shí)現(xiàn)的效果,包括帶有重力的swipe或者是類似新的信息app中的帶有彈簧效果聊天泡泡等。如果您還不太了解iOS7中信息app的效果,這里有一張gif圖可以幫您大概了解一下:

這次筆記的內(nèi)容主要就是實(shí)現(xiàn)一個(gè)這樣的效果。為了避免重復(fù)造輪子,我對(duì)這個(gè)效果進(jìn)行了一些簡(jiǎn)單的封裝,并連同這篇筆記的demo一起扔在了Github上,有需要的童鞋可以到這里自取。
iOS7的SDK中Apple最大的野心其實(shí)是想用SPRiteKit來結(jié)束iOS平臺(tái)游戲開發(fā)(至少是2D游戲開發(fā))的亂戰(zhàn),統(tǒng)一游戲開發(fā)的方式并建立良性社區(qū)。而UIKit Dynamics,個(gè)人猜測(cè)Apple在花費(fèi)力氣為SpriteKit開發(fā)了物理引擎的同時(shí),發(fā)現(xiàn)在UIKit中也可以使用,并能得到不錯(cuò)的效果,于是順便革新了一下設(shè)計(jì)理念,在UI設(shè)計(jì)中引入了不少物理的概念。在iOS系統(tǒng)中,最為典型的應(yīng)用是鎖屏界面打開相機(jī)時(shí)中途放棄后的重力下墜+反彈的效果,另一個(gè)就是信息應(yīng)用中的加入彈性的消息列表了。彈性列表在我自己上手試過以后覺得表現(xiàn)形式確實(shí)很生動(dòng),可以消除原來列表那種冷冰冰的感覺,是有可能在今后的設(shè)計(jì)中被大量使用的,因此決定學(xué)上一學(xué)。
首先我們需要知道要如何實(shí)現(xiàn)這樣一種效果,我們會(huì)用到哪些東西。毋庸置疑,如果不使用UIKit Dynamics的話,自己從頭開始來完成會(huì)是一件非常費(fèi)力的事情,你可能需要實(shí)現(xiàn)一套位置計(jì)算和物理模擬來使效果看起來真實(shí)滑潤(rùn)。而UIKit Dynamics中已經(jīng)給我們提供了現(xiàn)成的彈簧效果,可以用UIAttachmentBehavior進(jìn)行實(shí)現(xiàn)。另外,在說到彈性效果的時(shí)候,我們其實(shí)是在描述一個(gè)列表中的各個(gè)cell之間的關(guān)系,對(duì)于傳統(tǒng)的UITableView來說,描述UITableViewCell之間的關(guān)系是比較復(fù)雜的(因?yàn)锳pple已經(jīng)把絕大多數(shù)工作做了,包括計(jì)算cell位置和位移等。使用越簡(jiǎn)單,定制就會(huì)越麻煩在絕大多數(shù)情況下都是真理)。而UICollectionView則通過layout來完成cell之間位置關(guān)系的描述,給了開發(fā)者較大的空間來實(shí)現(xiàn)布局。另外,UIKit Dynamics為UICollectionView做了很多方便的Catagory,可以很容易地“指導(dǎo)”UICollectionView利用加入物理特性計(jì)算后的結(jié)果,在實(shí)現(xiàn)彈性效果的時(shí)候,UICollectionView是我們不二的選擇。
如果您在閱讀這篇筆記的時(shí)候遇到困難的話,建議您可以看看我之前的一些筆記,包括今年的UIKit Dynamics的介紹和去年的UICollectionView介紹。
話不多說,我們開工。首先準(zhǔn)備一個(gè)UICollectionViewFlowLayout的子類(在這里叫做VVSpringCollectionViewFlowLayout),然后在ViewController中用這個(gè)layout實(shí)現(xiàn)一個(gè)簡(jiǎn)單的collectionView:
//ViewController.m@interface ViewController ()<UICollectionViewDataSource, UICollectionViewDelegate>@property (nonatomic, strong) VVSpringCollectionViewFlowLayout *layout;@endstatic 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這部分沒什么可以多說的,現(xiàn)在我們有一個(gè)標(biāo)準(zhǔn)的FlowLayout的UICollectionView了。通過使用UICollectionViewFlowLayout的子類來作為開始的layout,我們可以節(jié)省下所有的初始cell位置計(jì)算的代碼,在上面代碼的情況下,這個(gè)collectionView的表現(xiàn)和一個(gè)普通的tableView并沒有太大不同。接下來我們著重來看看要如何實(shí)現(xiàn)彈性的layout。對(duì)于彈性效果,我們需要的是連接一個(gè)item和一個(gè)錨點(diǎn)間彈性連接的UIAttachmentBehavior,并能在滾動(dòng)時(shí)設(shè)置新的錨點(diǎn)位置。我們?cè)趕croll的時(shí)候,只要使用UIKit Dynamics的計(jì)算結(jié)果,替代掉原來的位置更新計(jì)算(其實(shí)就是簡(jiǎn)單的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 addBehavior:spring];        }    }}@endprepareLayout將在CollectionView進(jìn)行排版的時(shí)候被調(diào)用。首先當(dāng)然是call一下super的prepareLayout,你肯定不會(huì)想要全都要自己進(jìn)行設(shè)置的。接下來,如果是第一次調(diào)用這個(gè)方法的話,先初始化一個(gè)UIDynamicAnimator實(shí)例,來負(fù)責(zé)之后的動(dòng)畫效果。iOS7 SDK中,UIDynamicAnimator類專門有一個(gè)針對(duì)UICollectionView的Category,以使UICollectionView能夠輕易地利用UIKit Dynamics的結(jié)果。在UIDynamicAnimator.h中能夠找到這個(gè)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:進(jìn)行初始化后,這個(gè)UIDynamicAnimator實(shí)例便和我們的layout進(jìn)行了綁定,之后這個(gè)layout對(duì)應(yīng)的attributes都應(yīng)該由綁定的UIDynamicAnimator的實(shí)例給出。就像下面這樣:
//VVSpringCollectionViewFlowLayout.m@implementation VVSpringCollectionViewFlowLayout//...-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {    return [_animator itemsInRect:rect];}-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {    return [_animator layoutAttributesForCellAtIndexPath:indexPath];}@end讓我們回到-prepareLayout方法中,在創(chuàng)建了UIDynamicAnimator實(shí)例后,我們對(duì)于這個(gè)layout中的每個(gè)attributes對(duì)應(yīng)的點(diǎn),都創(chuàng)建并添加一個(gè)添加一個(gè)UIAttachmentBehavior(在iOS7 SDK中,UICollectionViewLayoutAttributes已經(jīng)實(shí)現(xiàn)了UIDynamicItem接口,可以直接參與UIKit Dynamic的計(jì)算中去)。創(chuàng)建時(shí)我們希望collectionView的每個(gè)cell就保持在原位,因此我們?cè)O(shè)定了錨點(diǎn)為當(dāng)前attribute本身的center。
接下來我們考慮滑動(dòng)時(shí)的彈性效果的實(shí)現(xiàn)。在系統(tǒng)的信息app中,我們可以看到彈性效果有兩個(gè)特點(diǎn):
隨著滑動(dòng)的速度增大,初始的拉伸和壓縮的幅度將變大隨著cell距離屏幕觸摸位置越遠(yuǎn),拉伸和壓縮的幅度對(duì)于考慮到這兩方面的特點(diǎn),我們所期望的滑動(dòng)時(shí)的各cell錨點(diǎn)的變化應(yīng)該是類似這樣的:

現(xiàn)在我們來實(shí)現(xiàn)這個(gè)錨點(diǎn)的變化。既然都是滑動(dòng),我們是不是可以考慮在UIScrollView的–scrollViewDidScroll:委托方法中來設(shè)定新的Behavior錨點(diǎn)值呢?理論上來說當(dāng)然是可以的,但是如果這樣的話我們大概就不得不面臨著將剛才的layout實(shí)例設(shè)置為collectionView的delegate這樣一個(gè)事實(shí)。但是我們都知道layout應(yīng)該做的事情是給collectionView提供必要的布局信息,而不應(yīng)該負(fù)責(zé)去處理它的委托事件。處理collectionView的回調(diào)更恰當(dāng)?shù)貞?yīng)該由處于collectionView的controller層級(jí)的類來完成,而不應(yīng)該由一個(gè)給collectionView提供數(shù)據(jù)和信息的類來響應(yīng)。在UICollectionViewLayout中,我們有一個(gè)叫做-shouldInvalidateLayoutForBoundsChange:的方法,每次layout的bounds發(fā)生變化的時(shí)候,collectionView都會(huì)詢問這個(gè)方法是否需要為這個(gè)新的邊界和更新layout。一般情況下只要layout沒有根據(jù)邊界不同而發(fā)生變化的話,這個(gè)方法直接不做處理地返回NO,表示保持現(xiàn)在的layout即可,而每次bounds改變時(shí)這個(gè)方法都會(huì)被調(diào)用的特點(diǎn)正好可以滿足我們更新錨點(diǎn)的需求,因此我們可以在這里面完成錨點(diǎn)的更新。
//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首先我們計(jì)算了這次scroll的距離scrollDelta,為了得到每個(gè)item與觸摸點(diǎn)的之間的距離,我們當(dāng)然還需要知道觸摸點(diǎn)的坐標(biāo)touchLocation。接下來,可以根據(jù)距離對(duì)每個(gè)錨點(diǎn)進(jìn)行設(shè)置了:簡(jiǎn)單地計(jì)算了原來錨點(diǎn)與觸摸點(diǎn)之間的距離distanceFromTouch,并由此計(jì)算一個(gè)系數(shù)。接下來,對(duì)于當(dāng)前的item,我們獲取其當(dāng)前錨點(diǎn)位置,然后將其根據(jù)scrollDelta的數(shù)值和剛才計(jì)算的系數(shù),重新設(shè)定錨點(diǎn)的位置。最后我們需要告訴UIDynamicAnimator我們已經(jīng)完成了對(duì)冒點(diǎn)的更新,現(xiàn)在可以開始更新物理計(jì)算,并隨時(shí)準(zhǔn)備collectionView來取LayoutAttributes的數(shù)據(jù)了。
也許你還沒有緩過神來?但是我們確實(shí)已經(jīng)做完了,讓我們來看看實(shí)際的效果吧:

當(dāng)然,通過調(diào)節(jié)damping,frequency和scrollResistance的系數(shù)等參數(shù),可以得到彈性不同的效果,比如更多的震蕩或者更大的幅度等等。
這個(gè)layout實(shí)現(xiàn)起來非常簡(jiǎn)單,我順便封裝了一下放到了Github上,大家有需要的話可以點(diǎn)擊這里下載并直接使用。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注