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

本人菜鸟小白,最近研究了下UICollectionView自定义布局实现瀑布流等布局,主要是应对公司需求,产品这么设计我也很无奈啊,初次写文章,如有不对之处,欢迎大家提出,谢谢。(笔者第一次写,发现排版布局太low,所以又写了一版markdown版本,供小伙伴参考查阅markdown)。

github地址

竖向等宽等间隔瀑布流

先上一张效果图


瀑布流

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

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

CandyFlowLayoutDelegate

.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(NSIntegerj =0; j < numberOfItem; j ++) {

            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];

            UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

            //找出每行最短的一列

            NSIntegerminIndex =0;

            CGFloat minY = [self.itemHeights[0] floatValue];

            for(NSIntegern =1; n

                // 依次取出高度

                CGFloatitemY = [self.itemHeights[n]floatValue];

                if(minY > itemY) {

                    minY = itemY;

                    minIndex = n;

                }

            }


            CGFloatxOffset =self.sectionInsets.left+ minIndex * (width +self.minItemSpacing);

            CGFloatheight =0;

            if(self.delegate&& [self.delegaterespondsToSelector:@selector(heightForItemAtIndexPath:)]) {

                height = [self.delegateheightForItemAtIndexPath:indexPath];

            }

            CGFloatyOffset = minY;

            if(yOffset !=self.sectionInsets.top) {

                // 不是第一行,要加间隔

                yOffset +=self.minLineSpacing;

            }


            // 更新高度

            self.itemHeights[minIndex] =@(height + yOffset);

            // 更新contentSize height

            CGFloatmaxHeight = [self.itemHeights[minIndex]floatValue];

            if(self.contentMaxHeight< maxHeight) {

                // 最短的一列 + 高度 > 之前的最高高度

                self.contentMaxHeight = maxHeight + self.sectionInsets.bottom;

            }

            attribute.frame=CGRectMake(xOffset, yOffset, width, height);

            [self.itemAttributesaddObject:attribute];

        }

    }

}

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

首先设置初始默认值

itemHeights默认值

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

最短列

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

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

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

等高等间隔不等宽

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

- (void)createSameHeightItemAttributes {

    self.contentMaxHeight = 0;

    // 每行实际的宽度

    CGFloat realWidth = ScreenWidth - self.sectionInsets.left - self.sectionInsets.right;

    CGFloatxOffset =0;

    CGFloatyOffset =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(NSIntegerj =0; j < numberOfItem; j ++) {

            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];

            UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];


            CGSizesize =CGSizeZero;

            if (self.delegate && [self.delegate respondsToSelector:@selector(sizeForItemAtIndexPath:)]) {

                size = [self.delegatesizeForItemAtIndexPath:indexPath];

            }

            CGFloatwidth = size.width;

            CGFloatheight = 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.itemAttributesaddObject: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;

    CGFloatxOffset =0;

    CGFloatyOffset =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(NSIntegerj =0; j < numberOfItem; j ++) {

            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];

            UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

            CGSizesize =CGSizeZero;

            if (self.delegate && [self.delegate respondsToSelector:@selector(sizeForItemAtIndexPath:)]) {

                size = [self.delegatesizeForItemAtIndexPath:indexPath];

            }


            if(xOffset + size.width> realWidth) {

                // 换行,超过一行

                // 取出每个secction的第一个

                UICollectionViewLayoutAttributes *firstAttribute = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:i]];

                CGRectframe = 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.itemAttributesaddObject:attribute];

        }

    }

}

换行之处已添加注释,重设x,y值即可,判断换行条件相同。

以上的方法都包含了双层for循环嵌套,如有小伙伴不喜欢太多嵌套,将循环内容代码添加至- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath方法即可,原理都是一样的,看喜欢哪种代码书写方式。

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

你可能感兴趣的:(iOS 自定义UICollectionViewFlowLayout,实现瀑布流布局)