web-dev-qa-db-fra.com

Ajout d'un zoom pincé à une UICollectionView

Intro

Je vais décrire l'effet que je veux atteindre, puis je donnerai des détails sur la façon dont j'essaie actuellement de mettre en œuvre cela et ce qui ne va pas avec son comportement actuel. Je mentionnerai également une autre approche que j'ai examinée, mais qui n'a pas pu fonctionner du tout.

Le code le plus pertinent est en ligne au bas de la question pour un accès rapide. Vous pouvez Télécharger un Zip de la source ou obtenir le projet en tant que - Mercurial Repository sur BitBucket. Le projet intègre désormais les correctifs de la réponse ci-dessous. Si vous voulez que la version cassée soit fournie initialement, elle est étiquetée avec "initial-buggy-version"

Le projet est une preuve de concept/pointe minimale pour évaluer si l'effet est viable, il est donc assez léger et simple!

Effet souhaité

L'application affichera un grand nombre de lignes d'informations discrètes qui forment un tableau vertical. Le tableau pourra défiler verticalement par l'utilisateur. Il s'agit d'un comportement standard avec un UITableView, et vous pouvez également utiliser un UICollectionView. Cependant, l'application doit également prendre en charge la mise à l'échelle par pincement. Lorsque vous pincez le zoom sur la table, toutes les lignes doivent se resserrer. Lorsque vous vous étirez, toutes les lignes doivent se séparer.

Dans ma preuve de concept, les cellules individuelles ne sont pas redimensionnées, elles sont simplement repositionnées plus près ou plus éloignées. C'est intentionnel: je ne pense pas que ce soit essentiel pour valider la faisabilité de l'idée.

Voici des captures d'écran montrant à quoi ressemble l'application actuelle avec un zoom arrière et un zoom avant:

Zoomed in imageZoomed out image

Mise en œuvre actuelle

J'utilise un UICollectionView avec une sous-classe UICollectionViewLayout personnalisée. La disposition positionne le UICollectionViewCells dans une belle onde sinusoïdale ondulante au milieu de l'écran. Chaque UICollectionViewCell n'est qu'un conteneur pour un UILabel contenant la ligne indexPath.

La sous-classe UICollectionViewLayout a un paramètre pour définir l'espacement vertical entre chaque cellule qu'elle décrit sur UICollectionView et l'ajustement permet au tableau d'être écrasé ou étiré verticalement comme vous le souhaitez.

Ma sous-classe UICollectionViewController a un UIPinchGestureRecognizer. Lorsque le module de reconnaissance détecte des changements d'échelle, l'espacement vertical des cellules dans la disposition de UICollectionView est modifié en conséquence.

Sans autre considération, la mise à l'échelle se produirait du haut du contenu, plutôt que du centre du geste tactile. La propriété UICollectionView de contentOffset est ajustée pendant le pincement pour fournir cette fonctionnalité.

Le reconnaisseur de gestes doit également prendre en compte les traînées qui se produisent lors du pincement. Ceci est également géré en changeant le UICollectionView's contentOffset. Un code supplémentaire permet au point central du geste tactile de changer lorsque les doigts sont ajoutés/supprimés du geste.

Notez que UICollectionView, étant une sous-classe UIScrollView, a son propre UIPanGestureRecognizer qui interagit avec le UIPinchGestureRecogniser ajouté par moi. Je ne sais pas si cela cause un problème ou non.

J'ai ajouté du code pour désactiver le défilement intégré de UICollectionView pendant mon geste de pincement, mais cela ne semble pas faire beaucoup de différence. J'ai essayé d'utiliser gestureRecognizer:shouldRequireFailureOfGestureRecognizer: pour que mon UIPinchGestureRecognizer échoue au UIPanGestureRecognizer intégré, mais cela a plutôt semblé empêcher ma reconnaissance de pincement de fonctionner. Je ne sais pas si c'est moi qui suis stupide ou un bug dans iOS.

Comme mentionné précédemment, les UICollectionViewCells actuels ne sont pas redimensionnés. Ils sont juste repositionnés. C'est intentionnel. Je ne pense pas que ce soit important pour valider ce concept.

Ce qui fonctionne

Les bits de travail fonctionnent assez bien. Vous pouvez faire glisser le tableau de haut en bas. Pendant un glissement, vous pouvez ajouter un doigt et commencer un pincement, puis relâcher un doigt et continuer le glissement, puis ajouter et pincer, etc. Tout est assez lisse. Sur un iPhone 5 d'origine, il prend en charge les pincements et les panoramiques avec plus de 200 vues à l'écran.

Ce qui ne fonctionne pas 1

Si vous essayez de vous pincer lorsque le haut ou le bas de la vue est à l'écran, tout devient un peu fou.

  • Sur les parchemins, la vue est autorisée à glisser pour être tirée au-delà du contenu visible (ce que je veux, car c'est le comportement standard pour une liste de données sur iOS).
  • Cependant, lors des changements d'échelle, la vue est rétablie afin que le contenu soit fixé à l'écran (je ne veux pas que cela se produise).

Ces deux se battent l'un contre l'autre pendant le geste de pincement, ce qui fait que le contenu scintille violemment de haut en bas (ce que je ne veux certainement pas!).

Ce qui ne fonctionne pas 2

Le défilement par défaut de UICollectionView a une décélération si vous lâchez pendant le défilement, et rebondit également en douceur lorsque vous faites défiler en dehors de celui-ci. Celles-ci ne sont pas du tout gérées actuellement.

  • Si vous relâchez le geste de pincement pendant le défilement, il s'arrête simplement.
  • Si vous faites défiler le contenu avec le geste de pincement, puis relâchez, il reste où il est et ne rebondit pas. Lorsque vous recommencez un défilement, le contenu saute en arrière.

Des choses que j'ai essayées mais que je n'ai pas pu mettre au travail

UICollectionView, étant un UIScrollView devrait avoir un UIPinchGestureRecogniser intégré s'il est correctement configuré pour prendre en charge le zoom. Je me demandais si je pouvais exploiter cela au lieu d'avoir mon propre UIPinchGestureRecogniser. J'ai essayé de configurer cela en définissant des échelles min et max et en ajoutant le gestionnaire de pincement de mon contrôleur. Cependant, je ne comprends pas vraiment ce que je devrais retourner de mon implémentation de viewForZoomingInScrollView:, donc je crée juste une vue fictive avec [[UIView alloc] initWithFrame: [[self collectionView] bounds]]. Cela rend la vue de défilement "réduite" à une seule ligne, ce qui n'est pas ce que je recherche!

Enfin (avant le code)

C'est une longue question, alors merci de la lire. Merci encore plus si vous pouvez aider avec une réponse. Je suis désolé si beaucoup de ce que j'ai dit ou ajouté n'est pas pertinent!

Code pour le contrôleur de vue

//  STViewController.m
#import "STViewController.h"
#import "STDataColumnsCollectionViewLayout.h"
#import "STCollectionViewLabelCell.h"

@interface STViewController () <UIGestureRecognizerDelegate>
@property (nonatomic, assign) CGFloat pinchStartVerticalPeriod;
@property (nonatomic, assign) CGFloat pinchNormalisedVerticalPosition;
@property (nonatomic, assign) NSInteger pinchTouchCount;
-(void) handlePinch: (UIPinchGestureRecognizer *) pinchRecogniser;
@end

@implementation STViewController

-(void) viewDidLoad
{
  [[self collectionView] registerClass: [STCollectionViewLabelCell class] forCellWithReuseIdentifier: [STCollectionViewLabelCell className]];

  UICollectionView *const collectionView = [self collectionView];
  [collectionView setAllowsSelection: NO];

  [_pinchRecogniser addTarget: self action: @selector(handlePinch:)];
  [_pinchRecogniser setDelegate: self];
  [_pinchRecogniser setCancelsTouchesInView:YES];
  [[self view] addGestureRecognizer: _pinchRecogniser];
}

#pragma mark -

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

-(UICollectionViewCell*) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
  STCollectionViewLabelCell *const cell = [[self collectionView] dequeueReusableCellWithReuseIdentifier: [STCollectionViewLabelCell className] forIndexPath: indexPath];
  [[cell label] setText: [NSString stringWithFormat: @"%d", [indexPath row]]];
  return cell;
}

