UIScrollView带间距的分页效果实现

最近在项目碰到一个需求:一个轮播视图,页面之间有一定间距,要求每次滚动时候,一次只能拖动一页并且页面居中。当时粗略一想,应该设置pagingEnabled,但是使用这个属性后,scrollView每次翻页就是它frame的宽度,貌似不能用(提前剧透下:确实要使用pagingEnabled,并结合clipsToBounds),再加上这种轮播视图习惯使用collectionView,就决定使用collectionView了。

关键代码如下:

-(UICollectionView *)collectionView{
    
    if (!_collectionView) {
        
        CommissionShopsLayout * flowLayout = [CommissionShopsLayout new];
        
        _collectionView = [[UICollectionView alloc]initWithFrame:CGRectZero collectionViewLayout:flowLayout];
        _collectionView.scrollsToTop = NO;
        _collectionView.backgroundColor = [UIColor whiteColor];
        _collectionView.showsHorizontalScrollIndicator = NO;
        _collectionView.showsVerticalScrollIndicator = NO;
        _collectionView.dataSource = self;
        _collectionView.delegate = self;
        _collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
       [_collectionView registerClass:[ImageClCell class] forCellWithReuseIdentifier:@"ImageClCell"];
        
    }
    return _collectionView;
}

@implementation CommissionShopsLayout
- (void)prepareLayout{
    [super prepareLayout];
    
    self.itemSize = CGSizeMake(SCREEN_WIDTH - 44, ceil((SCREEN_WIDTH - 44) / 1.655));
    self.minimumLineSpacing = 10;
    self.sectionInset = UIEdgeInsetsMake(0, 22, 0, 22);
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    
}
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
    
    return [super layoutAttributesForElementsInRect:rect];
}
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    
    return YES;
    
}

-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    ///计算一下当前的位置
    CGFloat contentOffset = self.collectionView.contentOffset.x;
    
    NSInteger currentIndex =  MAX(0,(contentOffset - self.sectionInset.left) / (self.itemSize.width + self.minimumLineSpacing));
    
    ///计算原本应该停留的位置
    CGRect lastRect ;
    lastRect.origin = proposedContentOffset;
    lastRect.size = self.itemSize;
    
    NSArray *array = [self.collectionView.collectionViewLayout layoutAttributesForElementsInRect:lastRect];
    
    CGFloat startX = proposedContentOffset.x;
    CGFloat adjustOffsetX = MAXFLOAT;
    ///居中吸附
    for (UICollectionViewLayoutAttributes *attrs in array) {
        
        CGFloat attrsX = CGRectGetMinX(attrs.frame);
        CGFloat attrsW = CGRectGetWidth(attrs.frame) ;
        if (startX - attrsX  < attrsW/2) {
            
            adjustOffsetX = -(startX - attrsX + self.sectionInset.left);
            
        }else{
            
            adjustOffsetX = attrsW - (startX - attrsX + self.sectionInset.left - self.minimumLineSpacing);
        }
        
        break ;
    }
    NSInteger calculateIndex = (proposedContentOffset.x + adjustOffsetX) / self.itemSize.width;
    
    NSInteger finalIndex = calculateIndex <= currentIndex ? currentIndex : currentIndex + 1;
    
    return CGPointMake((self.itemSize.width + self.minimumLineSpacing) * (finalIndex) , proposedContentOffset.y);
}


@end

这么写貌似可以完成需求,一次也是滚一页,每次翻页也是居中,但是体验很不好,设置了滑动的减速度为fast,如果每次拖动的距离很短,视图停止滚动的太快,跟原生的pagingEnabled的体验相比还是不好。但是如果你只想滑动结束的时候页面居中,这段代码还是可以用的,hahaha!(直接返回proposedContentOffset.x + adjustOffsetX,proposedContentOffset.y))
扯远了!那怎么办?回归基础!通过实现UIScrollView的代理方法自己实现分页,可能是自己太菜了,虽然最终位置没错,但是体验不好。这里介绍一下UIScrollViewDelegate几个常用的方法

UIScrollView代理方法调用顺序:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
scrollView滚动的时候就会调用这个方法

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
scrollView被拖拽的时候调用

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
scrollView的拖拽将要结束的时候,通过targetContentOffset可以获取到最后停留的那个位置,虽然没有经过测试,但是这个方法应该和collectionView layout的那个代理方法是一样的,

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
scrollView的拖拽结束的时候的调用。如果decelerate为yes,说明scrollView将进入一个减速滑动的状态;如果为no,说明减速已经停止,将会调用scrollViewDidEndDecelerating方法

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
scrollView将开始减速滑动,这个时候的decelerate为YES

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
scrollView结束滑动时候被调用。

