在UICollectionView上实现StickyHeader效果

前几天在用UICollectionView实现组合式布局的时候,想到如果在CollectionView上要求sticky header(也就是header自动悬停置顶)的效果应当如何实现。 我们都知道如果是在UITableView上实现非常简单,只要设置style为group然后多个section,headerView就会自动吸附在顶部。而collectionView虽然功能强大得多,但是原生并没有支持直接设置这个效果,所以需要自己实现。

作为一个懒人,首先想到的自然是站在巨人的肩膀上,CSStickyHeaderFlowLayout是一个比较知名的CollectionView布局控件,它支持sticky header效果以及头部的视差滚动效果,效果很赞。但是感觉稍微重了些,需要做更多的设置以及修改基类等。

然后我又找到了一篇文章,虽然已经是3年前的文章,但是写得非常简明清晰,而且阅读完有豁然开朗的感觉。

文章很短,全文如下:http://blog.radi.ws/post/32905838158/sticky-headers-for-uicollectionview-using

核心原理就是三句话:

  • The header should be positioned so it can never go further up than one header height above the first cell in the section.
  • The header should be positioned so it can never go further down than one header header height above the lower bounds of the last cell in the section.
  • The header should be positioned so it usually stays around the top edge, referencing the content offset of the collection view.

代码也很简单,只需要在UICollectionViewFlowLayout实现layoutAttributesForElementsInRectshouldInvalidateLayoutForBoundsChange两个方法。

接着我附上文中源代码以及自己写的注释,穿插了一些对文章初略的翻译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    /**
     *  返回当前显示区域的所有布局信息
     */
    NSMutableArray *answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
    UICollectionView * const cv = self.collectionView;
    CGPoint const contentOffset = cv.contentOffset;

    NSMutableIndexSet *missingSections = [NSMutableIndexSet indexSet];

    /**
     *  找出所有UICollectionElementCategoryCell类型的cell
     */
    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
        if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
            [missingSections addIndex:layoutAttributes.indexPath.section];
        }
    }
    /**
     *  再从里面删除所有UICollectionElementKindSectionHeader类型的cell
     */
    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
            [missingSections removeIndex:layoutAttributes.indexPath.section];
        }
    }

    /**
     *  默认情况下,为missingSections手动插入attributes,应该是为rect外的section生成attributes
     */
    [missingSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {

        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];

        UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];

        [answer addObject:layoutAttributes];

    }];


    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {

        /**
         *  从answer中储存的布局信息中,针对UICollectionElementKindSectionHeader...
         */
        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {

            NSInteger section = layoutAttributes.indexPath.section;
            NSInteger numberOfItemsInSection = [cv numberOfItemsInSection:section];

            /**
             *  为什么需要firstCellIndexPath和lastCellIndexPath呢?
             *  header应当保持着距离本section第一个cell的最大距离,简单来说就是header在置顶之前要贴着cell
             *  同理,header应当保持着与最后一个cell的最小距离,
             *  最后,在不违反上面两则约束的前提下,通过collection view的offset与header的高度来使header处于 origin.y = 0 的状态。
             */
            NSIndexPath *firstCellIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
            NSIndexPath *lastCellIndexPath = [NSIndexPath indexPathForItem:MAX(0, (numberOfItemsInSection - 1)) inSection:section];

            /**
             *  针对当前layoutAttributes的section, 找出第一个和最后一个普通cell的位置
             */
            UICollectionViewLayoutAttributes *firstCellAttrs = [self layoutAttributesForItemAtIndexPath:firstCellIndexPath];
            UICollectionViewLayoutAttributes *lastCellAttrs = [self layoutAttributesForItemAtIndexPath:lastCellIndexPath];

            /**
             *  获取当前处理header的高度和位置,然后通过firstCellAttrs和lastCellAttrs确定header是否置顶
             */
            CGFloat headerHeight = CGRectGetHeight(layoutAttributes.frame);
            CGPoint origin = layoutAttributes.frame.origin;
            origin.y = MIN(
                           MAX(
                               contentOffset.y,
                               (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)
                               ),
                           (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight)
                           );

            layoutAttributes.zIndex = 1024;
            layoutAttributes.frame = (CGRect){
                .origin = origin,
                .size = layoutAttributes.frame.size
            };
        }
    }
    return answer;
}

- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound {
    return YES;
}

感觉又涨了姿势,撒花。

Comments