targetContentOffsetForProposedContentOffset: withScrollingVelocity sans sous-classer UICollectionViewFlowLayout

99

J'ai une collectionView très simple dans mon application (juste une seule rangée d'images miniatures carrées).

Je voudrais intercepter le défilement pour que le décalage laisse toujours une image complète sur le côté gauche. Pour le moment, il défile n'importe où et laissera des images coupées.

Quoi qu'il en soit, je sais que j'ai besoin d'utiliser la fonction

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

pour ce faire, mais j'utilise juste une norme UICollectionViewFlowLayout. Je ne le sous-classe pas.

Existe-t-il un moyen d'intercepter cela sans sous UICollectionViewFlowLayout- classer ?

Merci

Fogmeister
la source

Réponses:

113

OK, la réponse est non, il n'y a aucun moyen de le faire sans sous-classer UICollectionViewFlowLayout.

Cependant, le sous-classement est incroyablement facile pour quiconque lira ceci à l'avenir.

J'ai d'abord configuré l'appel de sous-classe MyCollectionViewFlowLayout, puis dans le générateur d'interface, j'ai changé la disposition de la vue de collection sur Personnalisée et sélectionné ma sous-classe de disposition de flux.

Parce que vous le faites de cette façon, vous ne pouvez pas spécifier la taille des éléments, etc ... dans IB donc dans MyCollectionViewFlowLayout.m j'ai ceci ...

- (void)awakeFromNib
{
    self.itemSize = CGSizeMake(75.0, 75.0);
    self.minimumInteritemSpacing = 10.0;
    self.minimumLineSpacing = 10.0;
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    self.sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0);
}

Cela configure toutes les tailles pour moi et la direction de défilement.

Ensuite ...

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    CGFloat offsetAdjustment = MAXFLOAT;
    CGFloat horizontalOffset = proposedContentOffset.x + 5;

    CGRect targetRect = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);

    NSArray *array = [super layoutAttributesForElementsInRect:targetRect];

    for (UICollectionViewLayoutAttributes *layoutAttributes in array) {
        CGFloat itemOffset = layoutAttributes.frame.origin.x;
        if (ABS(itemOffset - horizontalOffset) < ABS(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset;
        }
    }

    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

Cela garantit que le défilement se termine par une marge de 5,0 sur le bord gauche.

C'est tout ce que j'avais à faire. Je n'avais pas du tout besoin de définir la disposition du flux dans le code.

Fogmeister
la source
1
Il est vraiment puissant lorsqu'il est utilisé correctement. Avez-vous regardé les sessions Collection View de la WWDC 2012? Ils valent vraiment la peine d'être regardés. Des trucs incroyables.
Fogmeister
2
targetContentOffsetForProposedContentOffset:withVelocityn'est pas appelé pour moi quand je fais défiler. Que se passe-t-il?
fatuhoku
4
@TomSawyer a défini le taux de déclaration de UICollectionView sur UIScrollViewDecelerationRateFast.
Clay Ellis
3
@fatuhoku assurez-vous que la propriété paginEnabled de votre collectionView est définie sur false
chrs
4
Holy Moly, j'ai dû faire défiler vers le bas comme un million de kilomètres pour voir cette réponse. :)
AnBisw
67

La solution de Dan est imparfaite. Il ne gère pas bien le feuilletage de l'utilisateur. Les cas où l'utilisateur feuillette rapidement et que le défilement ne bougeait pas tellement, ont des problèmes d'animation.

Ma mise en œuvre alternative proposée a la même pagination que celle proposée précédemment, mais gère le feuilletage de l'utilisateur entre les pages.

 #pragma mark - Pagination
 - (CGFloat)pageWidth {
     return self.itemSize.width + self.minimumLineSpacing;
 }

 - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
 {           
        CGFloat rawPageValue = self.collectionView.contentOffset.x / self.pageWidth;
        CGFloat currentPage = (velocity.x > 0.0) ? floor(rawPageValue) : ceil(rawPageValue);
        CGFloat nextPage = (velocity.x > 0.0) ? ceil(rawPageValue) : floor(rawPageValue);

        BOOL pannedLessThanAPage = fabs(1 + currentPage - rawPageValue) > 0.5;
        BOOL flicked = fabs(velocity.x) > [self flickVelocity];
        if (pannedLessThanAPage && flicked) {
            proposedContentOffset.x = nextPage * self.pageWidth;
        } else {
            proposedContentOffset.x = round(rawPageValue) * self.pageWidth;
        }

        return proposedContentOffset;
 }

 - (CGFloat)flickVelocity {
     return 0.3;
 }
DarkMike
la source
Je vous remercie! Cela a fonctionné comme un charme. Un peu difficile à comprendre mais y arriver.
Rajiev Timal
J'ai cette erreur: impossible d'attribuer à «x» dans «proposéContentOffset»? Vous utilisez Swift? comment puis-je attribuer une valeur x?
TomSawyer
@TomSawyer Les paramètres sont «let» par défaut. Essayez de déclarer la fonction comme ceci dans Swift (en utilisant var avant le param): override func targetContentOffsetForProposedContentOffset (var proposéContentOffset: CGPoint) -> CGPoint
DarthMike
1
Vous ne pouvez pas utiliser CGPointMake dans Swift. J'ai personnellement utilisé ceci: "var targetContentOffset: CGPoint if pannedLessThanAPage && flicked {targetContentOffset = CGPoint (x: nextPage * pageWidth (), y: proposéContentOffset.y);} else {targetContentOffset = CGPoint (x: round (rawPageValthue) * pageWidthue) ), y: recommendedContentOffset.y);} return proposéContentOffset "
Tracer
1
Ce devrait être la réponse choisie.
khunshan
26

Version rapide de la réponse acceptée.

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var offsetAdjustment = CGFloat.greatestFiniteMagnitude
    let horizontalOffset = proposedContentOffset.x
    let targetRect = CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: self.collectionView!.bounds.size)

    for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
        let itemOffset = layoutAttributes.frame.origin.x
        if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset
        }
    }

    return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}    

