web-dev-qa-db-fra.com

UICollectionView with paging - définition de la largeur de la page

J'ai une vue Collection qui peut afficher environ 3,5 cellules à la fois et je veux qu'elle soit activée pour la pagination. Mais j'aimerais qu'il s'accroche à chaque cellule (comme le fait l'application App Store) et ne défile pas toute la largeur de la vue. Comment puis je faire ça?

21
Guilherme

Vous pouvez accrocher des cellules en étant le délégué de la vue de collection et en implémentant la méthode:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset

Cela vous indique que l’utilisateur a terminé son glissement et vous permet de modifier la targetContentOffset pour l’aligner sur vos cellules (par exemple, à la cellule la plus proche). Notez que vous devez faire attention à la façon dont vous modifiez le targetContentOffset; en particulier, vous devez éviter de la modifier de sorte que la vue ait besoin de défiler dans le sens opposé de la vélocité passée, sinon vous aurez des problèmes d'animation. Vous pouvez probablement trouver de nombreux exemples de cela si vous recherchez un nom de méthode sur Google.

16
Jesse Rusak

Une autre méthode consiste à créer un UICollectionViewFlowLayout personnalisé et à remplacer la méthode de la manière suivante: 

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset 
                                 withScrollingVelocity:(CGPoint)velocity {

    CGRect cvBounds = self.collectionView.bounds;
    CGFloat halfWidth = cvBounds.size.width * 0.5f;
    CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;

    NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];

    UICollectionViewLayoutAttributes* candidateAttributes;
    for (UICollectionViewLayoutAttributes* attributes in attributesArray) {

        // == Skip comparison with non-cell items (headers and footers) == //
        if (attributes.representedElementCategory != 
            UICollectionElementCategoryCell) {
            continue;
        }

        // == First time in the loop == //
        if(!candidateAttributes) {
            candidateAttributes = attributes;
            continue;
        }

        if (fabsf(attributes.center.x - proposedContentOffsetCenterX) < 
            fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
            candidateAttributes = attributes;
        }
    }

    return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);

}

_ { https://Gist.github.com/mmick66/9812223 } _ 

  • remarque: cela fonctionnera lorsque nous afficherons les cellules de prévisualisation (même si elles ont un alpha de 0.0f). En effet, si les cellules d'aperçu ne sont pas disponibles au moment du défilement, leurs attributs ne seront pas transmis à la boucle ...

Si vous cherchez une solution Swift, j’ai également créé un petit tutoriel contenant du code .

50
Mike M

Voici ma mise en œuvre dans Swift 4.2 pour la pagination à base de cellules verticale:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    // Page height used for estimating and calculating paging.
    let pageHeight = self.itemSize.height + self.minimumLineSpacing

    // Make an estimation of the current page position.
    let approximatePage = self.collectionView!.contentOffset.y/pageHeight

    // Determine the current page based on velocity.
    let currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage)

    // Create custom flickVelocity.
    let flickVelocity = velocity.y * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - self.collectionView!.contentInset.top

    return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}

Quelques notes:

  • Ne glisse pas
  • N'oubliez pas de définir définir la pagination par défaut sur false!
  • Vous permet de définir facilement votre propre vitesse de balayage.
  • Si quelque chose ne fonctionne toujours pas après avoir essayé ceci, vérifiez si votre itemSize correspond bien à la taille de l'élément car c'est souvent un problème.
  • Cela fonctionne mieux lorsque vous définissez self.collectionView.decelerationRate = UIScollViewDecelerationRateFast.

Voici une version horizontale (je ne l’ai pas testée à fond, alors pardonnez vos erreurs):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    // Page width used for estimating and calculating paging.
    let pageWidth = self.itemSize.width + self.minimumLineSpacing

    // Make an estimation of the current page position.
    let approximatePage = self.collectionView!.contentOffset.x/pageWidth

    // Determine the current page based on velocity.
    let currentPage = (velocity.x < 0.0) ? floor(approximatePage) : ceil(approximatePage)

    // Create custom flickVelocity.
    let flickVelocity = velocity.x * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    // Calculate newHorizontalOffset.
    let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - self.collectionView!.contentInset.left

    return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y)
}
4
JoniVR

J'ai développé ma solution avant de regarder celles-ci. J'ai également créé un UICollectionViewFlowLayout personnalisé et substitué la méthode targetContentOffset.

Cela semble fonctionner correctement pour moi (c’est-à-dire que j’ai le même comportement que sur l’AppStore) même si j’ai beaucoup moins de code. La voici, n'hésitez pas à me signaler tout inconvénient auquel vous pourriez penser:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    let inset: Int = 10
    let vcBounds = self.collectionView!.bounds
    var candidateContentOffsetX: CGFloat = proposedContentOffset.x

    for attributes in self.layoutAttributesForElements(in: vcBounds)! as [UICollectionViewLayoutAttributes] {

        if vcBounds.Origin.x < attributes.center.x {
            candidateContentOffsetX = attributes.frame.Origin.x - CGFloat(inset)
            break
        }

    }

    return CGPoint(x: candidateContentOffsetX, y: proposedContentOffset.y)
}
2
Filip Juncu

