ios objective-c uicollectionview uicollectionviewlayout

ios - Paginación UICollectionView por celdas, no por pantalla



uicollectionview layout (11)

Tengo UICollectionView con desplazamiento horizontal y siempre hay 2 celdas una al lado de la otra por toda la pantalla. Necesito que el desplazamiento se detenga al comienzo de una celda. Con la paginación habilitada, la vista de colección desplaza toda la página, que es de 2 celdas a la vez, y luego se detiene.

Necesito habilitar el desplazamiento por una sola celda, o desplazarme por varias celdas deteniéndome en el borde de la celda.

Traté de subclase UICollectionViewFlowLayout y para implementar el método targetContentOffsetForProposedContentOffset , pero hasta ahora solo he podido romper mi vista de colección y dejó de desplazarse. ¿Hay alguna manera más fácil de lograr esto y cómo, o realmente necesito implementar todos los métodos de la subclase UICollectionViewFlowLayout ? Gracias.


Enfoque 1: Vista de la colección

flowLayout es la propiedad UICollectionViewFlowLayout

override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { if let collectionView = collectionView { targetContentOffset.memory = scrollView.contentOffset let pageWidth = CGRectGetWidth(scrollView.frame) + flowLayout.minimumInteritemSpacing var assistanceOffset : CGFloat = pageWidth / 3.0 if velocity.x < 0 { assistanceOffset = -assistanceOffset } let assistedScrollPosition = (scrollView.contentOffset.x + assistanceOffset) / pageWidth var targetIndex = Int(round(assistedScrollPosition)) if targetIndex < 0 { targetIndex = 0 } else if targetIndex >= collectionView.numberOfItemsInSection(0) { targetIndex = collectionView.numberOfItemsInSection(0) - 1 } print("targetIndex = /(targetIndex)") let indexPath = NSIndexPath(forItem: targetIndex, inSection: 0) collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Left, animated: true) } }

Enfoque 2: Controlador de vista de página

Podría usar UIPageViewController si cumple con sus requisitos, cada página tendría un controlador de vista por separado.


Paginación horizontal con ancho de página personalizado (Swift 4)

Muchas de las soluciones que se presentan aquí dan como resultado un comportamiento extraño que no parece una paginación implementada correctamente.

