本人菜鸟小白,最近研究了下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 delegate;
/** 瀑布流每行item总数,宽度等分 */
@property (nonatomic, assign) NSInteger waterfallRowNumber;
- (instancetype)initSectionInsets:(UIEdgeInsets)sectionInsets minItemSpacing:(CGFloat)minItemSpacing minLineSpacing:(CGFloat)minLineSpacing;
并自定义了初始化方法。其中CandyFlowLayoutDelegate协议主要实现两个方法
@protocol CandyFlowLayoutDelegate
@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自定义布局,所以就写篇文章记录一下,供有需要的小伙伴参考,如有错误之处,希望各位不吝赐教哈!