UIScrollView帶間距的分頁效果實現(xiàn)

最近在項目碰到一個需求:一個輪播視圖,頁面之間有一定間距,要求每次滾動時候,一次只能拖動一頁并且頁面居中。當(dāng)時粗略一想,應(yīng)該設(shè)置pagingEnabled,但是使用這個屬性后,scrollView每次翻頁就是它frame的寬度,貌似不能用(提前劇透下:確實要使用pagingEnabled,并結(jié)合clipsToBounds),再加上這種輪播視圖習(xí)慣使用collectionView,就決定使用collectionView了。

關(guān)鍵代碼如下:

-(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
{
    ///計算一下當(dāng)前的位置
    CGFloat contentOffset = self.collectionView.contentOffset.x;
    
    NSInteger currentIndex =  MAX(0,(contentOffset - self.sectionInset.left) / (self.itemSize.width + self.minimumLineSpacing));
    
    ///計算原本應(yīng)該停留的位置
    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

這么寫貌似可以完成需求,一次也是滾一頁,每次翻頁也是居中,但是體驗很不好,設(shè)置了滑動的減速度為fast,如果每次拖動的距離很短,視圖停止?jié)L動的太快,跟原生的pagingEnabled的體驗相比還是不好。但是如果你只想滑動結(jié)束的時候頁面居中,這段代碼還是可以用的,hahaha!(直接返回proposedContentOffset.x + adjustOffsetX,proposedContentOffset.y))
扯遠(yuǎn)了!那怎么辦?回歸基礎(chǔ)!通過實現(xiàn)UIScrollView的代理方法自己實現(xiàn)分頁,可能是自己太菜了,雖然最終位置沒錯,但是體驗不好。這里介紹一下UIScrollViewDelegate幾個常用的方法

UIScrollView代理方法調(diào)用順序:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
scrollView滾動的時候就會調(diào)用這個方法

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
scrollView被拖拽的時候調(diào)用

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
scrollView的拖拽將要結(jié)束的時候,通過targetContentOffset可以獲取到最后停留的那個位置,雖然沒有經(jīng)過測試,但是這個方法應(yīng)該和collectionView layout的那個代理方法是一樣的,

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
scrollView的拖拽結(jié)束的時候的調(diào)用。如果decelerate為yes,說明scrollView將進(jìn)入一個減速滑動的狀態(tài);如果為no,說明減速已經(jīng)停止,將會調(diào)用scrollViewDidEndDecelerating方法

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
scrollView將開始減速滑動,這個時候的decelerate為YES

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
scrollView結(jié)束滑動時候被調(diào)用。

再仔細(xì)分析一下需求(所以永遠(yuǎn)不要急著動手,多思考),還是應(yīng)該使用pagingEnabled,但是應(yīng)該結(jié)合一下其他屬性,后來發(fā)現(xiàn)使用clipsToBounds ,既然有思路了,那就干起來了吧

最終解決方法

思路大概設(shè)置pagingEnabled為yes,這樣一次只能翻一頁;設(shè)置clipsToBounds為NO,這樣就可以顯示溢出部分的內(nèi)容,但是scrollView的寬度要設(shè)置為一頁內(nèi)容的寬度 + 分頁之間的間距
這里我封裝了一個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;//當(dāng)前頁面的下標(biāo),默認(rèn)為0


/**
 @param frame 設(shè)置View大小
 @param distance 設(shè)置Scroll距離View兩側(cè)距離
 @param spacing 設(shè)置Scroll內(nèi)部 圖片間距,注意點(diǎn):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 ;
        
        /**  注意點(diǎn)
         *   1. ScrollView的width應(yīng)該等于單頁寬度 + spacing
         *   2. 假設(shè)單個頁面寬為 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);
        ///這里我內(nèi)部寫死了,就是一張圖片而已
        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{
    
    //點(diǎn)擊后的代理
    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)
    }
}

這里關(guān)于UIScrollViewDelegate的代理,主要是我每拖拽一次,其他控件就會對應(yīng)改變,只是處理一些邏輯問題,但是為什么是在這個方法調(diào)用,是因為scrollView在一次減速動畫還沒有結(jié)束的時候再次拖拽scrollView,didEndDecelerating這個代理方法是不會被調(diào)用的。
另外單頁圖片的frame和scrollView寬度設(shè)置只是一個數(shù)學(xué)問題了,就不再詳細(xì)介紹了,畢竟程序猿都是數(shù)學(xué)寶寶,要是不懂那只能轉(zhuǎn)行啦

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容