71 votes

La définition dynamique de la disposition sur UICollectionView entraîne un changement inexplicable du contentOffset

Selon la documentation d'Apple (et vantée lors de la WWDC 2012), il est possible de définir la mise en page sur UICollectionView de manière dynamique et même d'animer les changements :

Vous spécifiez normalement un objet de présentation lors de la création d'une vue de collection, mais vous pouvez également modifier la présentation d'une vue de collection de manière dynamique. L'objet de présentation est stocké dans la propriété collectionViewLayout. La définition de cette propriété permet de mettre à jour la présentation immédiatement, sans animer les modifications. Si vous souhaitez animer les modifications, vous devez appeler la méthode setCollectionViewLayout:animated :.

Toutefois, dans la pratique, j'ai constaté que UICollectionView apporte des modifications inexplicables, voire non valables, à la contentOffset Les cellules se déplacent alors de manière incorrecte, ce qui rend la fonction pratiquement inutilisable. Pour illustrer le problème, j'ai créé l'exemple de code suivant qui peut être attaché à un contrôleur de vue de collection par défaut placé dans un storyboard :

#import <UIKit/UIKit.h>

@interface MyCollectionViewController : UICollectionViewController
@end

@implementation MyCollectionViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CELL"];
    self.collectionView.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 1;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath];
    cell.backgroundColor = [UIColor whiteColor];
    return cell;
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
    [self.collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES];
    NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
}

@end

Le contrôleur définit une valeur par défaut de UICollectionViewFlowLayout en viewDidLoad et affiche une seule cellule à l'écran. Lorsque la cellule est sélectionnée, le contrôleur crée une autre cellule par défaut. UICollectionViewFlowLayout et la place dans la vue de la collection avec l'attribut animated:YES drapeau. Le comportement attendu est que la cellule ne bouge pas. En réalité, la cellule défile hors de l'écran, et il n'est alors même plus possible de la faire revenir à l'écran.

En regardant le journal de la console, on s'aperçoit que le contentOffset a inexplicablement changé (dans mon projet, de (0, 0) à (0, 205)). J'ai publié une solution pour le solution pour le cas non animé ( i.e. animated:NO ), mais comme j'ai besoin d'animation, je suis très intéressé de savoir si quelqu'un a une solution ou une solution de contournement pour le cas de l'animation.

Par ailleurs, j'ai testé des mises en page personnalisées et j'obtiens le même résultat.

1 votes

ENFIN RÉSOLUE. @Isaacliu a donné la bonne réponse. J'ai mis la version moderne de Swift.

0 votes

setCollectionViewLayout(_:animated:) a très bien fonctionné pour moi. Il suffit de faire quelque chose comme collectionView.setCollectionViewLayout(expanded ? self.verticalFlowLayout : self.horizontalFlowLayout, animated: true)

38voto

cdemiris99 Points 87

UICollectionViewLayout contient la méthode surchargeable targetContentOffsetForProposedContentOffset: qui vous permet de fournir le bon décalage de contenu lors d'un changement de mise en page, et qui s'animera correctement. Cette fonction est disponible à partir de la version 7.0 d'iOS.

0 votes

J'aimerais pouvoir voter plusieurs fois pour ce texte. Pour un certain sous-ensemble de cas impliquant cette question (par exemple, pour moi, où je veux maintenir la version actuelle de la contentOffset ), c'est la solution parfaite. Merci @cdemiris99.

1 votes

Cela résout mon problème immédiat, mais je me demande pourquoi. J'ai l'impression que je ne devrais pas avoir à le faire. Je vais chercher à mieux comprendre ce qui se passe ici et je vais écrire un bogue.

0 votes

Vous êtes un génie, cela résout parfaitement le problème !

33voto

tassinari Points 1481

