39

UICollectionView的灵活布局 --从一个需求谈起

 5 years ago
source link: http://www.cocoachina.com/ios/20190402/26705.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一切的起源在于一个这样的布局需求。

JbYrQ3Q.png!web

首先就想到collectionView。用tableView也能强行实现这个就是了,但是比较笨重,改动布局就得重画cell,所以本文就详细介绍下我怎么实现这个需求的。

此处先放点新手福利

如果你没接触过UICollectionView,但对UITableView比较熟悉的,可以看下这段,熟悉UICollectionView可以直接跳过。连UITableView都不熟的建议从基础开始学。

绘制UICollectionView类似于UITableView,满足delegate和dataSouce两个代理。

注意的区别是:

1、UICollectionView的indexPath,通常使用item的属性,印象中row的属性跟UITableView不一样,和section毫无关系。

2、每个section的head和footViewz在这个代理里实现。

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;

这个view可以像cell一样复用的。在初始化时注册:

[self.collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"UICollectionElementKindSectionHeader"];

使用时:

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { 
   UICollectionReusableView *reusableview = nil;
   if (kind == UICollectionElementKindSectionHeader) {
       UICollectionReusableView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"UICollectionElementKindSectionHeader" forIndexPath:indexPath];
       headerView.backgroundColor = [UIColor redColor];
       reusableview = headerView;
}
return reusableview;

3、UICollectionView的cell选中的时候是有backgroundView和selectedBackgroundView两个属性的,可以做出一些选择效果:

@property (nonatomic, strong, nullable) UIView *backgroundView;
@property (nonatomic, strong, nullable) UIView *selectedBackgroundView;

4、每个UICollectionView初始化都要有个UICollectionViewLayout来实现布局,用系统自带的布局UICollectionViewDelegateFlowLayout的话,满足这个代理设置高度:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath

UICollectionViewDelegateFlowLayout里还有好几个参数和其他的代理,有兴趣的同学可以去看看api,然后写个demo测试一下,因为蛮简单的,这里就不再深究了。。

新手福利结束

回归正题

现在回到我我们的需求上。

满足这个需求我们必须重写UICollectionViewLayout。

核心重写的方法如下:

//每次布局都会调用
- (void)prepareLayout;
//布局完成后设置contentSize
- (CGSize)collectionViewContentSize;
//返回每个item的属性
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
//返回所有item属性
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 

当然最重要的是,你的一些参数得通过代理或者Block传出来赋值。

/*
*  获取item宽高
*
*  @param block 返回宽高的block
*/
- (void)calculateItemSizeWithWidthBlock:(CGSize (^)(NSIndexPath *indexPath))block;

下面是代码,注释丰富,可以放心阅读:

HomeCollectionLayout.h:

typedef CGSize(^SizeBlock)(NSIndexPath *indexPath);

@interface HomeCollectionLayout : UICollectionViewLayout

/** 行间距 */
@property (nonatomic, assign) CGFloat rowSpacing;
/** 列间距 */
@property (nonatomic, assign) CGFloat lineSpacing;
/** 内边距 */
@property (nonatomic, assign) UIEdgeInsets sectionInset;

/*
*  获取item宽高
*
*  @param block 返回宽高的block
*/
- (void)calculateItemSizeWithWidthBlock:(CGSize (^)(NSIndexPath *indexPath))block;

HomeCollectionLayout.m

@interface HomeCollectionLayout()
/** 计算每个item高度的block,必须实现*/
@property (nonatomic, copy) SizeBlock block;

/** 存放元素高宽的键值对 */
@property (nonatomic, strong) NSMutableArray *arrOfSize;
/**存放所有item的attrubutes属性 */
@property (nonatomic, strong) NSMutableArray *array;
/**存放所有section的高度的 */
@property (nonatomic, strong) NSMutableArray *arrOfSectionHeight;

/**总section高度,用于直接输出contentSize */
@property (nonatomic,assign) CGFloat collectionSizeHeight;
/**总共item个数 */
@property (nonatomic,assign) NSInteger itemCount;

@property (nonatomic,assign) CGFloat collectionWidth;

@end

@implementation HomeCollectionLayout
- (instancetype)init
{
    self = [super init];
    if (self) {
        //对默认属性进行设置
        _arrOfSize = [NSMutableArray array];
        _array = [NSMutableArray array];
        _arrOfSectionHeight = [NSMutableArray array];

        self.itemCount = 0;

        self.collectionSizeHeight = 0;

        self.sectionInset = UIEdgeInsetsMake(2, 0, 0, 0);

        self.lineSpacing = 1;
        self.rowSpacing = 1;
    }
    return self;
}

