iOS UICollectionView自定义流水布局

UICollectionViewFlowLayout

1:直接设置FlowLayout对象

创建UICollectionViewLayout对象,通过设置UICollectionViewLayout对象属性的值可以设置item的基本布局,包括大小,间距,内边距等。

    UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
    flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical; // 滑动方向
    flowLayout.minimumLineSpacing = 8;    // 行间距
    flowLayout.minimumInteritemSpacing = 6;    // 列间距
    flowLayout.itemSize = CGSizeMake(kScreenWidth/2, 90);  // item 大小
    flowLayout.sectionInset = UIEdgeInsetsMake(20, 15, 20, 15); // 内边距

2:通过UICollectionViewDelegateFlowLayout代理方法返回

// item 大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
// 每个分区的内边距
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
// 行间距
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
// 列间距
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
// SectionHeader 大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
// SectionFooter 大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section;

注意:第一种和第二种方法可以混合使用,但不要同时使用。例如内边距行列间距通过FlowLayout对象设置,itemSize 通过代理动态返回。但是不要通过FlowLayout对象设置了行列间距之后, 还实现代理方法返回行列间距。同时使用不会报错,但没意义消耗系统资源。

3:自定义布局

当系统的布局无论你怎么设置都满足不了需求的时候,就需要用到自定义布局了。我们需要继承UICollectionViewFlowLayout,重写其计算布局的方法,来打到预期的效果。

iOS UICollectionView自定义流水布局_第1张图片

上图中,上半部分是系统默认的布局,下半部分是我自定义布局实现的。我们注意到系统布局是将 item 两端对齐,间距根据剩余的宽度自己缩放。UICollectionViewFlowLayout对象的minimumInteritemSpacing说的很清楚,我们设置的是最小间距。但是 UI 小姐姐希望间距相等,这个时候系统的布局就实现不了了。

自定义布局的具体实现

首先继承UICollectionViewFlowLayout不必多说,然后就是重写其布局方法。
1:重写prepareLayout,每次更新布局的时候collectionview都会首先调用这个方法,为将要开始的更新做准备,我们需要在此处计算好必要的布局信息并存储起来

- (void)prepareLayout{

    [super prepareLayout];

    NSMutableArray *layoutInfoArr = [NSMutableArray array];
    // 获取布局信息
    NSInteger numberOfSections = [self.collectionView numberOfSections];
    for (NSInteger section = 0; section < numberOfSections; section++){
        NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:section];
        NSMutableArray *subArr = [NSMutableArray arrayWithCapacity:numberOfItems];
        for (NSInteger item = 0; item < numberOfItems; item++){
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
            UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
            [subArr addObject:attributes];
        }

        // 添加到二维数组
        [layoutInfoArr addObject:[subArr copy]];
    }
    // 存储布局信息
    self.attributesArray = [layoutInfoArr copy];
}

2:重写并调用layoutAttributesForItemAtIndexPath方法计算布局

// 根据自己的需求,计算每一个item的布局,如果 itemSize是动态的,可以配合代理方法拿到当前 indexPath 的大小
// 这里只自定义了item的布局
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{

    // 拿到系统为我们计算的布局
    UICollectionViewLayoutAttributes *oldAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];

    // 创建一个我们期望的布局
    UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    CGFloat itemX = self.sectionInset.left;         // 默认X值
    CGFloat itemY = oldAttributes.frame.origin.y;   // Y值直接用系统算的
    CGSize itemSize = oldAttributes.size;           // 大小直接代理返回的

    // 无需换行 && indexPath.row !=0 调整X值  (indexPath.row=0时 self.lastFrame 还未赋值)  
    if (oldAttributes.frame.origin.x != itemX && indexPath.row != 0) {
        itemX = self.lastFrame.origin.x + self.lastFrame.size.width + self.minimumInteritemSpacing;
    }

    // 赋值
    attributes.frame = CGRectMake(itemX, itemY, itemSize.width, itemSize.height);

    // 更新上一个item的位置
    self.lastFrame = CGRectMake(itemX, itemY, itemSize.width, itemSize.height);
    return attributes;
}

3:重写layoutAttributesForElementsInRect:方法返回指定区域cell、Supplementary View和Decoration View的布局属性。

// 找出了与指定区域有交接的UICollectionViewLayoutAttributes对象放到一个数组中,然后返回
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    NSMutableArray *layoutAttributesArr = [NSMutableArray array];
    [self.attributesArray enumerateObjectsUsingBlock:^(NSArray *array, NSUInteger i, BOOL * _Nonnull stop) {
        [array enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if(CGRectIntersectsRect(obj.frame, rect)) { // 如果 item 在rect内
                [layoutAttributesArr addObject:obj];
            }
        }];
    }];
    return layoutAttributesArr;
}

