UICollectionView 02 - 自定义局篇
一,UICollectionViewLayout布局的具体思路:
-
设置
itemSzie
属性,它定义了每一个item的大小。在一个示例中通过设置layout的itemSize属性全局的设置了cell的尺寸。- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
-
设置间隔
间隔可以指定item之间的间隔和每一行之间的间隔。间隔和itemSzie
一样,既有全局属性,也可以对每一个item设定:
@property (nonatomic) CGFloat minimumLineSpacing; @property (nonatomic) CGFloat minimumInteritemSpacing; - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section; - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
-
设定滚动方向
typedef NS_ENUM(NSInteger, UICollectionViewScrollDirection) { UICollectionViewScrollDirectionVertical, UICollectionViewScrollDirectionHorizontal };
-
设置Header和Footer的尺寸
设置Header和Footer的尺寸也分为全局和局部。在这里需要注意滚动的方向,滚动方向不同,header和footer的宽度和高度只有一个会起作用。垂直滚动时section间宽度为尺寸的高。
@property (nonatomic) CGSize headerReferenceSize; @property (nonatomic) CGSize footerReferenceSize; - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section; - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section;
-
设置内边距
@property (nonatomic) UIEdgeInsets sectionInset; - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
二,
用UICollectionView实现瀑布流
实现瀑布流的方式有几种,但是比较简单的是通过UICollectionView,因为collectionView自己会实现cell的循环利用,所以自己不用实现循环利用的机制。瀑布就最重要的就是布局,要选取最短的那一列来排布,保证每一列之间的间距不会太大。
实现步骤
-
自定义继承自UICollectionViewLayout的子类来进行实现布局
- 调用
- (void)prepareLayout
进行初始化 - 重载
- (CGSize)collectionViewContentSize
返回内容的大小 - 重载
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
方法返回rect中所有元素的布局属性,返回的是一个数组 - 重载
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
方法返回对应的indexPath的位置的cell的布局属性。 - 重载
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;
方法返回对应indexPath的位置的追加视图的布局属性,如果没有就不用重载 - 重载
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath;
方法返回对应indexPath的位置的装饰视图的布局属性,如果没有也不需要重载 - 重载
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;
当边界发生改变时,是否应该刷新。
- 调用
三,自定义UICollectionViewLayout布局的示例代码
用代理来实现对item的布局属性的控制
-
.h文件
#import <UIKit/UIKit.h> @class LMHWaterFallLayout; @protocol LMHWaterFallLayoutDeleaget<NSObject> @required /** * 每个item的高度 */ - (CGFloat)waterFallLayout:(LMHWaterFallLayout *)waterFallLayout heightForItemAtIndexPath:(NSUInteger)indexPath itemWidth:(CGFloat)itemWidth; @optional /** * 有多少列 */ - (NSUInteger)columnCountInWaterFallLayout:(LMHWaterFallLayout *)waterFallLayout; /** * 每列之间的间距 */ - (CGFloat)columnMarginInWaterFallLayout:(LMHWaterFallLayout *)waterFallLayout; /** * 每行之间的间距 */ - (CGFloat)rowMarginInWaterFallLayout:(LMHWaterFallLayout *)waterFallLayout; /** * 每个item的内边距 */ - (UIEdgeInsets)edgeInsetdInWaterFallLayout:(LMHWaterFallLayout *)waterFallLayout; @end @interface LMHWaterFallLayout : UICollectionViewLayout /** 代理 */ @property (nonatomic, weak) id<LMHWaterFallLayoutDeleaget> delegate; @end
-
.m文件
#import "LMHWaterFallLayout.h" /** 默认的列数 */ static const CGFloat LMHDefaultColunmCount = 3; /** 每一列之间的间距 */ static const CGFloat LMHDefaultColunmMargin = 10; /** 每一行之间的间距 */ static const CGFloat LMHDefaultRowMargin = 10; /** 内边距 */ static const UIEdgeInsets LMHDefaultEdgeInsets = {10,10,10,10}; @interface LMHWaterFallLayout() /** 存放所有的布局属性 */ @property (nonatomic, strong) NSMutableArray * attrsArr; /** 存放所有列的当前高度 */ @property (nonatomic, strong) NSMutableArray *columnHeights; /** 内容的高度 */ @property (nonatomic, assign) CGFloat contentHeight; - (NSUInteger)colunmCount; - (CGFloat)columnMargin; - (CGFloat)rowMargin; - (UIEdgeInsets)edgeInsets; @end @implementation LMHWaterFallLayout #pragma mark 懒加载 - (NSMutableArray *)attrsArr{ if (!_attrsArr) { _attrsArr = [NSMutableArray array]; } return _attrsArr; } - (NSMutableArray *)columnHeights{ if (!_columnHeights) { _columnHeights = [NSMutableArray array]; } return _columnHeights; } #pragma mark - 数据处理 /** * 列数 */ - (NSUInteger)colunmCount{ if ([self.delegate respondsToSelector:@selector(columnCountInWaterFallLayout:)]) { return [self.delegate columnCountInWaterFallLayout:self]; }else{ return LMHDefaultColunmCount; } } /** * 列间距 */ - (CGFloat)columnMargin{ if ([self.delegate respondsToSelector:@selector(columnMarginInWaterFallLayout:)]) { return [self.delegate columnMarginInWaterFallLayout:self]; }else{ return LMHDefaultColunmMargin; } } /** * 行间距 */ - (CGFloat)rowMargin{ if ([self.delegate respondsToSelector:@selector(rowMarginInWaterFallLayout:)]) { return [self.delegate rowMarginInWaterFallLayout:self]; }else{ return LMHDefaultRowMargin; } } /** * item的内边距 */ - (UIEdgeInsets)edgeInsets{ if ([self.delegate respondsToSelector:@selector(edgeInsetdInWaterFallLayout:)]) { return [self.delegate edgeInsetdInWaterFallLayout:self]; }else{ return LMHDefaultEdgeInsets; } } /** * 初始化 */ - (void)prepareLayout{ [super prepareLayout]; self.contentHeight = 0; // 清除之前计算的所有高度 [self.columnHeights removeAllObjects]; // 设置每一列默认的高度 for (NSInteger i = 0; i < LMHDefaultColunmCount ; i ++) { [self.columnHeights addObject:@(LMHDefaultEdgeInsets.top)]; } // 清楚之前所有的布局属性 [self.attrsArr removeAllObjects]; // 开始创建每一个cell对应的布局属性 NSInteger count = [self.collectionView numberOfItemsInSection:0]; for (int i = 0; i < count; i++) { // 创建位置 NSIndexPath * indexPath = [NSIndexPath indexPathForItem:i inSection:0]; // 获取indexPath位置上cell对应的布局属性 UICollectionViewLayoutAttributes * attrs = [self layoutAttributesForItemAtIndexPath:indexPath]; [self.attrsArr addObject:attrs]; } } /** * 返回indexPath位置cell对应的布局属性 */ - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{ // 创建布局属性 UICollectionViewLayoutAttributes * attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; //collectionView的宽度 CGFloat collectionViewW = self.collectionView.frame.size.width; // 设置布局属性的frame CGFloat cellW = (collectionViewW - self.edgeInsets.left - self.edgeInsets.right - (self.colunmCount - 1) * self.columnMargin) / self.colunmCount; CGFloat cellH = [self.delegate waterFallLayout:self heightForItemAtIndexPath:indexPath.item itemWidth:cellW]; // 找出最短的那一列 NSInteger destColumn = 0; CGFloat minColumnHeight = [self.columnHeights[0] doubleValue]; for (int i = 1; i < LMHDefaultColunmCount; i++) { // 取得第i列的高度 CGFloat columnHeight = [self.columnHeights[i] doubleValue]; if (minColumnHeight > columnHeight) { minColumnHeight = columnHeight; destColumn = i; } } CGFloat cellX = self.edgeInsets.left + destColumn * (cellW + self.columnMargin); CGFloat cellY = minColumnHeight; if (cellY != self.edgeInsets.top) { cellY += self.rowMargin; } attrs.frame = CGRectMake(cellX, cellY, cellW, cellH); // 更新最短那一列的高度 self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame)); // 记录内容的高度 - 即最长那一列的高度 CGFloat maxColumnHeight = [self.columnHeights[destColumn] doubleValue]; if (self.contentHeight < maxColumnHeight) { self.contentHeight = maxColumnHeight; } return attrs; } /** * 决定cell的布局属性 */ - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{ return self.attrsArr; } /** * 内容的高度 */ - (CGSize)collectionViewContentSize{ return CGSizeMake(0, self.contentHeight + self.edgeInsets.bottom); }
- 结果
四,demo及注意
1.瀑布流中自定的cell时,不能使用纯frame布局,这是因为cell存在复用的问题。如果在初始化方法中直接使用frame布局,就会在复用cell时,造成cell内容与cell的控件存在混乱。
2.demo