自定義CollectionView布局---滑動的卡片

效果圖
效果圖

這是一種很常見的布局,可以使用CollectionViewFlowLayout,在其代理方法中通過相關設置來達到此效果,但還是比較麻煩。如若直接用CollectionViewLayout來實現(xiàn),會簡單不少,且靈活性較好。

其實此布局主要考慮的問題就兩點:

1.實現(xiàn)任意尺寸的分頁大小,即每次滑動后某一張卡片都能停在屏幕中間(默認collectionView開啟pageEnable后是以其尺寸來作為分頁大?。?。
2.放大過程中實現(xiàn)每張卡片的縮放。

官方文檔得知自定義CollectionViewLayout至少需要重寫以下方法:

// 當collectionView滑動的時候,可見區(qū)域改變的時候是否使當前布局失效以重新布局
1.shouldInvalidateLayoutForBoundsChange:

// 需要在此方法中返回collectionView的內(nèi)容大小
2.collectionViewContentSize

// 為每個Cell返回一個對應的Attributes,我們需要在該Attributes中設置對應的屬性,如Frame等
3.layoutAttributesForItemAtIndexPath:

// 可在此方法中對可見rect中的cell的屬性進行相應設置
4.layoutAttributesForElementsInRect:

我們就以橫向滾動的布局為例來實現(xiàn)此布局,首先,定義好所需的屬性:

@property (nonatomic, assign) CGFloat spacing; //cell間距
@property (nonatomic, assign) CGSize itemSize; //cell的尺寸
@property (nonatomic, assign) CGFloat scale; //縮放率
@property (nonatomic, assign) UIEdgeInsets edgeInset; //邊距

接下來是重寫上面提到的幾個方法
1. shouldInvalidateLayoutForBoundsChange:

// 由于是此布局是平鋪的效果,所以當collectionView的bounds變化時,所展現(xiàn)的cell的個數(shù)及顯示效果可能會發(fā)生變化,故此方法應返回YES。

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

2. collectionViewContentSize

// 由于此布局一般只有一個section,故在此例中僅考慮只有一個section的情況
// 內(nèi)容的寬度為: 左邊距 + n*cell的寬 + (n-1)*cell的間距 + 右邊距。

- (CGSize)collectionViewContentSize {
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    CGFloat width = count*(self.itemSize.width+self.spacing)-self.spacing+self.edgeInset.left+self.edgeInset.right;
    CGFloat height = self.collectionView.bounds.size.height;
    return CGSizeMake(width, height);
}

3.layoutAttributesForItemAtIndexPath:

// 此方法要求返回一個UICollectionViewLayoutAttributes * 類型的對象
// 該對象包含對應cell外觀所需的必要屬性,包括center、frame、transform、alpha及其他屬性
// 在此方法中只需要做一件事,那就是給cell設置好正確的frame。

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    attribute.size = self.itemSize;
    
    CGFloat x = self.edgeInset.left + indexPath.item*(self.spacing+self.itemSize.width);
    CGFloat y = 0.5*(self.collectionView.bounds.size.height - self.itemSize.height);
    attribute.frame = CGRectMake(x, y, attribute.size.width, attribute.size.height);
    
    return attribute;
}

4.layoutAttributesForElementsInRect:

// 可在此方法中設置縮放效果

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
   
    NSArray *indexPaths = [self indexPathsInRect:rect];
    
    //找到屏幕中間的位置
    CGFloat centerX =  self.collectionView.contentOffset.x + 0.5*self.collectionView.bounds.size.width;
   NSMutableArray *attributes = [NSMutableArray array];
    for (NSIndexPath *indexPath in indexPaths) {
        UICollectionViewLayoutAttributes* attribute = [self layoutAttributesForItemAtIndexPath:indexPath];
        // 判斷可見區(qū)域和此cell的frame是否有重疊,因為indexPathsInRect返回的indexPath并不是十分準確。
        if (!CGRectIntersectsRect(rect, attribute.frame)) {
            //若不重疊則無需進行以下的步驟
            continue;
        }
        [attributes addObject:attribute];
        //計算每一個cell離屏幕中間的距離
        CGFloat offsetX = ABS(attribute.center.x - centerX);
        //這是設置一個縮放區(qū)域的閾值,當cell在此區(qū)域之外不進行縮放,改值可視具體情況進行修改。
        CGFloat space = self.itemSize.width+self.spacing;
        if (offsetX<space) {
            CGFloat scale = 1+(1-offsetX/space)*(self.scale-1);
            attribute.transform = CGAffineTransformMakeScale(scale, scale);
           // 設置此屬性是為了當cell層疊后,使得位于中間的cell總是位于最前面,若不明白可將此行注釋一試便知。 
            attribute.zIndex = 1;
        }
    }
    return attributes;
}

- (NSArray *)indexPathsInRect:(CGRect)rect {
    
    NSInteger leftIndex = (rect.origin.x-self.edgeInset.left)/(self.itemSize.width+self.spacing);
    NSInteger rightIndex = (CGRectGetMaxX(rect)-self.edgeInset.left)/(self.itemSize.width+self.spacing);

    NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
    preIndex = preIndex<0 ? 0 : preIndex;
    latIndex = latIndex>=itemCount ? itemCount-1 : latIndex;
    
    NSMutableArray *indexPaths = [NSMutableArray array];
    for (NSInteger i=leftIndex; i<=rightIndex; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        [indexPaths addObject:indexPath];
    }
    return indexPaths;
}

至此,我們已經(jīng)實現(xiàn)了在滾動過程中靠近屏幕中間的cell放大的效果,但是還沒實現(xiàn)滾動停止時某一個cell正好在屏幕中間,要想實現(xiàn)此效果,需要在targetContentOffsetForProposedContentOffset:withScrollingVelocity方法中實現(xiàn)此邏輯。此方法會給一個系統(tǒng)默認計算好的collectionView應該停下來的位置,返回一個collectionView最后要停下來的位置。

// 需要在此方法中獲取默認情況下停止?jié)L動時離屏幕中間最近的那個cell,并計算兩者的距離,將此距離補到proposedContentOffset上即可。

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    
    CGRect rect = CGRectMake(proposedContentOffset.x, proposedContentOffset.y, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    NSArray *attributes = [self layoutAttributesForElementsInRect:rect];
    
    CGFloat centerX = proposedContentOffset.x + 0.5*self.collectionView.bounds.size.width;
    CGFloat minOffsetX = MAXFLOAT;
    for (UICollectionViewLayoutAttributes* attribute in attributes) {
        CGFloat offsetX = attribute.center.x - centerX;
        if (ABS(offsetX) < ABS(minOffsetX)) {
            minOffsetX = offsetX;
        }
    }

    return CGPointMake(proposedContentOffset.x + minOffsetX, proposedContentOffset.y);
}

現(xiàn)在已經(jīng)實現(xiàn)了文章開頭動圖中展示的效果了。再將之前定義的各個屬性的set方法重寫,以便在設置這些屬性的時候進行重新布局。

注意:若想實現(xiàn)多個section以及含有sectionHeader或sectionFooter,請參照此思路來實現(xiàn),并且重寫以下兩個方法:

// 含有sectionHeader或sectionFooter應重寫此方法
layoutAttributesForSupplementaryViewOfKind:atIndexPath:

// cell含有裝飾視圖時要重寫此方法。
layoutAttributesForDecorationViewOfKind:atIndexPath:

想要源碼的可以點擊這里獲取,GitHub上的版本支持水平和垂直布局且已加以優(yōu)化。
此外還有Swift版本的哦,請點擊這里以獲取。
若覺得對你有用的話,還請給個star哈~

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

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

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