Valable pour Swift 5 .

André Abreu
la source
Cette version fonctionne très bien, et elle fonctionne également bien pour l'axe Y si vous échangez le code.
Chris
Fonctionne principalement très bien ici. Mais si j'arrête de faire défiler et que je lève le doigt (avec précaution), il ne défilera vers aucune page et s'arrêtera là.
Christian A. Strømmen
@ ChristianA.Strømmen Bizarre, cela fonctionne très bien avec mon application.
André Abreu
@ AndréAbreu où placer cette fonction?
FlowUI. SimpleUITesting.com
2
@Jay Vous devez sous-classer UICollectionViewLayout ou toute classe qui le sous-classe déjà (par exemple UICollectionViewFlowLayout).
André Abreu
24

Voici mon implémentation dans Swift 5 pour la pagination verticale basée sur les cellules:

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

    guard let collectionView = self.collectionView else {
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    }

    // 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 = collectionView.contentOffset.y/pageHeight

    // Determine the current page based on velocity.
    let currentPage = velocity.y == 0 ? round(approximatePage) : (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) - collectionView.contentInset.top

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

Quelques notes:

  • Ne pique pas
  • RÉGLER LA PAGING SUR FAUX ! (sinon cela ne fonctionnera pas)
  • Vous permet de définir facilement votre propre vitesse de défilement .
  • Si quelque chose ne fonctionne toujours pas après avoir essayé cela, vérifiez si votre itemSizecorrespond réellement à la taille de l'élément car c'est souvent un problème, en particulier lors de l'utilisation collectionView(_:layout:sizeForItemAt:), utilisez plutôt une variable personnalisée avec le itemSize.
  • Cela fonctionne mieux lorsque vous définissez self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Voici une version horizontale (je ne l'ai pas testée à fond, veuillez pardonner toute erreur):

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

    guard let collectionView = self.collectionView else {
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    }

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

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

    // Determine the current page based on velocity.
    let currentPage = velocity.x == 0 ? round(approximatePage) : (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) - collectionView.contentInset.left

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

Ce code est basé sur le code que j'utilise dans mon projet personnel, vous pouvez le vérifier ici en le téléchargeant et en exécutant la cible Exemple.

JoniVR
la source
4
Vous êtes le sauveur de la vie! Important à noter pour RÉGLER LA PAGING SUR FAUX !!! Perdu comme 2 heures de ma vie à réparer votre fonction, qui fonctionne déjà ...
denis631
@ denis631 Je suis vraiment désolé! J'aurais dû ajouter cela, je modifierai le message pour refléter cela! Heureux que cela ait fonctionné :)
JoniVR
jesssus, je me demandais pourquoi cela ne fonctionnait pas jusqu'à ce que j'ai vu ce commentaire sur la désactivation de la pagination ... bien sûr, le mien était réglé sur vrai
Kam Wo
@JoniVR Cela me montre que cette méthode d'erreur ne remplace aucune méthode de sa superclasse
Muju
22

Bien que cette réponse m'ait été d'une grande aide, il y a un scintillement perceptible lorsque vous glissez rapidement sur une petite distance. Il est beaucoup plus facile de le reproduire sur l'appareil.

J'ai trouvé que cela arrive toujours quand collectionView.contentOffset.x - proposedContentOffset.xet velocity.xchante différemment.

Ma solution était de m'assurer que proposedContentOffsetc'est plus que contentOffset.xsi la vitesse est positive, et moins si elle est négative. C'est en C # mais devrait être assez simple à traduire en Objective C:

public override PointF TargetContentOffset (PointF proposedContentOffset, PointF scrollingVelocity)
{
    /* Determine closest edge */

    float offSetAdjustment = float.MaxValue;
    float horizontalCenter = (float) (proposedContentOffset.X + (this.CollectionView.Bounds.Size.Width / 2.0));

    RectangleF targetRect = new RectangleF (proposedContentOffset.X, 0.0f, this.CollectionView.Bounds.Size.Width, this.CollectionView.Bounds.Size.Height);
    var array = base.LayoutAttributesForElementsInRect (targetRect);

    foreach (var layoutAttributes in array) {
        float itemHorizontalCenter = layoutAttributes.Center.X;
        if (Math.Abs (itemHorizontalCenter - horizontalCenter) < Math.Abs (offSetAdjustment)) {
            offSetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
    }

    float nextOffset = proposedContentOffset.X + offSetAdjustment;

    /*
     * ... unless we end up having positive speed
     * while moving left or negative speed while moving right.
     * This will cause flicker so we resort to finding next page
     * in the direction of velocity and use it.
     */

    do {
        proposedContentOffset.X = nextOffset;

        float deltaX = proposedContentOffset.X - CollectionView.ContentOffset.X;
        float velX = scrollingVelocity.X;

        // If their signs are same, or if either is zero, go ahead
        if (Math.Sign (deltaX) * Math.Sign (velX) != -1)
            break;

        // Otherwise, look for the closest page in the right direction
        nextOffset += Math.Sign (scrollingVelocity.X) * SnapStep;
    } while (IsValidOffset (nextOffset));

    return proposedContentOffset;
}

bool IsValidOffset (float offset)
{
    return (offset >= MinContentOffset && offset <= MaxContentOffset);
}

Ce code utilise MinContentOffset, MaxContentOffsetet SnapStepqui devrait être simple à définir. Dans mon cas, ils se sont avérés être

float MinContentOffset {
    get { return -CollectionView.ContentInset.Left; }
}

float MaxContentOffset {
    get { return MinContentOffset + CollectionView.ContentSize.Width - ItemSize.Width; }
}

float SnapStep {
    get { return ItemSize.Width + MinimumLineSpacing; }
}
Dan Abramov
la source
7
Cela fonctionne vraiment bien. Je l'ai converti en Objective-C pour les personnes intéressées: gist.github.com/rkeniger/7687301
Rob Keniger
21

Après de longs tests, j'ai trouvé une solution pour s'aligner au centre avec une largeur de cellule personnalisée (chaque cellule a une largeur différente) qui corrige le scintillement. N'hésitez pas à améliorer le script.

- (CGPoint) targetContentOffsetForProposedContentOffset: (CGPoint) proposedContentOffset withScrollingVelocity: (CGPoint)velocity
{
    CGFloat offSetAdjustment = MAXFLOAT;
    CGFloat horizontalCenter = (CGFloat) (proposedContentOffset.x + (self.collectionView.bounds.size.width / 2.0));

    //setting fastPaging property to NO allows to stop at page on screen (I have pages lees, than self.collectionView.bounds.size.width)
    CGRect targetRect = CGRectMake(self.fastPaging ? proposedContentOffset.x : self.collectionView.contentOffset.x, 
                                   0.0,
                                   self.collectionView.bounds.size.width,
                                   self.collectionView.bounds.size.height);

    NSArray *attributes = [self layoutAttributesForElementsInRect:targetRect];
    NSPredicate *cellAttributesPredicate = [NSPredicate predicateWithBlock: ^BOOL(UICollectionViewLayoutAttributes * _Nonnull evaluatedObject,
                                                                             NSDictionary<NSString *,id> * _Nullable bindings) 
    {
        return (evaluatedObject.representedElementCategory == UICollectionElementCategoryCell); 
    }];        

    NSArray *cellAttributes = [attributes filteredArrayUsingPredicate: cellAttributesPredicate];

    UICollectionViewLayoutAttributes *currentAttributes;

    for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
    {
        CGFloat itemHorizontalCenter = layoutAttributes.center.x;
        if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offSetAdjustment))
        {
            currentAttributes   = layoutAttributes;
            offSetAdjustment    = itemHorizontalCenter - horizontalCenter;
        }
    }

    CGFloat nextOffset          = proposedContentOffset.x + offSetAdjustment;

    proposedContentOffset.x     = nextOffset;
    CGFloat deltaX              = proposedContentOffset.x - self.collectionView.contentOffset.x;
    CGFloat velX                = velocity.x;

    // detection form  gist.github.com/rkeniger/7687301
    // based on http://stackoverflow.com/a/14291208/740949
    if (fabs(deltaX) <= FLT_EPSILON || fabs(velX) <= FLT_EPSILON || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) 
    {

    } 
    else if (velocity.x > 0.0) 
    {
       // revert the array to get the cells from the right side, fixes not correct center on different size in some usecases
        NSArray *revertedArray = [[array reverseObjectEnumerator] allObjects];

        BOOL found = YES;
        float proposedX = 0.0;

        for (UICollectionViewLayoutAttributes *layoutAttributes in revertedArray)
        {
            if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell)
            {
                CGFloat itemHorizontalCenter = layoutAttributes.center.x;
                if (itemHorizontalCenter > proposedContentOffset.x) {
                     found = YES;
                     proposedX = nextOffset + (currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2);
                } else {
                     break;
                }
            }
        }

       // dont set on unfound element
        if (found) {
            proposedContentOffset.x = proposedX;
        }
    } 
    else if (velocity.x < 0.0) 
    {
        for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
        {
            CGFloat itemHorizontalCenter = layoutAttributes.center.x;
            if (itemHorizontalCenter > proposedContentOffset.x) 
            {
                proposedContentOffset.x = nextOffset - ((currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2));
                break;
            }
        }
    }

    proposedContentOffset.y = 0.0;

    return proposedContentOffset;
}
Pion
la source
10
La meilleure solution de tous, merci! Pour tous les futurs lecteurs, vous devez également désactiver la pagination pour que cela fonctionne.
sridvijay
1
Si l'on voulait l'aligner à partir de la gauche, au lieu de la cellule alignée à droite au centre, comment ferions-nous pour la changer?
CyberMew
Je ne sais pas si je comprends bien, mais si vous souhaitez démarrer les éléments au centre et les aligner au centre, vous devez modifier le paramètre contentInset. J'utilise ceci: gist.github.com/pionl/432fc8059dee3b540e38
Pion
Pour aligner la position X de la cellule au milieu de la vue, supprimez simplement + (layoutAttributes.frame.size.width / 2) dans la section de vitesse.
Pion le
1
@Jay Bonjour, créez simplement un délégué Flow personnalisé et ajoutez-y ce code. N'oubliez pas de définir la disposition personnalisée dans la plume ou le code.
Pion
18

