UICollectionView自定義pagingEnabled翻頁(yè)區(qū)域

這個(gè)標(biāo)題挺難起的:

UICollectionView設(shè)置翻頁(yè)區(qū)域?
UICollectionView依據(jù)items翻頁(yè),而不是屏幕寬度?
UICollectionView每一頁(yè)開頭第一個(gè)item不被切割,并且左間距固定?
。。。

直接看效果:

第一頁(yè)item4未展示全,第二頁(yè)從item4開始

比較一下直接設(shè)置collectionView.pagingEnabled = YES的效果:

第一頁(yè)item4展示一部分,第二頁(yè)展示item4剩下部分

Code

源碼在GitHub,item的寬度和間距使用宏定義,方便修改

關(guān)鍵步驟

一、新建UICollectionViewFlowLayout 子類,自定義滑動(dòng)位置

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity;

Discussion
If you want the scrolling behavior to snap to specific boundaries, you can override this method and use it to change the point at which to stop. For example, you might use this method to always stop scrolling on a boundary between items, as opposed to stopping in the middle of an item.

首先,proposedContentOffset參數(shù)的含義是系統(tǒng)根據(jù)用戶的滑動(dòng)手勢(shì)計(jì)算出來的將要滑動(dòng)到的目標(biāo)位置。
我們可以在UICollectionViewFlowLayout的子類里重寫這個(gè)方法,根據(jù)系統(tǒng)計(jì)算出來的期望目標(biāo)位置proposedContentOffset和滑動(dòng)速度velocity,自定義滑動(dòng)位置。

1. 新建UICollectionViewFlowLayout的子類MyCollectionFlowLayout

將item有關(guān)參數(shù)設(shè)置為宏,方便修改

#import "MyCollectionFlowLayout.h"

static CGFloat const kItemWidth = 70.f;     // item寬高
static CGFloat const kPaddingMid = 30.f;    // item間距
static CGFloat const kPaddingLeft = 20.f;   // 最左邊item左邊距


@interface MyCollectionFlowLayout()<UIScrollViewDelegate, UICollectionViewDelegate> {
    NSInteger _pageCapacity;    // 每頁(yè)可以完整展示的item個(gè)數(shù)
    NSInteger _currentIndex;    // 當(dāng)前頁(yè)碼(滑動(dòng)前)
}

@end
2. 重寫- (void)prepareLayout方法,設(shè)置sectionInset右縮進(jìn)

在這個(gè)方法里,需要計(jì)算:

  1. 每頁(yè)可以完整顯示的items個(gè)數(shù)
  2. 完整顯示所有items的總頁(yè)數(shù)
  3. 最后一頁(yè)item從左邊開始,那右邊的剩余空間有多少?即sectionInset右縮進(jìn)
- (void)prepareLayout
{
    [super prepareLayout];
    
    self.collectionView.delegate = self;
    
    // 計(jì)算paddingRight
    CGFloat paddingRight = 0.0;
    
    // item個(gè)數(shù)
    // collectionView調(diào)用reloadData后,layout會(huì)重新prepareLayout
    NSInteger itemsCount = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:0];
    
    // item間距
    self.minimumInteritemSpacing = kPaddingMid;
    self.minimumLineSpacing = kPaddingMid;
    self.itemSize = CGSizeMake(kItemWidth, kItemWidth);
    
    CGFloat collectionViewWidth = CGRectGetWidth(self.collectionView.bounds);
    
    // 每頁(yè)可以完整顯示的items個(gè)數(shù)
    NSInteger pageCapacity = (NSInteger)(collectionViewWidth - kPaddingLeft + kPaddingMid) / (NSInteger)(kItemWidth + kPaddingMid);
    _pageCapacity = pageCapacity;
    
    // 完整顯示所有items的總頁(yè)數(shù)
    NSInteger pages = itemsCount / pageCapacity;
    NSInteger remainder = itemsCount % pageCapacity;
    if (remainder == 0) {
        paddingRight = collectionViewWidth - pageCapacity * (kItemWidth + kPaddingMid) + kPaddingMid - kPaddingLeft;
    } else {
        paddingRight = collectionViewWidth - remainder * (kItemWidth + kPaddingMid) + kPaddingMid - kPaddingLeft;
        pages ++;
    }
    
    // padding top bottom
    CGFloat paddingVertical = (CGRectGetHeight(self.collectionView.bounds) - kItemWidth) / 2;
    self.sectionInset = UIEdgeInsetsMake(paddingVertical, kPaddingLeft, paddingVertical, paddingRight);
}
3. 重寫- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity方法

