很多时候我们需要用到CollectionView
,但sectionheader
在滚动的时候会超出屏幕,并不会像plainType
的tableView
那样让sectionheader
悬停,网上也有很多,只需重写UICollectionViewFlowLayout
布局并设置sectionheader
对应的Rect
和位置 我这里只把OC和Swift版总结于此方便使用
OC:
.h文件
@interface YQWPlainFlowLayout : UICollectionViewFlowLayout
@end
.m文件
#import "YQWPlainFlowLayout.h"
@implementation YQWPlainFlowLayout
-(instancetype)init
{
self = [super init];
if (self)
{
}
return self;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
//截取到父类所返回的数组(里面放的是当前屏幕所能展示的item的结构信息),并转化成不可变数组
NSMutableArray *superArray = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
//创建存索引的数组,无符号(正整数),无序(不能通过下标取值),不可重复(重复的话会自动过滤)
NSMutableIndexSet *noneHeaderSections = [NSMutableIndexSet indexSet];
//遍历superArray,得到一个当前屏幕中所有的section数组
for (UICollectionViewLayoutAttributes *attributes in superArray)
{
//如果当前的元素分类是一个cell,将cell所在的分区section加入数组,重复的话会自动过滤
if (attributes.representedElementCategory == UICollectionElementCategoryCell)
{
[noneHeaderSections addIndex:attributes.indexPath.section];
}
}
//遍历superArray,将当前屏幕中拥有的header的section从数组中移除,得到一个当前屏幕中没有header的section数组
//正常情况下,随着手指往上移,header脱离屏幕会被系统回收而cell尚在,也会触发该方法
for (UICollectionViewLayoutAttributes *attributes in superArray)
{
//如果当前的元素是一个header,将header所在的section从数组中移除
if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader])
{
[noneHeaderSections removeIndex:attributes.indexPath.section];
}
}
//遍历当前屏幕中没有header的section数组
[noneHeaderSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){
//取到当前section中第一个item的indexPath
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
//获取当前section在正常情况下已经离开屏幕的header结构信息
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
//如果当前分区确实有因为离开屏幕而被系统回收的header
if (attributes)
{
//将该header结构信息重新加入到superArray中去
[superArray addObject:attributes];
}
}];
//遍历superArray,改变header结构信息中的参数,使它可以在当前section还没完全离开屏幕的时候一直显示
for (UICollectionViewLayoutAttributes *attributes in superArray) {
//如果当前item是header
if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader])
{
//得到当前header所在分区的cell的数量
NSInteger numberOfItemsInSection = [self.collectionView numberOfItemsInSection:attributes.indexPath.section];
//得到第一个item的indexPath
NSIndexPath *firstItemIndexPath = [NSIndexPath indexPathForItem:0 inSection:attributes.indexPath.section];
//得到最后一个item的indexPath
NSIndexPath *lastItemIndexPath = [NSIndexPath indexPathForItem:MAX(0, numberOfItemsInSection-1) inSection:attributes.indexPath.section];
//得到第一个item和最后一个item的结构信息
UICollectionViewLayoutAttributes *firstItemAttributes, *lastItemAttributes;
if (numberOfItemsInSection>0)
{
//cell有值,则获取第一个cell和最后一个cell的结构信息
firstItemAttributes = [self layoutAttributesForItemAtIndexPath:firstItemIndexPath];
lastItemAttributes = [self layoutAttributesForItemAtIndexPath:lastItemIndexPath];
}else
{
//cell没值,就新建一个UICollectionViewLayoutAttributes
firstItemAttributes = [UICollectionViewLayoutAttributes new];
//然后模拟出在当前分区中的唯一一个cell,cell在header的下面,高度为0,还与header隔着可能存在的sectionInset的top
CGFloat y = CGRectGetMaxY(attributes.frame)+self.sectionInset.top;
firstItemAttributes.frame = CGRectMake(0, y, 0, 0);
//因为只有一个cell,所以最后一个cell等于第一个cell
lastItemAttributes = firstItemAttributes;
}
//获取当前header的frame
CGRect rect = attributes.frame;
//当前的滑动距离 + 因为导航栏产生的偏移量,默认为64(如果app需求不同,需自己设置)
CGFloat offset = self.collectionView.contentOffset.y + 0;
//第一个cell的y值 - 当前header的高度 - 可能存在的sectionInset的top
CGFloat headerY = firstItemAttributes.frame.origin.y - rect.size.height - self.sectionInset.top;
//哪个大取哪个,保证header悬停
//针对当前header基本上都是offset更加大,针对下一个header则会是headerY大,各自处理
CGFloat maxY = MAX(offset,headerY);
//最后一个cell的y值 + 最后一个cell的高度 + 可能存在的sectionInset的bottom - 当前header的高度
//当当前section的footer或者下一个section的header接触到当前header的底部,计算出的headerMissingY即为有效值
CGFloat headerMissingY = CGRectGetMaxY(lastItemAttributes.frame) + self.sectionInset.bottom - rect.size.height;
//给rect的y赋新值,因为在最后消失的临界点要跟谁消失,所以取小
rect.origin.y = MIN(maxY,headerMissingY);
//给header的结构信息的frame重新赋值
attributes.frame = rect;
//如果按照正常情况下,header离开屏幕被系统回收,而header的层次关系又与cell相等,如果不去理会,会出现cell在header上面的情况
//通过打印可以知道cell的层次关系zIndex数值为0,我们可以将header的zIndex设置成1,如果不放心,也可以将它设置成非常大,这里随便填了个7
attributes.zIndex = 7;
}
}
//转换回不可变数组,并返回
return [superArray copy];
}
//return YES;表示一旦滑动就实时调用上面这个layoutAttributesForElementsInRect:方法
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound
{
return YES;
}
@end
//使用方法:
YQWPlainFlowLayout *fallLayout = [[YQWPlainFlowLayout alloc] init];
fallLayout.sectionInset = UIEdgeInsetsMake(1, 0, 0, 0);
fallLayout.minimumLineSpacing = 1;
fallLayout.minimumInteritemSpacing = 0.5;
fallLayout.itemSize = CGSizeMake(self.view.bounds.size.width/2 - 0.5, 168);
_collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:fallLayout];
[_collectionView registerClass:[YQWIntegralCollReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:sectionHeaderID];
...
//下面的2个代理方法为设置的sectionheader
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section{
return CGSizeMake(collectionView.bounds.size.width, 45);
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
if ([kind isEqualToString:UICollectionElementKindSectionHeader])
{
YQWIntegralCollReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind :kind withReuseIdentifier:sectionHeaderID forIndexPath:indexPath];
return view;
}
return nil;
}
Swift: 一样写个子类
import UIKit
//自定义的具有粘性分组头的Collection View布局类
class TestHeadersFlowLayout: UICollectionViewFlowLayout {
//边界发生变化时是否重新布局(视图滚动的时候也会调用)
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
//所有元素的位置属性
override func layoutAttributesForElements(in rect: CGRect)
-> [UICollectionViewLayoutAttributes]? {
//从父类得到默认的所有元素属性
guard let layoutAttributes = super.layoutAttributesForElements(in: rect)
else { return nil }
//用于存储元素新的布局属性,最后会返回这个
var newLayoutAttributes = [UICollectionViewLayoutAttributes]()
//存储每个layout attributes对应的是哪个section
let sectionsToAdd = NSMutableIndexSet()
//循环老的元素布局属性
for layoutAttributesSet in layoutAttributes {
//如果元素师cell
if layoutAttributesSet.representedElementCategory == .cell {
//将布局添加到newLayoutAttributes中
newLayoutAttributes.append(layoutAttributesSet)
} else if layoutAttributesSet.representedElementCategory == .supplementaryView {
//将对应的section储存到sectionsToAdd中
sectionsToAdd.add(layoutAttributesSet.indexPath.section)
}
}
//遍历sectionsToAdd,补充视图使用正确的布局属性
for section in sectionsToAdd {
let indexPath = IndexPath(item: 0, section: section)
//添加头部布局属性
if let headerAttributes = self.layoutAttributesForSupplementaryView(ofKind:
UICollectionElementKindSectionHeader, at: indexPath) {
newLayoutAttributes.append(headerAttributes)
}
//添加尾部布局属性
if let footerAttributes = self.layoutAttributesForSupplementaryView(ofKind:
UICollectionElementKindSectionFooter, at: indexPath) {
newLayoutAttributes.append(footerAttributes)
}
}
return newLayoutAttributes
}
//补充视图的布局属性(这里处理实现粘性分组头,让分组头始终处于分组可视区域的顶部)
override func layoutAttributesForSupplementaryView(ofKind elementKind: String,
at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
//先从父类获取补充视图的布局属性
guard let layoutAttributes = super.layoutAttributesForSupplementaryView(ofKind:
elementKind, at: indexPath) else { return nil }
//如果不是头部视图则直接返回
if elementKind != UICollectionElementKindSectionHeader {
return layoutAttributes
}
//根据section索引,获取对应的边界范围
guard let boundaries = boundaries(forSection: indexPath.section)
else { return layoutAttributes }
guard let collectionView = collectionView else { return layoutAttributes }
//保存视图内入垂直方向的偏移量
let contentOffsetY = collectionView.contentOffset.y
//补充视图的frame
var frameForSupplementaryView = layoutAttributes.frame
//计算分组头垂直方向的最大最小值
let minimum = boundaries.minimum - frameForSupplementaryView.height
let maximum = boundaries.maximum - frameForSupplementaryView.height
//如果内容区域的垂直偏移量小于分组头最小的位置,则将分组头置于其最小位置
if contentOffsetY < minimum {
frameForSupplementaryView.origin.y = minimum
}
//如果内容区域的垂直偏移量大于分组头最小的位置,则将分组头置于其最大位置
else if contentOffsetY > maximum {
frameForSupplementaryView.origin.y = maximum
}
//如果都不满足,则说明内容区域的垂直便宜量落在分组头的边界范围内。
//将分组头设置为内容偏移量,从而让分组头固定在集合视图的顶部
else {
frameForSupplementaryView.origin.y = contentOffsetY
}
//更新布局属性并返回
layoutAttributes.frame = frameForSupplementaryView
return layoutAttributes
}
//根据section索引,获取对应的边界范围(返回一个元组)
func boundaries(forSection section: Int) -> (minimum: CGFloat, maximum: CGFloat)? {
//保存返回结果
var result = (minimum: CGFloat(0.0), maximum: CGFloat(0.0))
//如果collectionView属性为nil,则直接fanhui
guard let collectionView = collectionView else { return result }
//获取该分区中的项目数
let numberOfItems = collectionView.numberOfItems(inSection: section)
//如果项目数位0,则直接返回
guard numberOfItems > 0 else { return result }
//从流布局属性中获取第一个、以及最后一个项的布局属性
let first = IndexPath(item: 0, section: section)
let last = IndexPath(item: (numberOfItems - 1), section: section)
if let firstItem = layoutAttributesForItem(at: first),
let lastItem = layoutAttributesForItem(at: last) {
//分别获区边界的最小值和最大值
result.minimum = firstItem.frame.minY
result.maximum = lastItem.frame.maxY
//将分区都的高度考虑进去,并调整
result.minimum -= headerReferenceSize.height
result.maximum -= headerReferenceSize.height
//将分区的内边距考虑进去,并调整
result.minimum -= sectionInset.top
result.maximum += (sectionInset.top + sectionInset.bottom)
}
//返回最终的边界值
return result
}
}
let layout = TestHeadersFlowLayout()
let frame = CGRect(x: 0, y: 64, width: view.bounds.width, height:view.bounds.height - 64)
let collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
...