一个强大的iOS瀑布流布局LBWaterFallLayout

效果图

在这里插入图片描述

实现思路

UICollectionView的精髓就是UICollectionViewLayout。UICollectionViewLayout决定了UICollectionView是如何显示在界面上的。因此我们需要自定义一个UICollectionViewLayout 的子类,在子类里面重写生成布局的方法,创建我们自己需要的布局

实现原理

重写- (void)prepareLayout进行提前创建布局

重写- (CGSize)collectionViewContentSize返回内容的大小

重写 - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect方法返回rect中所有元素的布局属性,返回的是一个数组

重写 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath方法返回对应的indexPath的位置的cell的布局属性。

重写 - (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;方法返回对应indexPath的位置的追加视图的布局属性,如果没有就不用重载

重写 - (nullable UICollectionViewLayoutAttributes )layoutAttributesForDecorationViewOfKind:(NSString)elementKind atIndexPath:(NSIndexPath *)indexPath;方法返回对应indexPath的位置的装饰视图的布局属性,如果没有也不需要重载

重写 - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;当边界发生改变时,是否应该刷新。

注意:其中

(void)prepareLayout

(CGSize)collectionViewContentSize

(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect

这三个方法是必须要重写的,后面的方法可以选择性重写,因为最终collectionView 还是根据 - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect 方法来进行展示布局的,

而重写 prepareLayout是因为创建布局的时机 需要提前到 prepareLayout 里面。

而collectionViewContentSize 是返回内容宽和高的,也必须重写

核心计算逻辑

每次都获取到总高度最低的一列cell,在该列添加新的布局

实现代码

.h 文件

//
//  LBWaterFallLayout.h
//  LBWaterFallLayout
//
//  Created by Apple on 2021/9/25.
//

#import <UIKit/UIKit.h>
#import "LBWaterFallLayoutProtocol.h"

NS_ASSUME_NONNULL_BEGIN

@interface LBWaterFallLayout : UICollectionViewFlowLayout<LBWaterFallLayoutProtocol>

@property (nonatomic, assign) CGFloat minimumLineSpacing; // default 0.0
@property (nonatomic, assign) CGFloat minimumInteritemSpacing; // default 0.0
@property (nonatomic, assign) BOOL sectionHeadersPinToVisibleBounds; // default NO
@property (nonatomic, assign) CGFloat contentOffsetY;

/// 未悬停的header的布局属性
- (UICollectionViewLayoutAttributes *)originLayoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;
/// 适用于每一列的cell宽度都相等
- (CGFloat)itemWidthOfIndexPath:(NSIndexPath *)indexPath;

@end

NS_ASSUME_NONNULL_END

.m 文件

//
//  LBWaterFallLayout.m
//  LBWaterFallLayout
//
//  Created by Apple on 2021/9/25.
//

#import "LBWaterFallLayout.h"

@interface LBCollectionLayoutSectionModel : NSObject
@property (nonatomic, strong, nullable) UICollectionViewLayoutAttributes *headerLayoutAttributes;
@property (nonatomic, strong, nullable) NSMutableArray<UICollectionViewLayoutAttributes *> *itemLayoutAttributes_list;
@property (nonatomic, strong, nullable) UICollectionViewLayoutAttributes *footerLayoutAttributes;

@end

@implementation LBCollectionLayoutSectionModel

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.itemLayoutAttributes_list = [NSMutableArray new];
    }
    return self;
}
@end

@interface LBWaterFallLayout ()

@property (nonatomic, strong) 
@end

@implementation LBWaterFallLayout

@synthesize lb_layoutDelegate;



- (CGFloat)layoutFooterSection:(NSInteger)section
                maxOffsetValue:(CGFloat)maxOffsetValue
            sectionLayoutModel:(LBCollectionLayoutSectionModel *)sectionLayoutModel

