targetContentOffsetForProposedContentOffset: withScrollingVelocity senza sottoclasse UICollectionViewFlowLayout


99

Ho una collectionView molto semplice nella mia app (solo una singola riga di immagini in miniatura quadrate).

Vorrei intercettare lo scorrimento in modo che l'offset lasci sempre un'immagine piena sul lato sinistro. Al momento scorre ovunque e lascerà immagini tagliate.

Comunque, so che devo usare la funzione

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

per farlo, ma sto usando solo uno standard UICollectionViewFlowLayout. Non lo sto sottoclassando.

C'è un modo per intercettarlo senza sottoclassare UICollectionViewFlowLayout?

Grazie

Risposte:


113

OK, la risposta è no, non c'è modo di farlo senza creare una sottoclasse UICollectionViewFlowLayout.

Tuttavia, creare sottoclassi è incredibilmente facile per chiunque legga questo articolo in futuro.

Per prima cosa ho impostato la chiamata della sottoclasse MyCollectionViewFlowLayoute poi nel generatore di interfacce ho cambiato il layout della vista della raccolta in Personalizzato e ho selezionato la mia sottoclasse del layout di flusso.

Poiché lo stai facendo in questo modo, non puoi specificare le dimensioni degli elementi, ecc ... in IB quindi in MyCollectionViewFlowLayout.m ho questo ...

- (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);
}

Questo imposta tutte le dimensioni per me e la direzione di scorrimento.

Poi ...

- (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);
}

Ciò garantisce che lo scorrimento termini con un margine di 5,0 sul bordo sinistro.

È tutto quello che dovevo fare. Non avevo affatto bisogno di impostare il layout del flusso nel codice.


1
È davvero potente se usato correttamente. Hai guardato le sessioni di Collection View del WWDC 2012? Vale davvero la pena guardarli. Alcune cose incredibili.
Fogmeister

2
targetContentOffsetForProposedContentOffset:withVelocitynon viene chiamato per me quando scorro. Cosa sta succedendo?
fatuhoku

4
@TomSawyer ha impostato il tasso di dichiarazione di UICollectionView su UIScrollViewDecelerationRateFast.
Clay Ellis

3
@fatuhoku assicurati che la proprietà paginEnabled della tua collectionView sia impostata su false
chrs

4
Holy Moly, ho dovuto scorrere verso il basso come un milione di miglia per vedere questa risposta. :)
AnBisw

67

La soluzione di Dan è sbagliata. Non gestisce bene lo sfarfallio dell'utente. I casi in cui l'utente sfoglia velocemente e lo scorrimento non si muoveva così tanto, hanno problemi di animazione.

La mia proposta di implementazione alternativa ha la stessa impaginazione proposta in precedenza, ma gestisce lo scorrimento da parte dell'utente tra le pagine.

 #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;
 }

Grazie! Questo ha funzionato come un fascino. Un po 'difficile da capire ma arrivarci.
Rajiev Timal

Ho questo errore: Impossibile assegnare a "x" in "ProposedContentOffset"? Usando swift? come posso assegnare un valore x?
TomSawyer,

@TomSawyer I parametri sono "let" per impostazione predefinita. Prova a dichiarare la funzione come questa in Swift (usando var prima di param): override func targetContentOffsetForProposedContentOffset (var propostiContentOffset: CGPoint) -> CGPoint
DarthMike

1
Non puoi usare CGPointMake in swift. Personalmente ho usato questo: "var targetContentOffset: CGPoint if pannedLessThanAPage && flicked {targetContentOffset = CGPoint (x: nextPage * pageWidth (), y: offersContentOffset.y);} else {targetContentOffset = CGPoint (x: round (rawPageValue) * pageWidth ( ), y: offersContentOffset.y);} return PropossetContentOffset "
Traccia il

1
Dovrebbe essere la risposta selezionata.
Khunshan

26

Versione rapida della risposta accettata.

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

Valido per Swift 5 .


Questa versione funziona alla grande e funziona bene anche per l'asse Y se si scambia il codice.
Chris

Principalmente funziona alla grande qui. Ma se smetto di scorrere e alzo il dito (con attenzione), non scorrerà su nessuna pagina e si fermerà lì.
Christian A. Strømmen

@ ChristianA.Strømmen Strano, funziona perfettamente con la mia app.
André Abreu

@ AndréAbreu dove colloco questa funzione?
FlowUI. SimpleUITesting.com