se référer à cette réponse de Dan Abramov voici la version Swift

    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var _proposedContentOffset = CGPoint(x: proposedContentOffset.x, y: proposedContentOffset.y)
    var offSetAdjustment: CGFloat = CGFloat.max
    let horizontalCenter: CGFloat = CGFloat(proposedContentOffset.x + (self.collectionView!.bounds.size.width / 2.0))

    let targetRect = CGRect(x: proposedContentOffset.x, y: 0.0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)

    let array: [UICollectionViewLayoutAttributes] = self.layoutAttributesForElementsInRect(targetRect)! as [UICollectionViewLayoutAttributes]
    for layoutAttributes: UICollectionViewLayoutAttributes in array {
        if (layoutAttributes.representedElementCategory == UICollectionElementCategory.Cell) {
            let itemHorizontalCenter: CGFloat = layoutAttributes.center.x
            if (abs(itemHorizontalCenter - horizontalCenter) < abs(offSetAdjustment)) {
                offSetAdjustment = itemHorizontalCenter - horizontalCenter
            }
        }
    }

    var nextOffset: CGFloat = proposedContentOffset.x + offSetAdjustment

    repeat {
        _proposedContentOffset.x = nextOffset
        let deltaX = proposedContentOffset.x - self.collectionView!.contentOffset.x
        let velX = velocity.x

        if (deltaX == 0.0 || velX == 0 || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) {
            break
        }

        if (velocity.x > 0.0) {
            nextOffset = nextOffset + self.snapStep()
        } else if (velocity.x < 0.0) {
            nextOffset = nextOffset - self.snapStep()
        }
    } while self.isValidOffset(nextOffset)

    _proposedContentOffset.y = 0.0

    return _proposedContentOffset
}

