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)寫爛了续崖,這里只簡(jiǎn)述:
-
-(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