web-dev-qa-db-fra.com

Réorganiser les cellules d'UICollectionView

Considérons un UICollectionView avec une disposition de flux et une direction horizontale. Par défaut, les cellules sont classées de haut en bas, de gauche à droite. Comme ça:

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

Dans mon cas, la vue de la collection est paginée et elle a été conçue pour qu'un nombre spécifique de cellules tienne dans chaque page. Ainsi, un ordre plus naturel serait:

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

Quelle serait la plus simple pour y parvenir, à moins d'implémenter ma propre mise en page personnalisée? En particulier, je ne veux perdre aucune des fonctionnalités fournies gratuitement avec UICollectionViewFlowLayout (telles que les animations d'insertion/suppression).

Ou en général, comment implémentez-vous une fonction de réorganisation f(n) sur une disposition de flux? La même chose pourrait être applicable à une commande de droite à gauche, par exemple.

Mon approche jusqu'à présent

Ma première approche a été de sous-classer UICollectionViewFlowLayout et de remplacer layoutAttributesForItemAtIndexPath::

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

reorderedIndexPathOfIndexPath: Est f(n). En appelant super, je n'ai pas à calculer manuellement la disposition de chaque élément.

De plus, j'ai dû remplacer layoutAttributesForElementsInRect:, Qui est la méthode utilisée par la mise en page pour choisir les éléments à afficher.

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

Ici, j'essaie simplement chaque élément et s'il se trouve dans le rect donné, je le renvoie.

Si cette approche est la voie à suivre, j'ai les questions plus spécifiques suivantes:

  • Existe-t-il un moyen de simplifier le remplacement de layoutAttributesForElementsInRect: Ou de le rendre plus efficace?
  • Suis-je en train de manquer quelque chose? À tout le moins, l'échange de cellules de pages différentes produit des résultats étranges. Je soupçonne que c'est lié à initialLayoutAttributesForAppearingItemAtIndexPath: Et finalLayoutAttributesForDisappearingItemAtIndexPath:, Mais je ne peux pas identifier exactement quel est le problème.
  • Dans mon cas, f(n) dépend du nombre de colonnes et de lignes de chaque page. Existe-t-il un moyen d'extraire ces informations de UICollectionViewFlowLayout, à moins de les coder en dur moi-même? J'ai pensé à interroger layoutAttributesForElementsInRect: avec les limites de la vue de collection, et en déduire les lignes et les colonnes, mais cela semble également inefficace.
42
hpique

J'ai beaucoup réfléchi à votre question et suis venu aux considérations suivantes:

Le sous-classement de FlowLayout semble être le moyen le plus juste et le plus efficace de réorganiser les cellules et d'utiliser des animations de disposition de flux. Et votre approche fonctionne, à l'exception de deux choses importantes:

  1. Supposons que vous ayez une vue de collection avec seulement 2 cellules et que vous ayez conçu votre page de sorte qu'elle puisse contenir 9 cellules. La première cellule sera positionnée dans le coin supérieur gauche de la vue, comme dans la disposition de flux d'origine. Cependant, votre deuxième cellule doit être positionnée en haut de la vue et elle a un chemin d'index [0, 1]. Le chemin d'index réorganisé serait [0, 3] (chemin d'index de la cellule de disposition de flux d'origine qui serait à sa place). Et dans votre layoutAttributesForItemAtIndexPath surcharge, vous enverriez le message comme [super layoutAttributesForItemAtIndexPath:[0, 3]], vous obtiendrez un objet nil, simplement parce qu'il n'y a que 2 cellules: [0,0] et [0,1]. Et ce serait le problème pour votre dernière page.
  2. Même si vous pouvez implémenter le comportement de pagination en remplaçant targetContentOffsetForProposedContentOffset:withScrollingVelocity: et définissez manuellement des propriétés telles que itemSize, minimumLineSpacing et minimumInteritemSpacing, il faut beaucoup de travail pour que vos éléments soient symétriques, pour définir les bordures de pagination, etc.

I thnik, sous-classer la disposition de flux prépare beaucoup d'implémentation pour vous, car ce que vous voulez n'est plus une disposition de flux. Mais réfléchissons-y ensemble. Concernant vos questions:

  • votre layoutAttributesForElementsInRect: la substitution est exactement la façon dont l'implémentation originale Apple est, donc il n'y a aucun moyen de la simplifier. Pour votre cas, vous pouvez envisager ce qui suit: si vous avez 3 lignes d'éléments par page, et le cadre de l'article dans la première rangée coupe le cadre rect, puis (si tous les articles ont la même taille) les cadres des articles de deuxième et troisième rangs croisent ce rect.
  • désolé, je n'ai pas compris votre deuxième question
  • dans mon cas, la fonction de réorganisation ressemble à ceci: (a est le entier nombre de lignes/colonnes sur chaque page, lignes = colonnes)

f (n) = (n% a²) + (a - 1) (colonne) + a² (n/a²); col = (n% a²)% a; ligne = (n% a²)/a;

Répondant à la question, la disposition du flux n'a aucune idée du nombre de lignes dans chaque colonne, car ce nombre peut varier d'une colonne à l'autre en fonction de la taille de chaque élément. Il peut également ne rien dire sur le nombre de colonnes sur chaque page car cela dépend de la position de défilement et peut également varier. Il n'y a donc pas de meilleur moyen que d'interroger layoutAttributesForElementsInRect, mais cela inclura également les cellules, qui ne sont que partiellement visibles. Étant donné que vos cellules sont de taille égale, vous pourriez théoriquement savoir combien de lignes a votre vue de collection avec une direction de défilement horizontal: en commençant par itérer chaque cellule en les comptant et en cassant si leur frame.Origin.x changements.

Donc, je pense que vous avez deux options pour atteindre votre objectif:

  1. Sous-classe UICollectionViewLayout. Cela semble être beaucoup de travail d'implémenter toutes ces méthodes, mais c'est le seul moyen efficace. Vous pourriez avoir par exemple des propriétés comme itemSize, itemsInOneRow. Ensuite, vous pouvez facilement trouver une formule pour calculer le cadre de chaque élément en fonction de son nombre (la meilleure façon est de le faire dans prepareLayout et de stocker toutes les images dans un tableau, de sorte que vous ne puissiez pas accéder au cadre dont vous avez besoin dans layoutAttributesForItemAtIndexPath). L'implémentation de layoutAttributesForItemAtIndexPath, layoutAttributesForItemsInRect et collectionViewContentSize serait également très simple. Dans initialLayoutAttributesForAppearingItemAtIndexPath et finalLayoutAttributesForDisappearingItemAtIndexPath, vous pouvez simplement définir l'attribut alpha sur 0,0. C'est ainsi que fonctionnent les animations de disposition de flux standard. En remplaçant targetContentOffsetForProposedContentOffset:withScrollingVelocity: vous pouvez implémenter le "comportement de pagination".

  2. Envisagez de créer une vue de collection avec une disposition de flux, pagingEnabled = YES, sens de défilement horizontal et taille de l'élément égale à la taille de l'écran. Un élément par taille d'écran. Pour chaque cellule, vous pouvez définir une nouvelle vue de collection en tant que sous-vue avec une disposition de flux vertical et la même source de données que les autres vues de collection, mais avec un décalage. C'est très efficace, car vous réutilisez ensuite des vues de collection entières contenant des blocs de 9 (ou autre) cellules au lieu de réutiliser chaque cellule avec une approche standard. Toutes les animations devraient fonctionner correctement.

Ici vous pouvez télécharger un exemple de projet en utilisant l'approche de sous-classement de la mise en page. (# 2)

15
boweidmann

Ne serait-ce pas une solution simple d'avoir 2 vues de collection avec standart UICollectionViewFlowLayout?

Ou encore mieux: avoir un contrôleur de vue de page avec défilement horizontal, et chaque page serait une vue de collection avec une disposition de flux normale.

L'idée est la suivante: dans votre UICollectionViewController -init method vous créez une deuxième vue de collection avec le cadre décalé vers la droite de la largeur de la vue de collection d'origine. Ensuite, vous l'ajoutez en tant que sous-vue à la vue de collection d'origine. Pour basculer entre les vues de collection, ajoutez simplement un identificateur de balayage. Pour calculer les valeurs de décalage, vous pouvez stocker le cadre d'origine de la vue de collection dans ivar cVFrame. Pour identifier vos vues de collection, vous pouvez utiliser des balises.

Exemple de méthode 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];

Exemple de méthodes swipeRight et 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];
}

Et puis ce n'est pas un gros problème pour implémenter les méthodes DataSource (dans votre cas, vous voulez avoir 9 éléments sur chaque page):

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

Dans la méthode -collectionView:cellForRowAtIndexPath vous devrez obtenir des données de votre modèle avec décalage, s'il s'agit de la deuxième vue de collection.

N'oubliez pas non plus d'enregistrer la classe pour une cellule réutilisable pour votre deuxième vue de collection. Vous pouvez également créer un seul identificateur de gestes et reconnaître les balayages vers la gauche et vers la droite. C'est à vous.

Je pense que maintenant ça devrait marcher, essayez-le :-)

4
boweidmann

Vous avez un objet qui implémente le protocole UICollectionViewDataSource. À l'intérieur collectionView:cellForItemAtIndexPath: renvoyez simplement le bon article que vous souhaitez retourner. Je ne comprends pas où il y aurait un problème.

Edit: ok, je vois le problème. Voici la solution: http://www.skeuo.com/uicollectionview-custom-layout-tutorial , en particulier les étapes 17 à 25. Ce n'est pas une énorme quantité de travail et peut être réutilisé très facilement .

2
Rikkles

C'est ma solution, je n'ai pas testé avec des animations mais je pense que ça va bien marcher avec quelques petits changements, j'espère que ça vous sera utile

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];
    ...
}
0
Henry Doan