重寫這個(gè)方法就可以指定滑動(dòng)停止的位置,我的計(jì)算思路是先根據(jù)用戶的滑動(dòng)手勢(shì),判斷是向前翻頁(yè)還是向后翻頁(yè),向后翻頁(yè)則目標(biāo)頁(yè)碼index = _currentIndex + 1。 翻頁(yè)時(shí),實(shí)際的頁(yè)面寬度是每頁(yè)剛好可以完整展示的最多個(gè)item的寬度,即_pageCapacity * (kItemWidth + kPaddingMid),那么x軸目標(biāo)偏移就是point.x = 目標(biāo)頁(yè)碼 * 每頁(yè)實(shí)際寬度
這里需要知道滑動(dòng)前當(dāng)前的頁(yè)碼_currentIndex, 我是通過UIScrollViewDelegate的代理方法取到用戶將要滑動(dòng)時(shí)的x軸偏移計(jì)算的

#pragma mark --- UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    _currentIndex = (NSInteger)(scrollView.contentOffset.x ) / (NSInteger)(_pageCapacity * (kItemWidth + kPaddingMid));
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    
    NSInteger index = (NSInteger)proposedContentOffset.x / (NSInteger)(_pageCapacity * (kItemWidth + kPaddingMid));

    NSInteger remainder = (NSInteger)proposedContentOffset.x % (NSInteger)(_pageCapacity * (kItemWidth + kPaddingMid));

    if (remainder > 10 && velocity.x > 0.3) {
        index ++;
    }

    if (velocity.x < -0.3 && index > 0) {
        index --;
    }
    
    // 保證一次只滑動(dòng)一頁(yè)
    index = MAX(index, _currentIndex - 1);
    index = MIN(index, _currentIndex + 1);

    CGPoint point = CGPointMake(0, 0);
    if (index > 0) {
        point.x = index * _pageCapacity * (kItemWidth + kPaddingMid);
    }

    return point;
}

二、 不使用系統(tǒng)pagingEnabled

文章開頭已說明,設(shè)置scrollView.pagingEnabled = YES達(dá)不到我們的目標(biāo),本文介紹的方案里需要設(shè)置scrollView.pagingEnabled = NO,否則上面函數(shù)中自定義的滑動(dòng)位置不起作用

三、 盡量還原pagingEnabled效果

設(shè)置scrollView.decelerationRate = UIScrollViewDecelerationRateFast;,滑動(dòng)效果基本接近系統(tǒng)pagingEnabled

運(yùn)行起來后簡(jiǎn)單測(cè)試,需求基本滿足了


四、有兩個(gè)bug

1. 滑動(dòng)有時(shí)會(huì)卡頓
第二頁(yè)第一次向后翻頁(yè)時(shí),卡一下
2. 從后往前翻頁(yè)時(shí),有時(shí)會(huì)連續(xù)翻兩頁(yè)
第三頁(yè)向前翻頁(yè)時(shí),直接翻到了第一頁(yè)
3. 檢查出錯(cuò)原因,在MyCollectionFlowLayout.m文件里加上日志
#pragma mark --- UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    _currentIndex = (NSInteger)(scrollView.contentOffset.x) / (NSInteger)(_pageCapacity * (kItemWidth + kPaddingMid));
    NSLog(@"\n\n---------------------");
    NSLog(@"1. 預(yù)期每頁(yè)內(nèi)容寬度 %ld",(NSInteger)(_pageCapacity * (kItemWidth + kPaddingMid)));
    NSLog(@"2. 滑動(dòng)前的x軸偏移 %ld",(NSInteger)(scrollView.contentOffset.x));
    NSLog(@"3. 滑動(dòng)前當(dāng)前頁(yè)碼 %ld",_currentIndex);
}
log.png

從打印日志發(fā)現(xiàn)從第一頁(yè)翻到第二頁(yè),然后(未等滑動(dòng)完全停止)繼續(xù)滑動(dòng)時(shí),x軸偏移量比目標(biāo)偏移量小幾個(gè)像素,即滑動(dòng)還沒有完全結(jié)束。由于當(dāng)前頁(yè)的index是通過x軸偏移量取整求商得到的,這幾個(gè)像素的差異會(huì)導(dǎo)致index比預(yù)期小1

4. 解決方法

在計(jì)算當(dāng)前的頁(yè)碼_currentIndex時(shí),用一個(gè)item的寬度補(bǔ)償x軸偏移量,由于kItemWidth恒小于_pageCapacity * (kItemWidth + kPaddingMid),這種補(bǔ)償不會(huì)造成頁(yè)面index加1,是安全的

#pragma mark --- UIScrollViewDelegate
- (**void**)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    */**
** 分子scrollView.contentOffset.x為什么要+kItemWidth ??*
** 消除scrollView在擺動(dòng)的時(shí)候的誤差,此時(shí)contentOffset.x比預(yù)期減少了10左右像素,導(dǎo)致_currentIndex比預(yù)期小1*
**/*
    _currentIndex = (NSInteger)(scrollView.contentOffset.x + kItemWidth) / (NSInteger)(_pageCapacity * (kItemWidth + kPaddingMid));
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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