iOS 自定义UICollectionViewFlowLayout,实现瀑布流布局

本人菜鸟小白,最近研究了下UICollectionView自定义布局实现瀑布流等布局,主要是应对公司需求,产品这么设计我也很无奈啊,初次写文章,如有不对之处,欢迎大家提出,谢谢。

github地址

竖向等宽等间隔瀑布流

先上一张效果图
瀑布流

笔者自定义了CandyFlowLayout继承自UICollectionViewFlowLayout,自定义了几个属性,其实就是UICollectionViewFlowLayout的属性,只是重新命名了而已。

@interface CandyFlowLayout : UICollectionViewFlowLayout

/** default 0 */
@property (nonatomic, assign) UIEdgeInsets sectionInsets;
/** default 0  左右*/
@property (nonatomic, assign) CGFloat minItemSpacing;
/** default 0  上下*/
@property (nonatomic, assign) CGFloat minLineSpacing;

@property (nonatomic, assign) CandyFlowLayoutStyle style;

@property (nonatomic, weak) id<CandyFlowLayoutDelegate> delegate;

/** 瀑布流每行item总数,宽度等分 */
@property (nonatomic, assign) NSInteger waterfallRowNumber;

- (instancetype)initSectionInsets:(UIEdgeInsets)sectionInsets minItemSpacing:(CGFloat)minItemSpacing minLineSpacing:(CGFloat)minLineSpacing;

并自定义了初始化方法。其中CandyFlowLayoutDelegate协议主要实现两个方法

@protocol CandyFlowLayoutDelegate <NSObject>

@optional

