UICollectionViewLayout基礎(chǔ)知識(shí)
Custom Layout
官方描述
An abstract base class for generating layout information for a collection view
The job of a layout object is to determine the placement of cells, supplementary views, and decoration views inside the collection view’s bounds and to report that information to the collection view when asked
UICollectionViewLayout的功能為向UICollectionView提供布局信息,不僅包括cell的布局信息,也包括追加視圖和裝飾視圖的布局信息。實(shí)現(xiàn)一個(gè)自定義Custom Layout的常規(guī)做法是繼承UICollectionViewLayout類
重載的方法
-
prepareLayout:準(zhǔn)備布局屬性 -
layoutAttributesForElementsInRect:返回rect中的所有的元素的布局屬性UICollectionViewLayoutAttributes可以是cell,追加視圖或裝飾視圖的信息,通過不同的UICollectionViewLayoutAttributes初始化方法可以得到不同類型的UICollectionViewLayoutAttributes
layoutAttributesForCellWithIndexPath:layoutAttributesForSupplementaryViewOfKind:withIndexPath:layoutAttributesForDecorationViewOfKind:withIndexPath:
-
collectionViewContentSize返回contentSize
執(zhí)行順序
-
-(void)prepareLayout將被調(diào)用,默認(rèn)下該方法什么沒做,但是在自己的子類實(shí)現(xiàn)中,一般在該方法中設(shè)定一些必要的layout的結(jié)構(gòu)和初始需要的參數(shù)等 -
-(CGSize) collectionViewContentSize將被調(diào)用,以確定collection應(yīng)該占據(jù)的尺寸。注意這里的尺寸不是指可視部分的尺寸,而應(yīng)該是所有內(nèi)容所占的尺寸。collectionView的本質(zhì)是一個(gè)scrollView,因此需要這個(gè)尺寸來配置滾動(dòng)行為 -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
引入AutoLayout自動(dòng)計(jì)算的瀑布流
關(guān)于瀑布流
網(wǎng)上前輩們已經(jīng)寫爛了,這里只簡述:
-
-(void)prepareLayout中:就是通過一個(gè)記錄列高度的數(shù)組(或字典),在創(chuàng)建LayoutAttributes的frame時(shí)確定當(dāng)前最短列,根據(jù)外部傳入的相關(guān)的spacing及collectionView的inset屬性,確定寬度、frame等信息,存入Attributes的數(shù)組。 -
-(CGSize) collectionViewContentSize中:通過列高度數(shù)組很容易確定當(dāng)前范圍,contentSize不等于collectionview的bounds.size,計(jì)算時(shí)留意一下 -
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect:返回第一步中計(jì)算獲得的Attributes數(shù)組即可
以上可以幫助我們實(shí)現(xiàn)一個(gè)瀑布流的效果,但是離實(shí)際應(yīng)用還有一段差距。
分析:
實(shí)際應(yīng)用中,我們的網(wǎng)絡(luò)請(qǐng)求是會(huì)有一個(gè)pageSize的,而且列表的賦值通常是直接進(jìn)行數(shù)據(jù)源的賦值然后reloadData。所以數(shù)據(jù)源個(gè)數(shù)等于pageSize時(shí),我們認(rèn)為是刷新,大于時(shí),則為分頁加載。
根據(jù)這套邏輯,這里將pageSize及dataSource作為屬性引入到Custom Layout中,同時(shí)維護(hù)一個(gè)記錄計(jì)算結(jié)果的數(shù)組itemSizeArray,提高計(jì)算效率,具體代碼如下:
- (void)calculateAttributesWithItemWidth:(CGFloat)itemWidth{
BOOL isRefresh = self.datas.count <= self.pageSize;
if (isRefresh) {
[self refreshLayoutCache];
}
NSInteger cacheCount = self.itemSizeArray.count;
for (NSInteger i = cacheCount; i < self.datas.count; i ++) {
CGSize itemSize = [self calculateItemSizeWithIndex:i];
UICollectionViewLayoutAttributes *layoutAttributes = [self createLayoutAttributesWithItemSize:itemSize index:i];
[self.itemSizeArray addObject:[NSValue valueWithCGSize:itemSize]];
[self.layoutAttributesArray addObject:layoutAttributes];
}
}
- (UICollectionViewLayoutAttributes *)createLayoutAttributesWithItemSize:(CGSize)itemSize index:(NSInteger)index{
UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
struct SPColumnInfo shortestInfo = [self shortestColumn:self.columnHeightArray];
// x
CGFloat itemX = (self.itemWidth + self.interitemSpacing) * shortestInfo.columnNumber;
// y
CGFloat itemY = self.columnHeightArray[shortestInfo.columnNumber].floatValue + self.lineSpacing;
// size
layoutAttributes.frame = (CGRect){CGPointMake(itemX, itemY),itemSize};
self.columnHeightArray[shortestInfo.columnNumber] = @(CGRectGetMaxY(layoutAttributes.frame));
return layoutAttributes;
}
- (void)refreshLayoutCache{
[self.layoutAttributesArray removeAllObjects];
[self.columnHeightArray removeAllObjects];
[self.itemSizeArray removeAllObjects];
for (NSInteger index = 0; index < self.columnNumber; index ++) {
[self.columnHeightArray addObject:@(self.viewInset.top)];
}
}
代碼里可以看到,itemSizeArray的屬性,用于記錄自動(dòng)計(jì)算的itemSize,通過這個(gè)屬性可以幫助我們減少不必要的重復(fù)計(jì)算
關(guān)于自動(dòng)計(jì)算

