UICollectionView se ajusta a la celda cuando se desplaza horizontalmente
Sé que algunas personas han hecho esta pregunta antes, pero eran todos acerca de UITableViews
o UIScrollViews
y no pude conseguir la solución aceptada para trabajar para mí. Lo que me gustaría es el efecto de ajuste cuando se desplaza a través de mi UICollectionView
horizontalmente - al igual que lo que sucede en la AppStore iOS. iOS 9 + es mi compilación de destino, así que mira los cambios de UIKit antes de responder a esto.
Gracias.
13 answers
Mientras originalmente estaba usando Objective-C, desde entonces cambié tan Rápido y la respuesta original aceptada no fue suficiente.
Terminé creando una subclase UICollectionViewLayout
que proporciona la mejor experiencia (imo) en comparación con las otras funciones que alteran el desplazamiento de contenido o algo similar cuando el usuario ha dejado de desplazarse.
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)
}
}
Para la desaceleración de sensación más nativa con la subclase de diseño actual, asegúrese de establecer lo siguiente:
collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-26 15:04:57
Para lo que vale aquí es un cálculo simple que uso (en swift):
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
}
}
}
Llame donde lo necesite. Lo llamo en
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
snapToNearestCell(scrollView)
}
Y
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
snapToNearestCell(scrollView)
}
De dónde podría provenir collectionViewFlowLayout:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Set up collection view
collectionViewFlowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-18 10:52:51
SWIFT 3 versión de @Iowa15 respuesta
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)
}
}
Necesita implementar en UIScrollViewDelegate:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
scrollToNearestVisibleCollectionViewCell()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
scrollToNearestVisibleCollectionViewCell()
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:05:39
Basado en la respuesta de Mete y comentario de Chris Chute,
Aquí hay una extensión Swift 4 que hará justo lo que OP quiere. Se prueba en vistas de colección anidadas de una fila y doble fila y funciona bien.
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)
}
}
}
Debe implementar el protocolo UIScrollViewDelegate
para su vista de colección y luego agregar estos dos métodos:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.collectionView.scrollToNearestVisibleCollectionViewCell()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.collectionView.scrollToNearestVisibleCollectionViewCell()
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-01 21:37:08
Recibió una respuesta de SO post aquí y docs aquí
En primer lugar, lo que puede hacer es establecer el delegado de su clase de scrollview de su vista de colección haciendo de su clase un delegado de scrollview
MyViewController : SuperViewController<... ,UIScrollViewDelegate>
Luego establezca su controlador de vista como el delegado
UIScrollView *scrollView = (UIScrollView *)super.self.collectionView;
scrollView.delegate = self;
O hágalo en el creador de interfaces control + shift haciendo clic en la vista de su colección y luego control + arrastrar o haga clic con el botón derecho en arrastrar a su controlador de vista y seleccione delegado. (Usted debe saber cómo hacer esto). Eso no funciona. UICollectionView es una subclase de UIScrollView por lo que ahora podrás verla en el creador de interfaces control + shift haciendo clic en
Siguiente implementar el método delegado - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
MyViewController.m
...
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
}
Los documentos indican que:
Parámetros
ScrollView / El objeto scroll-view que está desacelerando el desplazamiento la vista de contenido.
Discusión La vista de desplazamiento llama a este método cuando el desplazamiento el movimiento se detiene. La propiedad decelerating de UIScrollView controla la desaceleración.
Disponibilidad Disponible en iOS 2.0 y versiones posteriores.
Luego, dentro de ese método, verifique qué celda estaba más cerca del centro de scrollview cuando dejó de desplazarse
- (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.
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:31:39
Aquí está mi implementación
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)
}
Implemente su delegados de vista de desplazamiento así
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
self.snapToNearestCell(scrollView: scrollView)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.snapToNearestCell(scrollView: scrollView)
}
También, para un mejor ajuste
self.cvCollectionView.decelerationRate = UIScrollViewDecelerationRateFast
Funciona como un encanto
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-09 05:14:03
Una modificación de la respuesta anterior que también puedes probar:
-(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];
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-07 15:35:03
Acabo de encontrar lo que creo que es la mejor solución posible a este problema:
Primero agregue un destino al Gesturecognizer ya existente de collectionView:
[self.collectionView.panGestureRecognizer addTarget:self action:@selector(onPan:)];
Tiene el punto selector a un método que toma un UIPanGestureRecognizer como parámetro:
- (void)onPan:(UIPanGestureRecognizer *)recognizer {};
Luego, en este método, fuerce a collectionView a desplazarse a la celda apropiada cuando el gesto de panorámica haya terminado. Hice esto obteniendo los elementos visibles de la vista de colección y determinando qué elemento Quiero desplazarme a dependiendo de la dirección de la bandeja.
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];
}
Tenga en cuenta que en mi caso solo dos elementos pueden ser visibles a la vez. Sin embargo, estoy seguro de que este método se puede adaptar para más elementos.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-20 17:51:40
Si desea un comportamiento nativo simple, sin personalización:
collectionView.pagingEnabled = YES;
Esto solo funciona correctamente cuando el tamaño de los elementos de diseño de la vista de colección son todos de un solo tamaño y la propiedad UICollectionViewCell
's clipToBounds
se establece en YES
.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-06 17:34:22
Ajustar a la celda más cercana, respetando la velocidad de desplazamiento.
Funciona sin ningún problema técnico.
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)
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-03 13:11:09
Esto es de un video WWDC de 2012 para una solución de Objective-C. Subclase UICollectionViewFlowLayout y agregué lo siguiente.
-(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);
}
Y la razón por la que llegué a esta pregunta fue por el chasquido con una sensación nativa, que obtuve de la respuesta aceptada de Mark... esto lo puse en el controlador de vista de collectionView.
collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-21 15:06:28
Esta solución proporciona una animación mejor y más suave.
Swift 3
Para obtener el primer y el último elemento en el centro, agregue inserciones:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, cellWidth/2, 0, cellWidth/2)
}
Luego use el targetContentOffset
en el método scrollViewWillEndDragging
para alterar la posición final.
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
}
Tal vez en su caso el totalContentWidth
se calcula de manera diferente, por ejemplo, sin un minimumInteritemSpacing
, así que ajuste eso en consecuencia.
También puedes jugar con el 300
utilizado en el velocity
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-01 19:29:40
Aquí hay una versión Swift 3.0, que debería funcionar tanto en direcciones horizontales como verticales según la sugerencia de Mark anterior:
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)
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-06 09:40:42