Ceci est ma solution. Fonctionne avec n'importe quelle largeur de page. 

Définissez self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast pour ressentir une véritable pagination.

La solution est basée sur une section de défilement paginée par les éléments.

- (CGFloat)pageWidth {

    return self.itemSize.width + self.minimumLineSpacing;
}

- (CGPoint)offsetAtCurrentPage {

    CGFloat width = -self.collectionView.contentInset.left - self.sectionInset.left;
    for (int i = 0; i < self.currentPage; i++)
        width += [self pageWidth];

    return CGPointMake(width, 0);
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset {

    return [self offsetAtCurrentPage];
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {

    // To scroll paginated
    /*
    if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfItemsInSection:0]-1) self.currentPage += 1;
    else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;

    return  [self offsetAtCurrentPage];
    */

    // To scroll and stop always at the center of a page
    CGRect proposedRect = CGRectMake(proposedContentOffset.x+self.collectionView.bounds.size.width/2 - self.pageWidth/2, 0, self.pageWidth, self.collectionView.bounds.size.height);
    NSMutableArray <__kindof UICollectionViewLayoutAttributes *> *allAttributes = [[self layoutAttributesForElementsInRect:proposedRect] mutableCopy];
    __block UICollectionViewLayoutAttributes *proposedAttributes = nil;
    __block CGFloat minDistance = CGFLOAT_MAX;
    [allAttributes enumerateObjectsUsingBlock:^(__kindof UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

        CGFloat distance = CGRectGetMidX(proposedRect) - obj.center.x;

        if (ABS(distance) < minDistance) {
            proposedAttributes = obj;
            minDistance = distance;
        }
    }];


    // Scroll always
    if (self.currentPage == proposedAttributes.indexPath.row) {
        if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfItemsInSection:0]-1) self.currentPage += 1;
        else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;
    }
    else  {
        self.currentPage = proposedAttributes.indexPath.row;
    }

    return  [self offsetAtCurrentPage];
}

Ceci est paginé par sections.

- (CGPoint)offsetAtCurrentPage {

    CGFloat width = -self.collectionView.contentInset.leff;
    for (int i = 0; i < self.currentPage; i++)
        width += [self sectionWidth:i];
    return CGPointMake(width, 0);
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
    return [self offsetAtCurrentPage];
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {

    // To scroll paginated
    /*
    if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfSections]-1) self.currentPage += 1;
    else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;

    return  [self offsetAtCurrentPage];
    */

    // To scroll and stop always at the center of a page
    CGRect proposedRect = CGRectMake(proposedContentOffset.x+self.collectionView.bounds.size.width/2 - [self sectionWidth:0]/2, 0, [self sectionWidth:0], self.collectionView.bounds.size.height);
    NSMutableArray <__kindof UICollectionViewLayoutAttributes *> *allAttributes = [[self layoutAttributesForElementsInRect:proposedRect] mutableCopy];
    __block UICollectionViewLayoutAttributes *proposedAttributes = nil;
    __block CGFloat minDistance = CGFLOAT_MAX;
    [allAttributes enumerateObjectsUsingBlock:^(__kindof UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

        CGFloat distance = CGRectGetMidX(proposedRect) - obj.center.x;

        if (ABS(distance) < minDistance) {
            proposedAttributes = obj;
            minDistance = distance;
        }
    }];

    // Scroll always
    if (self.currentPage == proposedAttributes.indexPath.section) {
        if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfSections]-1) self.currentPage += 1;
        else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;
    }
    else  {
        self.currentPage = proposedAttributes.indexPath.section;
    }

    return  [self offsetAtCurrentPage];
}
0
Javi

La solution que Mike M. a présentée dans le message précédent fonctionnait pour moi, mais dans mon cas, je souhaitais que la première cellule commence au milieu de la collection. J'ai donc utilisé la méthode de délégation du flux de collecte pour définir un encart (collectionView:layout:insetForSectionAtIndex:). Cela fait que le défilement entre la première et la deuxième cellule est bloqué et ne défile pas correctement vers la première cellule.

La raison en était que candidateAttributes.center.x - halfWidth avait une valeur négative. La solution était d’obtenir la valeur absolue, j’ajoute donc des outils de fabrication à cette ligne return CGPointMake(fabs(candidateAttributes.center.x - halfWidth), offset.y);

Les Fabs doivent être ajoutés par défaut pour couvrir toutes les situations.

0
lopes710