func isValidOffset(offset: CGFloat) -> Bool {
    return (offset >= CGFloat(self.minContentOffset()) && offset <= CGFloat(self.maxContentOffset()))
}

func minContentOffset() -> CGFloat {
    return -CGFloat(self.collectionView!.contentInset.left)
}

func maxContentOffset() -> CGFloat {
    return CGFloat(self.minContentOffset() + self.collectionView!.contentSize.width - self.itemSize.width)
}

func snapStep() -> CGFloat {
    return self.itemSize.width + self.minimumLineSpacing;
}

ou gist ici https://gist.github.com/katopz/8b04c783387f0c345cd9

Katopz
la source
4
Version mise à jour de ceci pour Swift 3: gist.github.com/mstubna/beed10327e00310d05f12bf4747266a4
mstubna
1
Dang it @mstubna, je suis allé de l'avant et j'ai copié ce qui précède, je l'ai mis à jour pour swift 3, j'ai commencé à faire une mise à jour de l'essentiel et je suis revenu ici pour collecter des notes / titre à quel point j'ai remarqué que vous aviez déjà fait un swift 3. Merci! Dommage que je l'ai manqué.
VaporwareWolf
16

Pour tous ceux qui recherchent une solution qui ...

  • NE PAS BRILLER lorsque l'utilisateur effectue un court défilement rapide (c'est-à-dire qu'il considère les vitesses de défilement positives et négatives)
  • prend en compte la zone collectionView.contentInset(et safeArea sur iPhone X)
  • considère uniquement les cellules visibles au point de défilement (pour la performance)
  • utilise des variables et des commentaires bien nommés
  • est Swift 4

