前言
最近项目中使用了很多UICollectionView,发现对其了解真是太少了,UICollectionViewLayout的自定义布局真的可以实现很多效果,趁这次机会好好记录一下知识点,以前虽然用过,但没有系统的整理总结过。这两天我为UICollectionView做一个整理。包括基本使用,自定义布局,动画这块我会的不多后面有机会在再记下来。
UICollectionView的构成部分
Cells
Supplementary Views 追加视图 (类似Header或者Footer)
Decoration Views 装饰视图 (用作背景展示)
而在表面下,由两个方面对UICollectionView进行支持。其中之一和tableView一样,即提供数据的UICollectionViewDataSource以及处理用户交互的UICollectionViewDelegate和布局方面的UICollectionViewDelegateFlowLayout。另一方面,对于cell的样式和组织方式,由于collectionView比tableView要复杂得多,因此没有按照类似于tableView的style的方式来定义,而是专门使用了一个类来对collectionView的布局和行为进行描述,这就是UICollectionViewLayout。
UICollectionView的创建
UICollectionView的创建方式和UITableview相似都是init方法,不同的是UICollectionView初始化时需要传入一个UICollectionViewLayout对象用于对其Item进行布局,这个UICollectionViewLayout对象可以使用系统的UICollectionViewFlowLayout,也可以自定义,如果想要实现一些炫酷的动画或者特定的样式(例如瀑布流)都需要去自定义这个对象。下面是创建UICollectionView的代码。
-(DWCollectionView*)dwCollectionView{
if (!_dwCollectionView) {
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(100, 30);
layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
_dwCollectionView = [[DWCollectionView alloc] initWithFrame:CGRectMake(0, 64, self.view.bounds.size.width, 400) collectionViewLayout:layout];
[_dwCollectionView registerClass:[DWCollectionViewCell class] forCellWithReuseIdentifier:@"UICollectionViewCellID"];
_dwCollectionView.delegate = self;
_dwCollectionView.dataSource = self;
_dwCollectionView.backgroundColor = [UIColor yellowColor];
}
return _dwCollectionView;
}
UICollectionView的常用属性和代理方法
UICollectionView有三个代理,除去和UITableview一样的delegate和dataSource,还有一个ios
10新增的prefetchDataSource,三个代理中前两个delegate用于处理用户交互,dataSource用于提供数据源。
dataSource代理方法
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath:
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
delegate常用代理方法
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
UICollectionViewDelegateFlowLayout
- (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;
属性方法
- (void)registerClass:(nullable Class)cellClass forCellWithReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(nullable UINib *)nib forCellWithReuseIdentifier:(NSString *)identifier;
///注册一个headerView或footerView
- (void)registerClass:(nullable Class)viewClass forSupplementaryViewOfKind:(NSString *)elementKind withReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(nullable UINib *)nib forSupplementaryViewOfKind:(NSString *)kind withReuseIdentifier:(NSString *)identifier;
///获取指定IndexPath的Cell和Supplementary Views
- (__kindof UICollectionViewCell *)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind withReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
其他还有很多属性和tableview相似就不多说了
UICollectionView的布局
UICollectionView的布局可以分为三种方式:
1.初始化时传入的UICollectionViewLayout对象,通过设置UICollectionViewLayout对象属性的值可以设置item的基本布局,包括大小,间距等。
2.也可以实现UICollectionViewLayoutDelegate协议对应的方法,返回布局需要的值。
3.自定义一个UICollectionViewLayout对象重写对应方法返回自定义的布局。
注意:同时设置1和2,2的优先级更高。
UICollectionViewFlowLayout常用属性
//item的最小行间距
@property (nonatomic) CGFloat minimumLineSpacing;
//item之间的最小间距,这个数据设置的是最小的间距,当间距小于这个值时,item就会换行显示,但是如果你设置的是10,实际间距是20也是不会换行的只有小于这个值时才会换行。
@property (nonatomic) CGFloat minimumInteritemSpacing;
///item的大小
@property (nonatomic) CGSize itemSize;
//此属性8.0以后有效,作用:类似一个占位符,当加载item时会先加载这个size,显示的时候 根据 子控件的autolayout 的约束算出自适应内容的 size;
/**
*1.设置estimatedItemSize的值,随便给也行
*2.对子控件进行约束
*3.如需进一步操作size可以在cell中重写preferredLayoutAttributesFittingAttributes方法
*/
@property (nonatomic) CGSize estimatedItemSize NS_AVAILABLE_IOS(8_0); // defaults to CGSizeZero - setting a non-zero size enables cells that self-size via -preferredLayoutAttributesFittingAttributes:
//设置滑动方向
@property (nonatomic) UICollectionViewScrollDirection scrollDirection; // default is UICollectionViewScrollDirectionVertical
//区头的size,设置宽度值无效
@property (nonatomic) CGSize headerReferenceSize;
//区尾的size,设置宽度值无效
@property (nonatomic) CGSize footerReferenceSize;
//section之间的上左下右的间距(假设有三个区竖向滑动,则top代表每个区第一行的item距离这个区上边的距离,boom代表这个区的最后一行item距这个区下边的距离)不是item之间的上下左右的间距。
@property (nonatomic) UIEdgeInsets sectionInset;
estimatedItemSize属性的简单使用
只需设置estimatedItemSize替代itemSize即可实现简单的布局,如果自适应的label的长度会超出collectionview的宽度那需要label设置最大的宽度,否则会出现布局错误。
@implementation EstimatedItemCollectionViewCell
-(instancetype)initWithFrame:(CGRect)frame{
if (self =[super initWithFrame:frame]) {
///设置contentView上下左右约束为0
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.top.bottom.equalTo(self).with.offset(0);
make.width.mas_equalTo([UIScreen mainScreen].bounds.size.width-20);
}];
UIImageView *topImageView = [[UIImageView alloc] init];
topImageView.backgroundColor = [UIColor blueColor];
[self.contentView addSubview:topImageView];
[topImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.top.right.bottom.equalTo(self.contentView).with.offset(0);
// make.right.equalTo(self.contentView.mas_right).with.offset(-250);
// make.height.mas_equalTo(200);
}];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.backgroundColor = [UIColor orangeColor];
label.textAlignment = NSTextAlignmentCenter;
label.numberOfLines = 0;
// [label sizeToFit];
[self.contentView addSubview:label];
self.label = label;
//2.设置好subviews的约束
[label mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.equalTo(self.contentView).with.offset(0);
// make.top.equalTo(topImageView.mas_bottom).with.offset(0);
make.width.mas_lessThanOrEqualTo([UIScreen mainScreen].bounds.size.width-20);
// make.width.mas_greaterThanOrEqualTo(@(100));
}];
}
return self;
}
///3.如果不需要更多关于UICollectionViewLayoutAttributes的操作,只是向下面这样重新赋值给size建议不要重写
//preferredLayoutAttributesFittingAttributes: 方法默认调整Size属性来适应 self-sizing Cell,所以重写的时候需要先调用父类方法,再在返回的 UICollectionViewLayoutAttributes 对象上做你想要做的修改。
- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
///此处调用父类所等到的attributes 和你最后得到的layoutAttributes的size完全一样,如果只是修改size完全没必要重写,如果layout没有设置layout.estimatedItemSize,则attributes和layoutAttributes的size一样
UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
NSLog(@"%f====%f",layoutAttributes.size.width,layoutAttributes.size.height);
NSLog(@"%f********%f",attributes.size.width,attributes.size.height);
///父类已经调用
CGSize size = [self.contentView systemLayoutSizeFittingSize:layoutAttributes.size];
CGRect newFrame = layoutAttributes.frame;
newFrame.size.width = size.width;
layoutAttributes.frame = newFrame;
NSLog(@"%f------%f",size.width,size.height);
return attributes;
}
自定义UICollectionViewFlowLayout
先看一张图,下面这张图是使用estimatedItemSize来布局的一个collcetionview,现在我不想让第一行两个item之间的空隙太大,这时候使用系统的layout向缩小是不可行的,因为就算我们设置minimumInteritemSpacing为10,上面已经说了这个属性只是item之间的最小间隔,item之间的实际间隔是可以大于10的,这时候如果用系统的layout就不是很好实现,如果我们通过自定义layout则是很好实现的。
这张图就可以实现我们上面的需求了,这就是通过自定义layout来实现的布局
自定义layout
自定义一个layout实际上很简单,你只需要创建一个继承与UICollectionViewLayout的子类即可,然后重写一部分方法,在这些方法里去实现你想要的布局方式。
常用的方法
每次layout更新期间collectionview都会首先调用这个方法,为将要开始的更新做准备,可以在此准备要使用的,你想要的layout的布局数组。
///每次更新layout布局都会首先调用此方法
-(void)prepareLayout{
NSLog(@"---------1");
///和init相似,必须call super的prepareLayout以保证初始化正确
[super prepareLayout];
///1.首先被调用
[self.attributesArray removeAllObjects];
NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
for (int i =0; i<itemCount; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attributesArray addObject:attributes];
if (i==self.widthArray.count-1) {
[self loadOldAttributes:attributes.frame];
}
}
}
collectionViewContentSize方法返回collectionview的ContentSize,每次更新会调用两次,第一次是开始更新时调用,第二次是layoutAttributesForElementsInRect方法返回所有item的约束后调用。
///返回collectionView的内容的尺寸
-(CGSize)collectionViewContentSize{
///2.其次被调用(layoutAttributesForElementsInRect 调用后会在此调用此方法)
NSLog(@"---%f------2",self.maxY);
return CGSizeMake(self.collectionView.bounds.size.width, self.maxY);
}
返回rect中的所有的元素的布局属性,返回的是包含UICollectionViewLayoutAttributes的NSArray,UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的,初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
///3.被调用
NSLog(@"---------3");
return self.attributesArray;
}
返回对应于indexPath的位置的cell的布局属性,返回指定indexPath的item的布局信息。子类必须重载该方法,该方法只能为cell提供布局信息,不能为补充视图和装饰视图提供。
///返回对应于indexPath的位置的cell的布局属性,返回指定indexPath的item的布局信息。子类必须重载该方法,该方法只能为cell提供布局信息,不能为补充视图和装饰视图提供。
-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath{
UICollectionViewLayoutAttributes *attributs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
NSNumber *currentWidthNumber = self.widthArray[indexPath.row];
CGFloat width = currentWidthNumber.floatValue;
///没有换行所以超出部分不显示(不写下面的代码也不会报错,不知道为啥)
if (width>[UIScreen mainScreen].bounds.size.width-(self.left+self.right)) {
width = [UIScreen mainScreen].bounds.size.width - (self.left+self.right);
}
CGFloat height = 30;
CGRect currentFrame = CGRectZero;
if (1) {
if (self.attributesArray.count!=0) {
///1.取出上一个item的attributes
UICollectionViewLayoutAttributes *lastAttributs = [self.attributesArray lastObject];
CGRect lastFrame = lastAttributs.frame;
///判断当前item和上一个item是否在同一个row
if (CGRectGetMaxX(lastAttributs.frame)+self.right==self.collectionView.bounds.size.width) {
///不在同一row
currentFrame.origin.x = self.left;
currentFrame.origin.y = CGRectGetMaxY(lastFrame) +self.top;
currentFrame.size.width = width;
currentFrame.size.height = height;
attributs.frame = currentFrame;
}else{
///上一个item的最大x值+当前item的宽度和左边距
CGFloat totleWidth = CGRectGetMaxX(lastFrame)+(self.between+width+self.right);
///判断上一个item所在row的剩余宽度是否还够显示当前item
if (totleWidth>=self.collectionView.bounds.size.width) {
///不足以显示当前item的宽度
///将和上一个item在同一个row的item的放在同一个数组
NSMutableArray *sameYArray = [NSMutableArray array];
for (UICollectionViewLayoutAttributes *subAttributs in self.attributesArray) {
if (subAttributs.frame.origin.y==lastFrame.origin.y) {
[sameYArray addObject:subAttributs];
}
}
///判断出上一row还剩下多少宽度
CGFloat sameYWidth = 0.0;
for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
sameYWidth += sameYAttributs.size.width;
}
sameYWidth = sameYWidth + (self.left+self.right+(sameYArray.count-1)*self.between);
///上一个row所剩下的宽度
CGFloat sameYBetween = (self.collectionView.bounds.size.width-sameYWidth)/sameYArray.count;
for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
CGFloat sameAttributeWidth = sameYAttributs.size.width;
CGFloat sameAttributeHeight = sameYAttributs.size.height;
CGRect sameYAttributsFrame = sameYAttributs.frame;
///更新sameYAttributs宽度使之均衡显示
sameAttributeWidth += sameYBetween;
sameYAttributs.size = CGSizeMake(sameAttributeWidth, sameAttributeHeight);
NSInteger index = [sameYArray indexOfObject:sameYAttributs];
sameYAttributsFrame.origin.x += (sameYBetween*index);
sameYAttributsFrame.size.width = sameAttributeWidth;
sameYAttributs.frame = sameYAttributsFrame;
}
currentFrame.origin.x = self.left;
currentFrame.origin.y = CGRectGetMaxY(lastFrame)+self.top;
currentFrame.size.width = width;
currentFrame.size.height = height;
attributs.frame = currentFrame;
}else{
currentFrame.origin.x = CGRectGetMaxX(lastFrame)+self.between;
currentFrame.origin.y = lastFrame.origin.y;
currentFrame.size.width = width;
currentFrame.size.height = height;
attributs.frame = currentFrame;
}
}
}else{
currentFrame.origin.x = self.left;
currentFrame.origin.y = self.top;
currentFrame.size.width = width;
currentFrame.size.height = height;
attributs.frame = currentFrame;
}
}
// attributs.size = CGSizeMake(width, 30);
self.maxY = CGRectGetMaxY(attributs.frame)+10;
NSLog(@"%f===%f===%f===%f",attributs.frame.origin.x,attributs.frame.origin.y,attributs.frame.size.width,attributs.frame.size.height);
return attributs;
}
///返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
-(UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
return [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
}
///返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
-(UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)decorationViewKind atIndexPath:(NSIndexPath*)indexPath{
return [super layoutAttributesForDecorationViewOfKind:decorationViewKind atIndexPath:indexPath];
}
///当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
return [super shouldInvalidateLayoutForBoundsChange:newBounds];
}
注意点: 另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIView的setNeedsLayout方法十分类似。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect来生成更新后的布局。
全部代码
#import "DWCollectionViewLayout.h"
#define DWScreenH = [UIScreen mainScreen].bounds.size.height
#define DWScreenW = [UIScreen mainScreen].bounds.size.width
@interface DWCollectionViewLayout ()
@property (nonatomic,strong) NSMutableArray *attributesArray;
@property (nonatomic,assign) CGFloat maxY;
@property (nonatomic,assign) CGFloat left;
@property (nonatomic,assign) CGFloat right;
@property (nonatomic,assign) CGFloat top;
@property (nonatomic,assign) CGFloat between;
@end
@implementation DWCollectionViewLayout
-(instancetype)initWithArray:(NSMutableArray*)widthArray edgeInsets:(UIEdgeInsets)insets{
if (self = [super init]) {
self.widthArray = widthArray;
NSLog(@"==***==%p",self.widthArray);
self.left = insets.left;
self.right = insets.right;
self.top = insets.top;
self.between = insets.bottom;
}
return self;
}
/**
*另外需要了解的是,在初始化一个UICollectionViewLayout实例后,会有一系列准备方法被自动调用,以保证layout实例的正确。
*首先,将被调用,默认下该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。
*/
-(void)prepareLayout{
NSLog(@"---------1");
///和init相似,必须call super的prepareLayout以保证初始化正确
[super prepareLayout];
///1.首先被调用
[self.attributesArray removeAllObjects];
NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
for (int i =0; i<itemCount; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attributesArray addObject:attributes];
if (i==self.widthArray.count-1) {
[self loadOldAttributes:attributes.frame];
}
}
}
///返回collectionView的内容的尺寸
-(CGSize)collectionViewContentSize{
///2.其次被调用(layoutAttributesForElementsInRect 调用后会在此调用此方法)
NSLog(@"---%f------2",self.maxY);
return CGSizeMake(self.collectionView.bounds.size.width, self.maxY);
}
///返回rect中的所有的元素的布局属性,返回的是包含UICollectionViewLayoutAttributes的NSArray
///UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的
///初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。
///rect 为collectionview 的rect,(高度超出当前屏幕的高度后,rect的height会翻倍)
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
///3.被调用
NSLog(@"---------3");
NSArray *array = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes *attributes in array) {
if (attributes.representedElementCategory == UICollectionElementCategoryCell) {
}
}
return self.attributesArray;
}
///返回对应于indexPath的位置的cell的布局属性,返回指定indexPath的item的布局信息。子类必须重载该方法,该方法只能为cell提供布局信息,不能为补充视图和装饰视图提供。
-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath{
UICollectionViewLayoutAttributes *attributs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
NSNumber *currentWidthNumber = self.widthArray[indexPath.row];
CGFloat width = currentWidthNumber.floatValue;
///没有换行所以超出部分不显示(不写下面的代码也不会报错,不知道为啥)
if (width>[UIScreen mainScreen].bounds.size.width-(self.left+self.right)) {
width = [UIScreen mainScreen].bounds.size.width - (self.left+self.right);
}
CGFloat height = 30;
CGRect currentFrame = CGRectZero;
if (1) {
if (self.attributesArray.count!=0) {
///1.取出上一个item的attributes
UICollectionViewLayoutAttributes *lastAttributs = [self.attributesArray lastObject];
CGRect lastFrame = lastAttributs.frame;
///判断当前item和上一个item是否在同一个row
if (CGRectGetMaxX(lastAttributs.frame)+self.right==self.collectionView.bounds.size.width) {
///不在同一row
currentFrame.origin.x = self.left;
currentFrame.origin.y = CGRectGetMaxY(lastFrame) +self.top;
currentFrame.size.width = width;
currentFrame.size.height = height;
attributs.frame = currentFrame;
}else{
///上一个item的最大x值+当前item的宽度和左边距
CGFloat totleWidth = CGRectGetMaxX(lastFrame)+(self.between+width+self.right);
///判断上一个item所在row的剩余宽度是否还够显示当前item
if (totleWidth>=self.collectionView.bounds.size.width) {
///不足以显示当前item的宽度
///将和上一个item在同一个row的item的放在同一个数组
NSMutableArray *sameYArray = [NSMutableArray array];
for (UICollectionViewLayoutAttributes *subAttributs in self.attributesArray) {
if (subAttributs.frame.origin.y==lastFrame.origin.y) {
[sameYArray addObject:subAttributs];
}
}
///判断出上一row还剩下多少宽度
CGFloat sameYWidth = 0.0;
for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
sameYWidth += sameYAttributs.size.width;
}
sameYWidth = sameYWidth + (self.left+self.right+(sameYArray.count-1)*self.between);
///上一个row所剩下的宽度
CGFloat sameYBetween = (self.collectionView.bounds.size.width-sameYWidth)/sameYArray.count;
for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
CGFloat sameAttributeWidth = sameYAttributs.size.width;
CGFloat sameAttributeHeight = sameYAttributs.size.height;
CGRect sameYAttributsFrame = sameYAttributs.frame;
///更新sameYAttributs宽度使之均衡显示
sameAttributeWidth += sameYBetween;
sameYAttributs.size = CGSizeMake(sameAttributeWidth, sameAttributeHeight);
NSInteger index = [sameYArray indexOfObject:sameYAttributs];
sameYAttributsFrame.origin.x += (sameYBetween*index);
sameYAttributsFrame.size.width = sameAttributeWidth;
sameYAttributs.frame = sameYAttributsFrame;
}
currentFrame.origin.x = self.left;
currentFrame.origin.y = CGRectGetMaxY(lastFrame)+self.top;
currentFrame.size.width = width;
currentFrame.size.height = height;
attributs.frame = currentFrame;
}else{
currentFrame.origin.x = CGRectGetMaxX(lastFrame)+self.between;
currentFrame.origin.y = lastFrame.origin.y;
currentFrame.size.width = width;
currentFrame.size.height = height;
attributs.frame = currentFrame;
}
}
}else{
currentFrame.origin.x = self.left;
currentFrame.origin.y = self.top;
currentFrame.size.width = width;
currentFrame.size.height = height;
attributs.frame = currentFrame;
}
}
// attributs.size = CGSizeMake(width, 30);
self.maxY = CGRectGetMaxY(attributs.frame)+10;
NSLog(@"%f===%f===%f===%f",attributs.frame.origin.x,attributs.frame.origin.y,attributs.frame.size.width,attributs.frame.size.height);
return attributs;
}
///返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
-(UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
return [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
}
///返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
-(UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)decorationViewKind atIndexPath:(NSIndexPath*)indexPath{
return [super layoutAttributesForDecorationViewOfKind:decorationViewKind atIndexPath:indexPath];
}
///当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
return [super shouldInvalidateLayoutForBoundsChange:newBounds];
}
/**
另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIView的setNeedsLayout方法十分类似。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect来生成更新后的布局。
*/
-(void)loadOldAttributes:(CGRect)lastFrame{
///将和上一个item在同一个row的item的放在同一个数组
NSMutableArray *sameYArray = [NSMutableArray array];
for (UICollectionViewLayoutAttributes *subAttributs in self.attributesArray) {
if (subAttributs.frame.origin.y==lastFrame.origin.y) {
[sameYArray addObject:subAttributs];
}
}
///判断出上一row还剩下多少宽度
CGFloat sameYWidth = 0.0;
for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
sameYWidth += sameYAttributs.size.width;
}
sameYWidth = sameYWidth + (self.left+self.right+(sameYArray.count-1)*self.between);
///上一个row所剩下的宽度
CGFloat sameYBetween = (self.collectionView.bounds.size.width-sameYWidth)/sameYArray.count;
for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
CGFloat sameAttributeWidth = sameYAttributs.size.width;
CGFloat sameAttributeHeight = sameYAttributs.size.height;
CGRect sameYAttributsFrame = sameYAttributs.frame;
///更新sameYAttributs宽度使之均衡显示
sameAttributeWidth += sameYBetween;
sameYAttributs.size = CGSizeMake(sameAttributeWidth, sameAttributeHeight);
NSInteger index = [sameYArray indexOfObject:sameYAttributs];
sameYAttributsFrame.origin.x += (sameYBetween*index);
sameYAttributsFrame.size.width = sameAttributeWidth;
sameYAttributs.frame = sameYAttributsFrame;
}
}
-(NSMutableArray*)attributesArray{
if (!_attributesArray) {
_attributesArray = [NSMutableArray array];
}
return _attributesArray;
}
@end
区头悬浮
#import "DWReusableLayout.h"
@interface DWReusableLayout ()
@property (nonatomic,assign) CGFloat naviHeight;
@end
@implementation DWReusableLayout
-(instancetype)init
{
self = [super init];
if (self)
{
self.naviHeight = 0.0;
}
return self;
}
/*
// 作用:返回指定区域的cell布局对象
// 什么时候调用:指定新的区域的时候调用
(<__kindof UICollectionViewLayoutAttributes *> iOS9之后的泛型 )
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
*/
/** iOS9 之前的写法 作用第24行代码有写*/
//UICollectionViewLayoutAttributes:我称它为collectionView中的item(包括cell和header、footer这些)的《结构信息》
- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect
{
//截取到父类所返回的数组(里面放的是当前屏幕所能展示的item的结构信息),并转化成不可变数组
NSMutableArray *superArray = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
//创建存索引的数组,无符号(正整数),无序(不能通过下标取值),不可重复(重复的话会自动过滤)
NSMutableIndexSet *noneHeaderSections = [NSMutableIndexSet indexSet];
//遍历superArray,得到一个当前屏幕中所有的section数组
for (UICollectionViewLayoutAttributes *attributes in superArray)
{
//如果当前的元素分类是一个cell,将cell所在的分区section加入数组,重复的话会自动过滤
if (attributes.representedElementCategory == UICollectionElementCategoryCell)
{
[noneHeaderSections addIndex:attributes.indexPath.section];
}
}
//遍历superArray,将当前屏幕中拥有的header的section从数组中移除,得到一个当前屏幕中没有header的section数组
//正常情况下,随着手指往上移,header脱离屏幕会被系统回收而cell尚在,也会触发该方法
for (UICollectionViewLayoutAttributes *attributes in superArray)
{
//如果当前的元素是一个header,将header所在的section从数组中移除
if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader])
{
[noneHeaderSections removeIndex:attributes.indexPath.section];
}
}
//遍历当前屏幕中没有header的section数组
[noneHeaderSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){
//取到当前section中第一个item的indexPath
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
//获取当前section在正常情况下已经离开屏幕的header结构信息
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
//如果当前分区确实有因为离开屏幕而被系统回收的header
if (attributes)
{
//将该header结构信息重新加入到superArray中去
[superArray addObject:attributes];
}
}];
//遍历superArray,改变header结构信息中的参数,使它可以在当前section还没完全离开屏幕的时候一直显示
for (UICollectionViewLayoutAttributes *attributes in superArray) {
//如果当前item是header
if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader])
{
//得到当前header所在分区的cell的数量
NSInteger numberOfItemsInSection = [self.collectionView numberOfItemsInSection:attributes.indexPath.section];
//得到第一个item的indexPath
NSIndexPath *firstItemIndexPath = [NSIndexPath indexPathForItem:0 inSection:attributes.indexPath.section];
//得到最后一个item的indexPath
NSIndexPath *lastItemIndexPath = [NSIndexPath indexPathForItem:MAX(0, numberOfItemsInSection-1) inSection:attributes.indexPath.section];
//得到第一个item和最后一个item的结构信息
UICollectionViewLayoutAttributes *firstItemAttributes, *lastItemAttributes;
if (numberOfItemsInSection>0)
{
//cell有值,则获取第一个cell和最后一个cell的结构信息
firstItemAttributes = [self layoutAttributesForItemAtIndexPath:firstItemIndexPath];
lastItemAttributes = [self layoutAttributesForItemAtIndexPath:lastItemIndexPath];
}else
{
//cell没值,就新建一个UICollectionViewLayoutAttributes
firstItemAttributes = [UICollectionViewLayoutAttributes new];
//然后模拟出在当前分区中的唯一一个cell,cell在header的下面,高度为0,还与header隔着可能存在的sectionInset的top
CGFloat y = CGRectGetMaxY(attributes.frame)+self.sectionInset.top;
firstItemAttributes.frame = CGRectMake(0, y, 0, 0);
//因为只有一个cell,所以最后一个cell等于第一个cell
lastItemAttributes = firstItemAttributes;
}
//获取当前header的frame
CGRect rect = attributes.frame;
//当前的滑动距离 + 因为导航栏产生的偏移量,默认为64(如果app需求不同,需自己设置)
CGFloat offset = self.collectionView.contentOffset.y + _naviHeight;
//第一个cell的y值 - 当前header的高度 - 可能存在的sectionInset的top
///firstItemAttributes.frame.origin.y-self.sectionInset.top = CGRectGetMaxY(attributes)(即紧贴header的最大Y值 = header.frame.origin.y+header.bounds.size.height)
///firstItemAttributes.frame.origin.y-self.sectionInset.top = header.frame.origin.y+header.bounds.size.height
///header.frame.origin.y = firstItemAttributes.frame.origin.y-self.sectionInset.top-header.bounds.size.height
CGFloat headerY = firstItemAttributes.frame.origin.y - rect.size.height - self.sectionInset.top;
//哪个大取哪个,保证header悬停
//针对当前header基本上都是offset更加大,针对下一个header则会是headerY大,各自处理
CGFloat maxY = MAX(offset,headerY);
//最后一个cell的y值 + 最后一个cell的高度 + 可能存在的sectionInset的bottom - 当前header的高度
//当当前section的footer或者下一个section的header接触到当前header的底部,计算出的headerMissingY即为有效值
CGFloat headerMissingY = CGRectGetMaxY(lastItemAttributes.frame) + self.sectionInset.bottom - rect.size.height;
//给rect的y赋新值,因为在最后消失的临界点要跟谁消失,所以取小
///两个区头没有接触之前,offset<headerMissingY,所以rect.origin.y==偏移量,接触时offset=headerMissingY,接触后,offset>headerMissingY
///所以接触后最小值就是headerMissingY(即上一个区的最大Y值-rect.size.height)
rect.origin.y = MIN(maxY,headerMissingY);
NSLog(@"%f-----%f----%f---%f",offset,headerY,headerMissingY,rect.origin.y);
//给header的结构信息的frame重新赋值
attributes.frame = rect;
//如果按照正常情况下,header离开屏幕被系统回收,而header的层次关系又与cell相等,如果不去理会,会出现cell在header上面的情况
//通过打印可以知道cell的层次关系zIndex数值为0,我们可以将header的zIndex设置成1,如果不放心,也可以将它设置成非常大,这里随便填了个7
attributes.zIndex = 7;
}
}
//转换回不可变数组,并返回
return [superArray copy];
}
//return YES;表示一旦滑动就实时调用上面这个layoutAttributesForElementsInRect:方法
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound
{
return YES;
}
@end
瀑布流
#import "DWWateFallLayout.h"
@interface DWWateFallLayout ()
@property (nonatomic,strong) NSMutableArray *attributesArray;
@property (nonatomic,strong) NSArray<NSNumber*> *itemHeightArray;
@property (nonatomic,strong) NSMutableArray<UICollectionViewLayoutAttributes*> *itemArray;
@end
@implementation DWWateFallLayout
-(instancetype)initWithHeightArray:(NSArray*)heightArray{
if (self = [super init]) {
self.itemHeightArray = heightArray;
}
return self;
}
-(void)prepareLayout{
///和init相似,必须call super的prepareLayout以保证初始化正确
[super prepareLayout];
///1.首先被调用
[self.attributesArray removeAllObjects];
[self.itemArray removeAllObjects];
///获取当前collectionView对应区的item
NSInteger count = [self.collectionView numberOfItemsInSection:0];
for (int i =0; i<count; i++) {
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
[self.attributesArray addObject:attributes];
}
}
///返回collectionView的内容的尺寸
-(CGSize)collectionViewContentSize{
///2.其次被调用(layoutAttributesForElementsInRect 调用后会在此调用此方法)
CGFloat maxContentHeight = CGRectGetMaxY([self.itemArray firstObject].frame);
for (UICollectionViewLayoutAttributes *attributes in self.itemArray) {
if (maxContentHeight<CGRectGetMaxY(attributes.frame)) {
maxContentHeight = CGRectGetMaxY(attributes.frame);
}
}
return CGSizeMake(self.collectionView.bounds.size.width, maxContentHeight);
}
///返回rect中的所有的元素的布局属性,返回的是包含UICollectionViewLayoutAttributes的NSArray
///UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的
///初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。
///rect 为collectionview 的rect,(高度超出collectionview高度后,rect的height会翻倍)
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
///3.被调用
return self.attributesArray;
}
///返回对应于indexPath的位置的cell的布局属性,返回指定indexPath的item的布局信息。子类必须重载该方法,该方法只能为cell提供布局信息,不能为补充视图和装饰视图提供。
-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath{
UICollectionViewLayoutAttributes *attributs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
///item的宽度,根据左右间距和中间间距算出item宽度
CGFloat itemWidth = (self.collectionView.bounds.size.width - (10+10+10+10))/3.0;
///item的高度
CGFloat itemHeight = self.itemHeightArray[indexPath.row].floatValue;
if (self.itemArray.count<3) {
[self.itemArray addObject:attributs];
CGRect itemFrame = CGRectMake(10+(itemWidth+10)*(self.itemArray.count-1), 10, itemWidth, itemHeight);
attributs.frame = itemFrame;
}else{
UICollectionViewLayoutAttributes *fristAttri = [self.itemArray firstObject];
CGFloat minY = CGRectGetMaxY(fristAttri.frame);
CGFloat Y = minY;
NSInteger index=0;
CGRect itemFrame = CGRectMake(fristAttri.frame.origin.x,CGRectGetMaxY(fristAttri.frame)+10, itemWidth, itemHeight);
for (UICollectionViewLayoutAttributes *attri in self.itemArray) {
if (minY>CGRectGetMaxY(attri.frame)) {
minY = CGRectGetMaxY(attri.frame);
Y = minY;
itemFrame = CGRectMake(attri.frame.origin.x,Y+10, itemWidth, itemHeight);
NSInteger currentIndex = [self.itemArray indexOfObject:attri];
index = currentIndex;
}
}
attributs.frame = itemFrame;
[self.itemArray replaceObjectAtIndex:index withObject:attributs];
}
return attributs;
}
-(NSMutableArray*)attributesArray{
if (!_attributesArray) {
_attributesArray = [NSMutableArray array];
}
return _attributesArray;
}
-(NSMutableArray*)itemArray{
if (!_itemArray) {
_itemArray = [NSMutableArray array];
}
return _itemArray;
}
@end