瀑布流已经是现在很常用的布局样式,用collectionView就可以很方便的实现,现在github上star最多的就是这个CHTCollectionViewWaterfallLayout,也是我项目中经常用的。
时间长了还是发现有满足不了需求的地方(比如始终缺失的像tableViewHeader一样的collectionViewHeader和collectionViewFooter),
还发现了个bug:当section个数大于1但第一个section中item个数为0时,第二个section中item的位置会出现错误(原因是当item==0时依然减去了itemSpacing,给他提了issue,但是没处理)
再加上其Swift版的库一直没更新,
所以还是决定自己仿写一个,学习一下大神的思路,顺便修复发现的bug,加上自己需要的header和footer,整理下代码结构。
地址在这里:https://github.com/Phelthas/LXMWaterfallLayout
效果如图:
CHTCollectionViewWaterfallLayout分析
CHTCollectionViewWaterfallLayout的大致思路是:
每个section有几个column是固定的,然后根据insect,spacing等属性,就可以计算出每一列的宽度,然后根据delegate返回的itemSize,就可以按比例缩放出实际显示的itemSize。
然后根据renderDirection属性,确定每个item的位置。
CHTCollectionViewWaterfallLayout想做一个跟UICollectionViewFlowLayout一样简单易用的类,所以整个结构就是仿照UICollectionViewFlowLayout来的,属性名称,协议名称等也是,所以看起来非常舒服。
首先看协议
跟UICollectionViewDelegateFlowLayout几乎一模一样,不过sizeForItem变成了required,referenceSize变成了height。
referenceSize变成height我觉得很合理,因为实际用过程中也只会用到height,width肯定是collectionView的宽度;
sizeForItem我觉得可以不用必须,如果没有设置,那按照计算出来的columnWidth设置成正方形就行了嘛,就像UICollectionViewFlowLayout一样也有个初始值。
再看属性
也跟UICollectionViewDelegateFlowLayout几乎一模一样,多了个columnCount,多了个itemRenderDirection,referenceSize变成了height,还多了minimumContentHeight和一个辅助方法。没啥好分析的。
还有私有属性
协议是继承UICollectionViewDelegate的,所以layout的delegate是取self.collectionView.delegate作为layout的delegate,用起来跟UICollectionViewFlowLayout完全一样,舒服~
columnHeights是个二维数组,保存每个section中每个column的高度,注意是具体某一列的累计高度,不是某个cell或者item的高度!!!这个很重要
unionRects是个比较难理解的东西,作者用它来优化layoutAttributesForElementsInRect这个方法,我个人感觉意义不大。。。或许是没领会到作者的深意吧。。。
headerAttribute和footerAttribute作者用了字典而不是二维数组,key是对应的section。
其他属性也都很好理解,不说了。
然后看实现
最重要的就是这个prepareLayout这个方法。
最开始初始化各个数组,然后开始遍历section计算所有的layoutAttributes。
1, 首先是sectionHeader,CHTCollectionViewWaterfallLayout是让sectionHeader也受sectionInset影响的,所以sectionHeader的起始x是sectionInset.left, 起始y是sectionInset.top;
而UICollectionViewFlowLayout的sectionHeader是不受sectionInset影响的,所以起始x是0,起始y也是0;
两种方式都有有道理,我更倾向于UICollectionViewFlowLayout的方式,因为section大小给大了,view可以用空白填补,但是要给小了想设置不用样式就难了,
所以我仿写的时候就就用UICollectionViewFlowLayout的方式了。
2, 然后是将columnHeights中对应section的数组的值全部设置为sectionHeader.fame.maxY。这一步是为了统一下面计算item位置的起始位置。
我觉得,既然是columnHeights,就不要把sectionHeader之类的高度也计算进去了,
所以仿写的时候采用了另外的计算方式:用一个私有变量contentHeight来保存当前的Y值,columnHeights只用来保存对应的column的高度,计算位置的时候将两者加起来即可
3, 然后是计算每个item的位置
先计算出item应该放在那一列,CHTCollectionViewWaterfallLayout用itemRenderDirection定义了三种排列方式:最短优先,从左往右,从右往左;
我感觉用处不是很大,既然是瀑布流了,那应该不太在意横着是怎么拍的,所以仿写的时候就只保留了最短优先这一种方式;
而确定哪个最短的方式也很简单,就是从columnHeights中找出最短的那一列的index;
然后创建attribute,根据indexPath设置frame,添加到数组即可
也就是这里,CHTCollectionViewWaterfallLayout有个bug:
如果某个section的item个数为0,那就不应该计算item的位置,而CHTCollectionViewWaterfallLayout目前的做法是在计算完成之后,判断columnHeights对应section的columnCount是否为0,
为0则再减去itemSpacing,问题是某个section的columnCount跟itemCount其实没啥关系,可能我计划这个section有2列,但是刚好没有数据,itemCount为0,这种情况下判断就出错了。。。
我仿写的做法是:把加spacing放在加item高度之前;首先判断item是否是该列的第一个,是第一个则不用加spacing,否则再加spacing;目前来看是很好的解决了这个问题~~
4, 然后是sectionFooter,方法同sectionHeader。
到这里其实整个布局所需要的属性就都已经计算出来了,下面只需要按需求重载UICollectionViewLayout的函数,返回对应数据即可。
这里CHTCollectionViewWaterfallLayout用了一个unionRect数据,貌似是用来优化计算:
写死了unionSize = 20,然后将所有的item分为几组,每组20个,计算出每组的unionRect,保存在数组中。
然后是需要重载的函数
- (CGSize)collectionViewContentSize;
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path;
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
以上这三个 根据上面的计算返回即可;
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;
这个方法一般都是判断新旧bounds是否一样,一样则返回NO,不一样返回YES。
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
是需要注意的,CHTCollectionViewWaterfallLayout也就是在这里用到了那个unionRects数组,
前后两个遍历找出所有与rect想交的rect,再返回需要的item的attributes
这里也是我最不明白的地方,感觉这个方法对效率的提升很有限呐,
反正我仿写的时候是直接遍历所有attributes数组,返回与rect相交的数组了。
有知道作者那么写有什么深意的,望不吝赐教~~~
LXMWaterfallLayout 改进及优化
1,完全是用Swift3.0写的,语法什么的应该都是最新的,没什么兼容性问题
2,加入了 collectionViewHeaderHeight和collectionViewFooterHeight两个属性,
用法同sectionHeader和sectionFooter,需要在collectionView注册nib或者class,然后在
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
方法中取出来设置即可
这里因为collectionView的布局完全受layout控制,所以像tableView一样直接设置为collectionView的属性是不行的,肯定会涉及到layout,目前只想到了这种方式,如果谁有什么好的想法,欢迎讨论~
3,加入了默认的itemSize实现
所有的协议方法默认都可以不实现,不实现的时候,就相当是一个每个section固定有几列,且支持collectionViewHeader和collectionViewFooter的layout
4,自以为代码写的还算比较规范,结构还算清晰,看起来比较舒服(⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄)