前言:
现在很多应用流行使用瀑布流的方式来显示资源,本文简单的介绍一下瀑布流的实现方式。所谓瀑布流就是根据一个外国的网站得来的,能够大量展现信息的方式。这种界面的布局好像没有规律,其实他的排列还是有规律的,按照一般应用最普遍的把屏幕分成等宽的三列,然后将图片加载在每一列中。在加入到列之前,要首先判断哪一列的高度最低,然后把图片加到列高度最低的那列中。其实就是哪列高度低就加哪。
下面我们来看一下瀑布流的实现方案:
方案一:最底层是个ScrollView,上面添加多个TableView
方案分析:效率低,而且这样不能循环利用cell,因为只有当单元格滚动出tableView才能循环利用,而本方案是整体滚动ScrollView,tableview并没有发生滚动。所以,我们无法循环利用单元格。使用该方式添加瀑布流是将资源一次性全部加载,比较消耗性能,一般不推荐使用这种方式来实现瀑布流。
方案二:用UIScrollView展示,每一项对应一个子控件
方案分析:我们需要自己实现资源加载的缓存或释放。首先如果子控件尚未显示在scrollView上,我们需要将对应的做资源缓存处理。其次,我们需要时刻监听scrollView,看哪些子控件不再显示在父视图上,将其从父视图上移除以节省资源,而后将其添加到缓存用的set集合中,以便以后复用。最后需要自己定义每个子控件的布局,这样工作量就非常大了,实现起来也较为复杂,所有我们同样不采用这种方式来实现瀑布流。
方案三:采用UICollectionView,而后使用自定义的布局,来设置Cell
方案分析:UICollectionView显示,系统已经做好了控件的复用机制和资源的缓存机制,简单易用,实现起来也比较轻松。
主要从效率(复用)、与缓存机制,两个方面进行分析。最终我们采用最合理简单的第三方案,并且这也是现在主流的瀑布流实现方案。下面我们来看一下第三个方案的基本设计思路:
定义流水布局:指定滚动方向、默认列数、行间距、列间距、以及指定单元格cell的大小itemSize。
- 可以提供一个数组columnMaxHeight,用来纪录当前每一列的高度的最大值,用来确定最新资源的加载列。
可以提供一个数组attributeArray,用来存放每个单元格的布局属性。
基于上面的实现思路,我们来自定义一个UICollectionView的布局类UICollectionViewLayout。
设置布局类默认常量
- 设置布局类的基本属性,在prepareLayout 方法中获取所有的cell布局属性,而每一个cell布局属性通过调用layoutAttributesForItemAtIndexPath:方式获取,而调用该方法,就会执行第4步。
- 复写父类的布局方法对返回单元格属性,在layoutAttributesForElementsInRect:方法(返回所有元素的布局属性数组 ) 返回之前保存的所有Cell的布局属性数组。
- 计算每个单元格的frame属性,我们可以在layoutAttributesForItemAtIndexPath:方法来调整 Cell的布局属性,指定Cell的frame。
示例代码:
#import "WaterfallFlowLayout.h"
#pragma mark - 1.设置布局类默认常量
// 默认的列数
static const NSInteger kDefaultColumnCount = 2;
// 默认每一列之间的间距
static const CGFloat kDefaultColumnSpace = 10;
// 每一行之间的间距
static const CGFloat kDefaultRowSpace = 10;
// 边缘偏移间距
static const UIEdgeInsets kDefaultEdgeInsets = {10, 10, 10, 10};
@interface WaterfallFlowLayout ()
// 布局属性的数组
@property (nonatomic, strong) NSMutableArray *attributesArray;
// 列的最大高度数组
@property (nonatomic, strong) NSMutableArray *columnMaxHeights;
@end
@implementation WaterfallFlowLayout
#pragma mark - 2.设置布局类的基本属性
// 初始化布局属性数组 - 存放所有单元格的布局属相
- (NSMutableArray *)attributesArray {
if (!_attributesArray) {
_attributesArray = [[NSMutableArray alloc] init];
}
return _attributesArray;
}
// 初始化列的最大高度数组 - 存放所有列的当前最大高度
- (NSMutableArray *)columnMaxHeights {
if (!_columnMaxHeights) {
_columnMaxHeights = [[NSMutableArray alloc] init];
}
return _columnMaxHeights;
}
#pragma mark - 2.复写prepareLayout方法,保存单元格的初始布局信息
/*
1.集合视图在第一次做布局是调用该方法一次,按照一个布局的实例对象
2.集合视图在布局无效之后以及重新查询布局信息之前再次调用该方法
3.子类应该使用调用super,如果需要覆盖的话
*/
- (void)prepareLayout {// 准备布局
// 1. 计算并保存每列的最大高度的初始值
// 清除之前保存的所有列的最大高度,避免高度数据重复
[self.columnMaxHeights removeAllObjects];
for (NSInteger i = 0; i < kDefaultColumnCount; i ++) {
// 每列的起始高度都为默认的边缘间距的顶部间距
NSNumber *maxHeight = [NSNumber numberWithInteger:kDefaultEdgeInsets.top];
[self.columnMaxHeights addObject:maxHeight];
}
// 2. 创建并保存UICollectionView上每一个单元格的布局属性
// 清除之前保存的所有的单元格的布局属性,避免数据重复
[self.attributesArray removeAllObjects];
// 获取collectionView上的单元格分组数
NSInteger sectionNumber = [self.collectionView numberOfSections];
for (NSInteger i = 0; i < sectionNumber; i ++) {
// 获取collectionView上的每个分组的单元格数
NSInteger cellNumber = [self.collectionView numberOfItemsInSection:i];
for (NSInteger j = 0; j < cellNumber; j ++) {
// 创建单元格位置
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
// 获取indexPath对应位置的布局信息
UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
// 保存到布局信息数组
[self.attributesArray addObject:attrs];
}
}
}
#pragma mark - 3. 复写父类的布局方法对返回单元格属性
// UICollectionView调用下面的四个方法来确定布局信息。
/**
* 决定cell的排布
*/
// 实现-layoutAttributesForElementsInRect:返回布局属性来补充或装饰视图或执行布局as-needed-on-screen时尚。
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
return self.attributesArray;
}// 返回一个包含所有视图(在给定的矩形/窗口内)的布局属性实例的数组
// 所有布局子类应该实现-layoutAttributesForItemAtIndexPath:对需求特殊的位置的单元格返回一个布局属性实例。
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
// 根据位置给对应位置的单元格创建布局属性
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
// 设置对应位置但一个布局属性的frame
// 1. 单元格的宽 = 父视图的宽 - 左右的边缘偏移 - 单元格的间距
// 获取collectionView的宽度
CGFloat collectionViewW = self.collectionView.frame.size.width;
CGFloat itemW = (collectionViewW - kDefaultEdgeInsets.left - kDefaultEdgeInsets.right - (kDefaultColumnCount - 1) * kDefaultColumnSpace) / kDefaultColumnCount;
// 2. 单元格的高
// 一个不定值,所以我在50高度的基础上添加了一个0-100的随机值
CGFloat itemH = 150 + arc4random_uniform(150);
// 找出高度最短的那一列,用来计算对应单元格的x坐标
NSInteger destColumn = 0;// 默认加载该单元格的列
// 获取前一状态高度最小的列的高度
CGFloat columnMinHeight = [self.columnMaxHeights[0] doubleValue];
for (NSInteger i = 1; i < kDefaultColumnCount; i++) {
// 取得第i列的高度
CGFloat columnHeight = [self.columnMaxHeights[i] doubleValue];
// 判断每列的高度与当前最小高度的大小
if (columnHeight < columnMinHeight) {
columnMinHeight = columnHeight;
destColumn = i;// 获取到高度最小的列
}
}
// 3. 单元格的x坐标
// 单元格的x坐标 = 边缘左侧偏移 + 列数 * (单元格宽 + 单元格间距)
CGFloat x = kDefaultEdgeInsets.left + destColumn * (itemW + kDefaultRowSpace);
// 4. 单元格的y坐标
CGFloat y = kDefaultEdgeInsets.top;// 初始单元格的默认y坐标
// 判断列的最小高度和初始的单元格的顶部边缘偏移量不同
if (columnMinHeight != kDefaultEdgeInsets.top) {
// 单元格的y坐标 = 最小的列的高度 + 行间距
y = columnMinHeight + kDefaultRowSpace;
}
// 设置单元格的frame属性
attrs.frame = CGRectMake(x, y, itemW, itemH);
// 更新最短那列的高度
self.columnMaxHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
return attrs;
}
// it should also implement the respective atIndexPath: methods for those types.
//如果布局支持任何补充或装饰视图类型,它还应该实现各自的at indexpath:这些类型的方法。
//- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath {
//
//}
//- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath {
//
//}
//子类必须重写此方法,并使用它来返回集合视图的宽度和高度的内容。这些值代表的所有内容的宽度和高度,不只是目前可见的内容。集合视图使用这些信息来配置自己的内容大小促进滚动。
- (CGSize)collectionViewContentSize
{
CGFloat maxColumnHeight = [self.columnMaxHeights[0] doubleValue];
for (NSInteger i = 1; i < kDefaultColumnCount; i++) {
// 取得第i列的高度
CGFloat columnHeight = [self.columnMaxHeights[i] doubleValue];
if (maxColumnHeight < columnHeight) {
maxColumnHeight = columnHeight;
}
}
return CGSizeMake(0, maxColumnHeight + kDefaultEdgeInsets.bottom);
}
@end