Reorderar celdas de UICollectionView

Considere una UICollectionView con layout de flujo y dirección horizontal. De forma pnetworkingeterminada, las celdas se orderan de arriba hacia abajo, de izquierda a derecha. Me gusta esto:

 1 4 7 10 13 16 2 5 8 11 14 17 3 6 9 12 15 18 

En mi caso, la vista de colección se localiza y se ha diseñado para que un número específico de celdas se ajuste a cada página. Por lo tanto, un orderamiento más natural sería:

 1 2 3 10 11 12 4 5 6 - 13 14 15 7 8 9 16 17 18 

¿Cuál sería el más simple para lograr esto, además de implementar mi propio layout personalizado? En particular, no quiero perder ninguna de las funcionalidades que vienen de forma gratuita con UICollectionViewFlowLayout (como insert / eliminar animaciones).

O en general, ¿cómo implementa una function de reorderamiento f(n) en un layout de flujo? Lo mismo podría aplicarse a un pedido de derecha a izquierda, por ejemplo.

Mi enfoque hasta el momento

Mi primer enfoque fue subclass UICollectionViewFlowLayout y anular layoutAttributesForItemAtIndexPath: ::

 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { NSIndexPath *reordenetworkingIndexPath = [self reordenetworkingIndexPathOfIndexPath:indexPath]; UICollectionViewLayoutAttributes *layout = [super layoutAttributesForItemAtIndexPath:reordenetworkingIndexPath]; layout.indexPath = indexPath; return layout; } 

Donde reordenetworkingIndexPathOfIndexPath: es f(n) . Al llamar a super , no tengo que calcular manualmente el layout de cada elemento.

Además, tuve que replace layoutAttributesForElementsInRect: que es el método que utiliza el layout para elegir qué elementos mostrar.

 - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSMutableArray *result = [NSMutableArray array]; NSInteger sectionCount = 1; if ([self.collectionView.dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) { sectionCount = [self.collectionView.dataSource numberOfSectionsInCollectionView:self.collectionView]; } for (int s = 0; s < sectionCount; s++) { NSInteger itemCount = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:s]; for (int i = 0; i < itemCount; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:s]; UICollectionViewLayoutAttributes *layout = [self layoutAttributesForItemAtIndexPath:indexPath]; if (CGRectIntersectsRect(rect, layout.frame)) { [result addObject:layout]; } } } return result; } 

Aquí solo bash cada elemento y si está dentro del rect dado, lo devuelvo.

Si este enfoque es el path a seguir, tengo las siguientes preguntas más específicas:

  • ¿Hay alguna manera de que pueda simplificar el layoutAttributesForElementsInRect: anular o hacer que sea más eficiente?
  • ¿Me estoy perdiendo de algo? Al less, el intercambio de celdas de diferentes páginas produce resultados extraños. Sospecho que está relacionado con initialLayoutAttributesForAppearingItemAtIndexPath: y finalLayoutAttributesForDisappearingItemAtIndexPath: pero no puedo determinar exactamente cuál es el problema.
  • En mi caso, f(n) depende del número de columnas y filas de cada página. ¿Hay alguna manera de extraer esta información de UICollectionViewFlowLayout , además de codificarla yo mismo? Pensé en consultar layoutAttributesForElementsInRect: con los límites de la vista de colección, y deducir las filas y columnas de allí, pero esto también se siente ineficiente.

He pensado mucho sobre su pregunta y he llegado a las siguientes consideraciones:

Subclasificar FlowLayout parece ser la manera más correcta y efectiva de reorderar células y hacer uso de las animaciones de layout de flujo. Y su enfoque funciona, excepto dos cosas importantes:

  1. Supongamos que tiene una vista de colección con solo 2 celdas y que ha diseñado su página para que pueda contener 9 celdas. La primera celda se colocará en la esquina superior izquierda de la vista, como en el layout de flujo original. Sin embargo, su segunda celda debe ubicarse en la parte superior de la vista y tiene una ruta de índice [0, 1]. La ruta de índice reorderada sería [0, 3] (ruta de índice de la celda de disposition de flujo original que estaría en su lugar). Y en su layoutAttributesForItemAtIndexPath override, usted enviaría el post como [super layoutAttributesForItemAtIndexPath:[0, 3]] , obtendría un object nil , solo porque solo hay 2 celdas: [0,0] y [0,1]. Y este sería el problema de tu última página.
  2. Aunque puede implementar el comportamiento de búsqueda anulando targetContentOffsetForProposedContentOffset:withScrollingVelocity: y establecer manualmente properties como itemSize , minimumInteritemSpacing y minimumInteritemSpacing , es mucho trabajo hacer que sus elementos sean simétricos, para definir los bordes de búsqueda y así sucesivamente.