{
    UICollectionView *collectionView = self.collectionView;
    
    UIEdgeInsets const contentInset = collectionView.contentInset;
    CGFloat const contentWidth = collectionView.bounds.size.width - contentInset.left - contentInset.right;
    
    CGFloat footerHeight = 0.0;
    if ([self.lb_layoutDelegate respondsToSelector:@selector(collectionView:customLayout:referenceSizeForFooterInSection:)]) {
        CGSize footerSize = [self.lb_layoutDelegate collectionView:collectionView customLayout:self referenceSizeForFooterInSection:section];
        footerHeight = footerSize.height;
    }
    UICollectionViewLayoutAttributes *footerLayoutAttribute = [[UICollectionViewLayoutAttributes alloc] init];
    footerLayoutAttribute.indexPath = [NSIndexPath indexPathForItem:0 inSection:section];
    if (footerHeight > 0) {
        footerLayoutAttribute = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]];
    }
    footerLayoutAttribute.frame = CGRectMake(0.0, self.contentHeight + maxOffsetValue, contentWidth, footerHeight);
    [self.footerLayoutAttributes addObject:footerLayoutAttribute];
    
    [self.originFooterLayoutAttributes addObject:[footerLayoutAttribute copy]];
    sectionLayoutModel.footerLayoutAttributes = [footerLayoutAttribute copy];
    return footerHeight;
}

- (CGSize)collectionViewContentSize
{
    if ([self.lb_layoutDelegate respondsToSelector:@selector(useCustomContentSize)] &&
        [self.lb_layoutDelegate respondsToSelector:@selector(customContentSize)] &&
        [self.lb_layoutDelegate useCustomContentSize]) {
        ///使用自定义contentSize,、
        return [self.lb_layoutDelegate customContentSize];;
    }
    UIEdgeInsets contentInset = self.collectionView.contentInset;
    CGFloat width = CGRectGetWidth(self.collectionView.bounds) - contentInset.left - contentInset.right;
    CGFloat height = MAX(CGRectGetHeight(self.collectionView.bounds), self.contentHeight);
    return CGSizeMake(width, height);
}

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray<UICollectionViewLayoutAttributes *> *result = [NSMutableArray array];
    
    // 悬停
    if (self.sectionHeadersPinToVisibleBounds) {
        for (UICollectionViewLayoutAttributes *attriture in self.headerLayoutAttributes) {
            NSInteger section = attriture.indexPath.section;
            UIEdgeInsets contentInsetOfSection = [self contentInsetForSection:section];
            NSIndexPath *firstIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
            UICollectionViewLayoutAttributes *itemAttribute = [self layoutAttributesForItemAtIndexPath:firstIndexPath];
            if (!itemAttribute) {
                continue;
            }
            CGFloat headerHeight = CGRectGetHeight(attriture.frame);
            CGRect frame = attriture.frame;
            frame.origin.y = MIN(
                                 MAX(self.collectionView.contentOffset.y + self.contentOffsetY, CGRectGetMinY(itemAttribute.frame)-headerHeight-contentInsetOfSection.top),
                                 CGRectGetMinY(attriture.frame)+[self.heightOfSections[section] floatValue]-headerHeight
                                 );
            attriture.frame = frame;
            attriture.zIndex = (NSIntegerMax/2)+section;
        }
    }
    
    [self.sectionLayoutModels enumerateObjectsUsingBlock:^(LBCollectionLayoutSectionModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSUInteger section = idx;
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section];
        [self mutableArray:result addDecorationViewAttributWithIndexPath:indexPath];
        if(obj.headerLayoutAttributes
           && !(obj.headerLayoutAttributes.frame.size.height == 0)
           && CGRectIntersectsRect(rect, obj.headerLayoutAttributes.frame)) {
            [result addObject:obj.headerLayoutAttributes];
        }
        
        if (obj.itemLayoutAttributes_list.count > 0) {
            [obj.itemLayoutAttributes_list enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                if(CGRectIntersectsRect(rect, obj.frame)) {
                    [result addObject:obj];
                }
                            
            }];
        }
        
        if(obj.footerLayoutAttributes
           && !(obj.footerLayoutAttributes.frame.size.height == 0)
           && CGRectIntersectsRect(rect, obj.footerLayoutAttributes.frame)) {
            [result addObject:obj.footerLayoutAttributes];
        }
        
    }];
    
    return result;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSArray<UICollectionViewLayoutAttributes *> *tempArray = (NSArray<UICollectionViewLayoutAttributes *> *)self.itemLayoutAttributes[indexPath.section];
    return tempArray[indexPath.item];
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
{
    if ([elementKind isEqualToString:UICollectionElementKindSectionHeader]) {
        return self.headerLayoutAttributes[indexPath.section];
    }
    if ([elementKind isEqualToString:UICollectionElementKindSectionFooter]) {
        return self.footerLayoutAttributes[indexPath.section];
    }
    return nil;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    if (self.sectionHeadersPinToVisibleBounds) {
        return YES;
    } else {
        return [super shouldInvalidateLayoutForBoundsChange:newBounds];
    }
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)decorationViewKind atIndexPath:(NSIndexPath *)indexPath{
    
    UICollectionViewLayoutAttributes* att = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:decorationViewKind withIndexPath:indexPath];
    CGFloat sectionHeight = [self.heightOfSections[indexPath.section] floatValue];
    CGFloat origin_y = [self originYofSection:indexPath.section];
    UIEdgeInsets sectionInsets = [self contentInsetForSection:indexPath.section];
    att.frame = CGRectMake(0, origin_y + sectionInsets.top, self.collectionView.contentSize.width, sectionHeight -(sectionInsets.top + sectionInsets.bottom));
    att.zIndex= -1;
    return att;
}

