本节我们用 CollectionView Layout 来实现一个类似于 tableView 向上滚动时的 Sticky Headers 效果,该效果展示如下:Section Title 在当前 section 的 cells 未完全滚出界面时一直保持固定在页面顶部,直到下一个 section Title 将其顶出取代他的位置,依次反复
为了实现这种效果,我们需要遵守三个规则:
实现这三个规则其实相当简单粗暴,在此之前,我们先来关注一下 Missing Headers(消失的 Headers),我们在 layoutAttributesForElementsInRect(_:)
方法中一般是先调用 super 方法返回一个包含 UICollectionViewLayoutAttributes 的数组,但这里要注意的是,只有和该方法传入的参数 rect 相交的部分,这部分元素的 layoutAttributes 才会被返回(下图中橙色线框区域)
此时会存在一个问题,当一个 Header 向上滑出屏幕后,调用该方法是不能得到屏幕外元素的 layoutAttributes,因此我们一开始就要手动标识这些需要的 header 元素;这一步是必须的,因为我们需要在 scroll collection view 时,实时调整 header 的位置,使其保持在屏幕顶端(正常情况下,早就滑出到屏幕外)
下面我们来看一下具体实现:
创建一个 UICollectionViewFlowLayout 的子类 StickyHeadersLayout
实现两个方法:
class StickyHeadersLayout: UICollectionViewFlowLayout { override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { return true } override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? { // 先调用 super,只返回当前可见 elements 的 attributes,包括 cells, supplementary views, 和 decoration views var layoutAttributes = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes] let headerNeedingLayout = NSMutableIndexSet() // TODO } }
在 layoutAttributesForElementsInRect 方法中,我们先调用了 super,他会返回所有可视范围内的元素的 attributes,因为我们感兴趣的是 header,下面就定义一个 NSMutableIndexSet 对象来保持对 header 的索引,这样即使在他滑出屏幕外也能追踪到。
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? { // 先调用 super,只返回当前可见 elements 的 attributes,包括 cells, supplementary views, 和 decoration views var layoutAttributes = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes] let headerNeedingLayout = NSMutableIndexSet() // 找出当前 cell 对应的 section 索引 for attributes in layoutAttributes { if attributes.representedElementCategory == .Cell { headerNeedingLayout.addIndex(attributes.indexPath.section) } } // 遍历当前屏幕上显示的所有 header,然后将还显示在屏幕上的 header 对应的索引从 headerNeedingLayout 中移除,这样就只保持了对刚刚移出屏幕 header 的追踪 for attributes in layoutAttributes { if let elementKind = attributes.representedElementKind { if elementKind == UICollectionElementKindSectionHeader { headerNeedingLayout.removeIndex(attributes.indexPath.section) } } } // 将刚移出屏幕的 header(Missing Headers)加入 layoutAttributes headerNeedingLayout.enumerateIndexesUsingBlock { index, stop in let indexPath = NSIndexPath(forItem: 0, inSection: index) let attributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath) layoutAttributes.append(attributes) } // TODO }
经过上面的操作,layoutAttributes 里保存着当前屏幕上所有元素 + 上一个 section header 的 attributes ,接下来我们找出这两个 header 的 attributes(接上 TODO)
for attributes in layoutAttributes { // 找出 header 的 attributes 如果是 Cell 的话 representedElementKind 就为 nil if let elementKind = attributes.representedElementKind { if elementKind == UICollectionElementKindSectionHeader { // 找出 header 当前所在的 section let section = attributes.indexPath.section // 分别返回当前 section 中第一个 item 和 最后一个 item 所对应的 attributes let attributesForFirstItemInSection = layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: 0, inSection: section)) let attributesForLastItemInSection = layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: collectionView!.numberOfItemsInSection(section) - 1, inSection: section)) // 得到 header 的 frame var frame = attributes.frame // 找出当前的滑动距离 let offset = collectionView!.contentOffset.y // 接下来我们来践行一开始提到的三个规则 let minY = CGRectGetMinY(attributesForFirstItemInSection.frame) - frame.height let maxY = CGRectGetMaxY(attributesForLastItemInSection.frame) - frame.height // minY ≤ offset ≤ maxY let y = min(max(offset, minY), maxY) frame.origin.y = y attributes.frame = frame attributes.zIndex = 99 } } }
我们根据当前 section 中的第一个 item 和最后一个 item 得到当前 header origin y 坐标的最大值和最小值,在此范围内,根据 contentOffset 动态调整 header origin.y,保持 header Sticky 在屏幕顶部,但一旦超过 maxY,header origin.y 就不再增大了,也就被移除屏幕
最后 finished demo 地址 请自行 convert to lastest swift syntax