[iOS]UIScrollview自定义分页的实现方法

UIScrollView及其子类UITableView, UICollectionView为iOS开发带来了极大的方便, 其分页(pagingEnabled)功能也很常用, 但是功能却有些局限, 页只能按UIScrollviewbounds尺寸划分, 如果要实现自定义分页宽度或高度就需要一些技巧.

方法一

类似iOS 6中MobileSafari, 如图所示的分页方法:

 

StackOverflow上提供的实现思路如下:

 

  1. UIScrollviewbounds限制再屏幕中间那一页的位置
  2. 禁用UIScrollviewclipsToBouds, 从而能显示超出bounds的内容

此时已经可以在借助UIScrollview原有分页功能的基础上实现了”视觉上”与MobileSafari类似的效果, 但因为UIScrollview的尺寸限制, 超出范围的触摸事件不会被UIScrollview收到, 影响体验, 要把触摸事件传递给UIScrollview, 方法如下:

  1. 用一个全屏的(或者其他你想要的尺寸)UIView子类, 这里我们命名为CustomUIView来做UIScrollviewsuperView
  2. 重写CustomUIViewhitTest:withEvent方法:
     - (UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event {
       if ([self pointInside:point withEvent:event]) {
         return scrollView;
       }
       return nil;
     }
    

    通过重写hitTest, 就可以将CustomUIView接受到的触摸事件传递给UIScrollView

方法二

前一种方法保留了UIScrollView原有的分页功能, 而下面这种更加灵活的自定义分页方法则完全自制了一个分页功能:

DMPagingScrollView就是一个很好的实现, 其自定义分页的思路大致如下(需要参考DMPagingScrollView.m代码):

  1. 用实例变量pageWidth记录自定义分页的宽度
  2. 通过实现UIScrollView的delegate方法, 来记录每次拖动UIScrollView动作的相关参数(位移, 加速度…)
  3. scrollViewDidEndDecelerating:等一些标志拖动结束的delegate方法中调用snapToPage方法, 在snapToPage方法中, 借助之前收集的动作参数计算出contentOffset, 并通过setContentOffset:offset animated:YES来移动.

这种方式的关键就在contentOffset的计算中(DMPagingScrollView.m pageOffsetForComponent:方法), 如果计算的不正确, 就会严重影响拖动UIScrollView的手感.

DMPagingScrollView通过subclassing UIScrollview将自定义分页相关的过程操作封装在DMPagingScrollView里面, 同时支持横向纵向的自定义分页, 十分方便. 类似地, 我们也可以对UITableView, UICollectionView进行同样的处理.

此外, 通过参考pageOffsetForComponent:中的计算方法, 我们甚至可以让每一页的宽度都不同, 从而大大提升了分页的灵活性, 比如我实现的这种:

- (CGFloat)pageOffset {
    CGFloat totalWidth = self.collectionView.contentSize.width;
    CGFloat visibleWidth = self.collectionView.bounds.size.width;

    CGFloat currentOffset = self.collectionView.contentOffset.x;
    CGFloat dragVelocity = _dragVelocity.x;
    CGFloat dragDisplacement = _dragDisplacement.x;

    NSInteger nearestIndex = 0;
    CGFloat minDis = CGFLOAT_MAX;
    CGFloat nearestOffset = 0;
    CGFloat prevOffset = 0;
    CGFloat nextOffset = 0;
    CGFloat totalOffset = 0;

    // self.pagesWidths is an NSArray that contains each page's width
    for (NSInteger i = 0; i < self.pagesWidths.count; i++) {
        CGFloat dis = ABS(totalOffset - currentOffset);

        if (dis < minDis) {
            minDis = dis;

            prevOffset = nearestOffset;
            
            nearestOffset = totalOffset;

            if (i == self.pagesWidths.count - 1) {
                nextOffset = totalOffset;
            }
            else {
                nextOffset = totalOffset + [(NSNumber *)self.pagesWidths[i] floatValue];
            }
        }

        totalOffset += [(NSNumber *)self.pagesWidths[i] floatValue];
    }

    NSInteger lowerIndex;
    NSInteger upperIndex;
    CGFloat lowerOffset;
    CGFloat upperOffset;
    if (currentOffset - nearestOffset < 0) {
        lowerIndex = nearestIndex - 1;
        upperIndex = nearestIndex;

        lowerOffset = prevOffset;
        upperOffset = nearestOffset;
    }
    else {
        lowerIndex = nearestIndex;
        upperIndex = nearestIndex + 1;

        lowerOffset = nearestOffset;
        upperOffset = nextOffset;
    }

    CGFloat newOffset;
    if (ABS(dragDisplacement) < DRAG_DISPLACEMENT_THRESHOLD || dragDisplacement * dragVelocity < 0) {
        if (currentOffset - lowerOffset > upperOffset - currentOffset) {
            newOffset = upperOffset;
        } else {
            newOffset = lowerOffset;
        }
    } else {
        if (dragVelocity > 0) {
            newOffset = upperOffset;
        } else {
            newOffset = lowerOffset;
        }
    }

    if (newOffset > totalWidth - visibleWidth)
        newOffset = totalWidth - visibleWidth;

    if (newOffset < 0)
        newOffset = 0;

    return newOffset;
}

–以上–