iOS UICollectionViewFlowLayout 與瀑布流

UICollectionView


當我們使用代碼行對 UICollectionView 進行初始化時,都不忘在前面創建一個 UICollectionViewFlowLayout 對象。因爲我們可以通過UICollectionViewFlowLayout 來設定符合我們需求的 UICollectionView 佈局。接下來,就讓我們先來談談 UICollectionViewFlowLayout 的使用。

 

一、UICollectionViewFlowLayout 的使用

首先初始化一個  UICollectionViewFlowLayout 對象:

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];

對 UICollectionViewCell 的相關佈局約束:

 // 最小行間距,默認是0
 layout.minimumLineSpacing = 5;
 // 最小左右間距,默認是10
 layout.minimumInteritemSpacing = 5;
 // 區域內間距,默認是 UIEdgeInsetsMake(0, 0, 0, 0)
 layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

在 UICollectionViewCell 上添加 UILabel 以便測試:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    
    // cell 序號
    UILabel *label = (UILabel *)[cell viewWithTag:10];
    
    if (label == nil) {
        
        [cell setBackgroundColor:[UIColor cyanColor]];
        
        label = [[UILabel alloc]init];
        label.textAlignment = NSTextAlignmentCenter;
        label.font = [UIFont systemFontOfSize:80];
        label.adjustsFontSizeToFitWidth = YES;
        label.tag = 10;
        [cell addSubview:label];
    }
    [label setFrame:CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height)];
    label.text = [[NSString alloc] initWithFormat:@"%ld", indexPath.row + 1];

    return cell;
}

其中行間距和左右間距只能設置最小值。
所謂的最小值,就是系統在設置佈局時,首先按 cell 的寬和最小左右間距橫着排版下去一行最多可以排多少個,如果還有剩餘空間的話,系統會將剩下的寬度平均分配該行的每一個左右間距,所以實際效果的左右間距會比設置的左右間距大的多。而行間距也是類似,在高度不一樣的一行 cell 中,只有最高的 cell 是最小行間距,其餘高度小的 cell 還會分配到與該行最高 cell 的高度差值“補貼”,具體情況後面有演示。

在 8 plus 模擬器上,cell 的最小左右間距設置爲 5
cell 設置大小爲 120 x 120 時,

layout.itemSize = CGSizeMake(120, 120);

效果如下:

cell 大小 120x120

cell 設置大小爲 100 x 100 時,

layout.itemSize = CGSizeMake(100, 100);

效果如下:

cell 大小 100x100

cell 設置大小爲 80 x 80 時,

layout.itemSize = CGSizeMake(80, 80);

效果如下:

cell 大小 80x80

從模擬器的展示結果來看,排版的剩餘寬度越大,左右間距也隨之分配到更多的空間。比較過程中,我們也可以看到區域內間距  sectionInset 始終都是不變的,但是左右間距會因排版剩餘寬度過多而變大,導致與不變的區域內間距差距就更大了。
有時候爲了美觀,我們需要將內間距和左右間距設置成一樣的大小。這時,我們需要像系統一樣先預算出在 cell 大小固定和左右間距取最小值的情況下,在一行中可以排版多少個 cell 。只不過,在分配剩餘的寬度空間時,我們不僅將其分配給左右間距,還有區域內間距。

cell 大小 80x80 爲例:

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(80, 80);
// 以最小間距爲10計算間距
// 每行可放多少 cell
NSInteger nCountCell = (kScreenWidth - 10) / (layout.itemSize.width + 10);
// 平均後的間距
CGFloat fSpacing = (kScreenWidth - layout.itemSize.width * nCountCell) / (nCountCell + 1);
layout.minimumInteritemSpacing = fSpacing;
layout.minimumLineSpacing = fSpacing;
layout.sectionInset = UIEdgeInsetsMake(fSpacing, fSpacing, fSpacing, fSpacing);

執行結果:


                    等間距

以上講的 UICollectionViewFlowLayout 使用是基於 cell 的大小是統一的情況。當有不同大小的 cell 放在同一個 UICollectionView 裏,還要符合我們的心意排版,這個時候就需要自定義 UICollectionViewFlowLayout 類了,而專門適用於這種參差不齊 cell 的佈局有個專業名詞--“瀑布流”。

以下來自百度百科關於瀑布流的介紹:

瀑布流,又稱瀑布流式佈局。是比較流行的一種網站頁面佈局,視覺表現爲參差不齊的多欄佈局,隨着頁面滾動條向下滾動,這種佈局還會不斷加載數據塊並附加至當前尾部。最早採用此佈局的網站是Pinterest,逐漸在國內流行開來。國內大多數清新站基本爲這類風格。

二、UICollectionViewFlowLayout 實現瀑布流

有句諺語是這樣講的:

巧婦難爲無米之炊

要想實現瀑布流佈局,就需要先實現大小參差不齊的 cell,而要實現大小參差不齊的 cell ,就需要用到 UICollectionViewDelegate 的擴展協議 UICollectionViewDelegateFlowLayout,這個協議是在 UICollectionViewDelegate 的基礎上增加了 UICollectionViewFlowLayout 一些可以根據 NSIndexPath 和 section來定製獨立的佈局屬性,該協議提供多個可選方法。

