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