如何像UITableView纯样式中的节头一样使UICollectionView中的补充​​视图浮动


68

我正在努力实现的“浮动节头”效果UICollectionView。在没有很多努力的UITableView情况下,(的默认行为UITableViewStylePlain)足够简单的事情似乎是不可能的UICollectionView。我想念明显的东西吗?

Apple没有提供有关如何实现此目的的文档。似乎必须继承UICollectionViewLayout并实现自定义布局才能实现此效果。这需要大量的工作,实现以下方法:

替代方法

每个布局对象都应实现以下方法:

collectionViewContentSize
layoutAttributesForElementsInRect:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath: (if your layout supports supplementary views)
layoutAttributesForDecorationViewOfKind:atIndexPath: (if your layout supports decoration views)
shouldInvalidateLayoutForBoundsChange:

但是,我不清楚如何使辅助视图浮动在单元格上方并“粘”在视图顶部,直到到达下一部分为止。布局属性中是否有此标志?

我会用过,UITableView但我需要创建一个相当复杂的集合层次结构,使用集合视图即可轻松实现。

任何指导或示例代码将不胜感激!

Answers:


69

在iOS9,苹果还跟在添加一个简单的属性UICollectionViewFlowLayoutsectionHeadersPinToVisibleBounds

这样,您就可以像表视图中那样使标头浮动。

let layout = UICollectionViewFlowLayout()
layout.sectionHeadersPinToVisibleBounds = true
layout.minimumInteritemSpacing = 1
layout.minimumLineSpacing = 1
super.init(collectionViewLayout: layout)

我在哪里添加此
摘要

在您的UICollectionViewController初始化方法中
ipraba,2016年

如果我将UIViewController与其中的collectionView一起使用怎么办
Happiehappie 2016年

2
好答案!我猜上面的答案只是针对iOS 9之前的解决方案?
Josip B.

7
只是一点校正:要使用yourCollectionView.collectionViewLayout您需要将布局转换为UICollectionViewFlowLayout(self.yourCollectionView.collectionViewLayout as! UICollectionViewFlowLayout).sectionHeadersPinToVisibleBounds = true
JohnnyAW,2016年

47

要么实现以下委托方法:

collectionView:layout:sizeForItemAtIndexPath:
– collectionView:layout:insetForSectionAtIndex:
– collectionView:layout:minimumLineSpacingForSectionAtIndex:
– collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
– collectionView:layout:referenceSizeForHeaderInSection:
– collectionView:layout:referenceSizeForFooterInSection:

在具有您的:cellForItemAtIndexPath方法的视图控制器中(只需返回正确的值)。或者,也可以不使用委托方法,而直接在布局对象中设置这些值,例如[layout setItemSize:size];

使用这两种方法中的任一种,您都可以使用代码而不是IB来设置设置,因为在设置“自定义布局”时会删除它们。记住也要添加<UICollectionViewDelegateFlowLayout>到您的.h文件中!

创建一个新的子类UICollectionViewFlowLayout,随意调用它,并确保H文件具有:

#import <UIKit/UIKit.h>

@interface YourSubclassNameHere : UICollectionViewFlowLayout

@end

在实施文件中,确保其具有以下内容:

- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {

    NSMutableArray *answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
    UICollectionView * const cv = self.collectionView;
    CGPoint const contentOffset = cv.contentOffset;

    NSMutableIndexSet *missingSections = [NSMutableIndexSet indexSet];
    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
        if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
            [missingSections addIndex:layoutAttributes.indexPath.section];
        }
    }
    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
            [missingSections removeIndex:layoutAttributes.indexPath.section];
        }
    }

    [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) {

        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {

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

            NSIndexPath *firstCellIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
            NSIndexPath *lastCellIndexPath = [NSIndexPath indexPathForItem:MAX(0, (numberOfItemsInSection - 1)) inSection:section];

            NSIndexPath *firstObjectIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
            NSIndexPath *lastObjectIndexPath = [NSIndexPath indexPathForItem:MAX(0, (numberOfItemsInSection - 1)) inSection:section];

            UICollectionViewLayoutAttributes *firstObjectAttrs;
            UICollectionViewLayoutAttributes *lastObjectAttrs;

            if (numberOfItemsInSection > 0) {
                firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
                lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
            } else {
                firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                                    atIndexPath:firstObjectIndexPath];
                lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
                                                                   atIndexPath:lastObjectIndexPath];
            }

            CGFloat headerHeight = CGRectGetHeight(layoutAttributes.frame);
            CGPoint origin = layoutAttributes.frame.origin;
            origin.y = MIN(
                           MAX(
                               contentOffset.y + cv.contentInset.top,
                               (CGRectGetMinY(firstObjectAttrs.frame) - headerHeight)
                               ),
                           (CGRectGetMaxY(lastObjectAttrs.frame) - headerHeight)
                           );

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

        }

    }

    return answer;

}

- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound {

    return YES;

}

在Interface Builder中为流布局选择“ Custom”,然后选择刚创建的“ YourSubclassNameHere”类。快跑!

(注意:上面的代码可能不遵守contentInset.bottom值,或者特别是大或小的页脚对象,或具有0个对象但没有页脚的集合。)


3
如果为numberOfItemsInSection == 0,您将EXC_ARITHMETIC code=EXC_I386_DIV在此行上(除以零?)崩溃UICollectionViewLayoutAttributes *firstCellAttrs = [self layoutAttributesForItemAtIndexPath:firstCellIndexPath];。我还注意到,contentOffset该代码的某些部分在我的实现中未考虑集合视图的最高插入值(可能与上一期相关)。将提出代码更改/要旨/等。
toblerpwn

3
gist.github.com/toblerpwn/5393460-随时进行迭代,尤其是要点中指出的限制。:)
toblerpwn13年

很大的帮助,谢谢。通过重命名相关类(UICollectionView / PSTCollectionView等),这与PSTCollectionView兼容性类完美地结合使用。github.com/steipete/ PSTCollectionView
Chris Blunt

1
如果不使每个凸台更改布局无效,这是否可行?
mattsson

1
@cocotutch自iOS 8 SDK启动以来,您在使用此工具时遇到任何问题吗?即使已定义,此Flow中的单元格布局也默认为50x50的大小collectionView:layout:sizeForItemAtIndexPath:。更具体地说,内部单元格的内容为50x50,但是单元格的大小仍然正确。仅在针对iOS 8 SDK构建但在iOS 7.0.X上运行时发生。针对iOS 7.1 SDK构建并在7.0.X上运行正常。
马特·贝克

8

如果您要将单个标头视图固定在UICollectionView的顶部,则这是一种相对简单的方法。请注意,这意味着尽可能简单-假设您在单个部分中使用单个标头。

//Override UICollectionViewFlowLayout class
@interface FixedHeaderLayout : UICollectionViewFlowLayout
@end

@implementation FixedHeaderLayout
//Override shouldInvalidateLayoutForBoundsChange to require a layout update when we scroll 
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

//Override layoutAttributesForElementsInRect to provide layout attributes with a fixed origin for the header
- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {

    NSMutableArray *result = [[super layoutAttributesForElementsInRect:rect] mutableCopy];

    //see if there's already a header attributes object in the results; if so, remove it
    NSArray *attrKinds = [result valueForKeyPath:@"representedElementKind"];
    NSUInteger headerIndex = [attrKinds indexOfObject:UICollectionElementKindSectionHeader];
    if (headerIndex != NSNotFound) {
        [result removeObjectAtIndex:headerIndex];
    }

    CGPoint const contentOffset = self.collectionView.contentOffset;
    CGSize headerSize = self.headerReferenceSize;

    //create new layout attributes for header
    UICollectionViewLayoutAttributes *newHeaderAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
    CGRect frame = CGRectMake(0, contentOffset.y, headerSize.width, headerSize.height);  //offset y by the amount scrolled
    newHeaderAttributes.frame = frame;
    newHeaderAttributes.zIndex = 1024;

    [result addObject:newHeaderAttributes];

    return result;
}
@end