@protocol UICollectionViewDelegateFlowLayout <UICollectionViewDelegate>
@optional

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section;

@end

而我們要實現大小參差不齊的 cell ,就需要用到這個方法:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;

我們使用隨機數 arc4random() 將 cell 的高度設置在4080之間:

#pragma mark - UICollectionViewDelegateFlowLayout
//  cell 大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    // 隨機高度
    return CGSizeMake(80,  40 + arc4random() % 40);
}

執行結果:

高度在40~80之間的 cell

從執行結果圖中,我們可以看出系統在佈局參差不齊的 cell 時 ,將會以每行高度最高的 cell 爲基準,其餘矮的 cell 的中心和這個高 cell 的中心保持在同一條水平線上。因此我們實現瀑布流,就需要先打破這個規則(每個  cell 依賴於它所在行的最高 cell 來佈局它的位置),讓每一個 cell 往已經排好且最短的列排版。
而要讓每一個 cell 像人一樣能思考地進行排版,我們需要重載 UICollectionViewFlowLayout 對所有 cell 屬性的佈局方法:

-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;

因爲 UICollectionView 的滾動內容大小是由 cell 的大小和數量決定的,系統又會以每一行最高 cell 的高度定爲該行的高,然後逐個排版下去,導致重載佈局方法之後,滾動到底部會出現一大堆空白。

佈局之後剩餘的空白區

雖然 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;方法可以爲我們定製不同 cell的大小,但是 UICollectionView 滾動內容大小是根據系統的排版標準去算的,會導致滾動空間的溢出,所以這個方法在這種有上下縮進的情況下不適用。
因此爲了準確地計算出 UICollectionView 合理的滾動內容大小,首先,我們需要在每一次更新佈局前就引入各個 cell 的大小到自定義的 UICollectionViewFlowLayout 中,然後在準備方法 - (void)prepareLayout; 中,規劃具體佈局,接着,計算出瀑布流佈局所需要滾動內容的高,以及在這麼大的滾動內容大小下,每一個 cell 的 itemsize 要設置多大的平均值。

具體代碼實現。
自定義瀑布流頭文件 MyFlowLayout.h :

#import <UIKit/UIKit.h>

/**
 自定義瀑布流佈局
 */
@interface MyFlowLayout : UICollectionViewFlowLayout

/**
 瀑布流佈局方法
 
 @param itemWidth item 的寬度
 @param itemHeightArray item 的高度數組
 */
- (void)flowLayoutWithItemWidth:(CGFloat)itemWidth itemHeightArray:(NSArray<NSNumber *> *)itemHeightArray;

@end

實現文件 MyFlowLayout.m :

#import "MyFlowLayout.h"



@interface MyFlowLayout ()

/**
 item 的高度數組
 */
@property (nonatomic, copy) NSArray<NSNumber *> *arrItemHeight;

/**
 cell 佈局屬性集
 */
@property (nonatomic, strong) NSArray<UICollectionViewLayoutAttributes *> *arrAttributes;

@end

@implementation MyFlowLayout

/**
 瀑布流佈局方法
 
 @param itemWidth item 的寬度
 @param itemHeightArray item 的高度數組
 */
- (void)flowLayoutWithItemWidth:(CGFloat)itemWidth itemHeightArray:(NSArray<NSNumber *> *)itemHeightArray {
    
    self.itemSize = CGSizeMake(itemWidth, 0);
    self.arrItemHeight = itemHeightArray;
    [self.collectionView reloadData];
}

