Beim horizontalen Scrollen von UICollectionView zur Mitte einer Zelle ausrichten


69

Ich weiß, dass einige Leute diese Frage schon einmal gestellt haben, aber es ging ihnen nur um UITableViewsoder UIScrollViewsund ich konnte nicht die akzeptierte Lösung finden, um für mich zu arbeiten. Was ich möchte, ist der Schnappeffekt beim UICollectionViewhorizontalen Scrollen durch meine - ähnlich wie im iOS AppStore. iOS 9+ ist mein Ziel-Build. Sehen Sie sich daher die UIKit-Änderungen an, bevor Sie darauf antworten.

Vielen Dank.

Antworten:


118

Während ich ursprünglich Objective-C verwendete, wechselte ich seitdem so schnell und die ursprünglich akzeptierte Antwort reichte nicht aus.

UICollectionViewLayoutAm Ende habe ich eine Unterklasse erstellt, die die beste (imo) Erfahrung bietet, im Gegensatz zu den anderen Funktionen, die den Inhaltsversatz oder ähnliches ändern, wenn der Benutzer aufgehört hat zu scrollen.

class SnappingCollectionViewLayout: UICollectionViewFlowLayout {

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

        var offsetAdjustment = CGFloat.greatestFiniteMagnitude
        let horizontalOffset = proposedContentOffset.x + collectionView.contentInset.left

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

        let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect)

        layoutAttributesArray?.forEach({ (layoutAttributes) in
            let itemOffset = layoutAttributes.frame.origin.x
            if fabsf(Float(itemOffset - horizontalOffset)) < fabsf(Float(offsetAdjustment)) {
                offsetAdjustment = itemOffset - horizontalOffset
            }
        })

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

Stellen Sie für die nativste Gefühlsverzögerung mit der aktuellen Layout-Unterklasse Folgendes ein:

collectionView?.decelerationRate = UIScrollViewDecelerationRateFast


3
Aus irgendeinem Grund führt dies dazu, dass meine Sammlungsansicht vertikal ist und die Größe aller meiner Zellen ändert.
Jerland2

1
Letzte Zeile war ein toller Tipp! Noch nie von dieser Immobilie gehört!
Lucien

4
Für diejenigen, die sectionInset.leftgesetzt haben, ersetzen Sie die Rückgabeerklärung durch:return CGPoint(x: proposedContentOffset.x + offsetAdjustment - sectionInset.left, y: proposedContentOffset.y)
Andrey Gordeev

2
Diese Lösung schnappt zwar Zellen, aber die Animation ist sehr fehlerhaft, da sie die Geschwindigkeit nicht berücksichtigt. Wenn der Benutzer sehr schnell wischt, können Sie sehen, dass es häufig zu Störungen kommt.
NSPunk

2
Ich brauchte dies, um beim Wischen immer zur nächsten Zelle zu springen, also habe ich es ein wenig angepasst, indem ich die Richtung des Wischens überprüft habe:layoutAttributesArray?.forEach({ (layoutAttributes) in let itemOffset = layoutAttributes.frame.origin.x let itemWidth = Float(layoutAttributes.frame.width) let direction: Float = velocity.x > 0 ? 1 : -1 if fabsf(Float(itemOffset - horizontalOffset)) < fabsf(Float(offsetAdjustment)) + itemWidth * direction { offsetAdjustment = itemOffset - horizontalOffset } })
Leah Culver

31

Basierend auf der Antwort von Mete und dem Kommentar von Chris Chute ,

Hier ist eine Swift 4-Erweiterung, die genau das tut, was OP will. Es wurde in verschachtelten Sammlungsansichten mit einer oder zwei Zeilen getestet und funktioniert einwandfrei.

extension UICollectionView {
    func scrollToNearestVisibleCollectionViewCell() {
        self.decelerationRate = UIScrollViewDecelerationRateFast
        let visibleCenterPositionOfScrollView = Float(self.contentOffset.x + (self.bounds.size.width / 2))
        var closestCellIndex = -1
        var closestDistance: Float = .greatestFiniteMagnitude
        for i in 0..<self.visibleCells.count {
            let cell = self.visibleCells[i]
            let cellWidth = cell.bounds.size.width
            let cellCenter = Float(cell.frame.origin.x + cellWidth / 2)

            // Now calculate closest cell
            let distance: Float = fabsf(visibleCenterPositionOfScrollView - cellCenter)
            if distance < closestDistance {
                closestDistance = distance
                closestCellIndex = self.indexPath(for: cell)!.row
            }
        }
        if closestCellIndex != -1 {
            self.scrollToItem(at: IndexPath(row: closestCellIndex, section: 0), at: .centeredHorizontally, animated: true)
        }
    }
}

Sie müssen das UIScrollViewDelegateProtokoll für Ihre Sammlungsansicht implementieren und dann diese beiden Methoden hinzufügen:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    self.collectionView.scrollToNearestVisibleCollectionViewCell()
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !decelerate {
        self.collectionView.scrollToNearestVisibleCollectionViewCell()
    }
}

Diese Antwort hat bei mir am besten funktioniert. Es war schön und glatt. Ich habe eine Änderung vorgenommen, da ich einen Zellabstand hatte und nicht wollte, dass er zentriert wird. if closestCellIndex != -1 { UIView.animate(withDuration: 0.1) { let toX = (cellWidth + cellHorizontalSpacing) * CGFloat(closestCellIndex) scrollView.contentOffset = CGPoint(x: toX, y: 0) scrollView.layoutIfNeeded() } }
Wollte auch

@ vahid-amiri Genial. Vielen Dank. Wie um alles in der Welt hat das gelernt !! Langer Weg für mich :)
Ashishn

Funktioniert es für vertikale Schriftrollen? horizontale Schriftrolle keine Probleme
karthikeyan

Diese Lösung hat auch bei mir funktioniert, obwohl Sie sie in Paging Enabledder collectionView deaktivieren möchten, für die Sie diese Erweiterung implementieren. Wenn Paging aktiviert war, war das Verhalten nicht vorhersehbar. Ich glaube, das lag daran, dass die automatische Paging-Funktion die manuelle Berechnung störte.
B-Rad

26

Rasten Sie unter Berücksichtigung der Bildlaufgeschwindigkeit zur nächsten Zelle ein.

Funktioniert ohne Störungen.

import UIKit

class SnapCenterLayout: UICollectionViewFlowLayout {
  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) }
    let parent = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)

    let itemSpace = itemSize.width + minimumInteritemSpacing
    var currentItemIdx = round(collectionView.contentOffset.x / itemSpace)

    // Skip to the next cell, if there is residual scrolling velocity left.
    // This helps to prevent glitches
    let vX = velocity.x
    if vX > 0 {
      currentItemIdx += 1
    } else if vX < 0 {
      currentItemIdx -= 1
    }

    let nearestPageOffset = currentItemIdx * itemSpace
    return CGPoint(x: nearestPageOffset,
                   y: parent.y)
  }
}

1
Dies ist bei weitem der beste Ansatz. Es respektiert die Geschwindigkeit auch bei kleinen Berührungen. Stellen Sie bei Projekten, die verwenden contentInset, sicher, dass Sie es entweder mit var hinzufügen oder entfernennearestPageOffset .
NSPunk

@NSPunk stimmt zu ... Es gibt viele beliebte Apps mit ähnlichem Design und alle haben diesen seltsamen Fehler, wenn noch Geschwindigkeit vorhanden ist, aber nicht genug, um zur nächsten Zelle zu gelangen.
Richard Topchii

Ich musste itemLpace MinimumLineSpacing hinzufügen, um es richtig zu machen. Gute Arbeit!
Nico S.

Dies spiegelt sich nicht richtig wider itemSize. Dieser Ansatz wird nur itemSizedirekt layoutitemSizeUICollectionViewDelegateFlowLayout
aktiviert

25

Für das, was es hier wert ist, ist eine einfache Berechnung, die ich (schnell) verwende:

func snapToNearestCell(_ collectionView: UICollectionView) {
    for i in 0..<collectionView.numberOfItems(inSection: 0) {

        let itemWithSpaceWidth = collectionViewFlowLayout.itemSize.width + collectionViewFlowLayout.minimumLineSpacing
        let itemWidth = collectionViewFlowLayout.itemSize.width

        if collectionView.contentOffset.x <= CGFloat(i) * itemWithSpaceWidth + itemWidth / 2 {                
            let indexPath = IndexPath(item: i, section: 0)
            collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
            break
        }
    }
}

Rufen Sie an, wo Sie es brauchen. Ich rufe es an

func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    snapToNearestCell(scrollView)
}

Und

func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
    snapToNearestCell(scrollView)
}

Woher collectionViewFlowLayout kommen könnte:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    // Set up collection view
    collectionViewFlowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
}

1
Vielen Dank! Dies war meiner Meinung nach die sauberste und einfachste Antwort
David C

Kein Problem. Ich hatte tatsächlich einige Probleme, wenn ich viele Zellen hatte, weil die Berechnung falsch war. Also meine Antwort aktualisiert.
Nam

Wo ist collectionViewFlowLayout von
Ethan SK

Diese Lösung funktioniert, fühlt sich jedoch nicht nativ an, da sie erst beginnt, wenn sie vollständig abgebremst ist.
fl034

19

SWIFT 3 Version von @ Iowa15 Antwort

func scrollToNearestVisibleCollectionViewCell() {
    let visibleCenterPositionOfScrollView = Float(collectionView.contentOffset.x + (self.collectionView!.bounds.size.width / 2))
    var closestCellIndex = -1
    var closestDistance: Float = .greatestFiniteMagnitude
    for i in 0..<collectionView.visibleCells.count {
        let cell = collectionView.visibleCells[i]
        let cellWidth = cell.bounds.size.width
        let cellCenter = Float(cell.frame.origin.x + cellWidth / 2)

        // Now calculate closest cell
        let distance: Float = fabsf(visibleCenterPositionOfScrollView - cellCenter)
        if distance < closestDistance {
            closestDistance = distance
            closestCellIndex = collectionView.indexPath(for: cell)!.row
        }
    }
    if closestCellIndex != -1 {
        self.collectionView!.scrollToItem(at: IndexPath(row: closestCellIndex, section: 0), at: .centeredHorizontally, animated: true)
    }
}

Muss in UIScrollViewDelegate implementiert werden:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    scrollToNearestVisibleCollectionViewCell()
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !decelerate {
        scrollToNearestVisibleCollectionViewCell()
    }
}

2
Das hat bei mir super funktioniert. Ich würde hinzufügen, dass Sie dem App Store "Gefühl" näher kommen, wenn Sie collectionView.decelerationRate = UIScrollViewDecelerationRateFastirgendwann einstellen . Ich würde auch hinzufügen, dass FLT_MAXin Zeile 4 geändert werden Float.greatestFiniteMagnitudesollte, um Xcode-Warnungen zu vermeiden.
Chris Chute

13

Hier ist meine Implementierung

func snapToNearestCell(scrollView: UIScrollView) {
     let middlePoint = Int(scrollView.contentOffset.x + UIScreen.main.bounds.width / 2)
     if let indexPath = self.cvCollectionView.indexPathForItem(at: CGPoint(x: middlePoint, y: 0)) {
          self.cvCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
     }
}

Implementieren Sie Ihre Scroll View-Delegaten wie folgt

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    self.snapToNearestCell(scrollView: scrollView)
}

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    self.snapToNearestCell(scrollView: scrollView)
}

Auch zum besseren Einrasten

self.cvCollectionView.decelerationRate = UIScrollViewDecelerationRateFast

Klappt wunderbar


1
Sie haben vergessen, Ihr erstes Code-Snippet in eine Funktion namens "snapToNearestCell (scrollView: UIScrollView)" zu legen
Starsky

12

Wenn Sie ein einfaches natives Verhalten ohne Anpassung wünschen:

collectionView.pagingEnabled = YES;

Dies funktioniert nur dann richtig , wenn die Größe der Layout - Elemente Sammlung Ansicht sind alle nur eine Größe und die UICollectionViewCell‚s - clipToBoundsEigenschaft auf YES.


Dies gibt nicht das schönste Feedback und funktioniert auch nicht richtig mit Sammlungsansichten, die mehrere Zeilen haben, aber es ist sicher sehr einfach.
Vahid Amiri

Wenn Sie dies verwendet haben und Ihre Zellen nicht zentriert sind, stellen Sie sicher, minimumLineSpacingdass Sie für Ihr Layout den
M Reza

10

Ich habe sowohl @ Mark Bourke- als auch @ Mrcrowley-Lösungen ausprobiert, aber sie liefern die gleichen Ergebnisse mit unerwünschten Klebeeffekten.

Ich habe es geschafft, das Problem zu lösen, indem ich das berücksichtigt habe velocity. Hier ist der vollständige Code.

final class BetterSnappingLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else {
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
    }

    var offsetAdjusment = CGFloat.greatestFiniteMagnitude
    let horizontalCenter = proposedContentOffset.x + (collectionView.bounds.width / 2)

    let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height)
    let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect)
    layoutAttributesArray?.forEach({ (layoutAttributes) in
        let itemHorizontalCenter = layoutAttributes.center.x

        if abs(itemHorizontalCenter - horizontalCenter) < abs(offsetAdjusment) {
            if abs(velocity.x) < 0.3 { // minimum velocityX to trigger the snapping effect
                offsetAdjusment = itemHorizontalCenter - horizontalCenter
            } else if velocity.x > 0 {
                offsetAdjusment = itemHorizontalCenter - horizontalCenter + layoutAttributes.bounds.width
            } else { // velocity.x < 0
                offsetAdjusment = itemHorizontalCenter - horizontalCenter - layoutAttributes.bounds.width
            }
        }
    })

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

}}


Dieser hat am besten für mich funktioniert , als ich collectionView.decelerationRate = UIScrollView.DecelerationRate.fast zur collectionView selbst hinzugefügt habe
Lance Samaria

1
Ich habe mich nur angemeldet, um dies zu verbessern und zu sagen: Dies ist bei weitem die beste Lösung auf der Seite, und habe sofort genau so gearbeitet, wie ich es mir erhofft hatte. Ich habe viel zu lange damit verbracht, das am besten gewählte Beispiel zu optimieren. Danke Nhon!
Andrew Konoff

Ja, das scheint so zu sein.
Winston Du

4

Ich habe eine Antwort von SO Post hier und Dokumente hier

Zunächst können Sie festlegen, dass der Scrollview-Delegat Ihrer Sammlungsansicht Ihre Klasse delegiert, indem Sie Ihre Klasse zu einem Scrollview-Delegaten machen

MyViewController : SuperViewController<... ,UIScrollViewDelegate>

Legen Sie dann Ihren View Controller als Delegaten fest

UIScrollView *scrollView = (UIScrollView *)super.self.collectionView;
scrollView.delegate = self;

Oder tun Sie dies im Interface Builder, indem Sie bei gedrückter Ctrl-Taste + Umschalttaste auf Ihre Sammlungsansicht klicken und dann Strg + Ziehen oder Rechtsklick auf Ihren Ansichts-Controller ziehen und Delegieren auswählen. (Sie sollten wissen, wie das geht). Das geht nicht UICollectionView ist eine Unterklasse von UIScrollView, sodass Sie sie jetzt im Interface Builder durch Drücken von Strg + Umschalt anzeigen können

Implementieren Sie als Nächstes die Delegate-Methode - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

MyViewController.m

... 

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{

}

In den Dokumenten heißt es:

Parameter

scrollView | Das Objekt in der Bildlaufansicht, das das Bildlauf in der Inhaltsansicht verlangsamt.

Diskussion Die Bildlaufansicht ruft diese Methode auf, wenn die Bildlaufbewegung zum Stillstand kommt. Die Verzögerungseigenschaft von UIScrollView steuert die Verzögerung.

Verfügbarkeit Verfügbar in iOS 2.0 und höher.

Überprüfen Sie dann innerhalb dieser Methode, welche Zelle der Mitte der Bildlaufansicht am nächsten liegt, wenn der Bildlauf beendet wird

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
  //NSLog(@"%f", truncf(scrollView.contentOffset.x + (self.pictureCollectionView.bounds.size.width / 2)));

float visibleCenterPositionOfScrollView = scrollView.contentOffset.x + (self.pictureCollectionView.bounds.size.width / 2);

//NSLog(@"%f", truncf(visibleCenterPositionOfScrollView / imageArray.count));


NSInteger closestCellIndex;

for (id item in imageArray) {
    // equation to use to figure out closest cell
    // abs(visibleCenter - cellCenterX) <= (cellWidth + cellSpacing/2)

    // Get cell width (and cell too)
    UICollectionViewCell *cell = (UICollectionViewCell *)[self collectionView:self.pictureCollectionView cellForItemAtIndexPath:[NSIndexPath indexPathWithIndex:[imageArray indexOfObject:item]]];
    float cellWidth = cell.bounds.size.width;

    float cellCenter = cell.frame.origin.x + cellWidth / 2;

    float cellSpacing = [self collectionView:self.pictureCollectionView layout:self.pictureCollectionView.collectionViewLayout minimumInteritemSpacingForSectionAtIndex:[imageArray indexOfObject:item]];

    // Now calculate closest cell

    if (fabsf(visibleCenterPositionOfScrollView - cellCenter) <= (cellWidth + (cellSpacing / 2))) {
        closestCellIndex = [imageArray indexOfObject:item];
        break;
    }
}

if (closestCellIndex != nil) {

[self.pictureCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathWithIndex:closestCellIndex] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];

// This code is untested. Might not work.

}

Ich arbeite jetzt an der groben Berechnung, bearbeite sie in ungefähr 10 Minuten mit einer grundlegenden Arbeitsberechnung.
Minebomber

Wenn Sie mit mehr als einer Sammlungsansicht arbeiten und einen viewController als Delegaten festgelegt haben, können Sie die scrollView einfach mit der collectionView vergleichen, da die collectionView von der scrollView erbt. Hier erfahren Sie, welche collectionView den Bildlauf beendet hat.
NYC Tech Engineer

@Minebomber Hey Mann, habe das gerade ausprobiert und ich bekomme immer wieder einen schlechten Zugriffsfehler, wenn ich versuche, cellForItemAtIndexPath in die for-Schleife zu bekommen. Ich war überrascht, dass ein Fehler aufgetreten ist, weil ich dachte, mit einer collectionView ist Ihnen immer eine Zelle garantiert.
NYC Tech Engineer

Ja, es war eine schlechte Idee, die Rückrufe von Delegierten innerhalb des eigentlichen Delegaten explizit aufzurufen.
NYC Tech Engineer

Also habe ich die collectionView.visibleCells durchlaufen, um mein Problem zu umgehen.
NYC Tech Engineer

2

Eine Modifikation der obigen Antwort, die Sie auch ausprobieren können:

-(void)scrollToNearestVisibleCollectionViewCell {
    float visibleCenterPositionOfScrollView = _collectionView.contentOffset.x + (self.collectionView.bounds.size.width / 2);

    NSInteger closestCellIndex = -1;
    float closestDistance = FLT_MAX;
    for (int i = 0; i < _collectionView.visibleCells.count; i++) {
        UICollectionViewCell *cell = _collectionView.visibleCells[i];
        float cellWidth = cell.bounds.size.width;

        float cellCenter = cell.frame.origin.x + cellWidth / 2;

        // Now calculate closest cell
        float distance = fabsf(visibleCenterPositionOfScrollView - cellCenter);
        if (distance < closestDistance) {
            closestDistance = distance;
            closestCellIndex = [_collectionView indexPathForCell:cell].row;
        }
    }

    if (closestCellIndex != -1) {
        [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:closestCellIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
    }
}

2

Ich habe gerade herausgefunden, was meiner Meinung nach die bestmögliche Lösung für dieses Problem ist:

Fügen Sie zunächst dem bereits vorhandenen gestureRecognizer von collectionView ein Ziel hinzu:

[self.collectionView.panGestureRecognizer addTarget:self action:@selector(onPan:)];

Lassen Sie den Selektor auf eine Methode zeigen, die einen UIPanGestureRecognizer als Parameter verwendet:

- (void)onPan:(UIPanGestureRecognizer *)recognizer {};

Erzwingen Sie dann bei dieser Methode, dass die collectionView nach Beendigung der Schwenkgeste zur entsprechenden Zelle blättert. Dazu habe ich die sichtbaren Elemente aus der Sammlungsansicht abgerufen und festgelegt, zu welchem ​​Element ich scrollen möchte, abhängig von der Richtung der Pfanne.

if (recognizer.state == UIGestureRecognizerStateEnded) {

        // Get the visible items
        NSArray<NSIndexPath *> *indexes = [self.collectionView indexPathsForVisibleItems];
        int index = 0;

        if ([(UIPanGestureRecognizer *)recognizer velocityInView:self.view].x > 0) {
            // Return the smallest index if the user is swiping right
            for (int i = index;i < indexes.count;i++) {
                if (indexes[i].row < indexes[index].row) {
                    index = i;
                }
            }
        } else {
            // Return the biggest index if the user is swiping left
            for (int i = index;i < indexes.count;i++) {
                if (indexes[i].row > indexes[index].row) {
                    index = i;
                }
            }
        }
        // Scroll to the selected item
        [self.collectionView scrollToItemAtIndexPath:indexes[index] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
    }

Beachten Sie, dass in meinem Fall nur zwei Elemente gleichzeitig sichtbar sein können. Ich bin mir jedoch sicher, dass diese Methode für weitere Elemente angepasst werden kann.


2

Dies aus einem WWDC-Video 2012 für eine Objective-C-Lösung. Ich habe UICollectionViewFlowLayout unterklassifiziert und Folgendes hinzugefügt.

-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
    {
        CGFloat offsetAdjustment = MAXFLOAT;
        CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2);

        CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
        NSArray *array = [super layoutAttributesForElementsInRect:targetRect];

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

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

Und der Grund, warum ich zu dieser Frage kam, war das Schnappen mit einem nativen Gefühl, das ich aus Marks akzeptierter Antwort erhielt ... dies habe ich in den View Controller der collectionView eingefügt.

collectionView.decelerationRate = UIScrollViewDecelerationRateFast;

2

Diese Lösung bietet eine bessere und flüssigere Animation.

Swift 3

Um das erste und letzte Element in die Mitte zu bringen, fügen Sie Einfügungen hinzu:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {

    return UIEdgeInsetsMake(0, cellWidth/2, 0, cellWidth/2)
}

Verwenden Sie dann die targetContentOffsetin der scrollViewWillEndDraggingMethode, um die Endposition zu ändern.

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    let numOfItems = collectionView(mainCollectionView, numberOfItemsInSection:0)
    let totalContentWidth = scrollView.contentSize.width + mainCollectionViewFlowLayout.minimumInteritemSpacing - cellWidth
    let stopOver = totalContentWidth / CGFloat(numOfItems)

    var targetX = round((scrollView.contentOffset.x + (velocity.x * 300)) / stopOver) * stopOver
    targetX = max(0, min(targetX, scrollView.contentSize.width - scrollView.frame.width))

    targetContentOffset.pointee.x = targetX
}

Vielleicht wird in Ihrem Fall das totalContentWidthanders berechnet, zB ohne a minimumInteritemSpacing, also passen Sie das entsprechend an. Auch kann man mit dem 300in der verwendeten herumspielenvelocity

PS Stellen Sie sicher, dass die Klasse das UICollectionViewDataSourceProtokoll übernimmt


Hey Roland, wie ist das Leben? Ich bin gerade auf Ihre Antwort auf diese Frage gestoßen ... Ziemlich alt, aber trotzdem nett, danke! Zu collectionView(mainCollectionView, numberOfItemsInSection:0)Ihrer Information, die Methode kann nur auf diese Weise verwendet werden, wenn Ihr Objekt die übernimmt UICollectionViewDataSource. Und warum subtrahieren Sie das cellWidthvon scrollView.contentSize.width, ist die Gesamtbreite nicht einfach immer das scrollView.contentSize.width?
Paul van Roosendaal

Hey Paul! In meiner Situation war das Subtrahieren des cellWidtherforderlich, um es so zu versetzen, dass es zentriert ist. Vielleicht wird in Ihrem Fall das totalContentWidthanders berechnet.
Roland Keesom

1

Hier ist eine Swift 3.0-Version, die sowohl in horizontaler als auch in vertikaler Richtung funktionieren sollte, basierend auf Marks obigem Vorschlag:

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

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

    let realOffset = CGPoint(
      x: proposedContentOffset.x + collectionView.contentInset.left,
      y: proposedContentOffset.y + collectionView.contentInset.top
    )

    let targetRect = CGRect(origin: proposedContentOffset, size: collectionView.bounds.size)

    var offset = (scrollDirection == .horizontal)
      ? CGPoint(x: CGFloat.greatestFiniteMagnitude, y:0.0)
      : CGPoint(x:0.0, y:CGFloat.greatestFiniteMagnitude)

    offset = self.layoutAttributesForElements(in: targetRect)?.reduce(offset) {
      (offset, attr) in
      let itemOffset = attr.frame.origin
      return CGPoint(
        x: abs(itemOffset.x - realOffset.x) < abs(offset.x) ? itemOffset.x - realOffset.x : offset.x,
        y: abs(itemOffset.y - realOffset.y) < abs(offset.y) ? itemOffset.y - realOffset.y : offset.y
      )
    } ?? .zero

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

Gibt es eine Idee, warum dies die Zellengröße meiner Sammlungsansicht ändert und meine Sammlungsansicht auf horizontal und vertikal eingestellt wird?
Jerland2

1

Swift 4.2. Einfach. Für feste itemSize. Horizontale Strömungsrichtung.

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
        let floatingPage = targetContentOffset.pointee.x/scrollView.bounds.width
        let rule: FloatingPointRoundingRule = velocity.x > 0 ? .up : .down
        let page = CGFloat(Int(floatingPage.rounded(rule)))
        targetContentOffset.pointee.x = page*(layout.itemSize.width + layout.minimumLineSpacing)
    }

}

Dies hat eine unglückliche Auswirkung, jumping to previous cellwenn Sie nach links scrollen und sofort klicken (während die Bildlaufanimation noch ausgeführt wird). Sie werden sehen, dass die Schriftrolle "abgebrochen" wurde und auf die vorherige Zelle gesprungen ist.
Gondo

1

Ich habe dieses Problem gelöst, indem ich im Attributinspektor in der uicollectionview die Option "Paging aktiviert" festgelegt habe.

Für mich geschieht dies, wenn die Breite der Zelle der Breite der uicollectionview entspricht.

Keine Codierung erforderlich.

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.