Adicionando zoom de pitada a um UICollectionView

Introdução

Vou descrever o efeito que quero alcançar e, em seguida, darei detalhes sobre como estou tentando implementar isso e o que há de errado com o seu comportamento atual. Mencionarei também outra abordagem que observei, mas que não conseguiu dar certo.

O código mais relevante está embutido na parte inferior da pergunta para acesso rápido. Você podeFaça o download de um zip da fonte ou obtenha o projeto como umRepositório Mercurial no BitBucket. O projeto agora incorpora as correções da resposta abaixo. Se você deseja que a versão quebrada seja fornecida inicialmente, ela é marcada com"versão inicial com buggy"

O projeto é uma prova de conceito / pico mínimo para avaliar se o efeito é viável, por isso é bastante leve e simples!

Efeito desejado

O aplicativo exibirá um grande número de linhas discretas de informações que formam uma tabela vertical. A tabela será rolada verticalmente pelo usuário. Esse é um comportamento padrão com umUITableViewe você pode usar umUICollectionView também. No entanto, o aplicativo também deve suportar o dimensionamento de pitada. Quando você aperta o zoom na mesa,tudo das linhas deve esmagar juntos. Enquanto você estica,tudo das linhas deve se separar.

Na minha prova de conceito, as células individuais não são redimensionadas, elas são apenas reposicionadas mais próximas umas das outras ou mais afastadas. Isso é intencional: não acredito que seja crítico validar a viabilidade da ideia.

Aqui estão as capturas de tela que mostram como o aplicativo atual parece com menos e mais zoom:

Implementação atual

Estou usando umUICollectionView com um costumeUICollectionViewLayout subclasse. O layout posiciona oUICollectionViewCells em uma bela onda senoidal oscilante no meio da tela. CadaUICollectionViewCell é apenas um contêiner para umUILabel segurando oindexPath linha.

oUICollectionViewLayout subclasse tem um parâmetro para definir o espaçamento vertical entre cada célula que descreve para oUICollectionView e ajustar isso permite que a mesa seja comprimida ou esticada verticalmente, conforme desejado.

MinhasUICollectionViewController subclasse tem umUIPinchGestureRecognizer. Quando o reconhecedor detecta alterações de escala, o espaçamento vertical da célula noUICollectionViewO layout do é alterado de acordo.

Sem uma análise mais aprofundada, a escala ocorreria na parte superior do conteúdo, e não no centro do gesto de toque. oUICollectionViewécontentOffset A propriedade é ajustada durante a compressão para fornecer esse recurso.

O reconhecedor de gestos também precisa acomodar arrastões que ocorrem durante o beliscão. Isso também é tratado por alterações,UICollectionViewécontentOffset. Algum código adicional permite que o ponto central do gesto de toque seja alterado conforme os dedos são adicionados / removidos do gesto.

Observe queUICollectionView, Começar umUIScrollView subclasse, tem sua própriaUIPanGestureRecognizer que interage com oUIPinchGestureRecogniser adicionado por mim. Não tenho certeza se isso está causando um problema ou não.

Eu adicionei código para desativar oUICollectionViewé construído em rolagem durante o meu gesto de pinça, mas isso não parece fazer muita diferença. Eu tentei usargestureRecognizer:shouldRequireFailureOfGestureRecognizer: fazer o meuUIPinchGestureRecognizer falhar o construído emUIPanGestureRecognizer, mas isso pareceu impedir que meu reconhecedor de pitada funcionasse. Não sei se estou sendo idiota ou se há um bug no iOS.

Como mencionado anteriormente, atualize oUICollectionViewCells não são redimensionados. Eles são apenas reposicionados. Isso é intencional. Não acho importante validar esse conceito.

O que funciona

Os bits de trabalho funcionam muito bem. Você pode arrastar a tabela para cima e para baixo. Durante um arrasto, você pode adicionar um dedo e iniciar uma pitada, depois solte um dedo e continue o arrasto, depois adicione e aperte, etc. Tudo é bem suave. Em um iPhone 5 original, ele suporta suavemente pitada e movimento com> 200 visualizações na tela.

O que não funciona 1

Se você tentar entrar e sair quando a parte superior ou inferior da exibição estiver na tela, tudo ficará um pouco louco.

Nos pergaminhos, a visualização pode arrastar para que seja puxada além do conteúdo visível (o que eu quero, pois é um comportamento padrão para uma lista de dados no iOS).Em mudanças de escala, no entanto, a visualização é retrocedida para que o conteúdo fique preso na tela (não quero que isso aconteça).

Esses dois brigam entre si durante o gesto de beliscar, o que faz o conteúdo vibrar violentamente para cima e para baixo (o que eu definitivamente não quero!).

O que não funciona 2

oUICollectionViewA rolagem padrão da câmera tem desaceleração se você soltar a rolagem e também devolve o conteúdo suavemente quando você rola para fora dele. No momento, elas não são tratadas.

Se você soltar o gesto de pinçar enquanto estiver rolando, ele simplesmente para.Se você rolar além do conteúdo com o gesto de pinçar e soltar, ele permanecerá onde está e não se recuperará. Quando você inicia um pergaminho novamente, o conteúdo é retornado.Coisas que tentei, mas não consegui trabalhar

UICollectionView, Começar umUIScrollView deve ter um construído emUIPinchGestureRecogniser se estiver configurado corretamente para suportar o zoom. Gostaria de saber se eu poderia aproveitar isso em vez de ter o meu próprioUIPinchGestureRecogniser. Tentei configurar isso configurando as escalas mín e máx e adicionando o manipulador de pitada do meu controlador. No entanto, eu realmente não entendo o que eu deveria retornar da minha implementação doviewForZoomingInScrollView:, então estou apenas criando uma visualização fictícia com[[UIView alloc] initWithFrame: [[self collectionView] bounds]]. Faz com que a exibição de rolagem "desmorone" em uma única linha, que não é o que eu estou procurando!

Finalmente (antes do código)

Essa é uma pergunta longa, então obrigado por lê-la. Agradeço ainda mais se puder ajudar com uma resposta. Sinto muito se muito do que eu disse ou adicionei é irrelevante!

Código para o controlador de exibição
//  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

questionAnswers(1)

yourAnswerToTheQuestion