- (void)prepareLayout {
    
    [super prepareLayout];
    
    // item 數量爲零不做處理
    if ([self.arrItemHeight count] == 0) {
        
        return;
    }
    
    // 計算一行可以放多少個項
    NSInteger nItemInRow = (self.collectionViewContentSize.width - self.sectionInset.left - self.sectionInset.right + self.minimumInteritemSpacing) / (self.itemSize.width + self.minimumInteritemSpacing);
    // 對列的長度進行累計
    NSMutableArray *arrmColumnLength = [NSMutableArray arrayWithCapacity:100];
    for (NSInteger i = 0; i < nItemInRow; i++) {
        
        [arrmColumnLength addObject:@0];
    }
    
    NSMutableArray *arrmTemp = [NSMutableArray arrayWithCapacity:100];
    // 遍歷設置每一個item的佈局
    for (NSInteger i = 0; i < [self.arrItemHeight count]; i++) {
        
        // 設置每個item的位置等相關屬性
        NSIndexPath *index = [NSIndexPath indexPathForItem:i inSection:0];
        // 創建每一個佈局屬性類,通過indexPath來創建
        UICollectionViewLayoutAttributes *attris = [self layoutAttributesForItemAtIndexPath:index];
        CGRect recFrame = attris.frame;
        // 有數組得到的高度
        recFrame.size.height = [self.arrItemHeight[i] doubleValue];
        // 最短列序號
        NSInteger nNumShort = 0;
        // 最短的長度
        CGFloat fShortLength = [arrmColumnLength[0] doubleValue];
        // 比較是否存在更短的列
        for (int i = 1; i < [arrmColumnLength count]; i++) {

            CGFloat fLength = [arrmColumnLength[i] doubleValue];
            if (fLength < fShortLength) {

                nNumShort = i;
                fShortLength = fLength;
            }
        }
        // 插入到最短的列中
        recFrame.origin.x = self.sectionInset.left + (self.itemSize.width + self.minimumInteritemSpacing) * nNumShort;
        recFrame.origin.y = fShortLength + self.minimumLineSpacing;
        // 更新列的累計長度
        arrmColumnLength[nNumShort] = [NSNumber numberWithDouble:CGRectGetMaxY(recFrame)];
        
        // 更新佈局
        attris.frame = recFrame;
        [arrmTemp addObject:attris];
    }
    self.arrAttributes = arrmTemp;
    
    // 因爲使用了瀑布流佈局使得滾動範圍是根據 item 的大小和個數決定的,所以以最長的列爲基準,將高度平均到每一個 cell 中
    // 最長列序號
    NSInteger nNumLong = 0;
    // 最長的長度
    CGFloat fLongLength = [arrmColumnLength[0] doubleValue];
    // 比較是否存在更短的列
    for (int i = 1; i < [arrmColumnLength count]; i++) {

        CGFloat fLength = [arrmColumnLength[i] doubleValue];
        if (fLength > fLongLength) {

            nNumLong = i;
            fLongLength = fLength;
        }
    }
    // 在大小一樣的情況下,有多少行
    NSInteger nRows = ([self.arrItemHeight count] + nItemInRow - 1) / nItemInRow;
    self.itemSize = CGSizeMake(self.itemSize.width, (fLongLength + self.minimumLineSpacing) / nRows - self.minimumLineSpacing);
    
}

// 返回所有的 cell 佈局數組
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    
    return self.arrAttributes;
}
@end

在視圖控制器上的調用:

MyFlowLayout *layout = [[MyFlowLayout alloc] init];
// 創建隨機高度的數組
NSMutableArray *arrmHeight = [NSMutableArray arrayWithCapacity:100];
for (NSInteger i = 0; i < 50; i++) {
    
    // 40~80 的隨機高度
    [arrmHeight addObject:[NSNumber numberWithDouble:40 + arc4random() % 40]];
}
[layout flowLayoutWithItemWidth:80 itemHeightArray:arrmHeight];
// 以最小間距爲10計算間距
// 每行可放多少 cell
NSInteger nCountCell = (kScreenWidth - 10) / (layout.itemSize.width + 10);
// 平均後的間距
CGFloat fSpacing = (kScreenWidth - layout.itemSize.width * nCountCell) / (nCountCell + 1);
layout.minimumInteritemSpacing = fSpacing;
layout.minimumLineSpacing = fSpacing;
layout.sectionInset = UIEdgeInsetsMake(fSpacing, fSpacing, fSpacing, fSpacing);

self.collection = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 64, kScreenWidth, kScreenHeight - 64) collectionViewLayout:layout];
_collection.backgroundColor = [UIColor whiteColor];
_collection.delegate = self;
_collection.dataSource = self;
[self.view addSubview:_collection];

// 註冊 cell
[_collection registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:cellId];

執行結果:

平均 cell 的 itemSize 之後

除了這種高度參差不齊的 cell,我們在開發過程中,還會遇到寬度參差不齊的佈局。

三、UICollectionViewFlowLayout 的靠左佈局

因爲在寬度參差不齊的佈局中,不會影響滾動內容的高度,所以我們可以通過 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath; 來實現 itemSize 的不規則:

//  cell 大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    // 隨機寬度
    return CGSizeMake(40 + arc4random() % 4 * 20, 50);
}

UICollectionViewFlowLayout 的屬性設置:

//創建流水佈局對象
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

UICollectionViewDelegate 和 UICollectionViewDataSource 的相關實現:

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {

    return 70;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
        
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    
    // Configure the cell
    UILabel *label = (UILabel *)[cell viewWithTag:10];
    
    if (label == nil) {

        // 添加子控件
        label = [[UILabel alloc]init];
        label.textAlignment = NSTextAlignmentCenter;
        label.font = [UIFont systemFontOfSize:30];
        label.adjustsFontSizeToFitWidth = YES;
        label.tag = 10;
        [cell addSubview:label];
    }
    [label setFrame:CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height)];
    label.text = [[NSString alloc] initWithFormat:@"%ld", indexPath.row + 1];

    return cell;
}

執行結果:

寬度不規則的 cell

從結果中可以看出,因爲寬度不一樣導致每一行排版剩餘的空間不一樣,導致左右間距也就相差較大了。
爲了排版美觀,我們有時候需要靠左佈局。之前因爲項目有過這樣的產品需求,在網絡上找到了一個第三方 UICollectionViewFlowLayout 自定義類--UICollectionViewLeftAlignedLayout
UICollectionViewLeftAlignedLayout 下載地址

在視圖控制器上的使用:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

執行結果:

靠左佈局的 cell

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章