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

相关文章
相关标签/搜索