/** 返回item size */
- (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath;

/** 返回item height 瀑布流时使用 */
- (CGFloat)heightForItemAtIndexPath:(NSIndexPath *)indexPath;

@end

.m文件主要实现几个方法就能自定义布局

  • (void)prepareLayout // 一定要实现此方法,笔者将布局信息全部在此重写,当然也可以写到每个item的布局方法中,也就是- (UICollectionViewLayoutAttributes)layoutAttributesForItemAtIndexPath:(NSIndexPath)indexPath方法中,效果等同。

  • (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect // 返回存放所有item的布局信息数组

  • (UICollectionViewLayoutAttributes)layoutAttributesForItemAtIndexPath:(NSIndexPath)indexPath // 返回单个item的布局信息

  • (CGSize)collectionViewContentSize // 返回正确的contentSize,这样就可以在外部得到contentSize,笔者主要是应对collectionView无滑动效果设置正确的height=contentsize.height。此方法可以不用重写。
    接下来看下竖向等宽等间隔瀑布流布局代码:

- (void)createWaterfallItemAttributes {
    self.contentMaxHeight = 0;
    [self.itemHeights removeAllObjects];
    for (NSInteger i = 0; i < self.waterfallRowNumber; i ++) {
        // 默认都是top
        [self.itemHeights addObject:@(self.sectionInsets.top)];
    }
    
    // 计算item width
    CGFloat width = (ScreenWidth - self.sectionInsets.left - self.sectionInsets.right - (self.waterfallRowNumber - 1) * self.minItemSpacing) / self.waterfallRowNumber * 1.0;
    
    for (NSInteger i = 0; i < self.numberOfSection; i ++) {
        NSInteger numberOfItem = [self.collectionView numberOfItemsInSection:i];
        for (NSInteger j = 0; j < numberOfItem; j ++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
            UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
            //找出每行最短的一列
            NSInteger minIndex = 0;
            CGFloat minY = [self.itemHeights[0] floatValue];
            for (NSInteger n = 1; n < self.waterfallRowNumber; n ++) {
                // 依次取出高度
                CGFloat itemY = [self.itemHeights[n] floatValue];
                if (minY > itemY) {
                    minY = itemY;
                    minIndex = n;
                }
            }
            
            CGFloat xOffset = self.sectionInsets.left + minIndex * (width + self.minItemSpacing);
            CGFloat height = 0;
            if (self.delegate && [self.delegate respondsToSelector:@selector(heightForItemAtIndexPath:)]) {
                height = [self.delegate heightForItemAtIndexPath:indexPath];
            }
            CGFloat yOffset = minY;
            if (yOffset != self.sectionInsets.top) {
                // 不是第一行,要加间隔
                yOffset += self.minLineSpacing;
            }
            
            // 更新高度
            self.itemHeights[minIndex] = @(height + yOffset);
            // 更新contentSize height
            CGFloat maxHeight = [self.itemHeights[minIndex] floatValue];
            if (self.contentMaxHeight < maxHeight) {
                // 最短的一列 + 高度 > 之前的最高高度
                self.contentMaxHeight = maxHeight + self.sectionInsets.bottom;
            }
            attribute.frame = CGRectMake(xOffset, yOffset, width, height);
            [self.itemAttributes addObject:attribute];
        }
    }
}

主要思路:找出每行最短的一列,将下一个item置于此列下方。那怎样找出最短的一列呢?笔者用数组itemHeights来记录每列的高度。
1.首先设置初始默认值

for (NSInteger i = 0; i < self.waterfallRowNumber; i ++) {
        // 默认都是top
        [self.itemHeights addObject:@(self.sectionInsets.top)];
    }

2.两个for循环嵌套即可遍历每个item

//找出每行最短的一列
NSInteger minIndex = 0;
CGFloat minY = [self.itemHeights[0] floatValue];
for (NSInteger n = 1; n < self.waterfallRowNumber; n ++) {
    // 依次取出高度
    CGFloat itemY = [self.itemHeights[n] floatValue];
    if (minY > itemY) {
        minY = itemY;
        minIndex = n;
    }
 }

找出最短列的方法如上,minIndex即最短列所在的列数。此时最难点已经解决,下面就是设置frame大小即可。注意设置完每个item大小,要更新itemHeights数据。笔者稍后会上传完整代码。

等高等间隔不等宽的排列布局

笔者主要用于类型筛选,每个文字宽度不等并且换行,先上一张效果图:
等高等间隔不等宽

此布局最主要的难点就在于何时换行,换行之后的y如何设置,下面贴出代码:

- (void)createSameHeightItemAttributes {
    self.contentMaxHeight = 0;
    // 每行实际的宽度
    CGFloat realWidth = ScreenWidth - self.sectionInsets.left - self.sectionInsets.right;
    CGFloat xOffset = 0;
    CGFloat yOffset = 0;
    for (NSInteger i = 0; i < self.numberOfSection; i ++) {
        NSInteger numberOfItem = [self.collectionView numberOfItemsInSection:i];
        xOffset = self.sectionInsets.left;
        yOffset = self.sectionInsets.top + self.contentMaxHeight;
        for (NSInteger j = 0; j < numberOfItem; j ++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
            UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
            
            CGSize size = CGSizeZero;
            if (self.delegate && [self.delegate respondsToSelector:@selector(sizeForItemAtIndexPath:)]) {
                size = [self.delegate sizeForItemAtIndexPath:indexPath];
            }
            CGFloat width = size.width;
            CGFloat height = size.height;
            
            if (xOffset + width > realWidth) {
                // 换行
                xOffset = self.sectionInsets.left;
                yOffset = yOffset + self.minLineSpacing + height;
                attribute.frame = CGRectMake(xOffset, yOffset, width, height);
                xOffset = xOffset + width + self.minItemSpacing;
                // 更新contentSize height
                self.contentMaxHeight = yOffset + height + self.sectionInsets.bottom;
            } else {
                attribute.frame = CGRectMake(xOffset, yOffset, width, height);
                xOffset = xOffset + width + self.minItemSpacing;
                // 更新contentSize height
                self.contentMaxHeight = yOffset + height + self.sectionInsets.bottom;
            }
            
            [self.itemAttributes addObject:attribute];
        }
    }
}

注意之处:判断换行的关键,实际宽度 ScreenWidth - self.sectionInsets.left - self.sectionInsets.right,换行之后x,y的值要设置正确,其余无难点。

特殊处理-首行带有类型名称或者全部等

产品大大要这么设计,笔者只能照办了,先来张效果图:

其实也挺常见的,类型筛选或者展示时,时常带有标题或者全部字样。只需要简单处理下,再换行的时候空出每个section第一个item的宽度距离即可,下面上代码:

- (void)createSpecialItemAttributes {
    self.contentMaxHeight = 0;
    CGFloat realWidth = ScreenWidth - self.sectionInsets.left - self.sectionInsets.right;
    CGFloat xOffset = 0;
    CGFloat yOffset = 0;
    for (NSInteger i = 0; i < self.numberOfSection; i ++) {
        NSInteger numberOfItem = [self.collectionView numberOfItemsInSection:i];
        xOffset = self.sectionInsets.left;
        yOffset = self.sectionInsets.top + self.contentMaxHeight;
        for (NSInteger j = 0; j < numberOfItem; j ++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
            UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
            CGSize size = CGSizeZero;
            if (self.delegate && [self.delegate respondsToSelector:@selector(sizeForItemAtIndexPath:)]) {
                size = [self.delegate sizeForItemAtIndexPath:indexPath];
            }
            
            if (xOffset + size.width > realWidth) {
                // 换行,超过一行
                // 取出每个secction的第一个
                UICollectionViewLayoutAttributes *firstAttribute = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:i]];
                CGRect frame = firstAttribute.frame;
                // x偏移,空出第一个width
                xOffset = CGRectGetMaxX(frame) + self.minItemSpacing;
                yOffset = yOffset + size.height + self.minLineSpacing;
                attribute.frame = CGRectMake(xOffset, yOffset, size.width, size.height);
                xOffset = xOffset + size.width + self.minItemSpacing;
                self.contentMaxHeight = CGRectGetMaxY(attribute.frame) + self.sectionInsets.bottom;
            } else {
                attribute.frame = CGRectMake(xOffset, yOffset, size.width, size.height);
                xOffset = xOffset + size.width + self.minItemSpacing;
                self.contentMaxHeight = CGRectGetMaxY(attribute.frame) + self.sectionInsets.bottom;
            }
        
            [self.itemAttributes addObject:attribute];
        }
    }
}

换行之处已添加注释,重设x,y值即可,判断换行条件相同。
以上的方法都包含了双层for循环嵌套,如有小伙伴不喜欢太多嵌套,将循环内容代码添加至- (UICollectionViewLayoutAttributes)layoutAttributesForItemAtIndexPath:(NSIndexPath)indexPath方法即可,原理都是一样的,看喜欢哪种代码书写方式。

笔者也是小白,正好多次用到了UICollectionViewFlowLayout自定义布局,所以就写篇文章记录一下,供有需要的小伙伴参考,如有错误之处,希望各位不吝赐教哈!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容