J'ai du mal à obtenir un effet "en-tête de section flottante" avec UICollectionView
. Quelque chose qui a été assez facile dans UITableView
(le comportement par défaut pour UITableViewStylePlain
) semble impossible dans UICollectionView
sans beaucoup de travail acharné. Est-ce que je manque l'évidence?
Apple ne fournit aucune documentation pour y parvenir. Il semble qu'il faille sous-classer UICollectionViewLayout
et implémenter une présentation personnalisée pour obtenir cet effet. Cela nécessite beaucoup de travail, en implémentant les méthodes suivantes:
Méthodes pour remplacer
Chaque objet de présentation doit implémenter les méthodes suivantes:
collectionViewContentSize
layoutAttributesForElementsInRect:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath: (if your layout supports supplementary views)
layoutAttributesForDecorationViewOfKind:atIndexPath: (if your layout supports decoration views)
shouldInvalidateLayoutForBoundsChange:
Cependant, il m'est difficile de faire en sorte que la vue supplémentaire flotte au-dessus des cellules et reste "collée" au sommet de la vue jusqu'à ce que la section suivante soit atteinte. Existe-t-il un indicateur pour cela dans les attributs de mise en page?
J'aurais utilisé UITableView
mais je dois créer une hiérarchie assez complexe de collections, facile à réaliser avec une vue de collection.
Toute orientation ou exemple de code serait grandement apprécié!
Dans iOS9, Apple a eu la gentillesse d’ajouter une simple propriété dans UICollectionViewFlowLayout
appelé sectionHeadersPinToVisibleBounds
.
Grâce à cela, vous pouvez faire flotter les en-têtes de cette manière dans les vues de tableau.
let layout = UICollectionViewFlowLayout()
layout.sectionHeadersPinToVisibleBounds = true
layout.minimumInteritemSpacing = 1
layout.minimumLineSpacing = 1
super.init(collectionViewLayout: layout)
Implémentez les méthodes de délégation suivantes:
– collectionView:layout:sizeForItemAtIndexPath:
– collectionView:layout:insetForSectionAtIndex:
– collectionView:layout:minimumLineSpacingForSectionAtIndex:
– collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
– collectionView:layout:referenceSizeForHeaderInSection:
– collectionView:layout:referenceSizeForFooterInSection:
Dans votre contrôleur de vue utilisant votre méthode :cellForItemAtIndexPath
(il suffit de renvoyer les valeurs correctes). Ou, au lieu d'utiliser les méthodes de délégation, vous pouvez également définir ces valeurs directement dans votre objet de présentation, par exemple. [layout setItemSize:size];
.
L'utilisation de l'une de ces méthodes vous permettra de définir vos paramètres dans Code plutôt que dans IB, car ils sont supprimés lorsque vous définissez une disposition personnalisée. Pensez aussi à ajouter <UICollectionViewDelegateFlowLayout>
à votre fichier .h!
Créez une nouvelle sous-classe de UICollectionViewFlowLayout
, appelez-la comme vous voulez et assurez-vous que le fichier H a:
#import <UIKit/UIKit.h>
@interface YourSubclassNameHere : UICollectionViewFlowLayout
@end
Dans le fichier d'implémentation, assurez-vous qu'il contient les éléments suivants:
- (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;
}
Choisissez "Personnalisé" dans Interface Builder pour la structure de flux, choisissez votre classe "YourSubclassNameHere" que vous venez de créer. Et courir!
(Remarque: le code ci-dessus peut ne pas respecter les valeurs contentInset.bottom, ni les objets de pied de page petits ou grands, ni les collections contenant 0 objet mais pas de pied de page.)
Voici mon point de vue, je pense que c'est beaucoup plus simple qu'un aperçu ci-dessus. La principale source de simplicité est que je ne sous-classe pas une disposition de flux, mais que je roule ma propre disposition (beaucoup plus facile, si vous me le demandez).
Veuillez noter Je suppose que vous êtes déjà capable d'implémenter votre propre variable UICollectionViewLayout
qui affichera les cellules et les en-têtes sans mise en œuvre flottante. Une fois cette implémentation écrite, le code ci-dessous n’aura alors aucun sens. Encore une fois, cela est dû au fait que le PO posait spécifiquement des questions sur les en-têtes flottants.
quelques bonus:
remarque:
supplementaryLayoutAttributes
contient tous les attributs d'en-tête sans implémentation flottanteprepareLayout
, puisque je fais tous les calculs à l'avance.shouldInvalidateLayoutForBoundsChange
par 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
}
Si vous souhaitez épingler une seule vue d'en-tête en haut de votre UICollectionView, voici une méthode relativement simple pour le faire. Notez que cela se veut aussi simple que possible - cela suppose que vous utilisez un seul en-tête dans une seule 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
Si vous avez déjà défini la disposition du flux dans le fichier Storyboard
ou Xib
, essayez ceci:
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionHeadersPinToVisibleBounds = true
Si quelqu'un cherche une solution en Objective-C, mettez ceci dans viewDidload:
UICollectionViewFlowLayout *flowLayout =
(UICollectionViewFlowLayout*)_yourcollectionView.collectionViewLayout;
[flowLayout setSectionHeadersPinToVisibleBounds:YES];
J'ai rencontré le même problème et l'ai trouvé dans mes résultats Google. Je voudrais d’abord remercier Cocotutch d’avoir partagé sa solution. Cependant, je souhaitais que UICollectionView fasse défiler horizontalement et que les en-têtes restent à gauche de l'écran. J'ai donc dû modifier légèrement la solution.
En gros, je viens de changer ceci:
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
};
pour ça:
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
};
}
Voir: https://Gist.github.com/vigorouscoding/5155703 ou http://www.vigorouscoding.com/2013/03/uicollectionview-with-sticky-headers/
J'ai couru cela avec le code de vigorouscoding . Cependant, ce code ne considérait pas sectionInset.
J'ai donc changé ce code pour le défilement vertical
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)
);
Si vous voulez du code pour le défilement horizontal, reportez-vous au code ci-dessus.
@iPrabu a eu une excellente réponse avec sectionHeadersPinToVisibleBounds
. J'ajouterai simplement que vous pouvez également définir cette propriété dans Interface Builder:
sectionHeadersPinToVisibleBounds
, tapez Boolean et cochez la case.La vue en-tête par défaut a un arrière-plan transparent. Vous voudrez peut-être le rendre (partiellement) opaque ou ajouter une vue d'effet de flou.
J'ai ajouté un échantillon sur github qui est assez simple, je pense.
La stratégie consiste essentiellement à fournir une présentation personnalisée qui invalide les modifications de limites et à fournir des attributs de présentation pour la vue supplémentaire qui épouse les limites actuelles. Comme d'autres l'ont suggéré. J'espère que le code est utile.
Il y a un bug dans le post de cocotouch. Lorsqu'il n'y a aucun élément dans la section et que le pied de section était configuré pour ne pas être affiché, l'en-tête de la section sortira de la vue de collection et l'utilisateur ne pourra pas le voir.
En fait, 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];
}
dans:
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;
}
}
va résoudre ce problème.
VCollectionViewGridLayout fait des en-têtes collants. Il s'agit d'une disposition de grille simple à défilement vertical basée sur TLIndexPathTools . Essayez d’exécuter le projet Sticky Headers .
Cette disposition présente également un bien meilleur comportement d'animation de mise à jour par lots que UICollectionViewFlowLayout
. Quelques exemples de projets fournis vous permettent de basculer entre les deux présentations pour démontrer l'amélioration.