alors s'il vous plaît voir ci-dessous ...

public class CarouselCollectionViewLayout: UICollectionViewFlowLayout {

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

        guard let collectionView = collectionView else {
            return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        }

        // Identify the layoutAttributes of cells in the vicinity of where the scroll view will come to rest
        let targetRect = CGRect(origin: proposedContentOffset, size: collectionView.bounds.size)
        let visibleCellsLayoutAttributes = layoutAttributesForElements(in: targetRect)

        // Translate those cell layoutAttributes into potential (candidate) scrollView offsets
        let candidateOffsets: [CGFloat]? = visibleCellsLayoutAttributes?.map({ cellLayoutAttributes in
            if #available(iOS 11.0, *) {
                return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left - sectionInset.left
            } else {
                return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - sectionInset.left
            }
        })

        // Now we need to work out which one of the candidate offsets is the best one
        let bestCandidateOffset: CGFloat

        if velocity.x > 0 {
            // If the scroll velocity was POSITIVE, then only consider cells/offsets to the RIGHT of the proposedContentOffset.x
            // Of the cells/offsets to the right, the NEAREST is the `bestCandidate`
            // If there is no nearestCandidateOffsetToLeft then we default to the RIGHT-MOST (last) of ALL the candidate cells/offsets
            //      (this handles the scenario where the user has scrolled beyond the last cell)
            let candidateOffsetsToRight = candidateOffsets?.toRight(ofProposedOffset: proposedContentOffset.x)
            let nearestCandidateOffsetToRight = candidateOffsetsToRight?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffsetToRight ?? candidateOffsets?.last ?? proposedContentOffset.x
        }
        else if velocity.x < 0 {
            // If the scroll velocity was NEGATIVE, then only consider cells/offsets to the LEFT of the proposedContentOffset.x
            // Of the cells/offsets to the left, the NEAREST is the `bestCandidate`
            // If there is no nearestCandidateOffsetToLeft then we default to the LEFT-MOST (first) of ALL the candidate cells/offsets
            //      (this handles the scenario where the user has scrolled beyond the first cell)
            let candidateOffsetsToLeft = candidateOffsets?.toLeft(ofProposedOffset: proposedContentOffset.x)
            let nearestCandidateOffsetToLeft = candidateOffsetsToLeft?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffsetToLeft ?? candidateOffsets?.first ?? proposedContentOffset.x
        }
        else {
            // If the scroll velocity was ZERO we consider all `candidate` cells (regarless of whether they are to the left OR right of the proposedContentOffset.x)
            // The cell/offset that is the NEAREST is the `bestCandidate`
            let nearestCandidateOffset = candidateOffsets?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffset ??  proposedContentOffset.x
        }

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

}

