你的位置:首页 > 操作系统

[操作系统]瀑布流框架的搭建


瀑布流大家都应该熟悉了,现在大部分电商应用中或多或少的都用到瀑布流,它可以吸引用户的眼球,使用户不易产生视觉疲劳,苹果在iOS6中增添了UICollectionView控件,这个控件可以说是UITableView的升级版,通过这个控件我们就能很简单的做出瀑布流,后面通过自己的封装可以让其变成一个小框架,更简单的应用到我们之后的开发中

最近开通了简书欢迎大家关注,我会不定期的分享我的iOS开发经验  点击关注-->Melody_Zhy

如果想做瀑布流,那么就要自定义CollectionViewFlowLayout,因为这个类中有一个返回collectionView中所有子控件的布局属性(布局属性中有控件的frame和索引)的方法

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {  NSArray *arr = [super layoutAttributesForElementsInRect:rect];  for (UICollectionViewLayoutAttributes *attri in arr) {    attri.frame = CGRectMake(0, 0, 100, 300);  }  NSLog(@"%@", arr);  return arr;}

这个方法会将collectionView中的所有子控件的布局属性计算一次,计算之后就会被缓存起来,当已经计算过的cell,再次出现时也不会在重复去计算它的尺寸。

注意:如果要进行刷新数据那么要记得将之前的布局属性进行清空,不然会出现布局错误
// 把用来装所有布局属性的数据做清空处理[self.attrArrM removeAllObjects];

现在定义一个可变数组属性来存储一会自己计算的布局属性中的frame,让上面的方法返回自己定义的布局属性

// 用来保存所有布局属性的可变数组@property (nonatomic, strong) NSMutableArray *attrArrM;

那么我们在哪个方法中计算自己定义的布局属性呢?

有这么个方法 

