Während ich ursprünglich Objective-C verwendete, wechselte ich seitdem so schnell und die ursprünglich akzeptierte Antwort reichte nicht aus.
UICollectionViewLayout
Am 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
sectionInset.left
gesetzt haben, ersetzen Sie die Rückgabeerklärung durch:return CGPoint(x: proposedContentOffset.x + offsetAdjustment - sectionInset.left, y: proposedContentOffset.y)
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 } })
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
UIScrollViewDelegate
Protokoll 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() } }
quelle
if closestCellIndex != -1 { UIView.animate(withDuration: 0.1) { let toX = (cellWidth + cellHorizontalSpacing) * CGFloat(closestCellIndex) scrollView.contentOffset = CGPoint(x: toX, y: 0) scrollView.layoutIfNeeded() } }
Paging Enabled
der 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.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) } }
quelle
contentInset
, sicher, dass Sie es entweder mit var hinzufügen oder entfernennearestPageOffset
.itemSize
. Dieser Ansatz wird nuritemSize
direktlayout
itemSize
UICollectionViewDelegateFlowLayout
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 }
quelle
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() } }
quelle
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
irgendwann einstellen . Ich würde auch hinzufügen, dassFLT_MAX
in Zeile 4 geändert werdenFloat.greatestFiniteMagnitude
sollte, um Xcode-Warnungen zu vermeiden.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
quelle
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 -clipToBounds
Eigenschaft aufYES
.quelle
minimumLineSpacing
dass Sie für Ihr Layout denIch 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) }
}}
quelle
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:
Ü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. }
quelle
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]; } }
quelle
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.
quelle
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;
quelle
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
targetContentOffset
in derscrollViewWillEndDragging
Methode, 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
totalContentWidth
anders berechnet, zB ohne aminimumInteritemSpacing
, also passen Sie das entsprechend an. Auch kann man mit dem300
in der verwendeten herumspielenvelocity
PS Stellen Sie sicher, dass die Klasse das
UICollectionViewDataSource
Protokoll übernimmtquelle
collectionView(mainCollectionView, numberOfItemsInSection:0)
Ihrer Information, die Methode kann nur auf diese Weise verwendet werden, wenn Ihr Objekt die übernimmtUICollectionViewDataSource
. Und warum subtrahieren Sie dascellWidth
vonscrollView.contentSize.width
, ist die Gesamtbreite nicht einfach immer dasscrollView.contentSize.width
?cellWidth
erforderlich, um es so zu versetzen, dass es zentriert ist. Vielleicht wird in Ihrem Fall dastotalContentWidth
anders berechnet.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) }
quelle
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) } }
quelle
jumping to previous cell
wenn 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.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.
quelle