fileprivate extension Sequence where Iterator.Element == CGFloat {

    func toLeft(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {

        return filter() { candidateOffset in
            return candidateOffset < proposedOffset
        }
    }

    func toRight(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {

        return filter() { candidateOffset in
            return candidateOffset > proposedOffset
        }
    }

    func nearest(toProposedOffset proposedOffset: CGFloat) -> CGFloat? {

        guard let firstCandidateOffset = first(where: { _ in true }) else {
            // If there are no elements in the Sequence, return nil
            return nil
        }

        return reduce(firstCandidateOffset) { (bestCandidateOffset: CGFloat, candidateOffset: CGFloat) -> CGFloat in

            let candidateOffsetDistanceFromProposed = fabs(candidateOffset - proposedOffset)
            let bestCandidateOffsetDistancFromProposed = fabs(bestCandidateOffset - proposedOffset)

            if candidateOffsetDistanceFromProposed < bestCandidateOffsetDistancFromProposed {
                return candidateOffset
            }

            return bestCandidateOffset
        }
    }
}
Oliver Pearmain
la source
1
Merci! juste copié et collé, fonctionne parfaitement ... bien mieux que prévu.
Steven B.
1
Une et seule solution qui fonctionne réellement . Bon travail! Merci!
LinusGeffarth
1
return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left candidateOffsets - sectionInset.left il y a un problème dans cette ligne
Utku Dalmaz
1
@Dalmaz merci de m'avoir prévenu. J'ai résolu le problème maintenant.
Oliver Pearmain
1
Oui, juste copié et collé, vous économisez mon temps.
Wei
7

Voici ma solution Swift sur une vue de collection à défilement horizontal. C'est simple, doux et évite tout scintillement.

  override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else { return proposedContentOffset }

    let currentXOffset = collectionView.contentOffset.x
    let nextXOffset = proposedContentOffset.x
    let maxIndex = ceil(currentXOffset / pageWidth())
    let minIndex = floor(currentXOffset / pageWidth())

    var index: CGFloat = 0

    if nextXOffset > currentXOffset {
      index = maxIndex
    } else {
      index = minIndex
    }

    let xOffset = pageWidth() * index
    let point = CGPointMake(xOffset, 0)

    return point
  }

  func pageWidth() -> CGFloat {
    return itemSize.width + minimumInteritemSpacing
  }
Scott Kaiser
la source
qu'est ce que c'est itemSize??
Konstantinos Natsios
C'est la taille des cellules de collection. Ces fonctions sont utilisées lors du sous-classement de UICollectionViewFlowLayout.
Scott Kaiser
1
J'aime cette solution, mais j'ai quelques commentaires. pageWidth()devrait utiliser minimumLineSpacingcar il défile horizontalement. Et dans mon cas, j'ai une vue contentInsetpour la collection afin que la première et la dernière cellule puissent être centrées, donc j'utilise let xOffset = pageWidth() * index - collectionView.contentInset.left.
blwinters
6

un petit problème que j'ai rencontré lors de l'utilisation de targetContentOffsetForProposedContentOffset est un problème avec la dernière cellule ne s'ajustant pas en fonction du nouveau point que j'ai renvoyé.
J'ai découvert que le CGPoint que j'ai retourné avait une valeur Y plus grande que celle autorisée, j'ai donc utilisé le code suivant à la fin de mon implémentation targetContentOffsetForProposedContentOffset:

// if the calculated y is bigger then the maximum possible y we adjust accordingly
CGFloat contentHeight = self.collectionViewContentSize.height;
CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
CGFloat maxY = contentHeight - collectionViewHeight;
if (newY > maxY)
{
    newY = maxY;
}

return CGPointMake(0, newY);

juste pour que ce soit plus clair, voici mon implémentation de mise en page complète qui imite simplement le comportement de pagination verticale:

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

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
    CGFloat heightOfPage = self.itemSize.height;
    CGFloat heightOfSpacing = self.minimumLineSpacing;

    CGFloat numOfPage = lround(proposedContentOffset.y / (heightOfPage + heightOfSpacing));
    CGFloat newY = numOfPage * (heightOfPage + heightOfSpacing);

    // if the calculated y is bigger then the maximum possible y we adjust accordingly
    CGFloat contentHeight = self.collectionViewContentSize.height;
    CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
    CGFloat maxY = contentHeight - collectionViewHeight;
    if (newY > maxY)
    {
        newY = maxY;
    }

    return CGPointMake(0, newY);
}