- (void)prepareLayout {// 要调用父类的prepareLayout  [super prepareLayout];}

当collectionView中的所有子控件即将显示的时候就会来调用此方法做布局前的准备工作,准备itemSize...等等属性 同时当布局的属性发生变化时也会来调用此方法 当刷新数据之后也会来调用此方法重新做布局前的准备工作

在这个方法中可以通过collectionViewFlowLayout的collectionView的numberOfItemInSection这个方法获得一组中的所有cell

// 获得一组中的所有cellNSInteger cellCount = [self.collectionView numberOfItemsInSection:0];


通过for循环(循环次数为一组中有多少个cell)创建布局属性,在循环中需要计算每一个cell的frame 所以后台要给真实的图片尺寸(如果不给,自己计算的尺寸会造成图片闪)

同时显示时一般都会等比例缩放。

cell宽度的计算

cell的宽 =  (内容的宽 - (列数 - 1) * 最小间距) / 列数
内容的宽 = collectionView的宽 - 组的左边间距 - 右边间距
CGFloat contentWidth = self.collectionView.bounds.size.width - self.sectionInset.left - self.sectionInset.right;CGFloat cellW = (contentWidth - (self.columnCount - 1) * self.minimumInteritemSpacing) / self.columnCount;

cell高度的获取

当要获得cell高的时候需要通过控制器来获得模型图片的高度(高度要和itemW有一定的比例要不图片会过大 height / width * itemW;),因此需要让控制器成为我们的代理,在自定义CollectionViewFlowLayout.h文件中定义协议如下:

#import <UIKit/UIKit.h>@class ZHYCollectionViewFlowLayout;@protocol ZHYCollectionViewFlowLayoutDelegate <NSObject>@required- (CGFloat)waterFallFlowLayoutWithItemHeight:(ZHYCollectionViewFlowLayout *)flowLayout itemW:(CGFloat)itemW CellIndexPath:(NSIndexPath *)indexPath;@end


并且设置代理属性如下:
@property (weak, nonatomic) id<ZHYCollectionViewFlowLayoutDelegate> delegate



在计算cell高的时候调用代理方法获得cell的高度

CGFloat cellH = [self.delegate waterFallFlowLayoutWithItemHeight:self itemW:cellW CellIndexPath:indexPath];

 为什么要在代理方法中加入indexPath,因为要通过indexPath来获取模型

cellX的计算

在计算cellX的时候,如果通过 NSInteger col = i % self.columnCount;获取列号

要注意一个问题:

这样每次都是按顺序排列图片的,那么如果图片的尺寸参差不齐有的特别短有的又特别长,不巧的是长的都在一列短的又都在一列这样会造成美观性会很差,那么怎么解决这个问题呢,我们能不能让每一行添加完毕后,下一行在添加的时候将第一个添加在上一行高度最短的那面图片下面呢?答案当然是可以的-_-!

这时候我们就要定义一个可变字典属性,来存储每一列的高度,用列号来当字典的key

// 用来记录每一列的最的高度@property (nonatomic, strong) NSMutableDictionary *colDict;


在prepareLayout方法中给每一列的高度的字典一个默认的高度
for (NSInteger i = 0; i<有几列; i++) {    NSString *str = [NSString stringWithFormat:@"%ld", i];    self.colDict[str] = @(self.sectionInset.top);  }


获得最矮的那一列列号的方法




- (NSString *)minCol {  __block NSString *min = @"0";  [self.colDict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {    if ([obj floatValue] < [self.colDict[min] floatValue]) {      min = key;    }  }];  return min;}


因此列号为
NSInteger col = [[self minCol] integerValue];






 cellX为:
CGFloat cellX = self.sectionInset.left + (cellW + self.minimumInteritemSpacing) * col;



cellY的计算

计算cellY

// 用列号当字典的keyNSString *colStr = [NSString stringWithFormat:@"%ld", col];CGFloat cellY = [self.colDict[colStr] floatValue];// 累计每一列的高度self.colDict[colStr] = @(cellY + cellH + self.minimumLineSpacing);

这样我们就计算完了每一个cell的X,Y,W,H,我们来设置布局属性的frame

// 设置属性的frameattr.frame = CGRectMake(cellX, cellY, cellW, cellH);

同时我们也给尾部视图添加一个布局属性,代码如下
// 创建尾部视图的布局属性  // 创建footerview索引  NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];  // 必须是额外的layoutAttributesForSupplementaryViewOfKind  UICollectionViewLayoutAttributes *footerAttr = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind: UICollectionElementKindSectionFooter withIndexPath:indexPath];  footerAttr.frame = CGRectMake(0, [self.colDict[self.maxCol] floatValue] - self.minimumLineSpacing, self.collectionView.bounds.size.width, 50);  [self.attrArrM addObject:footerAttr];



这里要用到最高列,因为尾部视图要放在cell最下面,获得最高列索引的方法为:
// 用来取出最高那一列的列号- (NSString *)maxCol {  __block NSString *maxCol = @"0";  [self.colDict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {    if ([obj floatValue] > [self.colDict[maxCol] floatValue]) {      maxCol = key;    }  }];  return maxCol;}



最后我们通过上面说过的方法把布局属性返回

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {  return self.attrArrM;}


自定义布局属性的时候还要注意返回真实的contentSize,代码如下:




- (CGSize)collectionViewContentSize {  return CGSizeMake(0, [self.colDict[self.maxCol] floatValue] + self.footerReferenceSize.height - self.minimumLineSpacing);}


这时基本已经完成了,但如果我想要把这个瀑布流布局做成一个简单的框架就需要在简单的实现些初始化方法

在CollectionViewFlowLayout.h还要定义一个一行有几个cell的属性,当控制器引用这个类之后可以自行设置

@property (assign, nonatomic) NSInteger columnCount;

提供一些初始化方法,使其默认为一行有3个cell, cell间距及行间距为10,内边距为顶部20, footerReferenceSize, headerReferenceSize都为50,50

- (instancetype)init{  self = [super init];  if (self) {    self.columnCount = 3;    self.minimumInteritemSpacing = 10;    self.minimumLineSpacing = 10;    self.footerReferenceSize = CGSizeMake(50, 50);    self.headerReferenceSize = CGSizeMake(50, 50);    self.sectionInset = UIEdgeInsetsMake(20, 0, 0, 0);  }  return self;}

- (instancetype)initWithCoder:(NSCoder *)aDecoder{  self = [super initWithCoder:aDecoder];  if (self) {    self.columnCount = 3;    self.minimumInteritemSpacing = 10;    self.minimumLineSpacing = 10;    self.footerReferenceSize = CGSizeMake(50, 50);    self.headerReferenceSize = CGSizeMake(50, 50);    self.sectionInset = UIEdgeInsetsMake(20, 0, 0, 0);  }  return self;}

这样我们简单的瀑布流框架就搭建成功了~