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
的高度設置在40
到80
之間:
#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