- (UICollectionViewLayoutAttributes *)originLayoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
{
    if ([elementKind isEqualToString:UICollectionElementKindSectionHeader]) {
        return self.originHeaderLayoutAttributes[indexPath.section];
    }
    if ([elementKind isEqualToString:UICollectionElementKindSectionFooter]) {
        return self.originFooterLayoutAttributes[indexPath.section];
    }
    return nil;
}

- (CGFloat)itemWidthOfIndexPath:(NSIndexPath *)indexPath
{
    UIEdgeInsets contentInset = self.collectionView.contentInset;
    CGFloat contentWidth = self.collectionView.bounds.size.width - contentInset.left - contentInset.right;
    UIEdgeInsets contentInsetOfSection = [self contentInsetForSection:indexPath.section];
    CGFloat minimumInteritemSpacing = [self minimumInteritemSpacingForSection:indexPath.section];
    NSInteger columnOfSection = [self columnNumAtSection:indexPath.section];
    CGFloat contentWidthOfSection = contentWidth - contentInsetOfSection.left - contentInsetOfSection.right;
    CGFloat itemWidth = (contentWidthOfSection - (columnOfSection - 1) * minimumInteritemSpacing) / columnOfSection;
    return itemWidth;
}

#pragma mark - Private
- (UIEdgeInsets)contentInsetForSection:(NSInteger)section
{
    UIEdgeInsets edgeInsets = UIEdgeInsetsZero;
    if ([self.lb_layoutDelegate respondsToSelector:@selector(collectionView:customLayout:insetForSectionAtIndex:)]) {
        edgeInsets = [self.lb_layoutDelegate collectionView:self.collectionView customLayout:self insetForSectionAtIndex:section];
    }
    return edgeInsets;
}

- (CGFloat)minimumLineSpacingForSection:(NSInteger)section
{
    CGFloat minimumLineSpacing = self.minimumLineSpacing;
    if ([self.lb_layoutDelegate respondsToSelector:@selector(collectionView:customLayout:minimumLineSpacingForSectionAtIndex:)]) {
        minimumLineSpacing = [self.lb_layoutDelegate collectionView:self.collectionView customLayout:self minimumLineSpacingForSectionAtIndex:section];
    }
    return minimumLineSpacing;
}

- (CGFloat)minimumInteritemSpacingForSection:(NSInteger)section
{
    CGFloat minimumInteritemSpacing = self.minimumInteritemSpacing;
    if ([self.lb_layoutDelegate respondsToSelector:@selector(collectionView:customLayout:minimumInteritemSpacingForSectionAtIndex:)]) {
        minimumInteritemSpacing = [self.lb_layoutDelegate collectionView:self.collectionView customLayout:self minimumInteritemSpacingForSectionAtIndex:section];
    }
    return minimumInteritemSpacing;
}

- (NSInteger)columnNumAtSection:(NSInteger)section
{
    NSInteger columnOfSection = 1;
    if ([self.lb_layoutDelegate respondsToSelector:@selector(collectionView:customLayout:columnNumberAtSection:)]) {
        columnOfSection = [self.lb_layoutDelegate collectionView:self.collectionView customLayout:self columnNumberAtSection:section];
        return columnOfSection;
    } else {
#if DEBUG
        NSAssert(NO, @"未设置列数");
#endif
    }
    return 1;
}

