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宽度设置只是一个数学问题了,就不再详细介绍了,毕竟程序猿都是数学宝宝,要是不懂那只能转行啦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容