Yo thnik, la subsorting del layout de flujo está preparando mucha implementación para ti, ya que lo que quieres no es un layout de flujo más. Pero pensemos juntos en eso. Con respecto a sus preguntas:

  • su layoutAttributesForElementsInRect: override es exactamente la forma en que se implementa la manzana original, por lo que no hay forma de simplificarla. Sin embargo, para su caso, podría considerar lo siguiente: si tiene 3 filas de elementos por página, y el cuadro del elemento en la primera fila intersecta el cuadro del rectángulo, entonces (si todos los artículos tienen el mismo tamaño) los cuadros de los artículos de la segunda y tercera fila intersecte este rect.
  • perdón, no entendí tu segunda pregunta
  • en mi caso, la function de reorderamiento se ve así: (a es el número integer de filas / columnas en cada página, filas = columnas)

f (n) = (n% a²) + (a – 1) (col – fila) + a² (n / a²); col = (n% a²)% a; fila = (n% a²) / a;

Respondiendo a la pregunta, el layout del flujo no tiene idea de cuántas filas hay en cada columna, ya que este número puede variar de una columna a otra dependiendo del tamaño de cada elemento. Tampoco puede decir nada sobre el número de columnas en cada página porque depende de la position de desplazamiento y también puede variar. Por lo tanto, no hay mejor manera que la consulta de layoutAttributesForElementsInRect , pero esto includeá también las celdas, que solo son visibles parcialmente. Dado que sus celdas tienen el mismo tamaño, teóricamente podría descubrir cuántas filas tiene su vista de colección con la dirección de desplazamiento horizontal: comenzando iterando cada celda contándolas y rompiéndose si cambia su frame.origin.x .

Entonces, creo, tienes dos opciones para lograr tu propósito:

  1. Subclass UICollectionViewLayout . Parece ser mucho trabajo implementar todos esos methods, pero es la única forma efectiva. Podría tener, por ejemplo, properties como itemSize , itemsInOneRow . Entonces podría encontrar fácilmente una fórmula para calcular el marco de cada elemento en function de su número (la mejor manera es hacerlo en prepareLayout y almacenar todos los frameworks en la matriz, de modo que no pueda acceder al marco que necesita en layoutAttributesForItemAtIndexPath ). Implementar layoutAttributesForItemAtIndexPath , layoutAttributesForItemsInRect y collectionViewContentSize sería muy simple. En initialLayoutAttributesForAppearingItemAtIndexPath y finalLayoutAttributesForDisappearingItemAtIndexPath , puede simplemente establecer el atributo alpha en 0.0. Así funcionan las animaciones de layout de flujo estándar. Al anular targetContentOffsetForProposedContentOffset:withScrollingVelocity: puede implementar el "comportamiento de búsqueda".

  2. Considere hacer una vista de colección con layout de flujo, pagingEnabled = YES , dirección de desplazamiento horizontal y tamaño de elemento igual al tamaño de pantalla. Un elemento por tamaño de pantalla. Para cada celda, puede establecer una nueva vista de colección como subvista con layout de flujo vertical y la misma fuente de datos que otras vistas de colección, pero con un desplazamiento. Es muy eficiente, porque luego reutiliza vistas de colección completas que contienen bloques de 9 (o lo que sea) celdas en lugar de reutilizar cada celda con un enfoque estándar. Todas las animaciones deberían funcionar correctamente.

Aquí puede download un proyecto de muestra utilizando el enfoque de subclass de layout. (N. ° 2)