2
@Jay È necessario creare una sottoclasse di UICollectionViewLayout o qualsiasi classe che già la sottoclasse (ad es. UICollectionViewFlowLayout).
André Abreu

24

Ecco la mia implementazione in Swift 5 per il paging verticale basato su celle:

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

Alcune note:

  • Non ha problemi
  • IMPOSTARE PAGING SU FALSO ! (altrimenti questo non funzionerà)
  • Ti permette di impostare facilmente la tua flickvelocity .
  • Se qualcosa non funziona ancora dopo averlo provato, controlla se itemSizeeffettivamente corrisponde alla dimensione dell'articolo poiché spesso questo è un problema, specialmente quando usi collectionView(_:layout:sizeForItemAt:), usa invece una variabile personalizzata con itemSize.
  • Funziona meglio quando si imposta self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Ecco una versione orizzontale (non l'ho testata a fondo quindi perdona eventuali errori):

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

Questo codice si basa sul codice che utilizzo nel mio progetto personale, puoi verificarlo qui scaricandolo ed eseguendo il target di esempio.


4
Sei il salvatore della vita! Importante da notare per IMPOSTARE PAGING SU FALSO !!! Ho perso come 2 ore della mia vita a sistemare la tua funzione, che già funziona ...
denis631

@ denis631 Mi dispiace tanto! Avrei dovuto aggiungerlo, modificherò il post per riflettere questo! Sono contento che abbia funzionato :)
JoniVR

jesssus, mi chiedevo perché non funzionasse finché non ho visto questo commento sulla disabilitazione del paging ... ovviamente il mio era impostato su true
Kam Wo

@JoniVR Mi mostra che questo metodo di errore non sovrascrive alcun metodo dalla sua superclasse
Muju

22

Anche se questa risposta mi è stata di grande aiuto, c'è un evidente sfarfallio quando scorri velocemente su una piccola distanza. È molto più facile riprodurlo sul dispositivo.

Ho scoperto che questo accade sempre quando collectionView.contentOffset.x - proposedContentOffset.xe velocity.xcanta in modo diverso.

La mia soluzione era di assicurarmi che proposedContentOffsetfosse maggiore di contentOffset.xse la velocità fosse positiva e minore se negativa. È in C # ma dovrebbe essere abbastanza semplice da tradurre in 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);
}

Questo codice utilizza MinContentOffset, MaxContentOffsete SnapStepche dovrebbe essere banale per voi da definire. Nel mio caso si sono rivelati

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

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

float SnapStep {
    get { return ItemSize.Width + MinimumLineSpacing; }
}

7
Funziona davvero bene. L'ho convertito in Objective-C per gli interessati: gist.github.com/rkeniger/7687301
Rob Keniger

21

Dopo lunghi test ho trovato la soluzione per agganciare al centro con la larghezza della cella personalizzata (ogni cella ha larghezza diff.) Che risolve lo sfarfallio. Sentiti libero di migliorare lo 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;
}

10
La migliore soluzione di tutte, grazie! Anche per eventuali lettori futuri, è necessario disattivare il paging affinché funzioni.
sridvijay

1
Se si volesse allinearlo da sinistra, invece della cella allineata a destra al centro, come faremmo a cambiarlo?
CyberMew

Non sono sicuro di aver capito correttamente, ma se vuoi iniziare gli elementi al centro e allinearli al centro, devi modificare il contentInset. Uso questo: gist.github.com/pionl/432fc8059dee3b540e38
Pion

Per allinearlo nella posizione X della cella al centro della vista, rimuovi semplicemente + (layoutAttributes.frame.size.width / 2) nella sezione della velocità.
Pion

1
@Jay Ciao, crea semplicemente un delegato Flow personalizzato e aggiungici questo codice. Non dimenticare di impostare il layout personalizzato in pennino o codice.
Pion

18

fare riferimento a questa risposta di Dan Abramov ecco la versione 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;
}

o gist qui https://gist.github.com/katopz/8b04c783387f0c345cd9


4
Versione aggiornata di questo per Swift 3: gist.github.com/mstubna/beed10327e00310d05f12bf4747266a4
mstubna

1
Dang it @mstubna, sono andato avanti e ho copiato quanto sopra, l'ho aggiornato a swift 3, ho iniziato a fare un gist aggiornato e sono tornato qui per raccogliere note / titolo a quel punto ho notato che avevi già fatto un gist 3 rapido. Grazie! Peccato che mi sia mancato.
VaporwareWolf

16