- (void)vv_registerDecorationViews
{
    if (self.lb_layoutDelegate
        && [self.lb_layoutDelegate respondsToSelector:@selector(decorationViewClasses)]) {
        NSArray <Class>*classes = [self.lb_layoutDelegate decorationViewClasses];
        for (Class decorationViewClass in classes) {
            if ([decorationViewClass isSubclassOfClass:[UICollectionReusableView class]]) {
                [self registerClass:decorationViewClass forDecorationViewOfKind:NSStringFromClass([decorationViewClass class])];
            }
        }
    }
}

- (void)mutableArray:(NSMutableArray *)results addDecorationViewAttributWithIndexPath:(NSIndexPath *)indexPath
{
    if (self.lb_layoutDelegate
        && [self.lb_layoutDelegate respondsToSelector:@selector(decorationViewClassOfIndexPath:)]) {
        Class decorationViewClass = [self.lb_layoutDelegate decorationViewClassOfIndexPath:indexPath];
        if ([decorationViewClass isSubclassOfClass:[UICollectionReusableView class]]) {
            [results addObject:[self layoutAttributesForDecorationViewOfKind: NSStringFromClass([decorationViewClass class]) atIndexPath:indexPath]];
        }
    }
}

- (CGFloat)originYofSection:(NSUInteger)section
{
    CGFloat origin_y = 0;
    for (NSUInteger index = 0; index < section; index++) {
      CGFloat sectionHeight = [self.heightOfSections[index] floatValue];
        origin_y += sectionHeight;
    }
    return origin_y;
}

@end

使用方法



///初始化,并设置代理
- (UICollectionView *)collectionView
{
    if (!_collectionView) {
        LBWaterFallLayout *layout = [[LBWaterFallLayout alloc] init];
        layout.lb_layoutDelegate = self;
         
         _collectionView = [[UICollectionView alloc]initWithFrame:CGRectZero collectionViewLayout:layout];
         if (@available(iOS 10.0, *)) {
             _collectionView.prefetchingEnabled = NO;
         } else {
             // Fallback on earlier versions
         }
         _collectionView.scrollEnabled = YES;
         _collectionView.dataSource = self;
         _collectionView.delegate = self;
         _collectionView.showsVerticalScrollIndicator = NO;
         _collectionView.showsHorizontalScrollIndicator = NO;
        if (@available(iOS 11.0, *)) {
            _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
        } else {
            // Fallback on earlier versions
        }
         _collectionView.backgroundColor = [UIColor redColor];
        [_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:NSStringFromClass([UICollectionViewCell class])];
    }
    return _collectionView;
}

/// 实现代理方法
#pragma mark - VVCollectionCustomLayoutDelegate
/// 每个区多少列
- (NSInteger)collectionView:(UICollectionView *)collectionView customLayout:(UICollectionViewLayout *)collectionViewLayout columnNumberAtSection:(NSInteger )section
{
    LBSectionConfigModel *layout = self.sectionConfigArray[section];
    return layout.colomnsNum;
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView customLayout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
    LBSectionConfigModel *layout = self.sectionConfigArray[section];
    return layout.sectionEdgeInsets;
}

- (CGFloat)collectionView:(UICollectionView *)collectionView customLayout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
    LBSectionConfigModel *layout = self.sectionConfigArray[section];
    return layout.lineSpace;
}

- (CGFloat)collectionView:(UICollectionView *)collectionView customLayout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
    LBSectionConfigModel *layout = self.sectionConfigArray[section];
    return layout.itemSpace;
}

- (CGSize)collectionView:(UICollectionView *)collectionView customLayout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
{
    LBSectionConfigModel *layout = self.sectionConfigArray[section];
    return CGSizeMake(collectionView.contentSize.width, layout.headerHeight);
}

- (CGSize)collectionView:(UICollectionView *)collectionView customLayout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section
{
    LBSectionConfigModel *layout = self.sectionConfigArray[section];
    return CGSizeMake(collectionView.contentSize.width, layout.footerHeight);
}

- (CGSize)collectionView:(UICollectionView *)collectionView customLayout:(LBWaterFallLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *title = self.dataArray[indexPath.item % self.dataArray.count];
    CGFloat height = [title boundingRectWithSize:CGSizeMake(CGRectGetWidth(self.view.bounds)/2 - 30, 200) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:20]} context:nil].size.height;
    return CGSizeMake(CGRectGetWidth(self.view.bounds)/2 - 30, height + 150);
    //return CGSizeMake(, 100);
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值