此文已由作者肖峥荣授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
为了实现类滑动翻页效果,在每次滑动之后都能停在一个恰当的位置,我们需要重载targetContentOffsetForProposedContentOffset 方法,指定滑动停止时的位置。那么现在有几个问题需要先考虑一下
怎么判断是该滑往前一个item的frame还是滑往下一个item的frame,或者停在当前item?
最开始的时候我是判断速度velocity的正负来决定滑向哪一个页面,后来发现这样判断会出现超出滑动意向预期的滑动效果,比如你想滑到下一页,但是滑动结束的时候手指往回不小心勾了一下,就会出现往回滑的表现。所以这里做的优化就是判断中间的item往哪个方向偏移了,这样偏移的方向就是滑动翻页的方向。
这里的翻页是一页翻了多少?
这里的翻页并不是真的翻了一个屏幕宽度,因为每翻一页都是一个item居中,所以它只是翻了一个item的原始宽度,并不能设置collectionView的page属性来实现。
怎么获取需要滑到的contentOffset值呢?
因为翻页效果每翻一次是一个item的宽度,因此这里我们可以根据要滑到的item的index计算出contentOffset值。而首先需要知道滑动前处于中间的item的index,这里我的做法是在collectionView里面实现scrollViewWillBeginDragging来获取在滑动将要开始时居中item的index,将其传递给layout,然后在targetContentOffsetForProposedContentOffset方法中使用。这依赖于scrollViewWillBeginDragging是在将要开始拖拽的时候调用,后者是在拖拽结束的时候调用。
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { // 获取当前可见的items列表 NSArray<UICollectionViewCell *> *cells = [self.collectionView visibleCells]; if (!cells || cells.count==0) { return; } //计算item停靠中心位置 CGFloat centerX = self.collectionView.contentOffset.x + _layout.sectionInset.left + _layout.itemSize.width*0.5; NSInteger index = 0; CGFloat minDelta = MAXFLOAT; //获取距离中心点最近的Item的index for (NSInteger i=0; i<cells.count ; i++) { UICollectionViewCell *cell = cells[i]; if (minDelta > ABS(cell.center.x - centerX)) { minDelta = ABS(cell.center.x - centerX); index = i; } } NSIndexPath *indexPath = [self.collectionView indexPathForCell:cells[index]]; _layout.currentIndex = indexPath.row; }
解决好以上几个问题之后,便可以轻松的写出滑到适当位置的逻辑
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity { CGFloat leftX = self.collectionView.contentOffset.x + self.sectionInset.left; NSInteger itemCount = [self.collectionView numberOfItemsInSection:0]; //获取滑动前居中的item UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForRow:_currentIndex inSection:0]]; //根据滑动前居中的item的位置来判断需要滑到的item的index //不要忘记第一个item和最后一个item的情况 if (leftX > cell.frame.origin.x && _currentIndex+1 < itemCount) { _currentIndex += 1; }else if(leftX < cell.frame.origin.x && _currentIndex-1 >= 0){ _currentIndex -= 1; } //设置目标位置的contentOffset proposedContentOffset.x = (_itemSpace + _itemSize.width) * _currentIndex; return proposedContentOffset; }
看起来已经完美了,然而运行的结果有些差强人意。在给banner翻页的时候,假如以很慢的速度滑动,banner也会以很慢的速度慢腾腾的滑到下一页,banner翻页的速度完全取决于用户滑动的速度,这离达到视觉大大的要求还是有一段距离的。
那么怎么解决呢,也许可以直接不使用该方法的减速停止机制,而是直接设置collectionView的contentOffset。这样的效果会怎么样呢?
//设置目标停止位置和当前所在的位置一致,提前结束减速滑动效果proposedContentOffset.x = self.collectionView.contentOffset.x;//直接设置目标位置的contentOffsetself.collectionView.contentOffset = CGPointMake((_itemSpace + _itemSize.width) * _currentIndex, self.collectionView.contentOffset.y);
运行一遍果然是没有减速过程,但是直接瞬移到了目标位置,所以我们离结果只差一个滑动动画而已。
proposedContentOffset.x = self.collectionView.contentOffset.x;//动画滑动到指定位置[self.collectionView scrollRectToVisible:CGRectMake((_itemSpace + _itemSize.width) * _currentIndex, 0, self.collectionView.frame.size.width, self.collectionView.frame.size.height) animated:YES];
到这里,我们已经通过自定义UICollectionViewLayout完全实现了Banner的滑动特效,但是在上面的 targetContentOffsetForProposedContentOffset 中其实还存在着一个Bug,它也会导致某种情况下的滑动结果超出预期。
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForRow:_currentIndex inSection:0]];
相信大家都知道,collectionView中的cell在滑出屏幕的时候就会被回收,那么这时候通过index获取到的cell便是nil,这样在滑动的时候便会出现无脑滑到下一页的情况。复现操作就是对banner从左边缘滑到右边缘,这样可以观察到触发bug之后的表现。怎么解决呢,我们可以用自己保存的UICollectionViewLayoutAttributes来进行判断。
UICollectionViewLayoutAttributes *attr = _attributesArray[_currentIndex];if (leftX > attr.frame.origin.x && _currentIndex+1 < itemCount) { _currentIndex += 1; }else if(leftX < attr.frame.origin.x && _currentIndex-1 >= 0){ _currentIndex -= 1; }
修改后的完整代码如下:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity { CGFloat leftX = self.collectionView.contentOffset.x + self.sectionInset.left; UICollectionViewLayoutAttributes *attr = _attributesArray[_currentIndex]; //根据滑动前居中的item的位置来判断需要滑到的item的index //不要忘记第一个item和最后一个item的情况 if (leftX > attr.frame.origin.x && _currentIndex+1 < _attributesArray.count) { _currentIndex += 1; }else if(leftX < attr.frame.origin.x && _currentIndex-1 >= 0){ _currentIndex -= 1; } //设置目标停止位置和当前所在的位置一致,提前结束减速滑动效果 proposedContentOffset.x = self.collectionView.contentOffset.x; //动画滑动到指定位置 [self.collectionView scrollRectToVisible:CGRectMake((_itemSpace + _itemSize.width) * _currentIndex, 0, self.collectionView.frame.size.width, self.collectionView.frame.size.height) animated:YES]; return proposedContentOffset; }
到这里我们已经通过自定义UICollectionViewLayout的方式实现了想要的banner效果,如果有什么错漏的话,还请联系指正。
网易云免费体验馆,0成本体验20+款云产品!
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 网易云数据库架构设计实践
【推荐】 HashMap在并发场景下踩过的坑
【推荐】 Docker 的优势