j'espère que cela permettra à quelqu'un d'économiser du temps et des maux de tête

keisar
la source
1
Même problème, il semble que la vue de collection ignore les valeurs non valides au lieu de les arrondir à ses limites.
Mike M
6

Je préfère permettre à l'utilisateur de parcourir plusieurs pages. Voici donc ma version de targetContentOffsetForProposedContentOffset(basée sur la réponse de DarthMike) pour la mise en page verticale .

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    CGFloat approximatePage = self.collectionView.contentOffset.y / self.pageHeight;
    CGFloat currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage);

    NSInteger flickedPages = ceil(velocity.y / self.flickVelocity);

    if (flickedPages) {
        proposedContentOffset.y = (currentPage + flickedPages) * self.pageHeight;
    } else {
        proposedContentOffset.y = currentPage * self.pageHeight;
    }

    return proposedContentOffset;
}

- (CGFloat)pageHeight {
    return self.itemSize.height + self.minimumLineSpacing;
}

- (CGFloat)flickVelocity {
    return 1.2;
}
Anton Gaenko
la source
4

La réponse de Fogmeisters a fonctionné pour moi à moins que je ne fasse défiler jusqu'à la fin de la rangée. Mes cellules ne s'adaptent pas parfaitement à l'écran, il défilerait donc jusqu'à la fin et reviendrait en arrière avec une secousse de sorte que la dernière cellule chevauche toujours le bord droit de l'écran.

Pour éviter cela, ajoutez la ligne de code suivante au début de la méthode targetcontentoffset

if(proposedContentOffset.x>self.collectionViewContentSize.width-320-self.sectionInset.right)
    return proposedContentOffset;
Ajaxharg
la source
Je suppose que 320 est la largeur de la vue de votre collection :)
Au Ris
Je dois adorer regarder l'ancien code. Je suppose que ce chiffre magique était celui-là.
Ajaxharg le
2

@ André Abreu Code d'

Version Swift3

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        var offsetAdjustment = CGFloat.greatestFiniteMagnitude
        let horizontalOffset = proposedContentOffset.x
        let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
        for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
            let itemOffset = layoutAttributes.frame.origin.x
            if abs(itemOffset - horizontalOffset) < abs(offsetAdjustment){
                offsetAdjustment = itemOffset - horizontalOffset
            }
        }
        return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    }
}
Cruz
la source
Merci pour ça! Le meilleur comportement attendu Merci beaucoup!
G Clovs
2

Swift 4

La solution la plus simple pour la vue de collection avec des cellules de taille unique (défilement horizontal):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else { return proposedContentOffset }

    // Calculate width of your page
    let pageWidth = calculatedPageWidth()

    // Calculate proposed page
    let proposedPage = round(proposedContentOffset.x / pageWidth)

    // Adjust necessary offset
    let xOffset = pageWidth * proposedPage - collectionView.contentInset.left

    return CGPoint(x: xOffset, y: 0)
}

func calculatedPageWidth() -> CGFloat {
    return itemSize.width + minimumInteritemSpacing
}
lobstah
la source
2

Une solution plus courte (en supposant que vous mettez en cache vos attributs de mise en page):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
    let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
    return CGPoint(x: targetLayoutAttributes.frame.minX - horizontalPadding, y: 0)
}

