Как заставить Вспомогательное Представление плавать в UICollectionView, как Заголовки Разделов делают в простом стиле UITableView
Я изо всех сил пытаюсь добиться эффекта "плавающего заголовка раздела" с 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
но мне нужно создать довольно сложную иерархию коллекций, которая легко достигается с помощью представления коллекции.
Любое руководство или образец кода будет принята с благодарностью!
14 ответов
В iOS9 Apple любезно добавила простое свойство в UICollectionViewFlowLayout
называется sectionHeadersPinToVisibleBounds
,
При этом вы можете заставить заголовки плавать так же, как в табличных представлениях.
let layout = UICollectionViewFlowLayout()
layout.sectionHeadersPinToVisibleBounds = true
layout.minimumInteritemSpacing = 1
layout.minimumLineSpacing = 1
super.init(collectionViewLayout: layout)
Либо реализуйте следующие методы делегата:
– 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;
}
Выберите "Custom" в Интерфейсном Разработчике для Разметки потока, выберите ваш "YourSubclassNameHere" Класс, который вы только что создали. И беги!
(Примечание: приведенный выше код может не учитывать значения contentInset.bottom, особенно большие или маленькие объекты нижнего колонтитула, или коллекции, которые имеют 0 объектов, но не имеют нижнего колонтитула.)
Если у вас есть один заголовок, который вы хотите закрепить в верхней части вашего 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
Вот мой взгляд на это, я думаю, что это намного проще, чем то, что мы увидели выше. Основным источником простоты является то, что я не делаю подклассы макета потока, а скорее использую свой собственный макет (гораздо проще, если вы спросите меня).
Пожалуйста, обратите внимание, я предполагаю, что вы уже способны реализовать свой собственный UICollectionViewLayout
это будет отображать ячейки и заголовки без использования плавающего. Как только вы напишете эту реализацию, только тогда приведенный ниже код будет иметь смысл. Опять же, это потому, что ОП спрашивал конкретно о части плавающих заголовков.
несколько бонусов:
- Я плаваю два заголовка, а не только один
- Заголовки смещают предыдущие заголовки
- Смотри, быстро!
нота:
supplementaryLayoutAttributes
содержит все атрибуты заголовка без использования плавающего- Я использую этот код в
prepareLayout
, так как я делаю все вычисления заранее. - не забудьте переопределить
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
}
Если кто-то ищет решение в Objective-C, поместите это в viewDidload:
UICollectionViewFlowLayout *flowLayout =
(UICollectionViewFlowLayout*)_yourcollectionView.collectionViewLayout;
[flowLayout setSectionHeadersPinToVisibleBounds:YES];
Если уже установлен макет потока в Storyboard
или же Xib
файл, попробуйте это,
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionHeadersPinToVisibleBounds = true
У @iPrabu был отличный ответ: sectionHeadersPinToVisibleBounds
, Я просто добавлю, что вы также можете установить это свойство в Интерфейсном Разработчике:
- Выберите объект макета потока в навигаторе документов. (Если он свернут, сначала разверните его, используя кнопку панели инструментов в левом нижнем углу редактора.)
- Откройте инспектор удостоверений и добавьте пользовательский атрибут времени выполнения с ключевым путем
sectionHeadersPinToVisibleBounds
введите Boolean, и флажок установлен.
Вид заголовка по умолчанию имеет прозрачный фон. Возможно, вы захотите сделать его (частично) непрозрачным или добавить вид эффекта размытия.
Я запустил это с помощью кода 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)
);
Если вы, ребята, хотите код для горизонтальной прокрутки, обратитесь к коду.
Swift 5.0
Поместите в свой viewDidLoad следующее:
if let layout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
layout.sectionHeadersPinToVisibleBounds = true
}
Я столкнулся с той же проблемой и нашел это в моих результатах 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/5155703 или http://www.vigorouscoding.com/2013/03/uicollectionview-with-sticky-headers/
Кажется, я наткнулся на элегантное решение, которое может оказаться полезным для других :)
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)
Я добавил пример на GitHub, который довольно прост, я думаю.
По сути, стратегия состоит в том, чтобы предоставить пользовательский макет, который делает недействительным изменение границ, и предоставить атрибуты макета для дополнительного вида, который охватывает текущие границы. Как и предполагали другие. Я надеюсь, что код полезен.
В сообщении 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;
}
}
решит эту проблему.
VCollectionViewGridLayout делает липкие заголовки. Это вертикальная прокрутка с простой структурой сетки, основанная на TLIndexPathTools. Попробуйте запустить пример проекта Sticky Headers.
Этот макет также имеет намного лучшее поведение анимации пакетного обновления, чем UICollectionViewFlowLayout
, Существует несколько примеров проектов, которые позволяют переключаться между двумя макетами, чтобы продемонстрировать улучшение.