Je m'arrache les cheveux depuis des jours et j'ai trouvé une solution qui pourrait m'aider. Dans mon cas, j'ai une mise en page de photos qui s'effondre comme dans l'application photos sur l'ipad. Elle affiche des albums avec les photos les unes au-dessus des autres et lorsque vous appuyez sur un album, les photos s'agrandissent. J'ai donc deux UICollectionViewLayouts distincts et je passe de l'un à l'autre avec la commande [self.collectionView setCollectionViewLayout:myLayout animated:YES] J'avais exactement le même problème avec les cellules qui sautaient avant l'animation et j'ai réalisé que c'était à cause de la fonction contentOffset . J'ai tout essayé avec le contentOffset La solution de Tyler ci-dessus a fonctionné, mais l'animation était toujours perturbée.

J'ai ensuite remarqué que cela ne se produisait que lorsqu'il y avait quelques albums à l'écran, pas assez pour remplir l'écran. Ma mise en page remplace -(CGSize)collectionViewContentSize comme recommandé. Lorsqu'il n'y a que quelques albums, la taille du contenu de la vue de la collection est inférieure à celle du contenu de la vue. C'est ce qui provoque le saut lorsque je passe d'une présentation de collection à l'autre.

J'ai donc défini une propriété sur mes présentations, appelée minHeight, et je l'ai fixée à la hauteur du parent des vues de la collection. Ensuite, je vérifie la hauteur avant de retourner dans -(CGSize)collectionViewContentSize Je m'assure que la hauteur est >= à la hauteur minimale.

Ce n'est pas une vraie solution, mais cela fonctionne bien maintenant. J'essaierais de régler le paramètre contentSize de votre vue de collection doit être au moins égale à la longueur de la vue qu'elle contient.

éditer : Manicaesar a ajouté une solution de contournement simple si vous héritez de UICollectionViewFlowLayout :

-(CGSize)collectionViewContentSize { //Workaround
    CGSize superSize = [super collectionViewContentSize];
    CGRect frame = self.collectionView.frame;
    return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame)));
}

0 votes

J'ai joué avec cela et je suis arrivé à la conclusion que votre solution n'est fiable que lorsque le contentSize correspond exactement à la taille du collectionView. Je pense donc que cela fonctionne pour les vues de collection qui ne défilent pas.

0 votes

C'est presque correct, la seule chose dont vous avez besoin est de vous assurer que la méthode collectionViewContentSize renvoie une taille qui est au moins aussi grande que la vue de la collection. Ma présentation est à défilement vertical, j'ai donc simplement veillé à renvoyer une taille correspondant à la bonne largeur, suivie du maximum de la taille calculée par ma présentation et de la hauteur de la vue de la collection.

5 votes

Si vous héritez de UICollectionViewFlowLayout, c'est aussi simple que cela : - (CGSize)collectionViewContentSize { //Détournement CGSize superSize = [super collectionViewContentSize] ; CGRect frame = self.collectionView.frame ; return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame)) ; }.

7voto

tyler Points 903

Ce problème m'a également frappé et il semble qu'il s'agisse d'un bogue dans le code de transition. D'après ce que je peux voir, il essaie de se concentrer sur la cellule qui était la plus proche du centre de la disposition de la vue avant la transition. Cependant, s'il n'y a pas de cellule au centre de la vue avant la transition, il essaie quand même de centrer la cellule à l'endroit où elle se trouverait après la transition. Ceci est très clair si vous définissez alwaysBounceVertical/Horizontal sur YES, que vous chargez la vue avec une seule cellule et que vous effectuez ensuite une transition de mise en page.

J'ai pu contourner ce problème en demandant explicitement à la collection de se concentrer sur une cellule spécifique (la première cellule visible, dans cet exemple) après avoir déclenché la mise à jour de la mise en page.

[self.collectionView setCollectionViewLayout:[self generateNextLayout] animated:YES];

