當(dāng)我們使用代碼行對(duì)
UICollectionView
進(jìn)行初始化時(shí),都不忘在前面創(chuàng)建一個(gè) UICollectionViewFlowLayout
對(duì)象坛猪。因?yàn)槲覀兛梢酝ㄟ^(guò)UICollectionViewFlowLayout
來(lái)設(shè)定符合我們需求的 UICollectionView
布局脖阵。接下來(lái),就讓我們先來(lái)談?wù)?UICollectionViewFlowLayout
的使用墅茉。
一命黔、UICollectionViewFlowLayout 的使用
首先初始化一個(gè) UICollectionViewFlowLayout
對(duì)象:
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
對(duì) UICollectionViewCell
的相關(guān)布局約束:
// 最小行間距,默認(rèn)是0
layout.minimumLineSpacing = 5;
// 最小左右間距就斤,默認(rèn)是10
layout.minimumInteritemSpacing = 5;
// 區(qū)域內(nèi)間距悍募,默認(rèn)是 UIEdgeInsetsMake(0, 0, 0, 0)
layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
在 UICollectionViewCell
上添加 UILabel
以便測(cè)試:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
// cell 序號(hào)
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;
}
其中行間距和左右間距只能設(shè)置最小值。
所謂的最小值洋机,就是系統(tǒng)在設(shè)置布局時(shí)坠宴,首先按 cell
的寬和最小左右間距橫著排版下去一行最多可以排多少個(gè),如果還有剩余空間的話绷旗,系統(tǒng)會(huì)將剩下的寬度平均分配該行的每一個(gè)左右間距喜鼓,所以實(shí)際效果的左右間距會(huì)比設(shè)置的左右間距大的多忧设。而行間距也是類似,在高度不一樣的一行 cell
中颠通,只有最高的 cell
是最小行間距,其余高度小的 cell
還會(huì)分配到與該行最高 cell
的高度差值“補(bǔ)貼”膀懈,具體情況后面有演示顿锰。
在 8 plus
模擬器上,cell
的最小左右間距設(shè)置為 5
启搂,
cell
設(shè)置大小為 120 x 120
時(shí)硼控,
layout.itemSize = CGSizeMake(120, 120);
效果如下:
cell
設(shè)置大小為 100 x 100
時(shí),
layout.itemSize = CGSizeMake(100, 100);
效果如下:
cell
設(shè)置大小為 80 x 80
時(shí)胳赌,
layout.itemSize = CGSizeMake(80, 80);
效果如下:
從模擬器的展示結(jié)果來(lái)看牢撼,排版的剩余寬度越大,左右間距也隨之分配到更多的空間疑苫。比較過(guò)程中熏版,我們也可以看到區(qū)域內(nèi)間距 sectionInset
始終都是不變的,但是左右間距會(huì)因排版剩余寬度過(guò)多而變大捍掺,導(dǎo)致與不變的區(qū)域內(nèi)間距差距就更大了撼短。
有時(shí)候?yàn)榱嗣烙^,我們需要將內(nèi)間距和左右間距設(shè)置成一樣的大小挺勿。這時(shí)曲横,我們需要像系統(tǒng)一樣先預(yù)算出在 cell
大小固定和左右間距取最小值的情況下,在一行中可以排版多少個(gè) cell
不瓶。只不過(guò)禾嫉,在分配剩余的寬度空間時(shí),我們不僅將其分配給左右間距蚊丐,還有區(qū)域內(nèi)間距熙参。
以cell
大小 80x80
為例:
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(80, 80);
// 以最小間距為10計(jì)算間距
// 每行可放多少 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);
執(zhí)行結(jié)果:
以上講的 UICollectionViewFlowLayout
使用是基于 cell
的大小是統(tǒng)一的情況。當(dāng)有不同大小的 cell
放在同一個(gè) UICollectionView
里麦备,還要符合我們的心意排版尊惰,這個(gè)時(shí)候就需要自定義 UICollectionViewFlowLayout
類了,而專門適用于這種參差不齊 cell
的布局有個(gè)專業(yè)名詞--“瀑布流”泥兰。
以下來(lái)自百度百科關(guān)于瀑布流的介紹:
瀑布流弄屡,又稱瀑布流式布局。是比較流行的一種網(wǎng)站頁(yè)面布局鞋诗,視覺(jué)表現(xiàn)為參差不齊的多欄布局膀捷,隨著頁(yè)面滾動(dòng)條向下滾動(dòng),這種布局還會(huì)不斷加載數(shù)據(jù)塊并附加至當(dāng)前尾部削彬。最早采用此布局的網(wǎng)站是Pinterest全庸,逐漸在國(guó)內(nèi)流行開(kāi)來(lái)秀仲。國(guó)內(nèi)大多數(shù)清新站基本為這類風(fēng)格。
二壶笼、UICollectionViewFlowLayout 實(shí)現(xiàn)瀑布流
有句諺語(yǔ)是這樣講的:
巧婦難為無(wú)米之炊
要想實(shí)現(xiàn)瀑布流布局神僵,就需要先實(shí)現(xiàn)大小參差不齊的 cell
,而要實(shí)現(xiàn)大小參差不齊的 cell
覆劈,就需要用到 UICollectionViewDelegate
的擴(kuò)展協(xié)議 UICollectionViewDelegateFlowLayout
保礼,這個(gè)協(xié)議是在 UICollectionViewDelegate
的基礎(chǔ)上增加了 UICollectionViewFlowLayout
一些可以根據(jù) NSIndexPath
和 section
來(lái)定制獨(dú)立的布局屬性,該協(xié)議提供多個(gè)可選方法责语。
@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
而我們要實(shí)現(xiàn)大小參差不齊的 cell
炮障,就需要用到這個(gè)方法:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
我們使用隨機(jī)數(shù) arc4random()
將 cell
的高度設(shè)置在40
到80
之間:
#pragma mark - UICollectionViewDelegateFlowLayout
// cell 大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
// 隨機(jī)高度
return CGSizeMake(80, 40 + arc4random() % 40);
}
執(zhí)行結(jié)果:
從執(zhí)行結(jié)果圖中,我們可以看出系統(tǒng)在布局參差不齊的 cell
時(shí) 坤候,將會(huì)以每行高度最高的 cell
為基準(zhǔn)胁赢,其余矮的 cell
的中心和這個(gè)高 cell
的中心保持在同一條水平線上。因此我們實(shí)現(xiàn)瀑布流白筹,就需要先打破這個(gè)規(guī)則(每個(gè) cell
依賴于它所在行的最高 cell
來(lái)布局它的位置)智末,讓每一個(gè) cell
往已經(jīng)排好且最短的列排版。
而要讓每一個(gè) cell
像人一樣能思考地進(jìn)行排版徒河,我們需要重載 UICollectionViewFlowLayout
對(duì)所有 cell
屬性的布局方法:
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
因?yàn)?UICollectionView
的滾動(dòng)內(nèi)容大小是由 cell
的大小和數(shù)量決定的吹害,系統(tǒng)又會(huì)以每一行最高 cell
的高度定為該行的高,然后逐個(gè)排版下去虚青,導(dǎo)致重載布局方法之后它呀,滾動(dòng)到底部會(huì)出現(xiàn)一大堆空白。
雖然 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
方法可以為我們定制不同 cell
的大小棒厘,但是 UICollectionView
滾動(dòng)內(nèi)容大小是根據(jù)系統(tǒng)的排版標(biāo)準(zhǔn)去算的纵穿,會(huì)導(dǎo)致滾動(dòng)空間的溢出,所以這個(gè)方法在這種有上下縮進(jìn)的情況下不適用奢人。
因此為了準(zhǔn)確地計(jì)算出 UICollectionView
合理的滾動(dòng)內(nèi)容大小谓媒,首先,我們需要在每一次更新布局前就引入各個(gè) cell
的大小到自定義的 UICollectionViewFlowLayout
中何乎,然后在準(zhǔn)備方法 - (void)prepareLayout;
中句惯,規(guī)劃具體布局,接著支救,計(jì)算出瀑布流布局所需要滾動(dòng)內(nèi)容的高抢野,以及在這么大的滾動(dòng)內(nèi)容大小下,每一個(gè) cell
的 itemsize
要設(shè)置多大的平均值各墨。
具體代碼實(shí)現(xiàn)指孤。
自定義瀑布流頭文件 MyFlowLayout.h
:
#import <UIKit/UIKit.h>
/**
自定義瀑布流布局
*/
@interface MyFlowLayout : UICollectionViewFlowLayout
/**
瀑布流布局方法
@param itemWidth item 的寬度
@param itemHeightArray item 的高度數(shù)組
*/
- (void)flowLayoutWithItemWidth:(CGFloat)itemWidth itemHeightArray:(NSArray<NSNumber *> *)itemHeightArray;
@end
實(shí)現(xiàn)文件 MyFlowLayout.m
:
#import "MyFlowLayout.h"
@interface MyFlowLayout ()
/**
item 的高度數(shù)組
*/
@property (nonatomic, copy) NSArray<NSNumber *> *arrItemHeight;
/**
cell 布局屬性集
*/
@property (nonatomic, strong) NSArray<UICollectionViewLayoutAttributes *> *arrAttributes;
@end
@implementation MyFlowLayout
/**
瀑布流布局方法
@param itemWidth item 的寬度
@param itemHeightArray item 的高度數(shù)組
*/
- (void)flowLayoutWithItemWidth:(CGFloat)itemWidth itemHeightArray:(NSArray<NSNumber *> *)itemHeightArray {
self.itemSize = CGSizeMake(itemWidth, 0);
self.arrItemHeight = itemHeightArray;
[self.collectionView reloadData];
}
- (void)prepareLayout {
[super prepareLayout];
// item 數(shù)量為零不做處理
if ([self.arrItemHeight count] == 0) {
return;
}
// 計(jì)算一行可以放多少個(gè)項(xiàng)
NSInteger nItemInRow = (self.collectionViewContentSize.width - self.sectionInset.left - self.sectionInset.right + self.minimumInteritemSpacing) / (self.itemSize.width + self.minimumInteritemSpacing);
// 對(duì)列的長(zhǎng)度進(jìn)行累計(jì)
NSMutableArray *arrmColumnLength = [NSMutableArray arrayWithCapacity:100];
for (NSInteger i = 0; i < nItemInRow; i++) {
[arrmColumnLength addObject:@0];
}
NSMutableArray *arrmTemp = [NSMutableArray arrayWithCapacity:100];
// 遍歷設(shè)置每一個(gè)item的布局
for (NSInteger i = 0; i < [self.arrItemHeight count]; i++) {
// 設(shè)置每個(gè)item的位置等相關(guān)屬性
NSIndexPath *index = [NSIndexPath indexPathForItem:i inSection:0];
// 創(chuàng)建每一個(gè)布局屬性類,通過(guò)indexPath來(lái)創(chuàng)建
UICollectionViewLayoutAttributes *attris = [self layoutAttributesForItemAtIndexPath:index];
CGRect recFrame = attris.frame;
// 有數(shù)組得到的高度
recFrame.size.height = [self.arrItemHeight[i] doubleValue];
// 最短列序號(hào)
NSInteger nNumShort = 0;
// 最短的長(zhǎng)度
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;
// 更新列的累計(jì)長(zhǎng)度
arrmColumnLength[nNumShort] = [NSNumber numberWithDouble:CGRectGetMaxY(recFrame)];
// 更新布局
attris.frame = recFrame;
[arrmTemp addObject:attris];
}
self.arrAttributes = arrmTemp;
// 因?yàn)槭褂昧似俨剂鞑季质沟脻L動(dòng)范圍是根據(jù) item 的大小和個(gè)數(shù)決定的,所以以最長(zhǎng)的列為基準(zhǔn)恃轩,將高度平均到每一個(gè) cell 中
// 最長(zhǎng)列序號(hào)
NSInteger nNumLong = 0;
// 最長(zhǎng)的長(zhǎng)度
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 布局?jǐn)?shù)組
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
return self.arrAttributes;
}
@end
在視圖控制器上的調(diào)用:
MyFlowLayout *layout = [[MyFlowLayout alloc] init];
// 創(chuàng)建隨機(jī)高度的數(shù)組
NSMutableArray *arrmHeight = [NSMutableArray arrayWithCapacity:100];
for (NSInteger i = 0; i < 50; i++) {
// 40~80 的隨機(jī)高度
[arrmHeight addObject:[NSNumber numberWithDouble:40 + arc4random() % 40]];
}
[layout flowLayoutWithItemWidth:80 itemHeightArray:arrmHeight];
// 以最小間距為10計(jì)算間距
// 每行可放多少 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];
// 注冊(cè) cell
[_collection registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:cellId];
執(zhí)行結(jié)果:
除了這種高度參差不齊的 cell
,我們?cè)陂_(kāi)發(fā)過(guò)程中叉跛,還會(huì)遇到寬度參差不齊的布局松忍。
三、UICollectionViewFlowLayout 的靠左布局
因?yàn)樵趯挾葏⒉畈积R的布局中筷厘,不會(huì)影響滾動(dòng)內(nèi)容的高度鸣峭,所以我們可以通過(guò) - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
來(lái)實(shí)現(xiàn) itemSize
的不規(guī)則:
// cell 大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
// 隨機(jī)寬度
return CGSizeMake(40 + arc4random() % 4 * 20, 50);
}
UICollectionViewFlowLayout
的屬性設(shè)置:
//創(chuàng)建流水布局對(duì)象
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
UICollectionViewDelegate
和 UICollectionViewDataSource
的相關(guān)實(shí)現(xiàn):
- (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;
}
執(zhí)行結(jié)果:
從結(jié)果中可以看出,因?yàn)閷挾炔灰粯訉?dǎo)致每一行排版剩余的空間不一樣敞掘,導(dǎo)致左右間距也就相差較大了。
為了排版美觀楣铁,我們有時(shí)候需要靠左布局玖雁。之前因?yàn)轫?xiàng)目有過(guò)這樣的產(chǎn)品需求,在網(wǎng)絡(luò)上找到了一個(gè)第三方 UICollectionViewFlowLayout
自定義類--UICollectionViewLeftAlignedLayout
盖腕。
UICollectionViewLeftAlignedLayout 下載地址
在視圖控制器上的使用:
UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
執(zhí)行結(jié)果:
因?yàn)檫@次寫(xiě)得比較久赫冬,思路有些亂,希望有問(wèn)題幫忙指出來(lái)溃列,謝謝劲厌!