简单瀑布流实现
程序员文章站
2022-07-12 15:29:59
...
实现效果
这里实现了一个较为复杂的瀑布流效果,在基本的高度不一的瀑布流上,增加了占据两列宽度大小的cell,效果如图:
具体实现
1,技术基础
这里使用了UICollectionView来展示瀑布流,通过自定义UICollectionView对象的collectionViewLayout,来实现对UICollectionView中cell的布局计算。
要自定义UICollectionView对象的collectionViewLayout,需要创建一个UICollectionViewLayout的子类。
@interface JKRFallsLayout : UICollectionViewLayout
@property (nonatomic, weak) id<JKRFallsLayoutDelegate> delegate;
@end
通过重写以下方法来实现布局计算:
// collectionView 首次布局和之后重新布局的时候会调用
// 并不是每次滑动都调用,只有在数据源变化的时候才调用
- (void)prepareLayout
{
// 重写必须调用super方法
[super prepareLayout];
}
// 返回布局属性,一个UICollectionViewLayoutAttributes对象数组
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
return [super layoutAttributesForElementsInRect:rect];
}
// 计算布局属性
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [super layoutAttributesForItemAtIndexPath:indexPath];
}
// 返回collectionView的ContentSize
- (CGSize)collectionViewContentSize
{
return [super collectionViewContentSize];
}
2,布局计算
要实现布局的计算,需要创建以下几个属性
@property (nonatomic, strong) NSMutableArray<UICollectionViewLayoutAttributes *> *attrsArray; ///< 所有的cell的布局
@property (nonatomic, strong) NSMutableArray *columnHeights; ///< 每一列的高度
@property (nonatomic, assign) NSInteger noneDoubleTime; ///< 没有生成大尺寸次数
@property (nonatomic, assign) NSInteger lastDoubleIndex; ///< 最后一次大尺寸的列数
@property (nonatomic, assign) NSInteger lastFixIndex; ///< 最后一次对齐矫正列数
- (CGFloat)columnCount; ///< 列数
- (CGFloat)columnMargin; ///< 列边距
- (CGFloat)rowMargin; ///< 行边距
- (UIEdgeInsets)edgeInsets; ///< collectionView边距
熟悉在- (void)prepareLayout方法中遍历需要计算的cell,调用计算布局的方法,并将获取的布局属性保存到attrsArray数组中:
// 当列高度数组为空时,即为第一行计算,每一列的基础高度加上collection的边框的top值
if (!self.columnHeights.count) {
for (NSInteger i = 0; i < self.columnCount; i++) {
[self.columnHeights addObject:@(self.edgeInsets.top)];
}
}
// 遍历所有的cell,计算所有cell的布局
for (NSInteger i = self.attrsArray.count; i < [self.collectionView numberOfItemsInSection:0]; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
// 计算布局属性并将结果添加到布局属性数组中
[self.attrsArray addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
计算每个cell的布局需要用到columnHeights数组,这个数组的作用就是保存每一列的总高度,当一个cell要计算他的布局的时候,先看所有列哪一列的高度最小,就把cell放在哪一列下。这样cell的x和y就能够确定,cell的x为列数*cell的宽度+间距调节的值,cell的y值就是高度最小的那一列的总高度。
cell的宽度和高度,我是通过随机数+不重复放大+选择性矫正的原则来计算的,下面有代码详细注释:
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
// collectionView的宽度
CGFloat collectionViewW = self.collectionView.frame.size.width;
// cell的宽度
CGFloat w = (collectionViewW - self.edgeInsets.left - self.edgeInsets.right -
self.columnMargin * (self.columnCount - 1)) / self.columnCount;
// cell的高度
NSUInteger randomOfHeight = arc4random() % 100;
CGFloat h = w * (randomOfHeight >= 50 ? 250 : 320) / 200;
// cell应该拼接的列数
NSInteger destColumn = 0;
// 高度最小的列数高度
CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];
// 获取高度最小的列数
for (NSInteger i = 1; i < self.columnCount; i++) {
CGFloat columnHeight = [self.columnHeights[i] doubleValue];
if (minColumnHeight > columnHeight) {
minColumnHeight = columnHeight;
destColumn = i;
}
}
// 计算cell的x
CGFloat x = self.edgeInsets.left + destColumn * (w + self.columnMargin);
// 计算cell的y
CGFloat y = minColumnHeight;
if (y != self.edgeInsets.top) {
y += self.rowMargin;
}
// 随机数,用来随机生成大尺寸cell
NSUInteger randomOfWhetherDouble = arc4random() % 100;
// 判断是否放大
if (destColumn < self.columnCount - 1 // 放大的列数不能是最后一列(最后一列方法超出屏幕)
&& _noneDoubleTime >= 1 // 如果前个cell有放大就不放大,防止连续出现两个放大
&& (randomOfWhetherDouble >= 45 || _noneDoubleTime >= 8) // 45%几率可能放大,如果累计8次没有放大,那么满足放大条件就放大
&& [self.columnHeights[destColumn] doubleValue] == [self.columnHeights[destColumn + 1] doubleValue] // 当前列的顶部和下一列的顶部要对齐
&& _lastDoubleIndex != destColumn) { // 最后一次放大的列不等当前列,防止出现连续两列出现放大不美观
_noneDoubleTime = 0;
_lastDoubleIndex = destColumn;
// 重定义当前cell的布局:宽度*2,高度*2
attrs.frame = CGRectMake(x, y, w * 2 + self.columnMargin, h * 2 + self.rowMargin);
// 当前cell列的高度就是当前cell的最大Y值
self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
// 当前cell列下一列的高度也是当前cell的最大Y值,因为cell宽度*2,占两列
self.columnHeights[destColumn + 1] = @(CGRectGetMaxY(attrs.frame));
} else {
// 正常cell的布局
if (_noneDoubleTime <= 3 || _lastFixIndex == destColumn) { // 如果没有放大次数小于3且当前列等于上次矫正的列,就不矫正
attrs.frame = CGRectMake(x, y, w, h);
} else if (self.columnHeights.count > destColumn + 1 // 越界判断
&& y + h - [self.columnHeights[destColumn + 1] doubleValue] < w * 0.1) { // 当前cell填充后和上一列的高度偏差不超过cell最大高度的10%,就和下一列对齐
attrs.frame = CGRectMake(x, y, w, [self.columnHeights[destColumn + 1] doubleValue] - y);
_lastFixIndex = destColumn;
} else if (destColumn >= 1 // 越界判断
&& y + h - [self.columnHeights[destColumn - 1] doubleValue] < w * 0.1) { // 当前cell填充后和上上列的高度偏差不超过cell最大高度的10%,就和下一列对齐
attrs.frame = CGRectMake(x, y, w, [self.columnHeights[destColumn - 1] doubleValue] - y);
_lastFixIndex = destColumn;
} else {
attrs.frame = CGRectMake(x, y, w, h);
}
// 当前cell列的高度就是当前cell的最大Y值
self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
_noneDoubleTime += 1;
}
// 返回计算获取的布局
return attrs;
3,Demo源码地址:https://github.com/Joker-388/JKRComplexFallsDemo
可以在这里下载到完整的可运行Demo,项目中加了详细的注视。