web-dev-qa-db-fra.com

Comment créer une vue supplémentaire flottante dans UICollectionView comme le font les en-têtes de section dans un style UITableView

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é!

63
100grams

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)
62
iPrabu

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.)

47
topLayoutGuide

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:

  1. Je fais flotter deux en-têtes, pas un seul
  2. En-têtes Repousser les en-têtes précédents
  3. Regardez, rapide!

remarque:

  1. supplementaryLayoutAttributes contient tous les attributs d'en-tête sans implémentation flottante
  2. J'utilise ce code dans prepareLayout, puisque je fais tous les calculs à l'avance.
  3. n'oubliez pas de remplacer 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
}
7
Mazyod

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

Voir: https://Gist.github.com/4613982

6
MikeV

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
3
Zaid Pathan

Si quelqu'un cherche une solution en Objective-C, mettez ceci dans viewDidload:

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

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/

2
vigorouscoding

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.

2
user2273634

@iPrabu a eu une excellente réponse avec sectionHeadersPinToVisibleBounds. J'ajouterai simplement que vous pouvez également définir cette propriété dans Interface Builder:

  1. Sélectionnez l'objet de présentation de flux dans le navigateur de document. (Si réduit, développez-le d'abord à l'aide du bouton de la barre d'outils situé dans le coin inférieur gauche de l'éditeur.)

 Selecting the flow layout object in the document navigator.

  1. Ouvrez l'inspecteur d'identité et ajoutez un attribut d'exécution défini par l'utilisateur avec le chemin d'accès clé sectionHeadersPinToVisibleBounds, tapez Boolean et cochez la case.

 Setting the runtime attribute in the Identity inspector.

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.

1

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.

1
Derrick Hathaway

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.

1
WeZZard

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.

0
Timothy Moose