/**
 *  准备好布局时调用
 */
- (void)prepareLayout {
    [super prepareLayout];

    //reload的时候清空原有数据
    [_array removeAllObjects];
    [_arrOfSize removeAllObjects];
    [_arrOfSectionHeight removeAllObjects];
    _collectionSizeHeight = 0;
    _itemCount = 0;

    NSInteger sectionCount = [self.collectionView numberOfSections];
    //根据每个indexPath储存
    for (NSInteger i = 0 ; i < sectionCount; i++) {
        NSInteger rowCount = [self.collectionView numberOfItemsInSection:i];
        //存储item的总数目
        self.itemCount += rowCount;
        //存储每个列数的长度
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];

        //计算该section列数
        NSInteger lines = 0;
        CGSize size = CGSizeZero;
        if (self.block != nil) {
            size = self.block([NSIndexPath indexPathForRow:0 inSection:i]);
        }else{
            NSAssert(size.width != 0 ,@"未实现block");
        }
        lines = self.collectionWidth/size.width;

        //存储每个列数的长度
        for (NSInteger k = 0; k < lines; k++) {
            [dict setObject:@(self.sectionInset.top) forKey:[NSString stringWithFormat:@"%ld",(long)k]];
        }
        [_arrOfSize addObject:dict];

        for (NSInteger j = 0; j < rowCount; j++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
            //调用item计算。
            [_array addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
        }

        //此时dict已经改变
        NSMutableDictionary *mdict = _arrOfSize[i];
        //计算每个section的高度
        __block NSString *maxHeightline = @"0";
        [mdict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *obj, BOOL *stop) {
            if ([mdict[maxHeightline] floatValue] < [obj floatValue] ) {
                maxHeightline = key;
            }
        }];
        [self.arrOfSectionHeight addObject:mdict[maxHeightline]];
        self.collectionSizeHeight += [mdict[maxHeightline] floatValue];

        NSLog(@"\ncontentSize = %@ height = %f\n\n",NSStringFromCGSize(CGSizeMake(self.collectionView.bounds.size.width, self.collectionSizeHeight)),[mdict[maxHeightline] floatValue]);
    }
}
/**
 *  设置可滚动区域范围
 */
- (CGSize)collectionViewContentSize {
    return CGSizeMake(self.collectionView.bounds.size.width, self.collectionSizeHeight);
}
/**
 *  计算indexPath下item的属性的方法
 *
 *  @return item的属性
 */
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{

    //创建item的属性
    UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    CGSize size = CGSizeZero;
    if (self.block != nil) {
        size = self.block(indexPath);
    }else{
        NSAssert(size.width != 0 ,@"未实现block");
    }
    CGRect frame;
    frame.size = size;

    NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[_arrOfSize objectAtIndex:indexPath.section]];
    //循环遍历找出高度最短行
    __block NSString *lineMinHeight = @"0";
    [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *obj, BOOL *stop) {
        if ([dict[lineMinHeight] floatValue] > [obj floatValue]) {
            lineMinHeight = key;
        }
    }];
    int line = [lineMinHeight intValue];


    //找出最短行后,计算item位置
    frame.origin = CGPointMake(line * (size.width + self.lineSpacing), [dict[lineMinHeight] floatValue] + self.collectionSizeHeight);
    dict[lineMinHeight] = @(frame.size.height + self.rowSpacing + [dict[lineMinHeight] floatValue]);
    //存储高度
    [_arrOfSize replaceObjectAtIndex:indexPath.section withObject:dict];
    attr.frame = frame;

    NSLog(@"\nframe = %@,indexPath = %@\n\n",NSStringFromCGRect(frame),indexPath);



    return attr;
}
/**
 *  返回视图框内item的属性,可以直接返回所有item属性
 */
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    return _array;
}

#pragma mark - data source

/**
 *  设置计算高度block方法
 *
 *  @param block 计算item高度的block
 */
- (void)calculateItemSizeWithWidthBlock:(CGSize (^)(NSIndexPath *indexPath))block {
    if (self.block != block) {
        self.block = block;
    }
}

#pragma mark - getter & setter
- (CGFloat)collectionWidth {
    return self.collectionView.frame.size.width;
}
@end

demo地址: 本文demo

小结:跟一般瀑布流不同,这种布局collectionItem的size全部要自己定制,比起强行画来说,这么做以后更好改。就是算死我了,算法还是需要加强。

另外,这里另外一名作者 Tuberose 写了篇更详细的关于瀑布流的文章:

想更深入研究的同学可以移步这里: 瀑布流小框架

本文来自大灰灰的小专栏: https://xiaozhuanlan.com/topic/3247869501


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK