targetContentOffsetForProposedContentOffset: withScrollingVelocity ohne Unterklasse UICollectionViewFlowLayout


97

Ich habe eine sehr einfache Sammlungsansicht in meiner App (nur eine einzelne Reihe quadratischer Miniaturbilder).

Ich möchte das Scrollen abfangen, damit der Versatz immer ein vollständiges Bild auf der linken Seite hinterlässt. Im Moment scrollt es zu jedem Ort und hinterlässt abgeschnittene Bilder.

Wie auch immer, ich weiß, dass ich die Funktion verwenden muss

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

Um dies zu tun, verwende ich nur einen Standard UICollectionViewFlowLayout. Ich klassifiziere es nicht.

Gibt es eine Möglichkeit, dies abzufangen, ohne eine Unterklasse zu erstellen UICollectionViewFlowLayout?

Vielen Dank

Antworten:


113

OK, Antwort ist nein, es gibt keine Möglichkeit, dies zu tun, ohne UICollectionViewFlowLayout zu unterklassifizieren.

Das Unterklassen ist jedoch für jeden, der dies in Zukunft liest, unglaublich einfach.

Zuerst habe ich den Unterklassenaufruf eingerichtet MyCollectionViewFlowLayoutund dann im Interface Builder das Layout der Sammlungsansicht in Benutzerdefiniert geändert und meine Unterklasse für das Flusslayout ausgewählt.

Da Sie dies auf diese Weise tun, können Sie in IB keine Artikelgrößen usw. angeben. In MyCollectionViewFlowLayout.m habe ich diese ...

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

Dadurch werden alle Größen für mich und die Bildlaufrichtung festgelegt.

Dann ...

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

Dies stellt sicher, dass das Scrollen mit einem Rand von 5,0 am linken Rand endet.

Das war alles was ich tun musste. Ich musste das Ablauflayout überhaupt nicht im Code festlegen.


1
Es ist wirklich mächtig, wenn es richtig verwendet wird. Haben Sie die Collection View-Sitzungen von WWDC 2012 gesehen? Sie sind wirklich sehenswert. Einige unglaubliche Sachen.
Fogmeister

2
targetContentOffsetForProposedContentOffset:withVelocitywird nicht für mich gerufen, wenn ich scrolle. Was ist los?
Fatuhoku

4
@TomSawyer hat die Deklarationsrate von UICollectionView auf UIScrollViewDecelerationRateFast gesetzt.
Clay Ellis

3
@ Fatuhoku stellen Sie sicher, dass die Eigenschaft paginEnabled Ihrer collectionView auf false gesetzt ist
chrs

4
Holy Moly, ich musste wie eine Million Meilen nach unten scrollen, um diese Antwort zu sehen. :)
AnBisw

67

Dans Lösung ist fehlerhaft. Der Benutzer kann nicht gut mit dem Schnippen umgehen. Die Fälle, in denen der Benutzer schnell schnippt und der Bildlauf sich nicht so stark bewegt, weisen Animationsfehler auf.

Meine vorgeschlagene alternative Implementierung hat dieselbe Paginierung wie zuvor vorgeschlagen, behandelt jedoch das Wechseln zwischen Seiten.

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

Danke dir! Das hat wie ein Zauber gewirkt. Ein bisschen schwer zu verstehen, aber dorthin zu gelangen.
Rajiev Timal

Ich habe diesen Fehler: Kann 'x' in 'vorgeschlagenenContentOffset' nicht zuweisen? Mit Swift? Wie kann ich einen x-Wert zuweisen?
TomSawyer

@ TomSawyer-Parameter sind standardmäßig 'let'. Versuchen Sie, die Funktion in Swift wie folgt zu deklarieren (mit var vor param): überschreiben Sie func targetContentOffsetForProposedContentOffset (var vorgeschlagenenContentOffset: CGPoint) -> CGPoint
DarthMike

1
Sie können CGPointMake nicht schnell verwenden. Ich persönlich habe dies verwendet: "var targetContentOffset: CGPoint if pannedLessThanAPage && hat {targetContentOffset = CGPoint (x: nextPage * pageWidth (), y: vorgeschlagenenContentOffset.y);} else {targetContentOffset = CGPoint (x: round (rawPageValue) * pageWth ), y: vorgeschlagenerContentOffset.y);} Rückgabe vorgeschlagenerContentOffset "
Plot

1
Es sollte die ausgewählte Antwort sein.
Khunshan

25

Schnelle Version der akzeptierten Antwort.

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

Gültig für Swift 5 .


Diese Version funktioniert hervorragend und auch für die Y-Achse, wenn Sie den Code vertauschen.
Chris

Funktioniert hier meistens super. Wenn ich jedoch aufhöre zu scrollen und meinen Finger (vorsichtig) hebe, wird nicht zu einer Seite gescrollt und einfach dort angehalten.
Christian A. Strømmen

@ ChristianA.Strømmen Weird, es funktioniert gut mit meiner App.
André Abreu

@ AndréAbreu wo platziere ich diese Funktion?
FlowUI. SimpleUITesting.com

2
@Jay Sie müssen UICollectionViewLayout oder eine Klasse, die es bereits unterklassifiziert hat (z. B. UICollectionViewFlowLayout), in eine Unterklasse unterteilen.
André Abreu

24

Hier ist meine Implementierung in Swift 5 für vertikales zellbasiertes Paging:

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

Einige Notizen:

  • Passt nicht
  • SETZEN SIE PAGING AUF FALSCH ! (sonst funktioniert das nicht)
  • Ermöglicht das einfache Einstellen Ihrer eigenen Flickvelocity .
  • Wenn nach dem Versuch immer noch etwas nicht funktioniert, überprüfen Sie, ob Ihre itemSizeGröße tatsächlich mit der Größe des Elements übereinstimmt, da dies häufig ein Problem darstellt, insbesondere bei Verwendung collectionView(_:layout:sizeForItemAt:). Verwenden Sie stattdessen eine benutzerdefinierte Variable mit itemSize.
  • Dies funktioniert am besten, wenn Sie einstellen self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Hier ist eine horizontale Version (habe sie nicht gründlich getestet, bitte verzeihen Sie Fehler):

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

Dieser Code basiert auf dem Code, den ich in meinem persönlichen Projekt verwende. Sie können ihn hier überprüfen, indem Sie ihn herunterladen und das Beispielziel ausführen .


4
Du bist der Lebensretter! Es ist wichtig zu beachten, dass PAGING auf FALSE gesetzt wird !!! Verloren wie 2 Stunden meines Lebens, die Ihre Funktion reparieren, die bereits funktioniert ...
denis631

@ denis631 Es tut mir so leid! Ich hätte das hinzufügen sollen, ich werde den Beitrag bearbeiten, um dies widerzuspiegeln!
Ich bin

jesssus, ich habe mich gefragt, warum das nicht funktioniert, bis ich diesen Kommentar über das Deaktivieren von Paging gesehen habe ... natürlich wurde meiner auf true gesetzt
Kam Wo

@JoniVR Es zeigt mir, dass dieser Fehler keine Methode aus ihrer Oberklasse überschreibt
Muju

22

Während diese Antwort mir sehr geholfen hat, gibt es ein merkliches Flackern, wenn Sie auf einer kleinen Strecke schnell wischen. Es ist viel einfacher, es auf dem Gerät zu reproduzieren.

Ich fand, dass dies immer passiert, wenn collectionView.contentOffset.x - proposedContentOffset.xund velocity.xverschiedene Singen haben.

Meine Lösung bestand darin, sicherzustellen, dass dies proposedContentOffsetmehr ist als contentOffset.xwenn die Geschwindigkeit positiv ist, und weniger, wenn sie negativ ist. Es ist in C #, sollte aber ziemlich einfach in Ziel C zu übersetzen sein:

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

Dieser Code wird verwendet MinContentOffset, MaxContentOffsetund SnapStepdie trivial sein sollte für Sie zu definieren. In meinem Fall stellte sich heraus, dass sie es waren

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
Das funktioniert wirklich gut. Ich habe es für Interessierte in Objective-C konvertiert: gist.github.com/rkeniger/7687301
Rob Keniger

21

Nach langen Tests fand ich eine Lösung zum Einrasten in die Mitte mit einer benutzerdefinierten Zellenbreite (jede Zelle hat eine unterschiedliche Breite), die das Flackern behebt. Fühlen Sie sich frei, das Skript zu verbessern.

- (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
Beste Lösung von allen, danke! Auch für zukünftige Leser müssen Sie das Paging deaktivieren, damit dies funktioniert.
sridvijay

1
Wenn man es von links ausrichten möchte, anstatt die Zelle rechts in der Mitte auszurichten, wie würden wir es ändern?
CyberMew

Ich bin mir nicht sicher, ob ich das richtig verstehe, aber wenn Sie die Elemente in der Mitte starten und an der Mitte ausrichten möchten, müssen Sie das contentInset ändern. Ich benutze dies dies: gist.github.com/pionl/432fc8059dee3b540e38
Pion

Um die X-Position der Zelle in der Mitte der Ansicht auszurichten, entfernen Sie einfach + (layoutAttributes.frame.size.width / 2) im Geschwindigkeitsbereich.
Pion

1
@ Jay Hallo, erstelle einfach einen benutzerdefinierten Flow-Delegaten und füge diesen Code hinzu. Vergessen Sie nicht, das benutzerdefinierte Layout in Feder oder Code festzulegen.
Pion

18

Beziehen Sie sich auf diese Antwort von Dan Abramov in der Swift-Version

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

oder hier https://gist.github.com/katopz/8b04c783387f0c345cd9



1
Dang it @mstubna, ich habe das Obige kopiert, es auf Swift 3 aktualisiert, angefangen, ein aktualisiertes Gist zu erstellen, und bin hierher zurückgekommen, um Notizen / Titel zu sammeln. Zu diesem Zeitpunkt habe ich festgestellt, dass Sie bereits ein schnelles 3 Gist erstellt haben. Vielen Dank! Schade, dass ich es verpasst habe.
VaporwareWolf

16

Für alle, die nach einer Lösung suchen, die ...

  • NICHT GLITCH, wenn der Benutzer einen kurzen schnellen Bildlauf durchführt (dh er berücksichtigt positive und negative Bildlaufgeschwindigkeiten)
  • berücksichtigt die collectionView.contentInset(und safeArea auf dem iPhone X)
  • Berücksichtigt nur die Zellen, die am Bildlaufpunkt sichtbar sind (für die Leistung).
  • verwendet gut benannte Variablen und Kommentare
  • ist Swift 4

dann siehe unten ...

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
Vielen Dank! einfach kopiert und eingefügt, funktioniert perfekt .. viel besser als erwartet.
Steven B.

1
Eine einzige Lösung, die tatsächlich funktioniert. Gut gemacht! Vielen Dank!
LinusGeffarth

1
Rückgabe cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left KandidatOffsets - sectionInset.left Es gibt ein Problem in dieser Zeile
Utku Dalmaz

1
@ Dalmaz danke für die Benachrichtigung. Ich habe das Problem jetzt behoben.
Oliver Pearmain

1
Ja, nur kopiert und eingefügt, Sie sparen meine Zeit.
Wei

7

Hier ist meine Swift-Lösung für eine horizontal scrollende Sammlungsansicht. Es ist einfach, süß und vermeidet jegliches Flackern.

  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
  }