再仔细分析一下需求(所以永远不要急着动手,多思考),还是应该使用pagingEnabled,但是应该结合一下其他属性,后来发现使用clipsToBounds ,既然有思路了,那就干起来了吧

最终解决方法

思路大概设置pagingEnabled为yes,这样一次只能翻一页;设置clipsToBounds为NO,这样就可以显示溢出部分的内容,但是scrollView的宽度要设置为一页内容的宽度 + 分页之间的间距
这里我封装了一个PageView视图控件。
核心代码如下:
.h文件

#import <UIKit/UIKit.h>

@protocol PageViewDelegate <NSObject>

-(void)didSelectPicWithIndexPath:(NSInteger)index;

-(void)roll_scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset;

@end

@interface PageView : UIView

@property (nonatomic, assign) id<PageViewDelegate> delegate;

@property (nonatomic, assign) NSInteger selectIndex;//当前页面的下标,默认为0


/**
 @param frame 设置View大小
 @param distance 设置Scroll距离View两侧距离
 @param spacing 设置Scroll内部 图片间距,注意点:distance + spacing / 2 = (pageView的宽度 - 单页宽度)/ 2
 @return 初始化返回值
 */
- (instancetype)initWithFrame:(CGRect)frame withDistanceToScrollView:(CGFloat)distance withSpacing:(CGFloat)spacing;

-(void)loadView:(NSArray *)data;

@end

.m文件

#import "PageView.h"

@interface PageView ()<UIScrollViewDelegate>

@property (nonatomic, strong) UIScrollView * scrollView;

@property (nonatomic, strong) NSArray * data;

@property (nonatomic, assign) CGFloat halfSpacing;

@end

@implementation PageView

#pragma mark - Lazy
-(UIScrollView *)scrollView{
    
    if (!_scrollView) {
        
        _scrollView = [[UIScrollView alloc] initWithFrame:CGRectZero];
        _scrollView.scrollsToTop = NO;
        _scrollView.delegate = self;
        _scrollView.pagingEnabled = YES;
        _scrollView.clipsToBounds = NO;
        
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapAction:)];
        tap.numberOfTapsRequired = 1;
        tap.numberOfTouchesRequired = 1;
        [_scrollView addGestureRecognizer:tap];
        _scrollView.showsHorizontalScrollIndicator = NO;
        _scrollView.showsVerticalScrollIndicator = NO;
    }
    return _scrollView;
}
-(NSArray *)data{

    if (!_data) {
        
        _data = @[];
    }
    return _data;
}
#pragma mark - Init
- (instancetype)initWithFrame:(CGRect)frame withDistanceToScrollView:(CGFloat)distance withSpacing:(CGFloat)spacing{
    
    if (self = [super initWithFrame:frame]) {
        
        self.halfSpacing = spacing * 0.5;
        
        self.selectIndex = 0;
        
        [self addSubview:self.scrollView];
         self.scrollView.frame = CGRectMake(distance, 0, self.frame.size.width - 2 * distance, self.frame.size.height);
        
    }
    
    
    return self;
}

