自定义 UICollectionViewLayout系列——了解

2022-10-20  本文已影响0人  felix9

尽管 UICollectionView 是iOS 日常开发遇到的高频控件之一,但很多时候我们对其使用仅仅是为了满足一些横向滚动的场景。再复杂的场景我们可能继承UICollectionViewFlowLayout然后稍作调整。重头开始写一个UICollectionViewLayout? Oh, no. 这真的太复杂了。但是自定义 UICollectionViewLayout可以帮助我们深度定制 UI 和行为以及针对性的优化滚动性能。那么就让我带大家来重新认识一下UICollectionViewLayout

这个系列计划分为两篇,分别是:

布局核心数据结构

在 xcode 中打开UICollectionViewLayout.h我们会看到几个和UICollectionViewLayout相关的核心类。他们是:

布局核心过程

让我们想一下 collectionview 的布局过程,其实就是 collectionview 和 layout的沟通过程。当 collectionview 需要布局信息的时候,他会通过特定的方法来向 layout 获取。

collectionviewlayout1-1

你的自定义 layout 必须实现一下方法:

计算布局属性

上面我们知道了我们需要实现什么方法,但是我们应该怎样计算布局属性呢?为了方便接下来的讲述,我会使用最常见的瀑布流 StreamLayout 来做例子。

首先对于一个瀑布流来说,你需要动态的计算每一个 item 的高度,也就是需要声明一个 protocol 来获取信息。

那么回到代码,在实现我们的 StreamLayout 之前,我们需要声明 protocol

@protocol StreamLayoutDelegate <UICollectionViewDelegate>

- (CGFloat)collectionView:(UICollectionView *)collectionView
                   layout:(WCFinderStreamLayout2 *)collectionViewLayout
    cellHeightAtIndexPath:(NSIndexPath *)indexPath
                withWidth:(CGFloat)width;

@end

实现这个 protocol 的实例就需要实现这个方法来提供每个 cell 的高度。在我们开始写布局代码之前,我们需要在 StreamLayout 中声明一些属性来帮助布局。

@interface StreamLayout : UICollectionViewLayout

@property (nonatomic, assign) NSUInteger columnCount;
@property (nonatomic, assign) CGSize cellSpace;
@property (nonatomic, assign) CGFloat cellHeight;
@property (nonatomic, assign) CGSize contentSize;
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UICollectionViewLayoutAttributes *> *cellsAttr;

@end

从属性的命名上,我们可以很容易的理解其作用。其中 cellsAttr 是所有 cell 的布局信息缓存,这样可以避免大部分的重复计算。

现在,你有了计算布局属性的所有信息,可以得到所有cell 的位置,为了让大家更容易理解计算的过程,看下图:

collectionviewlayout1-2

计算布局的过程其实就是计算每个 cell 的 frame 的过程,在这个过程中,你需要积累计算每个 cell 的 xOffset、yOffset。在我们这个例子中,假设我们的数据量级不大,那么可以在 prepareLayout中就计算出所有的cell 的布局信息。

代码如下:

- (void)prepareLayout {
    [super prepareLayout];
        //1.
    if (self.cellsAttr) {
        return;
    }
    self.cellsAttr = [NSMutableDictionary dictionary];

    NSUInteger cellCount = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:section];
    if (cellCount == 0) {
        return;
    }
    NSUInteger columnCount = self.columnCount;
    CGSize cellSpace = self.cellSpace;
    CGFloat rowSpace = MAX(0.0, cellSpace.width);
    CGFloat columnSpace = MAX(0.0, cellSpace.height);
    CGFloat currentMaxY = edgeInsets.top;
    //2.
    NSMutableArray<NSNumber *> *columnHeights = [NSMutableArray array];
    for (int i = 0; i < columnCount; i++) {
        [columnHeights addObject:@(0)];
    }

    CGFloat maxHeight = 0;
    for (int i = 0; i < cellCount; i++) {
        //3.
        UICollectionViewLayoutAttributes *attrs =
        [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForItem:i inSection:section]];
        if (!attrs) {
            continue;
        }
        CGFloat width = (self.collectionView.width - (columnCount - 1) * rowSpace) / columnCount;
        CGFloat cellHeight = self.cellHeight;
        if ([self.delegate respondsToSelector:@selector(collectionView:layout:cellHeightAtIndexPath:withWidth:)]) {
            cellHeight = [self.delegate collectionView:self.collectionView
                                                layout:self
                                 cellHeightAtIndexPath:[NSIndexPath indexPathForItem:i inSection:section]
                                             withWidth:width];
        }
        cellHeight = MAX(0.0, cellHeight);
                //4.
        __block CGFloat minHeight = CGFLOAT_MAX;
        __block NSUInteger minIndex = 0;
        [columnHeights enumerateObjectsUsingBlock:^(NSNumber *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
            if (obj.floatValue < minHeight && obj.floatValue >= 0) {
                minHeight = obj.floatValue;
                minIndex = idx;
            }
        }];
        CGFloat offsetY = currentMaxY + minHeight;
        CGFloat newColumnHeight = minHeight + cellHeight + columnSpace;
        attrs.frame = CGRectMake((width + rowSpace) * minIndex, offsetY, width, cellHeight);
        columnHeights[minIndex] = @(newColumnHeight);
        maxHeight = MAX(maxHeight, newColumnHeight - columnSpace);
        self.cellsAttr safeSetObject:attrs forKey:@(i)];
    }
}
  1. 仅当缓存数据不存在的时候才计算
  2. 新建数组用来搜集每一列的最新高度
  3. 生成UICollectionViewLayoutAttributes
  4. 循环计算每一个 cell 的布局,每一个 cell 会被安排到最小高度的列。

因为 prepareLayout 在每次布局过程中都会被调用,而有很多情况很导致重新布局,比如 collectionview 的 size 发生变化,所以在特定时刻如invalidationContextForBoundsChange 需要清除缓存。本篇暂时不考虑这种情况。

得到了所有 cell 的布局信息,我们就需要把数据传递给 collectionview。

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    if (self.cellsAttr == nil) {
        return nil;
    }
    NSMutableArray *attrs = [NSMutableArray array];
    [[self.cellsAttr allValues]
    enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *_Nonnull cell, NSUInteger idx, BOOL *_Nonnull stop) {
        if (CGRectIntersectsRect(cell.frame, rect)) {
            [attrs safeAddObject:cell];
        }
    }];
    return attrs;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    return self.cellsAttr[@(indexPath.row)];
}

在第一个方法中,我们找出了所有在 rect 范围的 cell,而在第二个方法中,我们返回了 indexpath 下对应的布局信息

注意:尽管两个方法都返回了UICollectionViewLayoutAttributes,但实际布局只会采用layoutAttributesForItemAtIndexPath返回的布局属性。

总结

在这篇简单的文章中,我们写了一个简单的瀑布流布局 StreamLayout,简单的了解了 UICollectionViewLayout 的核心内容。在下一篇性能优化,我们再来看看如何写出高性能的自定义UICollectionViewLayout吧。

上一篇下一篇

猜你喜欢

热点阅读