UIScrollView可以说是UIKit中最重要的类之一了包括UITableView和UICollectionView等重要的数据容器类都是UIScrollView的子类。在历年的WWDC上UIScrollView和相关的API都有专门的主题进行介绍也可以看出这个类的使用和变化之快。今年也不例外因为iOS7完全重新定义了UI这使得UIScrollView里原来不太会使用的一些用法和实现的效果在新的系统中得到了很好的表现。另外由于引入了UIKit Dynamics我们还可以结合ScrollView做出一些以前不太可能或者需要花费很大力气来实现的效果包括带有重力的swipe或者是类似新的信息app中的带有弹簧效果聊天泡泡等。如果您还不太了解iOS7中信息app的效果这里有一张gif图可以帮您大概了解一下
//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的改变就可以模拟出弹性的效果了。
//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。
现在我们来实现这个锚点的变化。既然都是滑动我们是不是可以考虑在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