4:如果需要自定义分区头部和尾部可以重写下面两个方法,并在prepareLayout里面做相应的处理

- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;

- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath;

5:重写- (CGSize)collectionViewContentSize方法,返回 contentSize。注意这个方法返回的尺寸是给UICollectionView的父类UIScrollView作为contentSize ,不是UICollectionView的视图尺寸。正是因为这一点,我们自定义layout如果想让它只能横向滑动,只需要将这个 size.height 设置成 collectionView.height 就行了。 这个方法会多次调用,所以最好是在prepareLayout里就计算好,设置为全局变量,直接返回

- (CGSize)collectionViewContentSize{
    return self.contentSize;
}

做完这些就可以实现上图的效果了

全部代码

#import "LGCollectionViewFlowLayout.h"

@interface LGCollectionViewFlowLayout ()
@property (nonatomic, assign) CGRect lastFrame; // 上一个item的 布局
@property (nonatomic, strong) NSMutableArray *attributesArray;
@end

@implementation LGCollectionViewFlowLayout

- (void)prepareLayout{

    [super prepareLayout];

    NSMutableArray *layoutInfoArr = [NSMutableArray array];
    // 获取布局信息
    NSInteger numberOfSections = [self.collectionView numberOfSections];
    for (NSInteger section = 0; section < numberOfSections; section++){
        NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:section];
        NSMutableArray *subArr = [NSMutableArray arrayWithCapacity:numberOfItems];
        for (NSInteger item = 0; item < numberOfItems; item++){
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
            UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
            [subArr addObject:attributes];
        }

        // 添加到二维数组
        [layoutInfoArr addObject:[subArr copy]];
    }
    // 存储布局信息
    self.attributesArray = [layoutInfoArr copy];
}

// 直接使用系统计算的大小 不做更新了
//- (CGSize)collectionViewContentSize{
//    return self.contentSize;
//}


// 找出了与指定区域有交接的UICollectionViewLayoutAttributes对象放到一个数组中,然后返回
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    NSMutableArray *layoutAttributesArr = [NSMutableArray array];
    [self.attributesArray enumerateObjectsUsingBlock:^(NSArray *array, NSUInteger i, BOOL * _Nonnull stop) {
        [array enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if(CGRectIntersectsRect(obj.frame, rect)) { // 如果 item 在rect内
                [layoutAttributesArr addObject:obj];
            }
        }];
    }];
    return layoutAttributesArr;
}


// 根据自己的需求,计算每一个item的布局,如果 itemSize是动态的,可以配合代理方法拿到当前 indexPath 的大小
// 这里只自定义了item的布局
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{

    // 拿到系统为我们计算的布局
    UICollectionViewLayoutAttributes *oldAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];

    // 创建一个我们期望的布局
    UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    CGFloat itemX = self.sectionInset.left;         // 默认X值
    CGFloat itemY = oldAttributes.frame.origin.y;   // Y值直接用系统算的
    CGSize itemSize = oldAttributes.size;           // 大小直接代理返回的

    // 同一行
    BOOL line = oldAttributes.frame.origin.y == self.lastFrame.origin.y;

    // 不换行 && (indexPath.row=0时  self.lastFrame 还未赋值)  调整X值
    if (oldAttributes.frame.origin.x != itemX && indexPath.row != 0 && line) {
        itemX =  self.lastFrame.origin.x + self.lastFrame.size.width + self.minimumLineSpacing;
    }

    // 赋值
    attributes.frame = CGRectMake(itemX, itemY, itemSize.width, itemSize.height);

    // 更新上一个item的位置
    self.lastFrame = CGRectMake(itemX, itemY, itemSize.width, itemSize.height);
    return attributes;
}

- (NSMutableArray *)attributesArray{
    if (!_attributesArray) {
        _attributesArray = [NSMutableArray array];
    }
    return _attributesArray;
}
@end

使用的时候直接换一下类名就好了

    // itemSize 是通过代理动态计算的
    LGCollectionViewFlowLayout *flowLayout = [[LGCollectionViewFlowLayout alloc] init];
    flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;
    flowLayout.minimumLineSpacing = 8;
    flowLayout.minimumInteritemSpacing = 6;
    flowLayout.sectionInset = UIEdgeInsetsMake(20, 15, 20, 15);

参考:https://www.cnblogs.com/daxueshan/p/6222000.html

你可能感兴趣的:(学习笔记)