// scroll to the first visible cell
if ( 0 < self.collectionView.indexPathsForVisibleItems.count ) {
    NSIndexPath *firstVisibleIdx = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0];
    [self.collectionView scrollToItemAtIndexPath:firstVisibleIdx atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}

0 votes

J'ai essayé cette solution de contournement dans mon exemple de code et c'était très proche. La cellule ne rebondit que légèrement. Cependant, cela n'a pas fonctionné aussi bien dans d'autres scénarios. Par exemple, changer numberOfItemsInSection: pour obtenir 100. Si vous touchez la première cellule, l'affichage défile vers le bas sur plusieurs lignes. Si vous tapez ensuite sur la première cellule visible, l'affichage revient en haut. Si vous touchez à nouveau la première cellule, elle reste immobile (comportement correct). Si vous touchez une cellule de la rangée inférieure, elle rebondit. Faites défiler la vue de plusieurs rangées vers le bas, touchez n'importe quelle rangée et la vue défile à nouveau vers le haut.

1 votes

J'ai trouvé que l'utilisation de animated:NO dans l'appel scrollToItemAtIndexPath : améliorait l'aspect des choses.

2 votes

Il semble qu'ils aient ajouté des API pour contrôler ce comportement dans iOS7.

7voto

Timothy Moose Points 5734

J'apporte une réponse tardive à ma propre question.

En TLLayoutTransitioning apporte une excellente solution à ce problème en réaffectant les API de transition interactives d'iOS7 à des transitions non interactives, d'une disposition à l'autre. Elle fournit effectivement une alternative à setCollectionViewLayout Il s'agit de résoudre le problème du décalage du contenu et d'ajouter plusieurs fonctionnalités :

  1. Durée de l'animation
  2. Plus de 30 courbes d'assouplissement (avec l'aimable autorisation de Warren Moore) Bibliothèque de l'AHEasing )
  3. Plusieurs modes de décalage du contenu

Les courbes d'assouplissement personnalisées peuvent être définies comme suit AHEasingFunction fonctions. Le décalage final du contenu peut être spécifié en termes d'un ou plusieurs chemins d'indexation avec des options de placement minimal, central, supérieur, gauche, inférieur ou droit.

Pour voir ce que je veux dire, essayez d'exécuter la commande Redimensionner la démo dans l'espace de travail Exemples et de jouer avec les options.

L'utilisation est la suivante. Tout d'abord, configurez votre contrôleur de vue pour qu'il renvoie une instance de TLTransitionLayout :

- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout
{
    return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout];
}

Ensuite, au lieu d'appeler setCollectionViewLayout , appeler transitionToCollectionViewLayout:toLayout défini dans le UICollectionView-TLLayoutTransitioning catégorie :

UICollectionViewLayout *toLayout = ...; // the layout to transition to
CGFloat duration = 2.0;
AHEasingFunction easing = QuarticEaseInOut;
TLTransitionLayout *layout = (TLTransitionLayout *)[collectionView transitionToCollectionViewLayout:toLayout duration:duration easing:easing completion:nil];

Cet appel déclenche une transition interactive et, en interne, un appel à l'aide. CADisplayLink qui pilote la progression de la transition avec la durée et la fonction d'assouplissement spécifiées.

L'étape suivante consiste à spécifier un décalage final du contenu. Vous pouvez spécifier n'importe quelle valeur arbitraire, mais l'option toContentOffsetForLayout définie dans la méthode UICollectionView-TLLayoutTransitioning offre un moyen élégant de calculer les décalages de contenu par rapport à un ou plusieurs chemins d'index. Par exemple, pour qu'une cellule spécifique se retrouve aussi près que possible du centre de la vue de la collection, faites l'appel suivant immédiatement après transitionToCollectionViewLayout :

NSIndexPath *indexPath = ...; // the index path of the cell to center
TLTransitionLayoutIndexPathPlacement placement = TLTransitionLayoutIndexPathPlacementCenter;
CGPoint toOffset = [collectionView toContentOffsetForLayout:layout indexPaths:@[indexPath] placement:placement];
layout.toContentOffset = toOffset;