La solución presentada en este tutorial , sin embargo, no parece tener ningún problema. Simplemente se siente como un algoritmo de paginación que funciona perfectamente. Puedes implementarlo en 5 sencillos pasos:

  1. Agregue la siguiente propiedad a su tipo: private var indexOfCellBeforeDragging = 0
  2. Establezca el delegate collectionView siguiente manera: collectionView.delegate = self
  3. Agregue conformidad con UICollectionViewDelegate través de una extensión: extension YourType: UICollectionViewDelegate { }
  4. Agregue el siguiente método a la extensión que implementa la conformidad UICollectionViewDelegate y establezca un valor para pageWidth :

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { let pageWidth = // The width your page should have (plus a possible margin) let proportionalOffset = collectionView.contentOffset.x / pageWidth indexOfCellBeforeDragging = Int(round(proportionalOffset)) }

  5. Agregue el siguiente método a la extensión que implementa la conformidad UICollectionViewDelegate , establezca el mismo valor para pageWidth (también puede almacenar este valor en un lugar central) y establezca un valor para collectionViewItemCount :

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { // Stop scrolling targetContentOffset.pointee = scrollView.contentOffset // Calculate conditions let pageWidth = // The width your page should have (plus a possible margin) let collectionViewItemCount = // The number of items in this section let proportionalOffset = collectionView.contentOffset.x / pageWidth let indexOfMajorCell = Int(round(proportionalOffset)) let swipeVelocityThreshold: CGFloat = 0.5 let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < collectionViewItemCount && velocity.x > swipeVelocityThreshold let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell) if didUseSwipeToSkipCell { // Animate so that swipe is just continued let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1) let toValue = pageWidth * CGFloat(snapToIndex) UIView.animate( withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: { scrollView.contentOffset = CGPoint(x: toValue, y: 0) scrollView.layoutIfNeeded() }, completion: nil) } else { // Pop back (against velocity) let indexPath = IndexPath(row: indexOfMajorCell, section: 0) collectionView.scrollToItem(at: indexPath, at: .left, animated: true) } }


Algo así como la respuesta de evya, pero un poco más suave porque no establece targetContentOffset en cero.

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { if ([scrollView isKindOfClass:[UICollectionView class]]) { UICollectionView* collectionView = (UICollectionView*)scrollView; if ([collectionView.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]]) { UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout; CGFloat pageWidth = layout.itemSize.width + layout.minimumInteritemSpacing; CGFloat usualSideOverhang = (scrollView.bounds.size.width - pageWidth)/2.0; // k*pageWidth - usualSideOverhang = contentOffset for page at index k if k >= 1, 0 if k = 0 // -> (contentOffset + usualSideOverhang)/pageWidth = k at page stops NSInteger targetPage = 0; CGFloat currentOffsetInPages = (scrollView.contentOffset.x + usualSideOverhang)/pageWidth; targetPage = velocity.x < 0 ? floor(currentOffsetInPages) : ceil(currentOffsetInPages); targetPage = MAX(0,MIN(self.projects.count - 1,targetPage)); *targetContentOffset = CGPointMake(MAX(targetPage*pageWidth - usualSideOverhang,0), 0); } } }


Aquí está mi versión de Swift 3. Calcule el desplazamiento después de que el desplazamiento finalizó y ajuste el desplazamiento con la animación.

collectionLayout es un UICollectionViewFlowLayout()

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { let index = scrollView.contentOffset.x / collectionLayout.itemSize.width let fracPart = index.truncatingRemainder(dividingBy: 1) let item= Int(fracPart >= 0.5 ? ceil(index) : floor(index)) let indexPath = IndexPath(item: item, section: 0) collectionView.scrollToItem(at: indexPath, at: .left, animated: true) }


En parte basado en la respuesta de StevenOjo. He probado esto usando un desplazamiento horizontal y sin Bounce UICollectionView. cellSize es el tamaño de CollectionViewCell. Puede modificar el factor para modificar la sensibilidad de desplazamiento.

override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset var factor: CGFloat = 0.5 if velocity.x < 0 { factor = -factor } let indexPath = IndexPath(row: (scrollView.contentOffset.x/cellSize.width + factor).int, section: 0) collectionView?.scrollToItem(at: indexPath, at: .left, animated: true) }


Esta es una forma directa de hacer esto.

El caso es simple, pero finalmente bastante común (desplazador de miniaturas típico con tamaño de celda fijo y espacio fijo entre las células)

var itemCellSize: CGSize = <your cell size> var itemCellsGap: CGFloat = <gap in between> override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let pageWidth = (itemCellSize.width + itemCellsGap) let itemIndex = (targetContentOffset.pointee.x) / pageWidth targetContentOffset.pointee.x = round(itemIndex) * pageWidth - (itemCellsGap / 2) } // CollectionViewFlowLayoutDelegate func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return itemCellSize } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return itemCellsGap }

Tenga en cuenta que no hay ninguna razón para llamar a scrollToOffset o sumergirse en diseños. El comportamiento de desplazamiento nativo ya lo hace todo.

Saludos a todos :)



Para las personas que buscan una búsqueda basada en celdas verticales (ya que eso era lo que estaba buscando inicialmente) He aquí una parte del código de una library que estoy creando que hace esto (debe poner esto dentro de una subclase de UICollectionViewFlowLayout ).

Probablemente también pueda cambiarlo para que funcione horizontalmente.

código:

/** This property enables paging per card. The default value is true. */ public var isPagingEnabled: Bool = true // MARK: cell paging override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { // MARK: If the property `isPagingEnabled` is set to false, we don''t enable paging and thus return the current contentoffset guard isPagingEnabled else { let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) return latestOffset } var offsetAdjustment = CGFloat.greatestFiniteMagnitude let verticalOffset = proposedContentOffset.y let contentInset = collectionView!.contentInset.top let targetRect = CGRect(origin: CGPoint(x: 0, y: verticalOffset), size: self.collectionView!.bounds.size) for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! { // MARK: we adjust for the contentInset let itemOffset = layoutAttributes.frame.origin.y - contentInset if (abs(itemOffset - verticalOffset) < abs(offsetAdjustment)) { offsetAdjustment = itemOffset - verticalOffset } } return CGPoint(x: proposedContentOffset.x, y: verticalOffset + offsetAdjustment) }


Swift 3 versión de la respuesta de Evya:

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset let pageWidth:Float = Float(self.view.bounds.width) let minSpace:Float = 10.0 var cellToSwipe:Double = Double(Float((scrollView.contentOffset.x))/Float((pageWidth+minSpace))) + Double(0.5) if cellToSwipe < 0 { cellToSwipe = 0 } else if cellToSwipe >= Double(self.articles.count) { cellToSwipe = Double(self.articles.count) - Double(1) } let indexPath:IndexPath = IndexPath(row: Int(cellToSwipe), section:0) self.collectionView.scrollToItem(at:indexPath, at: UICollectionViewScrollPosition.left, animated: true) }


También puede crear vistas de desplazamiento falsas para manejar el desplazamiento.

Horizontal o Vertical

// === Defaults === let bannerSize = CGSize(width: 280, height: 170) let pageWidth: CGFloat = 290 // ^ + paging let insetLeft: CGFloat = 20 let insetRight: CGFloat = 20 // ================ var pageScrollView: UIScrollView! override func viewDidLoad() { super.viewDidLoad() // Create fake scrollview to properly handle paging pageScrollView = UIScrollView(frame: CGRect(origin: .zero, size: CGSize(width: pageWidth, height: 100))) pageScrollView.isPagingEnabled = true pageScrollView.alwaysBounceHorizontal = true pageScrollView.showsVerticalScrollIndicator = false pageScrollView.showsHorizontalScrollIndicator = false pageScrollView.delegate = self pageScrollView.isHidden = true view.insertSubview(pageScrollView, belowSubview: collectionView) // Set desired gesture recognizers to the collection view for gr in pageScrollView.gestureRecognizers! { collectionView.addGestureRecognizer(gr) } } func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView == pageScrollView { // Return scrolling back to the collection view collectionView.contentOffset.x = pageScrollView.contentOffset.x } } func refreshData() { ... refreshScroll() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() refreshScroll() } /// Refresh fake scrolling view content size if content changes func refreshScroll() { let w = collectionView.width - bannerSize.width - insetLeft - insetRight pageScrollView.contentSize = CGSize(width: pageWidth * CGFloat(banners.count) - w, height: 100) }


simplemente anule el método:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { *targetContentOffset = scrollView.contentOffset; // set acceleration to 0.0 float pageWidth = (float)self.articlesCollectionView.bounds.size.width; int minSpace = 10; int cellToSwipe = (scrollView.contentOffset.x)/(pageWidth + minSpace) + 0.5; // cell width + min spacing for lines if (cellToSwipe < 0) { cellToSwipe = 0; } else if (cellToSwipe >= self.articles.count) { cellToSwipe = self.articles.count - 1; } [self.articlesCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:cellToSwipe inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES]; }