#pragma mark -

-(BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
  return YES;
}

#pragma mark -

-(void) handlePinch: (UIPinchGestureRecognizer *) pinchRecogniser
{
  UICollectionView *const collectionView = [self collectionView];
  STDataColumnsCollectionViewLayout *const layout = (STDataColumnsCollectionViewLayout *)[self collectionViewLayout];

  if(([pinchRecogniser state] == UIGestureRecognizerStateBegan) || ([pinchRecogniser numberOfTouches] != _pinchTouchCount))
  {
    const CGFloat normalisedY = [pinchRecogniser locationInView: collectionView].y / [layout collectionViewContentSize].height;
    _pinchNormalisedVerticalPosition = normalisedY;
    _pinchTouchCount = [pinchRecogniser numberOfTouches];
  }

  switch ([pinchRecogniser state])
  {
    case UIGestureRecognizerStateBegan:
    {
      NSLog(@"Began");
      _pinchStartVerticalPeriod = [layout verticalPeriod];
      [collectionView setScrollEnabled: NO];
      break;
    }

    case UIGestureRecognizerStateChanged:
    {
      NSLog(@"Changed");
      STDataColumnsCollectionViewLayout *const layout = (STDataColumnsCollectionViewLayout *)[self collectionViewLayout];
      const CGFloat newVerticalPeriod = _pinchStartVerticalPeriod * [pinchRecogniser scale];
      [layout setVerticalPeriod: newVerticalPeriod];
      [[self collectionViewLayout] invalidateLayout];

      const CGPoint dragCenter = [pinchRecogniser locationInView: [collectionView superview]];
      const CGFloat currentY = _pinchNormalisedVerticalPosition * [layout collectionViewContentSize].height;
      [collectionView setContentOffset: CGPointMake(0, currentY - dragCenter.y) animated: NO];
    }

    case UIGestureRecognizerStateEnded:
    case UIGestureRecognizerStateCancelled:
    {
      [collectionView setScrollEnabled: YES];
    }

    default:
      break;
  }
}

@end
31
Benjohn

Le bon côté - comment le faire fonctionner

Quelques ajustements très mineurs au code ci-dessus ont résolu Ce qui ne fonctionne pas 1 & Ce qui ne fonctionne pas 2 dans la question.

J'ai ajouté les lignes suivantes à la méthode viewDidLoad de mon UICollectionViewController:

[collectionView setMinimumZoomScale: 0.25];
[collectionView setMaximumZoomScale: 4];

J'ai également mis à jour l'exemple de projet afin qu'au lieu d'étiquettes de texte, la vue soit faite de petits cercles. Lorsque vous effectuez un zoom avant ou arrière, ceux-ci sont redimensionnés. Voici à quoi cela ressemble maintenant (zoom arrière et zoom avant):

Image zoomed outImage zoomed in

Lors d'un zoom, les vues des cercles ne sont pas redessinées, mais simplement interpolées à partir de leur taille de pré-zoom. Le redessin est reporté jusqu'à la fin du zoom. Voici une capture de la façon dont cela se produit après un zoom avant de plusieurs fois:

During zoom

Ce serait formidable que le redessin pendant le zoom se produise dans un thread d'arrière-plan afin que les artefacts soient moins visibles, mais cela sort bien du cadre de cette question et je n'ai pas encore travaillé dessus.

Vous pouvez trouver l'ensemble du projet, avec des correctifs, sur Bit Bucket afin que vous puissiez récupérer les fichiers là-bas.

The Bad Part - Je ne sais pas pourquoi ça marche

J'espérais qu'avec cette question répondue, j'aurais beaucoup de nouvelles certitudes sur le zoom UIScrollView. Je ne.

D'après ce que j'ai lu sur UIScrollView, ce "correctif" n'aurait pas dû faire de différence et il aurait déjà dû fonctionner en premier lieu.

Un UIScrollView n'est pas censé activer le défilement jusqu'à ce que vous lui donniez un délégué qui implémente viewForZoomingInScrollView:, ce que je n'ai pas fait.

16
Benjohn