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

 

posted on 2020-07-30 14:05  梁飞宇  阅读(430)  评论(0编辑  收藏  举报