iOS 關(guān)于UICollectionView瀑布流布局

瀑布流有幾種實(shí)現(xiàn)方式,這里只說我寫的這一種

首先瀑布流直觀可以看出是塊分布,不難想到用 UICollectionView 去實(shí)現(xiàn)。
其次就是需要我們自定義一個類繼承 UICollectionViewFlowLayout 來計(jì)算瀑布流的顯示方式。

在寫代碼以前,要想好瀑布流到底是什么原理排列的
在寫代碼以前,要想好瀑布流到底是什么原理排列的
在寫代碼以前,要想好瀑布流到底是什么原理排列的

重要的事情說三遍: )

其實(shí)總結(jié)出來就是一個問題:
每次Add一個Object的時候,要加在哪一列

沒錯,就是加在上一排最短的那一列

那么我們要怎么知道每個item高度呢?
因?yàn)槊總€使用這個Layout的人的需求都不一樣,我們不能要求每個item都只是一個圖片(這里用圖片作為例子的原因是image.size
所以我們需要Coder在使用這個Layout的時候告訴我們這個item的高度

//這里類似UITableView的方法
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;

而且這個高度是強(qiáng)制性要獲取的,so。。。(@required你懂得)

UICollectionViewWaterfallLayout.h

#import <UIKit/UIKit.h>
@class UICollectionViewWaterfallLayout;

@protocol UICollectionViewWaterfallLayoutDelegate <NSObject>
@required
- (CGFloat)waterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout heightForRowAtIndexPath:(NSInteger)index itemWidth:(CGFloat)itemWidth;

@optional
//列數(shù)
- (int)columnCountInWaterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout;
//每列間距(不算邊距)
- (CGFloat)columnMarginInWaterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout;
//行間距(不算邊距)
- (CGFloat)rowMarginInWaterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout;
//邊距
- (UIEdgeInsets)edgeInsetsInWaterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout;
@end

@interface UICollectionViewWaterfallLayout : UICollectionViewLayout

@property (nonatomic ,weak) id<UICollectionViewWaterfallLayoutDelegate> delegate;

@end

同時我也定義了4個Optional方法,Coder有需求就對號入座去實(shí)現(xiàn)就好了

UICollectionViewWaterfallLayout.m

既然有4個可修改項(xiàng),那么我們就需要一些Default屬性(不了解UICollectionView有哪些可以改的東西,例如邊距,可以自己Com+點(diǎn)進(jìn)去自己看看,這里就不多做解釋了)

//默認(rèn)的列數(shù)
static const NSInteger DefaultColumnCount = 3;

//每一列之間的間距
static const CGFloat DefaultColumnMargin = 10;

//沒一行之間的間距
static const CGFloat DefaultRowMargin = 10;

//邊緣間距
static const UIEdgeInsets DefaultEdgeInsets = {10, 10, 10, 10};

- (CGFloat)rowMargin
{
    if ([self.delegate respondsToSelector:@selector(rowMarginInWaterfallLayout:)]) {
        return [self.delegate rowMarginInWaterfallLayout:self];
    } else {
        return DefaultRowMargin;
    }
}

- (CGFloat)columnMargin
{
    if ([self.delegate respondsToSelector:@selector(columnMarginInWaterfallLayout:)]) {
        return [self.delegate columnMarginInWaterfallLayout:self];
    } else {
        return DefaultColumnMargin;
    }
}

- (NSInteger)columnCount
{
    if ([self.delegate respondsToSelector:@selector(columnCountInWaterfallLayout:)]) {
        return [self.delegate columnCountInWaterfallLayout:self];
    } else {
        return DefaultColumnCount;
    }
}

- (UIEdgeInsets)edgeInsets
{
    if ([self.delegate respondsToSelector:@selector(edgeInsetsInWaterfallLayout:)]) {
        return [self.delegate edgeInsetsInWaterfallLayout:self];
    } else {
        return DefaultEdgeInsets;
    }
}

前面已經(jīng)說了如何布局的思想

這里需要創(chuàng)建3個屬性來實(shí)現(xiàn)我們的想法

//存放所有cell的布局屬性
@property (nonatomic, strong) NSMutableArray *attrsArray;
//存放所有列的當(dāng)前高度
@property (nonatomic, strong) NSMutableArray *columnHeights;
//內(nèi)容的高度
@property (nonatomic, assign) CGFloat contentHeight;

重寫其中兩個簡單的方法 我就不多做解釋了 注釋已經(jīng)寫的很詳細(xì)了

//方法是返回UICollectionView的可滾動范圍
- (CGSize)collectionViewContentSize {
    return CGSizeMake(0, self.contentHeight + self.edgeInsets.bottom);
}

//方法返回的是一個裝著UICollectionViewLayoutAttributes的數(shù)組
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    return self.attrsArray;
}

劃重點(diǎn)了?。。?/strong>
重寫核心方法

- (void)prepareLayout;
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath

首先reload的時候不難發(fā)現(xiàn)會走- (void)prepareLayout;方法,所以每次我們都要清空之前的計(jì)算記錄再重新計(jì)算
注:這個方法必須調(diào)用一下父類的實(shí)現(xiàn),不然你可以試試會怎么樣??

