问题
I\'m struggling to achieve a \"floating section header\" effect with UICollectionView
. Something that\'s been easy enough in UITableView
(default behavior for UITableViewStylePlain
) seems impossible in UICollectionView
without lots of hard work. Am I missing the obvious?
Apple provides no documentation on how to achieve this. It seems that one has to subclass UICollectionViewLayout
and implement a custom layout just to achieve this effect. This entails quite a bit of work, implementing the following methods:
Methods to Override
Every layout object should implement the following methods:
collectionViewContentSize
layoutAttributesForElementsInRect:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath: (if your layout supports supplementary views)
layoutAttributesForDecorationViewOfKind:atIndexPath: (if your layout supports decoration views)
shouldInvalidateLayoutForBoundsChange:
However its not clear to me how to make the supplementary view float above the cells and \"stick\" to the top of the view until the next section is reached. Is there a flag for this in the layout attributes?
I would have used UITableView
but I need to create a rather complex hierarchy of collections which is easily achieved with a collection view.
Any guidance or sample code would be greatly appreciated!
回答1:
In iOS9, Apple was kind enough to add a simple property in UICollectionViewFlowLayout called sectionHeadersPinToVisibleBounds.
With this, you can make the headers float like that in table views.
let layout = UICollectionViewFlowLayout()
layout.sectionHeadersPinToVisibleBounds = true
layout.minimumInteritemSpacing = 1
layout.minimumLineSpacing = 1
super.init(collectionViewLayout: layout)
回答2:
Either implement the following delegate methods:
– collectionView:layout:sizeForItemAtIndexPath:
– collectionView:layout:insetForSectionAtIndex:
– collectionView:layout:minimumLineSpacingForSectionAtIndex:
– collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
– collectionView:layout:referenceSizeForHeaderInSection:
– collectionView:layout:referenceSizeForFooterInSection:
In your view controller that has your :cellForItemAtIndexPath
method (just return the correct values). Or, instead of using the delegate methods, you may also set these values directly in your layout object, e.g. [layout setItemSize:size];
.
Using either of these methods will enable you to set your settings in Code rather than IB as they're removed when you set a Custom Layout. Remember to add <UICollectionViewDelegateFlowLayout>
to your .h file, too!
Create a new Subclass of UICollectionViewFlowLayout
, call it whatever you want, and make sure the H file has:
#import <UIKit/UIKit.h>
@interface YourSubclassNameHere : UICollectionViewFlowLayout
@end
Inside the Implementation File make sure it has the following:
- (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;
}
Choose "Custom" in Interface Builder for the Flow Layout, choose your "YourSubclassNameHere" Class that you just created. And Run!
(Note: the code above may not respect contentInset.bottom values, or especially large or small footer objects, or collections that have 0 objects but no footer.)
回答3:
If you have a single header view that you want pinned to the top of your UICollectionView, here's a relatively simple way to do it. Note this is meant to be as simple as possible - it assumes you are using a single header in a single section.
//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
See: https://gist.github.com/4613982
回答4:
Here is my take on it, I think it's a lot simpler than what a glimpsed above. The main source of simplicity is that I'm not subclassing flow layout, rather rolling my own layout (much easier, if you ask me).
Please Note I am assuming you are already capable of implementing your own custom UICollectionViewLayout
that will display cells and headers without floating implemented. Once you have that implementation written, only then will the code below make any sense. Again, this is because the OP was asking specifically about the floating headers part.
a few bonuses:
- I am floating two headers, not just one
- Headers push previous headers out of the way
- Look, swift!
note:
supplementaryLayoutAttributes
contains all header attributes without floating implemented- I am using this code in
prepareLayout
, since I do all computation upfront. - don't forget to override
shouldInvalidateLayoutForBoundsChange
to true!
// 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
}
回答5:
If already set flow layout in Storyboard
or Xib
file then try this,
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionHeadersPinToVisibleBounds = true
回答6:
In case anybody is looking for a solution in Objective-C, put this in viewDidload:
UICollectionViewFlowLayout *flowLayout =
(UICollectionViewFlowLayout*)_yourcollectionView.collectionViewLayout;
[flowLayout setSectionHeadersPinToVisibleBounds:YES];
回答7:
@iPrabu had an excellent answer with sectionHeadersPinToVisibleBounds
. I’ll just add that you can set this property in Interface Builder as well:
- Select the flow layout object in the document navigator. (If collapsed, expand it first using the toolbar button in the lower-left corner of the editor.)
- Open the Identity inspector and add a user-defined runtime attribute with key path
sectionHeadersPinToVisibleBounds
, type Boolean, and the checkbox checked.
The default header view has a transparent background. You might want to make it (partially) opaque or add a blur effect view.
回答8:
I ran into the same problem and found this in my google results. First I would like to thank cocotutch for sharing his solution. However, I wanted my UICollectionView to scroll horizontally and the headers to stick to the left of the screen, so I had to change the solution a bit.
Basically I just changed this:
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
};
to this:
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
};
}
See: https://gist.github.com/vigorouscoding/5155703 or http://www.vigorouscoding.com/2013/03/uicollectionview-with-sticky-headers/
回答9:
I ran this with vigorouscoding's code. However that code did not consider sectionInset.
So I changed this code for vertical scroll
origin.y = MIN(
MAX(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)),
(CGRectGetMaxY(lastCellAttrs.frame) - headerHeight)
);
to
origin.y = MIN(
MAX(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight - self.sectionInset.top)),
(CGRectGetMaxY(lastCellAttrs.frame) - headerHeight + self.sectionInset.bottom)
);
If you guys want code for horizontal scroll, refer to code aove.
回答10:
I've added a sample on github that is pretty simple, I think.
Basically the strategy is to provide a custom layout that invalidates on bounds change and provide layout attributes for the supplementary view that hug the current bounds. As others have suggested. I hope the code is useful.
回答11:
There is a bug in cocotouch's post. When there is no items in section and the section footer were set to be not displayed, the section header will go outside of the collection view and the user will be not able to see it.
In fact change:
if (numberOfItemsInSection > 0) {
firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
} else {
firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
atIndexPath:firstObjectIndexPath];
lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
atIndexPath:lastObjectIndexPath];
}
into:
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;
}
}
will solve this issue.
回答12:
VCollectionViewGridLayout does sticky headers. It is a vertical scrolling simple grid layout based on TLIndexPathTools. Try running the Sticky Headers sample project.
This layout also has much better batch update animation behavior than UICollectionViewFlowLayout
. There are a couple of sample projects provided that let you toggle between the two layouts to demonstrate the improvement.
来源:https://stackoverflow.com/questions/13511733/how-to-make-supplementary-view-float-in-uicollectionview-as-section-headers-do-i