0 votes

J'avais de gros problèmes d'animation avec les vues supplémentaires et j'ai trouvé ceci. Merci beaucoup !

2voto

Tommy Points 56749

Si cela peut contribuer à enrichir le corpus d'expériences : J'ai rencontré ce problème de manière persistante, quelle que soit la taille de mon contenu, que j'aie ou non défini un encart de contenu, ou tout autre facteur évident. Ma solution a donc été quelque peu radicale. Tout d'abord, j'ai sous-classé UICollectionView et j'ai ajouté un paramètre de décalage du contenu inapproprié :

- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
    if(_declineContentOffset) return;
    [super setContentOffset:contentOffset];
}

- (void)setContentOffset:(CGPoint)contentOffset
{
    if(_declineContentOffset) return;
    [super setContentOffset:contentOffset];
}

- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
    _declineContentOffset ++;
    [super setCollectionViewLayout:layout animated:animated];
    _declineContentOffset --;
}

- (void)setContentSize:(CGSize)contentSize
{
    _declineContentOffset ++;
    [super setContentSize:contentSize];
    _declineContentOffset --;
}

Je n'en suis pas fier, mais la seule solution possible semble être de rejeter complètement toute tentative de la vue de la collection de définir son propre décalage de contenu résultant d'un appel à la fonction setCollectionViewLayout:animated: . Empiriquement, il semble que ce changement se produise directement dans l'appel immédiat, ce qui n'est évidemment pas garanti par l'interface ou la documentation, mais qui est logique du point de vue de l'animation de base, de sorte que je ne suis peut-être que 50% mal à l'aise avec l'hypothèse.

Cependant, il y avait un deuxième problème : UICollectionView ajoutait un petit saut aux vues qui restaient au même endroit lors d'une nouvelle disposition de la vue de la collection - les poussant vers le bas d'environ 240 points et les animant ensuite pour les ramener à leur position d'origine. Je ne sais pas exactement pourquoi, mais j'ai modifié mon code pour y remédier en supprimant l'élément CAAnimation qui ont été ajoutées à toutes les cellules qui, en fait, ne bougeaient pas :

- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
    // collect up the positions of all existing subviews
    NSMutableDictionary *positionsByViews = [NSMutableDictionary dictionary];
    for(UIView *view in [self subviews])
    {
        positionsByViews[[NSValue valueWithNonretainedObject:view]] = [NSValue valueWithCGPoint:[[view layer] position]];
    }

    // apply the new layout, declining to allow the content offset to change
    _declineContentOffset ++;
    [super setCollectionViewLayout:layout animated:animated];
    _declineContentOffset --;

    // run through the subviews again...
    for(UIView *view in [self subviews])
    {
        // if UIKit has inexplicably applied animations to these views to move them back to where
        // they were in the first place, remove those animations
        CABasicAnimation *positionAnimation = (CABasicAnimation *)[[view layer] animationForKey:@"position"];
        NSValue *sourceValue = positionsByViews[[NSValue valueWithNonretainedObject:view]];
        if([positionAnimation isKindOfClass:[CABasicAnimation class]] && sourceValue)
        {
            NSValue *targetValue = [NSValue valueWithCGPoint:[[view layer] position]];

            if([targetValue isEqualToValue:sourceValue])
                [[view layer] removeAnimationForKey:@"position"];
        }
    }
}

Cela ne semble pas inhiber les vues qui se déplacent réellement, ni les faire se déplacer de manière incorrecte (comme si elles s'attendaient à ce que tout ce qui les entoure soit en baisse d'environ 240 points et à ce qu'elles s'animent en même temps qu'elles pour se positionner correctement).

Voici donc ma solution actuelle.

0 votes

Tommy, merci pour ce partage. Je viens d'ajouter une solution à ma propre question qui pourrait vous être utile.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X