转载

Custom Collection View Layouts(三)

Sticky Headers

本节我们用 CollectionView Layout 来实现一个类似于 tableView 向上滚动时的 Sticky Headers 效果,该效果展示如下:Section Title 在当前 section 的 cells 未完全滚出界面时一直保持固定在页面顶部,直到下一个 section Title 将其顶出取代他的位置,依次反复

Custom Collection View Layouts(三)

为了实现这种效果,我们需要遵守三个规则:

Custom Collection View Layouts(三)

  • section 中第一个 cell 到屏幕顶部的距离为一个 header height 时,header 就不能再向上滑动了(被固定在屏幕顶部)
  • 向下滑动时,section header 一旦和下面的 section 底部距离超过一个 header height 就不能再向下了(同样被固定在屏幕顶部)
  • 在遵循前两个规则的基础上,其余的时间我们使用 contentOffset

实现这三个规则其实相当简单粗暴,在此之前,我们先来关注一下 Missing Headers(消失的 Headers),我们在 layoutAttributesForElementsInRect(_:) 方法中一般是先调用 super 方法返回一个包含 UICollectionViewLayoutAttributes 的数组,但这里要注意的是,只有和该方法传入的参数 rect 相交的部分,这部分元素的 layoutAttributes 才会被返回(下图中橙色线框区域)

Custom Collection View Layouts(三)

此时会存在一个问题,当一个 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 就不再增大了,也就被移除屏幕

Custom Collection View Layouts(三)

最后 finished demo 地址 请自行 convert to lastest swift syntax

-EOF-
正文到此结束
Loading...