[super prepareLayout];
//清除之前計(jì)算的所有高度,因?yàn)樗⑿碌臅r候回調(diào)用這個方法
self.contentHeight = 0;
[self.columnHeights removeAllObjects];
[self.attrsArray removeAllObjects];

第一件事,我們是要確定瀑布流的列數(shù)

int forCount = [self.delegate respondsToSelector:@selector(columnCountInWaterfallLayout:)] ? [self.delegate columnCountInWaterfallLayout:self] : DefaultColumnCount;

然后向我們的數(shù)組中加入初始值(這里不加會崩潰)

for (NSInteger i = 0; i < forCount; i++) {
        [self.columnHeights addObject:@(self.edgeInsets.top)];
}

開始創(chuàng)建每一個cell對應(yīng)的布局屬性

for (int section = 0; section < self.collectionView.numberOfSections; section++) {
        
        for (NSInteger i = 0; i < [self.collectionView numberOfItemsInSection:section]; i++) {
            // 創(chuàng)建位置
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section];
            // 獲取indexPath位置cell對應(yīng)的布局屬性
            UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
            [self.attrsArray addObject:attrs];
        }
        
    }

這樣- (void)prepareLayout;就算是完成了,這里面調(diào)用了另一個需要重寫的方法- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath里面大部分都是計(jì)算和存儲高度的code,就不多做解釋了

//方法返回indexPath位置的UICollectionViewLayoutAttributes
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    
    CGFloat collectionViewW = self.collectionView.frame.size.width;
    
    CGFloat w = (collectionViewW - self.edgeInsets.left - self.edgeInsets.right -(self.columnCount - 1) * self.columnMargin) / self.columnCount;
    
    CGFloat h = [self.delegate waterfallLayout:self heightForRowAtIndexPath:indexPath.item itemWidth:w];
    
    NSInteger destColumn = 0;
    
    CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];
    for (NSInteger i = 0; i < self.columnCount; i++) {
        CGFloat columnHeight = [self.columnHeights[i] doubleValue];
        
        if (minColumnHeight > columnHeight) {
            minColumnHeight = columnHeight;
            destColumn = i;
        }
    }
    
    CGFloat x = self.edgeInsets.left + destColumn * (w + self.columnMargin);
    CGFloat y = minColumnHeight;
    if (y != self.edgeInsets.top) {
        y += self.rowMargin;
    }
    
    attrs.frame = CGRectMake(x, y, w, h);
    
    self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
    
    CGFloat columnHeight = [self.columnHeights[destColumn] doubleValue];
    if (self.contentHeight < columnHeight) {
        self.contentHeight = columnHeight;
    }
    return attrs;
    
}

到此為止,這個類算是完成了

然后到Controller里

<UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewWaterfallLayoutDelegate>

UICollectionViewWaterfallLayout *layout = [[UICollectionViewWaterfallLayout alloc] init];
layout.delegate = self;
self.collectionView.delegate = self;
self.collectionView.dataSource = self;

同UITableView一樣把該寫的都寫上
到了要實(shí)現(xiàn)那個必須實(shí)現(xiàn)的@required的方法的時候,也許真正用到項(xiàng)目里的Coder就會發(fā)現(xiàn)一個問題,大部分的情況下瀑布流是用來展示 圖片+Something,而圖片是網(wǎng)絡(luò)請求加載,異步獲取到圖片才能拿到寬高,這里我們項(xiàng)目也遇到過,也實(shí)現(xiàn)過這種需求,但體驗(yàn)不太好,不!是非常不好!
也許是因?yàn)槲覍τ趯慞laceHolderCell的理解不夠(個人覺得微博的效果就不錯,但我還不會??),經(jīng)過跟別的公司的小朋友討論+用戶體驗(yàn)的考慮下,90%的小朋友們的做法是后臺在返回圖片Url的同時給你圖片的W+H,這樣就能完美解決問題了,由于我們后臺云存儲獲取圖片的時候可在Url后控制返回圖片的大小,所以比較容易的實(shí)現(xiàn)了。

搞一個Model

@interface ImageModel : NSObject

//為了用戶體驗(yàn),異步加載網(wǎng)絡(luò)圖片,我們公司討論結(jié)果是后臺存儲好圖片寬高,與json數(shù)據(jù)一起返回,這樣用戶體驗(yàn)比較好
@property (nonatomic, readonly) CGFloat imageWidth;
@property (nonatomic, readonly) CGFloat imageHeight;
@property (nonatomic, strong) NSString *imageUrl;
@end

返回去實(shí)現(xiàn)協(xié)議方法

- (CGFloat)waterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout heightForRowAtIndexPath:(NSInteger)index itemWidth:(CGFloat)itemWidth {
    
    ImageModel *model = self.dataArray[index];
    
    return itemWidth * model.imageHeight / model.imageWidth;
    
}

至此如果沒什么問題運(yùn)行應(yīng)該就能看到效果了

效果圖

基礎(chǔ)類鏈接
Demo鏈接

有問題可以留言??
有幫助可以??一下

最后編輯于
?著作權(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)容