#pragma mark - load view
-(void)loadView:(NSArray *)data{
    
    self.data = data;
    
    if (!data.count) return;
    
    for (int i = 0; i < self.data.count; i++) {
        
        for (UIView *subView in self.scrollView.subviews) {
            
            if (subView.tag == 100 + i) {
                
                [subView removeFromSuperview];
            }
        }
        
        UIImageView *imageView = [[UIImageView alloc] init];
        imageView.userInteractionEnabled = YES;
        imageView.tag = 100 + i ;
        
        /**  注意点
         *   1. ScrollView的width应该等于单页宽度 + spacing
         *   2. 假设单个页面宽为 W 间距为 S, 想要居中,那么
         *  单个页面x值
         *  0 ->  1 * halfSpacing ;
         *  1 ->  3 * halfSpacing + W ;
         *  2 ->  5 * halfSpacing + 2 * W ;
         .
         .
         *  i   -> (2 * i + 1) *  halfSpacing + 2 *(W - 2 *  halfSpacing)
         */
        imageView.frame = CGRectMake((2 * i + 1) * self.halfSpacing + i * (self.scrollView.frame.size.width - 2 * self.halfSpacing), 0, (self.scrollView.frame.size.width - 2 * self.halfSpacing), self.frame.size.height);
        ///这里我内部写死了,就是一张图片而已
        imageView.backgroundColor = [UIColor redColor];
        imageView.contentMode = UIViewContentModeScaleAspectFill;
        imageView.layer.masksToBounds = YES;
        [imageView sd_setImageWithURL:[NSURL URLWithString:self.data[i]]];
        
        [self.scrollView addSubview:imageView];
    }
    
    self.scrollView.contentOffset = CGPointMake((2 * _selectIndex) * self.halfSpacing + self.selectIndex * (self.scrollView.frame.size.width - 2 * self.halfSpacing), 0);
    self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * self.data.count, 0);
}
#pragma mark - Action
-(void)tapAction:(UITapGestureRecognizer *)tap{
    
    //点击后的代理
    if ([_delegate respondsToSelector:@selector(didSelectPicWithIndexPath:)]) {
        [_delegate didSelectPicWithIndexPath:(self.scrollView.contentOffset.x / self.scrollView.frame.size.width)];
    }
    
}
#pragma mark - UIScrollViewDelegate
-(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    
    if ([_delegate respondsToSelector:@selector(roll_scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) {
        [_delegate roll_scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset];
    }
    
}

@end

swift 3.0代码:

import Foundation

@objc protocol PageViewDelegate{

    @objc optional func didSelectPicWithIndexPath(_ index:Int)
    
    @objc optional func page_scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
    
}

class PageView : UIView {
    
    var selectIndex : Int = 0
    weak var pageDelegate : PageViewDelegate?
    
    fileprivate var halfSpacing : CGFloat = 0.0
    fileprivate var data : [Any] = [Any]()
    fileprivate lazy var scrollView : UIScrollView = {
        
        let tmp = UIScrollView(frame:CGRect.zero)
        tmp.delegate = self
        tmp.isPagingEnabled = true
        tmp.clipsToBounds = false
        tmp.showsVerticalScrollIndicator = false
        tmp.showsHorizontalScrollIndicator = false
        
        let tap = UITapGestureRecognizer(target: self, action:#selector(tapAction(tap:)))
        tap.numberOfTapsRequired = 1
        tap.numberOfTouchesRequired = 1
        tmp.addGestureRecognizer(tap)
        
        
        return tmp
    }()
    
    init(frame:CGRect, withDistanceToScrollView distance:CGFloat, withSpacing spacing:CGFloat){
        
        super.init(frame: frame)
        
        self.halfSpacing = spacing * 0.5
        
        self.addSubview(self.scrollView)
        self.scrollView.frame = CGRect(x:distance,y:0,width:self.frame.size.width - 2 * distance,height:self.frame.size.height)
        
        
        
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    func loadView(_ data:[Any]) {
        
        self.data = data
        
        if data.count == 0{
            
            return
        }
        
        for i in 0..<data.count {
            
            for subView in self.scrollView.subviews {
                
                if subView.tag == 100 + i {
                    
                    subView.removeFromSuperview()
                }
            }
            
            let imageView = UIImageView()
            imageView.isUserInteractionEnabled = true
            imageView.tag = 100 + i
            imageView.backgroundColor = UIColor.red
            imageView.contentMode = .scaleAspectFill
            imageView.layer.masksToBounds = true
            self.scrollView.addSubview(imageView)
            
            let x : CGFloat = CGFloat(2 * i + 1) * self.halfSpacing + CGFloat(i) * (self.scrollView.frame.size.width - 2 * self.halfSpacing)
            let w: CGFloat = (self.scrollView.frame.size.width - 2 * self.halfSpacing)
            imageView.frame = CGRect(x:x,y:0,width:w,height:self.scrollView.frame.size.height)
        }
        
        let pointX : CGFloat = CGFloat(2 * self.selectIndex) * self.halfSpacing + CGFloat(self.selectIndex) * (self.scrollView.frame.size.width - 2 * self.halfSpacing)
        self.scrollView.contentOffset = CGPoint(x: pointX, y: 0)
        
        self.scrollView.contentSize = CGSize(width:self.scrollView.frame.size.width * CGFloat(self.data.count),height:0)
        

        
    }
    
    ///Action
    func tapAction(tap:UITapGestureRecognizer) {
        
        let index = Int(self.scrollView.contentOffset.x / self.scrollView.frame.size.width)
        self.pageDelegate?.didSelectPicWithIndexPath?(index)
    }


    
}
extension PageView : UIScrollViewDelegate{

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        
         self.pageDelegate?.page_scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
    }
}

这里关于UIScrollViewDelegate的代理,主要是我每拖拽一次,其他控件就会对应改变,只是处理一些逻辑问题,但是为什么是在这个方法调用,是因为scrollView在一次减速动画还没有结束的时候再次拖拽scrollView,didEndDecelerating这个代理方法是不会被调用的。
另外单页图片的frame和scrollView宽度设置只是一个数学问题了,就不再详细介绍了,毕竟程序猿都是数学宝宝,要是不懂那只能转行啦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容