69

我正在努力使用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,但我需要创建一个相当复杂的集合层次结构,这可以通过集合视图轻松实现。

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

4

14 回答 14

70

在 iOS9 中,Apple 很友好地在 called 中添加了一个简单的UICollectionViewFlowLayout属性sectionHeadersPinToVisibleBounds

有了这个,您可以使标题像在表格视图中那样浮动。

let layout = UICollectionViewFlowLayout()
layout.sectionHeadersPinToVisibleBounds = true
layout.minimumInteritemSpacing = 1
layout.minimumLineSpacing = 1
super.init(collectionViewLayout: layout)
于 2015-11-27T15:55:20.237 回答
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 文件中!

创建一个新的 Subclass 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 中为 Flow Layout 选择“Custom”,选择您刚刚创建的“YourSubclassNameHere”类。跑!

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

于 2012-12-03T07:27:39.883 回答
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

于 2013-01-23T21:55:38.897 回答
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
}
于 2014-11-26T21:31:03.823 回答
4

Storyboard如果已经在或文件中设置了流布局,Xib那么试试这个,

(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionHeadersPinToVisibleBounds = true
于 2016-06-07T06:50:56.360 回答
4

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

    UICollectionViewFlowLayout *flowLayout = 
    (UICollectionViewFlowLayout*)_yourcollectionView.collectionViewLayout;
    [flowLayout setSectionHeadersPinToVisibleBounds:YES];
于 2017-05-04T03:31:43.580 回答
3

@iPrabu 有一个很好的答案sectionHeadersPinToVisibleBounds。我将补充一点,您也可以在 Interface Builder 中设置此属性:

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

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

  1. 打开身份检查器并添加一个用户定义的运行时属性,其中包含 key path sectionHeadersPinToVisibleBounds、类型Boolean和选中的复选框。

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

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

于 2018-11-24T13:39:09.567 回答
2

我用剧烈编码的代码运行了这个。但是,该代码没有考虑 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)
           );

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

于 2013-04-12T09:02:22.410 回答
2

我遇到了同样的问题,并在我的谷歌搜索结果中发现了这个问题。首先,我要感谢 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/5155703或http://www.vigorouscoding.com/2013/03/uicollectionview-with-sticky-headers/

于 2013-03-13T20:21:49.497 回答
1

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

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

于 2013-06-04T16:45:53.360 回答
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;
    }
}

将解决这个问题。

于 2013-09-08T04:49:32.653 回答
1

斯威夫特 5.0

将以下内容放入您的 viewDidLoad:

if let layout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
    layout.sectionHeadersPinToVisibleBounds = true
}
于 2020-03-19T05:05:48.987 回答
0

VCollectionViewGridLayout做粘性标题。它是一个基于TLIndexPathTools的垂直滚动简单网格布局。尝试运行Sticky Headers示例项目。

此布局还具有比UICollectionViewFlowLayout. 提供了几个示例项目,可让您在两种布局之间切换以演示改进。

于 2013-07-11T04:21:03.843 回答
0

我似乎偶然发现了一个优雅的解决方案,其他人可能会觉得有用:)

    let layout = UICollectionViewCompositionalLayout(
        sectionProvider: { (_, layoutEnvironment) -> NSCollectionLayoutSection? in
            var configuration = UICollectionLayoutListConfiguration(appearance: .grouped)
            configuration.headerMode = .supplementary
            configuration.showsSeparators = true
            configuration.backgroundColor = UIColor.systemGray6

            var section = NSCollectionLayoutSection.list(
                using: configuration,
                layoutEnvironment: layoutEnvironment
            )
            let sectionSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .estimated(1)
            )
            let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
                layoutSize: sectionSize,
                elementKind: UICollectionView.elementKindSectionHeader,
                alignment: .top
            )
            sectionHeader.pinToVisibleBounds = true
            section.boundarySupplementaryItems = [sectionHeader]
            return section
    })
    let frame = CGRect(x: 0.0, y: 0.0, width: 300.0, height: 500.0)
    let view = UICollectionView(frame: frame, collectionViewLayout: layout)

我在其他解决方案中一直遇到的问题是找不到固定标头使其动画将另一个标头推到视野之外的方法。在下面的代码中,没有一个实例可以让您像上面的解决方案那样访问该pinToVisibleBounds属性:

    var configuration = UICollectionLayoutListConfiguration(appearance: .grouped)
    configuration.headerMode = .supplementary
    configuration.showsSeparators = true
    configuration.backgroundColor = UIColor.systemGray6
    let layout = UICollectionViewCompositionalLayout.list(using: configuration)
于 2022-03-02T22:31:18.240 回答