Per chi cerca una soluzione che ...

  • NON GLITCH quando l'utente esegue un breve scorrimento veloce (cioè considera le velocità di scorrimento positive e negative)
  • prende collectionView.contentInsetin considerazione (e safeArea su iPhone X)
  • considera solo le celle visibili nel punto di scorrimento (per prestazioni)
  • utilizza variabili e commenti ben denominati
  • è Swift 4

quindi vedi sotto ...

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
        }
    }
}

1
Grazie! appena copiato e incollato, funziona perfettamente .. molto meglio come previsto.
Steven B.

1
Una e unica soluzione che funziona davvero . Bel lavoro! Grazie!
LinusGeffarth

1
return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left candidateOffsets - sectionInset.left c'è un problema in questa riga
Utku Dalmaz

1
@Dalmaz, grazie per avermi avvisato. Ho risolto il problema ora.
Oliver Pearmain

1
Sì, appena copiato e incollato, mi risparmi tempo.
Wei

7

Ecco la mia soluzione Swift su una vista di raccolta a scorrimento orizzontale. È semplice, dolce ed evita lo sfarfallio.

  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
  }

che cos'è itemSize??
Konstantinos Natsios

È la dimensione delle celle di raccolta. Queste funzioni vengono utilizzate quando si crea una sottoclasse UICollectionViewFlowLayout.
Scott Kaiser


1
Mi piace questa soluzione, ma ho un paio di commenti. pageWidth()dovrebbe essere utilizzato minimumLineSpacingpoiché scorre orizzontalmente. E nel mio caso, ho una vista contentInsetper la raccolta in modo che la prima e l'ultima cella possano essere centrate, quindi uso let xOffset = pageWidth() * index - collectionView.contentInset.left.
Blwinters

6

un piccolo problema che ho riscontrato durante l'utilizzo di targetContentOffsetForProposedContentOffset è un problema con l'ultima cella che non si regola in base al nuovo punto che ho restituito.
Ho scoperto che il CGPoint che ho restituito aveva un valore Y più grande di quello consentito, quindi ho usato il seguente codice alla fine della mia implementazione 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);

solo per rendere più chiaro questa è la mia implementazione completa del layout che imita semplicemente il comportamento della paginazione 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);
}

si spera che questo salverà qualcuno un po 'di tempo e un mal di testa


1
Stesso problema, sembra che la vista della raccolta ignorerà i valori non validi invece di arrotondarli ai suoi limiti.
Mike M

6

Preferisco consentire all'utente di sfogliare diverse pagine. Quindi ecco la mia versione di targetContentOffsetForProposedContentOffset(basata sulla risposta di DarthMike) per il layout 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;
}

4

La risposta dei Fogmeisters ha funzionato per me a meno che non ho fatto scorrere fino alla fine della riga. Le mie celle non si adattano perfettamente allo schermo, quindi scorre fino alla fine e salta indietro con uno scatto in modo che l'ultima cella si sovrapponga sempre al bordo destro dello schermo.

Per evitare ciò, aggiungere la seguente riga di codice all'inizio del metodo targetcontentoffset

if(proposedContentOffset.x>self.collectionViewContentSize.width-320-self.sectionInset.right)
    return proposedContentOffset;

Suppongo che 320 sia la larghezza della visualizzazione della tua raccolta :)
Au Ris

Devo amare guardare indietro al vecchio codice. Immagino che quel numero magico fosse quello.
Ajaxharg

2

@ André Abreu 's Codice

Versione 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)
    }
}

Grazie per quello! Il miglior comportamento previsto Grazie è di grande aiuto!
G Clovs

2

Swift 4

La soluzione più semplice per la visualizzazione della raccolta con celle di una dimensione (scorrimento orizzontale):

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
}

2

Una soluzione più breve (supponendo che tu stia memorizzando nella cache gli attributi del layout):

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

Per metterlo nel contesto:

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

1

Per assicurarmi che funzioni nella versione Swift (swift 5 ora), ho usato la risposta di @ André Abreu, aggiungo qualche informazione in più:

Quando si crea una sottoclasse di UICollectionViewFlowLayout, "override func awakeFromNib () {}" non funziona (non so perché). Invece, ho usato "override init () {super.init ()}"

Questo è il mio codice inserito nella 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

}

Dopo la sottoclasse, assicurati di metterlo in ViewDidLoad ():

customCollectionView.collectionViewLayout = SubclassFlowLayout()
customCollectionView.isPagingEnabled = false
customCollectionView.decelerationRate = .fast //-> this for scrollView speed

0

Per chi cerca una soluzione in 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)
    }
}

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.