iphone ios uicollectionview

iphone - targetContentOffsetForProposedContentOffset: withScrollingVelocity sin subclasificar UICollectionViewFlowLayout



ios (16)

Tengo una collectionView muy simple en mi aplicación (solo una fila de imágenes en miniatura).

Me gustaría interceptar el desplazamiento para que el desplazamiento siempre deje una imagen completa en el lado izquierdo. Por el momento se desplaza a donde sea y dejará imágenes cortadas.

De todos modos, sé que necesito usar la función

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

para hacer esto, pero solo estoy usando un UICollectionViewFlowLayout estándar. No lo estoy subclasificando.

¿Hay alguna forma de interceptar esto sin subclasificar UICollectionViewFlowLayout ?

Gracias


Aquí está mi implementación en Swift 4.1 para paginación vertical basada en celdas (probablemente también funcionará horizontalmente si cambias las variables):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { // 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 = self.collectionView!.contentOffset.y/pageHeight // Determine the current page based on velocity. let currentPage = (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) - self.collectionView!.contentInset.top return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset) }

Esto no debe fallar y le permite establecer su propia flickvelocidad fácilmente.


Aquí está mi solución Swift en una vista de colección de desplazamiento horizontal. Es simple, dulce y evita cualquier parpadeo.

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 }


Código @ André Abreu

Versión Swift3

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout { override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { var offsetAdjustment = CGFloat.greatestFiniteMagnitude let horizontalOffset = proposedContentOffset.x let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height) for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! { let itemOffset = layoutAttributes.frame.origin.x if abs(itemOffset - horizontalOffset) < abs(offsetAdjustment){ offsetAdjustment = itemOffset - horizontalOffset } } return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y) } }


Después de largas pruebas, encontré que la solución se ajusta al centro con un ancho de celda personalizado (cada celda tiene un ancho diferente) lo que corrige el parpadeo. Siéntase libre de mejorar el guión.

- (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://.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; }


La respuesta de Fogmeisters funcionó para mí a menos que me desplazara hasta el final de la fila. Mis celdas no encajan perfectamente en la pantalla para que se desplace hacia el final y salte hacia atrás con un tirón, de modo que la última celda siempre se superponga al borde derecho de la pantalla.

Para evitar esto, agregue la siguiente línea de código al inicio del método targetcontentoffset

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


La solución de Dan es defectuosa. No maneja bien al usuario. Los casos en los que el usuario mueve rápidamente y el desplazamiento no se movió tanto, tienen fallas en la animación.

Mi implementación alternativa propuesta tiene la misma paginación que se propuso anteriormente, pero maneja el movimiento del usuario entre las páginas.

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


OK, la respuesta es no, no hay forma de hacerlo sin subclasificar UICollectionViewFlowLayout.

Sin embargo, crear subclases es increíblemente fácil para cualquiera que esté leyendo esto en el futuro.

Primero configuré la llamada de subclase MyCollectionViewFlowLayout y luego en el constructor de interfaz cambié el diseño de la vista de colección a Personalizado y seleccioné mi subclase de diseño de flujo.

Como lo está haciendo de esta manera, no puede especificar tamaños de elementos, etc ... en IB así que en MyCollectionViewFlowLayout.m tengo esto ...

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

Esto configura todos los tamaños para mí y la dirección de desplazamiento.

Entonces ...

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

Esto asegura que el desplazamiento finaliza con un margen de 5.0 en el borde izquierdo.

Eso es todo lo que necesitaba hacer. No necesité configurar el diseño de flujo en ningún código.


Para cualquiera que busque una solución que ...

  • NO GLITCH cuando el usuario realiza un desplazamiento rápido corto (es decir, considera las velocidades de desplazamiento positivas y negativas)
  • toma en consideración el collectionView.contentInset (y safeArea en iPhone X)
  • solo considera tres celdas visibles en el punto de desplazamiento (para rendimiento)
  • usa variables y comentarios bien nombrados
  • es Swift 4

entonces por favor mira abajo ...

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 } else { return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.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 } } }


Prefiero permitir que el usuario hojee varias páginas. Así que aquí está mi versión de targetContentOffsetForProposedContentOffset (que se basa en la respuesta de DarthMike) para el diseño vertical .

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


Si bien esta respuesta me ha sido de gran ayuda, hay un parpadeo notable cuando deslizas rápido a una distancia pequeña. Es mucho más fácil reproducirlo en el dispositivo.

Descubrí que esto siempre sucede cuando collectionView.contentOffset.x - proposedContentOffset.x y velocity.x tienen diferentes sonidos.

Mi solución fue asegurar que contentOffset.x proposedContentOffset sea ​​más que contentOffset.x si la velocidad es positiva, y menos si es negativa. Está en C # pero debería ser bastante simple de traducir al Objetivo C:

public override PointF TargetContentOffset (PointF proposedContentOffset, PointF scrollingVelocity) { /* Determine closest edge */ float offSetAdjustment = float.MaxValue; float horizontalCenter = (float) (proposedContentOffset.X + (this.CollectionView.Bounds.Size.Width / 2.0)); RectangleF targetRect = new RectangleF (proposedContentOffset.X, 0.0f, this.CollectionView.Bounds.Size.Width, this.CollectionView.Bounds.Size.Height); var array = base.LayoutAttributesForElementsInRect (targetRect); foreach (var layoutAttributes in array) { float itemHorizontalCenter = layoutAttributes.Center.X; if (Math.Abs (itemHorizontalCenter - horizontalCenter) < Math.Abs (offSetAdjustment)) { offSetAdjustment = itemHorizontalCenter - horizontalCenter; } } float nextOffset = proposedContentOffset.X + offSetAdjustment; /* * ... unless we end up having positive speed * while moving left or negative speed while moving right. * This will cause flicker so we resort to finding next page * in the direction of velocity and use it. */ do { proposedContentOffset.X = nextOffset; float deltaX = proposedContentOffset.X - CollectionView.ContentOffset.X; float velX = scrollingVelocity.X; // If their signs are same, or if either is zero, go ahead if (Math.Sign (deltaX) * Math.Sign (velX) != -1) break; // Otherwise, look for the closest page in the right direction nextOffset += Math.Sign (scrollingVelocity.X) * SnapStep; } while (IsValidOffset (nextOffset)); return proposedContentOffset; } bool IsValidOffset (float offset) { return (offset >= MinContentOffset && offset <= MaxContentOffset); }

Este código está utilizando MinContentOffset , MaxContentOffset y SnapStep que debería ser trivial para que usted defina. En mi caso, resultaron ser

float MinContentOffset { get { return -CollectionView.ContentInset.Left; } } float MaxContentOffset { get { return MinContentOffset + CollectionView.ContentSize.Width - ItemSize.Width; } } float SnapStep { get { return ItemSize.Width + MinimumLineSpacing; } }


Solo quiero poner aquí la versión Swift de la respuesta aceptada.

override func targetContentOffsetForProposedContentOffset(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) }

Válido para Swift 3 .


Swift 4

La solución más fácil para la vista de colección con celdas de un tamaño (desplazamiento horizontal):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collectionView = collectionView else { return proposedContentOffset } // Calculate width of your page let pageWidth = calculatedPageWidth() // Calculate proposed page let proposedPage = round(proposedContentOffset.x / pageWidth) // Adjust necessary offset let xOffset = pageWidth * proposedPage - collectionView.contentInset.left return CGPoint(x: xOffset, y: 0) } func calculatedPageWidth() -> CGFloat { return itemSize.width + minimumInteritemSpacing }


Una solución más corta (suponiendo que está almacenando en caché sus atributos de diseño):

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

Para poner esto en contexto:

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


refiérase a esta respuesta de Dan Abramov aquí está la versión Swift

override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { var _proposedContentOffset = CGPoint(x: proposedContentOffset.x, y: proposedContentOffset.y) var offSetAdjustment: CGFloat = CGFloat.max let horizontalCenter: CGFloat = CGFloat(proposedContentOffset.x + (self.collectionView!.bounds.size.width / 2.0)) let targetRect = CGRect(x: proposedContentOffset.x, y: 0.0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height) let array: [UICollectionViewLayoutAttributes] = self.layoutAttributesForElementsInRect(targetRect)! as [UICollectionViewLayoutAttributes] for layoutAttributes: UICollectionViewLayoutAttributes in array { if (layoutAttributes.representedElementCategory == UICollectionElementCategory.Cell) { let itemHorizontalCenter: CGFloat = layoutAttributes.center.x if (abs(itemHorizontalCenter - horizontalCenter) < abs(offSetAdjustment)) { offSetAdjustment = itemHorizontalCenter - horizontalCenter } } } var nextOffset: CGFloat = proposedContentOffset.x + offSetAdjustment repeat { _proposedContentOffset.x = nextOffset let deltaX = proposedContentOffset.x - self.collectionView!.contentOffset.x let velX = velocity.x if (deltaX == 0.0 || velX == 0 || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) { break } if (velocity.x > 0.0) { nextOffset = nextOffset + self.snapStep() } else if (velocity.x < 0.0) { nextOffset = nextOffset - self.snapStep() } } while self.isValidOffset(nextOffset) _proposedContentOffset.y = 0.0 return _proposedContentOffset } func isValidOffset(offset: CGFloat) -> Bool { return (offset >= CGFloat(self.minContentOffset()) && offset <= CGFloat(self.maxContentOffset())) } func minContentOffset() -> CGFloat { return -CGFloat(self.collectionView!.contentInset.left) } func maxContentOffset() -> CGFloat { return CGFloat(self.minContentOffset() + self.collectionView!.contentSize.width - self.itemSize.width) } func snapStep() -> CGFloat { return self.itemSize.width + self.minimumLineSpacing; }

o esencia aquí https://gist.github.com/katopz/8b04c783387f0c345cd9


un pequeño problema que encontré al utilizar targetContentOffsetForProposedContentOffset es un problema con la última celda que no se ajusta según el nuevo punto que devolví.
Descubrí que el CGPoint que devolví tenía un valor Y más grande que el permitido, así que utilicé el siguiente código al final de mi implementación targetContentOffsetForProposedContentOffset:

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

solo para aclarar esto, esta es mi implementación de diseño completo que solo imita el comportamiento de paginación vertical:

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

con suerte, esto le ahorrará a alguien un poco de tiempo y un dolor de cabeza


Para aquellos que buscan una solución en Swift:

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout { private let collectionViewHeight: CGFloat = 200.0 private let screenWidth: CGFloat = UIScreen.mainScreen().bounds.width override func awakeFromNib() { super.awakeFromNib() self.itemSize = CGSize(width: [InsertItemWidthHere], height: [InsertItemHeightHere]) self.minimumInteritemSpacing = [InsertItemSpacingHere] self.scrollDirection = .Horizontal let inset = (self.screenWidth - CGFloat(self.itemSize.width)) / 2 self.collectionView?.contentInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset) } override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { var offsetAdjustment = CGFloat.max let horizontalOffset = proposedContentOffset.x + ((self.screenWidth - self.itemSize.width) / 2) let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.screenWidth, height: self.collectionViewHeight) var array = super.layoutAttributesForElementsInRect(targetRect) for layoutAttributes in array! { let itemOffset = layoutAttributes.frame.origin.x if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) { offsetAdjustment = itemOffset - horizontalOffset } } return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y) } }