如何自定义UICollectionViewLayout定制自己的Banner(2)

叁叁肆2018-10-25 11:34

此文已由作者肖峥荣授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


实现翻页效果

存在的问题

为了实现类滑动翻页效果,在每次滑动之后都能停在一个恰当的位置,我们需要重载targetContentOffsetForProposedContentOffset 方法,指定滑动停止时的位置。那么现在有几个问题需要先考虑一下

  1. 怎么判断是该滑往前一个item的frame还是滑往下一个item的frame,或者停在当前item?

    最开始的时候我是判断速度velocity的正负来决定滑向哪一个页面,后来发现这样判断会出现超出滑动意向预期的滑动效果,比如你想滑到下一页,但是滑动结束的时候手指往回不小心勾了一下,就会出现往回滑的表现。所以这里做的优化就是判断中间的item往哪个方向偏移了,这样偏移的方向就是滑动翻页的方向。

  2. 这里的翻页是一页翻了多少?

    这里的翻页并不是真的翻了一个屏幕宽度,因为每翻一页都是一个item居中,所以它只是翻了一个item的原始宽度,并不能设置collectionView的page属性来实现。

  3. 怎么获取需要滑到的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效果,如果有什么错漏的话,还请联系指正。

需要demo的话可以点击这里

参考



网易云免费体验馆,0成本体验20+款云产品! 

更多网易技术、产品、运营经验分享请点击




相关文章:
【推荐】 网易云数据库架构设计实践
【推荐】 HashMap在并发场景下踩过的坑
【推荐】 Docker 的优势