注意:
- Self-size要求我們的約束自上而下設(shè)置,確保能夠通過Constraint計(jì)算獲得準(zhǔn)確的高度。具體不再贅述
- 本Demo僅適用圖片比例確定的瀑布流,如果需求是圖片size自適應(yīng),需要服務(wù)器返回能夠計(jì)算的必要參數(shù)
自動(dòng)計(jì)算的思路,類似UITableView-FDTemplateLayoutCell,通過xibName或className初始化一個(gè)template cell,注入數(shù)據(jù)并添加橫向約束后,利用systemLayoutSizeFittingSize方法獲取系統(tǒng)計(jì)算的高度后,移除添加的橫向約束(其中有個(gè)iOS10.2后的約束計(jì)算變化,需要我們手動(dòng)對(duì)cell.contentView添加四周的約束,AutoLayout才能準(zhǔn)確計(jì)算高度。請(qǐng)注意代碼中對(duì)系統(tǒng)判斷的一步)
這里我們?yōu)?code>UICollectionViewCell添加了一個(gè)Category,用于統(tǒng)一數(shù)據(jù)的傳入方式
#import <UIKit/UIKit.h>
@interface UICollectionViewCell (FeedData)
@property (nonatomic, strong) id feedData;
@property (nonatomic, strong) id subfeedData;
@end
// --------------------------------------
#import "UICollectionViewCell+FeedData.h"
#import <objc/runtime.h>
static NSString *AssociateKeyFeedData = @"AssociateKeyFeedData";
static NSString *AssociateKeySubFeedData = @"AssociateKeySubFeedData";
@implementation UICollectionViewCell (FeedData)
@dynamic feedData;
@dynamic subfeedData;
- (void)setFeedData:(id)feedData{
objc_setAssociatedObject(self, &AssociateKeyFeedData, feedData, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)feedData{
return objc_getAssociatedObject(self, &AssociateKeyFeedData);
}
- (void)setSubfeedData:(id)subfeedData{
objc_setAssociatedObject(self, &AssociateKeySubFeedData, subfeedData, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)subfeedData{
return objc_getAssociatedObject(self, &AssociateKeySubFeedData);
}
@end
關(guān)鍵代碼如下:
- itemSize
- (CGSize)calculateItemSizeWithIndex:(NSInteger)index{
NSAssert(index < self.datas.count, @"index is incorrect");
UICollectionViewCell *tempCell = [self templateCellWithReuseIdentifier:self.reuseIdentifier withIndex:index];
tempCell.feedData = self.datas[index];
CGFloat cellHeight = [self systemCalculateHeightForTemplateCell:tempCell];
return CGSizeMake(self.itemWidth, cellHeight);
}
- 獲取一個(gè)計(jì)算使用的Template Cell,保存避免重復(fù)提取
- (UICollectionViewCell *)templateCellwithIndex:(NSInteger)index{
if (!self.templateCell) {
if (self.className) {
Class cellClass = NSClassFromString(self.className);
UICollectionViewCell *templateCell = [[cellClass alloc] init];
self.templateCell = templateCell;
}else if (self.xibName){
UICollectionViewCell *templateCell = [[NSBundle mainBundle] loadNibNamed:self.xibName owner:nil options:nil].lastObject;
self.templateCell = templateCell;
}
}
return self.templateCell;
}
- AutoLayout Self-sizing
- (CGFloat)systemCalculateHeightForTemplateCell:(UICollectionViewCell *)cell{
CGFloat calculateHeight = 0;
NSLayoutConstraint *widthForceConstant = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:self.itemWidth];
static BOOL isSystemVersionEqualOrGreaterThen10_2 = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
isSystemVersionEqualOrGreaterThen10_2 = [UIDevice.currentDevice.systemVersion compare:@"10.2" options:NSNumericSearch] != NSOrderedAscending;
});
NSArray<NSLayoutConstraint *> *edgeConstraints;
if (isSystemVersionEqualOrGreaterThen10_2) {
// To avoid conflicts, make width constraint softer than required (1000)
widthForceConstant.priority = UILayoutPriorityRequired - 1;
// Build edge constraints
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0];
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeRight multiplier:1.0 constant:0];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0];
edgeConstraints = @[leftConstraint, rightConstraint, topConstraint, bottomConstraint];
[cell addConstraints:edgeConstraints];
}
// system calculate
[cell.contentView addConstraint:widthForceConstant];
calculateHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
// clear constraint
[cell.contentView removeConstraint:widthForceConstant];
if (isSystemVersionEqualOrGreaterThen10_2) {
[cell removeConstraints:edgeConstraints];
}
return calculateHeight;
}
如何使用
- 初始化時(shí)對(duì)所有必要屬性進(jìn)行賦值
SPWaterFlowLayout *flowlayout = [[SPWaterFlowLayout alloc] init];
flowlayout.columnNumber = 2;
flowlayout.interitemSpacing = 10;
flowlayout.lineSpacing = 10;
flowlayout.pageSize = 54;
flowlayout.xibName = @"TestView";
UICollectionView *test = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:flowlayout];
test.contentInset = UIEdgeInsetsMake(10, 10, 5, 10);
[self.view addSubview:test];
test.delegate = self;
test.dataSource = self;
[test registerNib:[UINib nibWithNibName:@"TestView" bundle:nil] forCellWithReuseIdentifier:@"Cell"];
test.backgroundColor = [UIColor whiteColor];
- Refresh及LoadMore中更新
dataSource
Refresh
test.refreshDataCallBack = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.pageTag = 0;
NSArray *datas = [SPProductModel productWithIndex:0];
flowlayout.datas = datas;
wtest.sp_datas = [datas mutableCopy];
[wtest doneLoadDatas];
[wtest reloadData];
});
};
LoadMore
test.loadMoreDataCallBack = ^{
self.pageTag ++;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSArray *datas = [SPProductModel productWithIndex:self.pageTag];
NSArray *total = [flowlayout.datas arrayByAddingObjectsFromArray:datas];
flowlayout.datas = total;
wtest.sp_datas = [total mutableCopy];
[wtest doneLoadDatas];
[wtest reloadData];
});
};
效果
題外話:iPhone X讓我們除了64,又記住了88和812,自己寫Refresh的朋友,記得看一下contentInset 在iOS11中,如果不關(guān)autoAdjust情況下 有什么變化

Demo地址
GitHub:SPWaterFlowLayout
筆者博客地址:Tr2e's Blog