¿No sería una solución simple tener 2 vistas de colección con standart UICollectionViewFlowLayout ?

O incluso mejor: tener un controller de vista de página con desplazamiento horizontal, y cada página sería una vista de colección con un layout de flujo normal.

La idea es seguir: en su UICollectionViewController -init method , crea una segunda vista de colección con desplazamiento de marco a la derecha por el ancho de vista de la colección original. Luego, lo agrega como subvista a la vista de colección original. Para cambiar entre las vistas de la colección, simplemente agregue un reconocedor de desplazamiento. Para calcular los valores de compensación, puede almacenar la vista de marco de colección original en ivar cVFrame . Para identificar tus vistas de colección puedes usar tags.

Ejemplo de método init :

 CGRect cVFrame = self.collectionView.frame; UICollectionView *secondView = [[UICollectionView alloc] initWithFrame:CGRectMake(cVFrame.origin.x + cVFrame.size.width, 0, cVFrame.size.width, cVFrame.size.height) collectionViewLayout:[UICollectionViewFlowLayout new]]; [secondView setBackgroundColor:[UIColor greenColor]]; [secondView setTag:1]; [secondView setDelegate:self]; [secondView setDataSource:self]; [self.collectionView addSubview:secondView]; UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipedRight)]; [swipeRight setDirection:UISwipeGestureRecognizerDirectionRight]; UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipedLeft)]; [swipeLeft setDirection:UISwipeGestureRecognizerDirectionLeft]; [self.collectionView addGestureRecognizer:swipeRight]; [self.collectionView addGestureRecognizer:swipeLeft]; 

Ejemplo de methods swipeRight y swipeLeft :

 -(void)swipedRight { // Switch to left collection view [self.collectionView setContentOffset:CGPointMake(0, 0) animated:YES]; } -(void)swipedLeft { // Switch to right collection view [self.collectionView setContentOffset:CGPointMake(cVFrame.size.width, 0) animated:YES]; } 

Y entonces no es un gran problema implementar los methods DataSource (en su caso, desea tener 9 elementos en cada página):

 -(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { if (collectionView.tag == 1) { // Second collection view return self.dataArray.count % 9; } else { // Original collection view return 9; // Or whatever } 

En method -collectionView:cellForRowAtIndexPath necesitará get datos de su model con compensación, si es la segunda vista de colección.

También no olvide registrar la class para la celda reutilizable para su segunda vista de la colección también. También puede crear solo un reconocedor de gestos y reconocer deslizar hacia la izquierda y hacia la derecha. Tu decides.

Creo que ahora debería funcionar, pruébalo 🙂

Tiene un object que implementa el protocolo UICollectionViewDataSource . Inside collectionView:cellForItemAtIndexPath: simplemente devuelve el elemento correcto que desea devolver. No entiendo donde habría un problema.

Edit: ok, veo el problema. Aquí está la solución: http://www.skeuo.com/uicollectionview-custom-layout-tutorial , específicamente los pasos 17 a 25. No es una gran cantidad de trabajo, y se puede reutilizar muy fácilmente.

Esta es mi solución, no probé con animaciones, pero creo que funcionará bien con algunos cambios, espero que sea útil

 CELLS_PER_ROW = 4; CELLS_PER_COLUMN = 5; CELLS_PER_PAGE = CELLS_PER_ROW * CELLS_PER_COLUMN; UICollectionViewFlowLayout* flowLayout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout; flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; flowLayout.itemSize = new CGSize (size.width / CELLS_PER_ROW - delta, size.height / CELLS_PER_COLUMN - delta); flowLayout.minimumInteritemSpacing = ..; flowLayout.minimumLineSpacing = ..; .. - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { NSInteger index = indexPath.row; NSInteger page = index / CELLS_PER_PAGE; NSInteger indexInPage = index - page * CELLS_PER_PAGE; NSInteger row = indexInPage % CELLS_PER_COLUMN; NSInteger column = indexInPage / CELLS_PER_COLUMN; NSInteger dataIndex = row * CELLS_PER_ROW + column + page * CELLS_PER_PAGE; id cellData = _data[dataIndex]; ... }