参见:https : //gist.github.com/4613982


7

这是我的看法,我认为它比上面瞥见的要简单得多。简单性的主要来源是,我不是将流布局归为子类,而是滚动自己的布局(如果您要的话,要容易得多)。

请注意,我假设您已经能够实现自己的自定义UICollectionViewLayout,该自定义将显示单元格和标头,而无需实现浮动。编写完该实现后,下面的代码才有意义。同样,这是因为OP专门询问了浮动标头部分。

一些奖金:

  1. 我浮动两个标头,而不仅仅是一个
  2. 标头将先前的标头推开
  3. 快看!

注意:

  1. supplementaryLayoutAttributes 包含所有标头属性,未实现浮动
  2. 我在中使用此代码prepareLayout,因为我先进行了所有计算。
  3. 不要忘记将其覆盖shouldInvalidateLayoutForBoundsChange为真实!

// float them headers
let yOffset = self.collectionView!.bounds.minY
let headersRect = CGRect(x: 0, y: yOffset, width: width, height: headersHeight)

var floatingAttributes = supplementaryLayoutAttributes.filter {
    $0.frame.minY < headersRect.maxY
}

// This is three, because I am floating 2 headers
// so 2 + 1 extra that will be pushed away
var index = 3
var floatingPoint = yOffset + dateHeaderHeight

while index-- > 0 && !floatingAttributes.isEmpty {

    let attribute = floatingAttributes.removeLast()
    attribute.frame.origin.y = max(floatingPoint, attribute.frame.origin.y)

    floatingPoint = attribute.frame.minY - dateHeaderHeight
}

不知道这意味着什么,代码不完整,注释并不能填补所有空白。
SafeFastExpressive

@RandyHill这段代码并不是通用解决方案,而是对如何实现浮动标头的指导。如果您在理解实现的任何部分时遇到困难,只需询问。(我会添加一条额外的注释,我认为这是您感到困惑的根源)。
Mazyod

1
没关系,这在问题上很清楚。我删除了我的否决票。
SafeFastExpressive

您可能有任何示例项目吗?我正在尝试实现2个浮动标头,但无法使其工作:-(
gasparuff


4

如果有人在Objective-C中寻找解决方案,请将其放在viewDidload中:

    UICollectionViewFlowLayout *flowLayout = 
    (UICollectionViewFlowLayout*)_yourcollectionView.collectionViewLayout;
    [flowLayout setSectionHeadersPinToVisibleBounds:YES];

3

@iPrabu有一个优秀的答案sectionHeadersPinToVisibleBounds。我将添加您也可以在Interface Builder中设置此属性:

  1. 在文档导航器中选择流布局对象。(如果折叠,请首先使用编辑器左下角的工具栏按钮将其展开。)

在文档导航器中选择流布局对象。

  1. 打开身份检查器,并添加一个用户定义的运行时属性,其关键路径为sectionHeadersPinToVisibleBounds,输入Boolean,并选中该复选框。

在身份检查器中设置运行时属性。

默认标题视图具有透明背景。您可能希望使其(部分)不透明或添加模糊效果视图。


2

我遇到了同样的问题,并在Google搜索结果中发现了这一问题。首先,我要感谢cocotutch分享他的解决方案。但是,我希望UICollectionView可以水平滚动,并且标头应保持在屏幕的左侧,因此我不得不稍微更改解决方案。

基本上我只是改变了这个:

        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
        };