was ist itemSize??
Konstantinos Natsios

Es ist die Größe der Sammelzellen. Diese Funktionen werden beim Unterklassen von UICollectionViewFlowLayout verwendet.
Scott Kaiser


1
Ich mag diese Lösung, aber ich habe ein paar Kommentare. pageWidth()sollte verwendet werden, minimumLineSpacingda es horizontal scrollt. In meinem Fall habe ich eine contentInsetAnsicht für die Sammlung, damit die erste und die letzte Zelle zentriert werden können let xOffset = pageWidth() * index - collectionView.contentInset.left.
Blwinters

6

Ein kleines Problem, das bei der Verwendung von targetContentOffsetForProposedContentOffset aufgetreten ist, ist ein Problem, bei dem die letzte Zelle nicht an den neuen Punkt angepasst wurde, den ich zurückgegeben habe.
Ich fand heraus, dass der von mir zurückgegebene CGPoint einen Y-Wert hatte, der größer als zulässig war, und verwendete daher am Ende meiner Implementierung von targetContentOffsetForProposedContentOffset den folgenden Code:

// 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);

Nur um es klarer zu machen, ist dies meine vollständige Layout-Implementierung, die nur das vertikale Paging-Verhalten imitiert:

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

hoffentlich spart dies jemandem etwas Zeit und Kopfschmerzen


1
Das gleiche Problem scheint, als würde die Sammlungsansicht ungültige Werte ignorieren, anstatt sie auf ihre Grenzen abzurunden.
Mike M

6

Ich ziehe es vor, dem Benutzer das Durchblättern mehrerer Seiten zu erlauben. Hier ist meine Version von targetContentOffsetForProposedContentOffset(basierend auf der DarthMike-Antwort) für das vertikale Layout.

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

Die Antwort von Fogmeisters hat bei mir funktioniert, es sei denn, ich habe bis zum Ende der Zeile gescrollt. Meine Zellen passen nicht gut auf den Bildschirm, sodass sie bis zum Ende scrollen und mit einem Ruck zurückspringen, sodass die letzte Zelle immer den rechten Bildschirmrand überlappt.

Um dies zu verhindern, fügen Sie am Anfang der Methode targetcontentoffset die folgende Codezeile hinzu

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

Ich nehme an, 320 ist Ihre Sammlungsansicht Breite :)
Au Ris

Ich muss es lieben, auf alten Code zurückzublicken. Ich denke, diese magische Zahl war das.
Ajaxharg

2

@ André Abreu Code

Swift3-Version

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

Dank dafür! Das beste erwartete Verhalten Vielen Dank, es ist viel Hilfe!
G Clovs

2

Swift 4

Die einfachste Lösung für die Sammlungsansicht mit Zellen einer Größe (horizontaler Bildlauf):

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

Eine kürzere Lösung (vorausgesetzt, Sie speichern Ihre Layoutattribute zwischen):

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

Um dies in einen Zusammenhang zu bringen:

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

Um sicherzustellen, dass es in der Swift-Version funktioniert (jetzt Swift 5), habe ich die Antwort verwendet von @ André Abreu verwendet und einige weitere Informationen hinzugefügt:

Wenn Sie UICollectionViewFlowLayout unterklassifizieren, funktioniert die Funktion "override func awakeFromNib () {}" nicht (ich weiß nicht warum). Stattdessen habe ich "override init () {super.init ()}" verwendet.

Dies ist mein Code in der Klasse 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

}

Stellen Sie nach dem Unterklassen sicher, dass Sie dies in ViewDidLoad () einfügen:

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

0

Für diejenigen, die nach einer Lösung in Swift suchen:

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.