Pour mettre cela en contexte:

class Layout : UICollectionViewLayout {
    private var cache: [UICollectionViewLayoutAttributes] = []
    private static let horizontalPadding: CGFloat = 16
    private static let interItemSpacing: CGFloat = 8

    override func prepare() {
        let (itemWidth, itemHeight) = (collectionView!.bounds.width - 2 * Layout.horizontalPadding, collectionView!.bounds.height)
        cache.removeAll()
        let count = collectionView!.numberOfItems(inSection: 0)
        var x: CGFloat = Layout.horizontalPadding
        for item in (0..<count) {
            let indexPath = IndexPath(item: item, section: 0)
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = CGRect(x: x, y: 0, width: itemWidth, height: itemHeight)
            cache.append(attributes)
            x += itemWidth + Layout.interItemSpacing
        }
    }

    override var collectionViewContentSize: CGSize {
        let width: CGFloat
        if let maxX = cache.last?.frame.maxX {
            width = maxX + Layout.horizontalPadding
        } else {
            width = collectionView!.width
        }
        return CGSize(width: width, height: collectionView!.height)
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cache.first { $0.indexPath == indexPath }
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return cache.filter { $0.frame.intersects(rect) }
    }

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
        let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
        return CGPoint(x: targetLayoutAttributes.frame.minX - Layout.horizontalPadding, y: 0)
    }
}
Niels
la source
1

Pour m'assurer que cela fonctionne dans la version Swift (swift 5 maintenant), j'ai utilisé la réponse de @ André Abreu, j'ajoute quelques informations supplémentaires:

Lors du sous-classement de UICollectionViewFlowLayout, la fonction "override func awakeFromNib () {}" ne fonctionne pas (je ne sais pas pourquoi). Au lieu de cela, j'ai utilisé "override init () {super.init ()}"

Voici mon code mis dans la classe SubclassFlowLayout: UICollectionViewFlowLayout {}:

let padding: CGFloat = 16
override init() {
    super.init()
    self.minimumLineSpacing = padding
    self.minimumInteritemSpacing = 2
    self.scrollDirection = .horizontal
    self.sectionInset = UIEdgeInsets(top: 0, left: padding, bottom: 0, right: 100) //right = "should set for footer" (Horizental)

}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var offsetAdjustment = CGFloat.greatestFiniteMagnitude
    let leftInset = padding
    let horizontalOffset = proposedContentOffset.x + leftInset // leftInset is for "where you want the item stop on the left"
    let targetRect = CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: self.collectionView!.bounds.size)

    for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
        let itemOffset = layoutAttributes.frame.origin.x
        if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset
        }
    }

    let targetPoint = CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    return targetPoint

}

Après le sous-classement, assurez-vous de mettre ceci dans ViewDidLoad ():

customCollectionView.collectionViewLayout = SubclassFlowLayout()
customCollectionView.isPagingEnabled = false
customCollectionView.decelerationRate = .fast //-> this for scrollView speed
Tung Dang
la source
0

Pour ceux qui recherchent une solution dans Swift:

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
    private let collectionViewHeight: CGFloat = 200.0
    private let screenWidth: CGFloat = UIScreen.mainScreen().bounds.width

    override func awakeFromNib() {
        super.awakeFromNib()

        self.itemSize = CGSize(width: [InsertItemWidthHere], height: [InsertItemHeightHere])
        self.minimumInteritemSpacing = [InsertItemSpacingHere]
        self.scrollDirection = .Horizontal
        let inset = (self.screenWidth - CGFloat(self.itemSize.width)) / 2
        self.collectionView?.contentInset = UIEdgeInsets(top: 0,
                                                         left: inset,
                                                         bottom: 0,
                                                         right: inset)
    }

    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        var offsetAdjustment = CGFloat.max
        let horizontalOffset = proposedContentOffset.x + ((self.screenWidth - self.itemSize.width) / 2)

        let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.screenWidth, height: self.collectionViewHeight)
        var array = super.layoutAttributesForElementsInRect(targetRect)

        for layoutAttributes in array! {
            let itemOffset = layoutAttributes.frame.origin.x
            if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
                offsetAdjustment = itemOffset - horizontalOffset
            }
        }

        return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    }
}
Husein Kareem
la source