targetContentOffsetForProposedContentOffset: withScrollingVelocity sin subclasss UICollectionViewFlowLayout

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

Me gustaría interceptar el desplazamiento para que el desplazamiento siempre deje una image completa en el lado izquierdo. En este momento se desplaza a donde quiera y dejará imágenes cortadas.

De todos modos, sé que necesito usar la function

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity 

Para hacer esto, solo estoy usando una UICollectionViewFlowLayout estándar. No estoy subclassando.

¿Hay alguna forma de interceptar esto sin subclasificar UICollectionViewFlowLayout ?

Gracias

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

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

Primero configuré la llamada de la subclass MyCollectionViewFlowLayout y luego, en el generador de interfaces, cambié el layout de vista de colección a Personalizado y seleccioné la subclass de layout de flujo.

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

 - (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 finalice con un margen de 5.0 en el borde izquierdo.

Eso es todo lo que necesitaba hacer. No necesitaba configurar el layout de flujo en el código en absoluto.

La solución de Dan es defectuosa. No maneja bien el movimiento del usuario. Los casos en los que el usuario hace un rápido movimiento y el desplazamiento no se movió tanto, tienen fallas de animation.

Mi implementación alternativa propuesta tiene la misma pagination que la propuesta anteriormente, pero maneja el movimiento del usuario entre 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; } 

Después de largas testings, encontré una solución para ajustarse al centro con un ancho de celda personalizado (cada celda tiene un ancho diferente) 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]; NSPnetworkingicate *cellAttributesPnetworkingicate = [NSPnetworkingicate pnetworkingicateWithBlock: ^BOOL(UICollectionViewLayoutAttributes * _Nonnull evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) { return (evaluatedObject.representedElementCategory == UICollectionElementCategoryCell); }]; NSArray *cellAttributes = [attributes filtenetworkingArrayUsingPnetworkingicate: cellAttributesPnetworkingicate]; 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; } 

Si bien esta respuesta ha sido de gran ayuda para mí, hay un parpadeo notable cuando deslizas rápido a una pequeña distancia. 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 cantes.

Mi solución era garantizar que proposedContentOffset es más que contentOffset.x si la velocidad es positiva y less 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 utiliza MinContentOffset , MaxContentOffset y SnapStep que deberían ser triviales para que usted pueda definir. 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; } } 

consulte 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 la esencia aquí https://gist.github.com/katopz/8b04c783387f0c345cd9

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 .

Prefiero permitir que el usuario hojee varias páginas. Así que aquí está mi versión de targetContentOffsetForProposedContentOffset (que basado en la respuesta de DarthMike) para el layout 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; } 

La respuesta de Fogmeisters funcionó para mí a less que me desplazara hasta el final de la fila. Mis células no encajan perfectamente en la pantalla, por lo que se desplazaría hasta el final y retrocederían con una sacudida, de modo que la última célula siempre se solaparía con el borde derecho de la pantalla.

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

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

un pequeño problema que encontré al usar 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 mayor 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 aclararlo, esta es mi implementación de layout completo que simplemente imita el comportamiento de búsqueda 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 time y un dolor de cabeza.

Aquí está mi solución Swift en una vista de colección horizontalmente desplazable. 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 de 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) } } 

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 caching sus attributes de layout):

 override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height) let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }! return CGPoint(x: targetLayoutAttributes.frame.minX - horizontalPadding, y: 0) } 

Para poner esto en context:

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

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