对此:

        if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
            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
            };
        } else {
            CGFloat headerWidth = CGRectGetWidth(layoutAttributes.frame);
            CGPoint origin = layoutAttributes.frame.origin;
            origin.x = MIN(
                           MAX(contentOffset.x, (CGRectGetMinX(firstCellAttrs.frame) - headerWidth)),
                           (CGRectGetMaxX(lastCellAttrs.frame) - headerWidth)
                           );

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

参见:https : //gist.github.com/vigorouscoding/5155703http://www.vigorouscoding.com/2013/03/uicollectionview-with-sticky-headers/


2

我用vigorouscoding的代码来运行它。但是,该代码未考虑sectionInset。

所以我将代码更改为垂直滚动

origin.y = MIN(
              MAX(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)),
              (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight)
           );

origin.y = MIN(
           MAX(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight - self.sectionInset.top)),
           (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight + self.sectionInset.bottom)
           );

如果你们想要水平滚动的代码,请参阅上面的代码。


感谢您添加水平!:-) -coco
topLayoutGuide

1

我认为我在github上添加了一个非常简单的示例。

基本上,策略是提供一个自定义布局,该布局在边界更改时无效,并为拥抱当前边界的补充视图提供布局属性。正如其他人所建议的。我希望代码有用。


与表视图部分的页眉和页脚相比,此代码中的页眉和页脚视图的行为有所不同。即使滚动到比末端更远的位置,它们也始终会粘住(以便弹起)。
Ortwin Gentz

1

cocotouch的帖子中有一个错误。如果节中没有项目,并且节脚设置为不显示,则节标题将位于集合视图的外部,用户将无法看到它。

实际上改变:

if (numberOfItemsInSection > 0) {
    firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
    lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
} else {
    firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                            atIndexPath:firstObjectIndexPath];
    lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
                                                           atIndexPath:lastObjectIndexPath];
}

变成:

if (numberOfItemsInSection > 0) {
    firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
    lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
} else {
    firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                            atIndexPath:firstObjectIndexPath];
    lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
                                                           atIndexPath:lastObjectIndexPath];
    if (lastObjectAttrs == nil) {
        lastObjectAttrs = firstObjectAttrs;
    }
}

将解决此问题。


0

VCollectionViewGridLayout会执行粘性标头。它是基于TLIndexPathTools的垂直滚动简单网格布局。尝试运行粘性标题示例项目。

此布局还具有比更好的批处理更新动画行为UICollectionViewFlowLayout。提供了几个示例项目,使您可以在两种布局之间切换以演示改进。


我试过了 实际上,如果您已经拥有一个具有正常工作的数据委托的UICollectionView,那么转移到这一点将很困难,因为您的数据模型必须建模为TLIndexDataModel。我放弃了这一点。
xaphod

@xaphod实际上不是。来自GitHub自述文件:“内部实现需要TLIndexPathTools。集合视图本身不一定需要使用TLIndexPathTools,但示例项目需要。”
Timothy Moose 2014年

是的,我也读过。实际上,这是不正确的。试试看。
xaphod

@xaphod我应该提到我写的库。我将研究添加一个不使用TLIPT的示例项目。使用此库的唯一要求是您必须实现VCollectionViewGridLayoutDelegate委托方法,而委托方法本身不需要TLIPT。但是,该库的主要目的是解决流布局中的某些动画问题。粘性标头功能是次要的,因此对于那些只对粘性标头感兴趣的人可能会有更好的选择。
蒂莫西·穆斯

好的,我确实努力不使用TLIPT,但是我可以在布局类中看到正在使用TLIPT。我看不到它。您是正确的,我所需要的只是粘性标题...但是我还没有取得任何成功(上述cocotutch的解决方案也对我不起作用,这表明问题是我的错误)
xaphod

0

斯威夫特5.0

将以下内容放入您的viewDidLoad中:

if let layout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
    layout.sectionHeadersPinToVisibleBounds = true
}

(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionHeadersPinToVisibleBounds = true
PhoneyDeveloper
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.