UIScrollView: paginação horizontalmente, rolagem vertical?

88

Como posso forçar um UIScrollViewem que a paginação e a rolagem estão ativadas para se mover apenas vertical ou horizontalmente em um determinado momento?

Meu entendimento é que a directionalLockEnabledpropriedade deve atingir isso, mas um deslizar diagonal ainda faz a visualização rolar diagonalmente em vez de restringir o movimento a um único eixo.

Editar: para ser mais claro, gostaria de permitir ao usuário rolar horizontalmente OU verticalmente, mas não ambos simultaneamente.

Cœur
fonte
Você deseja restringir a rolagem de uma visualização APENAS Horiz / Vert ou deseja que o usuário role o Horiz OU Vert, mas não os dois simultaneamente?
Will Hartung,
Você pode me fazer um favor e mudar o título para que pareça um pouco mais interessante e não trivial? Algo como “UIScrollView: paging horizontalmente, scrolling verticalmente?”
Andrey Tarantsov
Infelizmente, postei a pergunta como um usuário não registrado em um computador ao qual não tenho mais acesso (antes de me registrar com o mesmo nome quando cheguei em casa), o que significa que não posso editá-la. Isso também deve me explicar o acompanhamento de baixo para cima em vez de editar a pergunta original. Desculpe por quebrar a etiqueta do Stack desta vez!
Nick

Respostas:

117

Você está em uma situação muito difícil, devo dizer.

Observe que você precisa usar um UIScrollView com pagingEnabled=YESpara alternar entre as páginas, mas precisa pagingEnabled=NOrolar verticalmente.

Existem 2 estratégias possíveis. Não sei qual vai funcionar / é mais fácil de implementar, então tente os dois.

Primeiro: UIScrollViews aninhados. Francamente, ainda estou para ver uma pessoa que faça isso funcionar. No entanto, eu não tentei o suficiente pessoalmente, e minha prática mostra que quando você se esforça o suficiente, você pode fazer o UIScrollView fazer o que quiser .

Portanto, a estratégia é permitir que a exibição de rolagem externa trate apenas a rolagem horizontal e as exibições de rolagem interna apenas a rolagem vertical. Para fazer isso, você deve saber como o UIScrollView funciona internamente. Ele substitui o hitTestmétodo e sempre retorna a si mesmo, de forma que todos os eventos de toque vão para UIScrollView. Em seguida touchesBegan, dentro , touchesMovedetc , ele verifica se ele está interessado no evento e trata ou passa para os componentes internos.

Para decidir se o toque deve ser tratado ou encaminhado, o UIScrollView inicia um cronômetro quando você o toca pela primeira vez:

  • Se você não moveu seu dedo significativamente em 150 ms, ele passa o evento para a visualização interna.

  • Se você moveu seu dedo significativamente em 150 ms, ele começa a rolar (e nunca passa o evento para a visualização interna).

    Observe como quando você toca em uma tabela (que é uma subclasse da visualização de rolagem) e começa a rolar imediatamente, a linha que você tocou nunca é destacada.

  • Se você não moveu seu dedo significativamente em 150 ms e o UIScrollView começou a passar os eventos para a visualização interna, mas então você moveu o dedo o suficiente para a rolagem começar, o UIScrollView chama touchesCancelledna visualização interna e começa a rolagem.

    Observe como, quando você toca em uma mesa, mantém o dedo um pouco pressionado e começa a rolar, a linha que você tocou é destacada primeiro, mas desmarcada depois.

Essa sequência de eventos pode ser alterada pela configuração do UIScrollView:

  • Se delaysContentTouchesfor NÃO, então nenhum cronômetro é usado - os eventos vão imediatamente para o controle interno (mas são cancelados se você mover o dedo longe o suficiente)
  • Se cancelsTouchesfor NÃO, uma vez que os eventos forem enviados para um controle, a rolagem nunca acontecerá.

Note que é UIScrollView que recebe tudo touchesBegin , touchesMoved, touchesEndede touchesCanceledeventos de CocoaTouch (porque a suahitTest diz-lhe para fazê-lo). Em seguida, ele os encaminha para a visão interna se quiser, pelo tempo que quiser.

Agora que você sabe tudo sobre o UIScrollView, pode alterar seu comportamento. Posso apostar que você deseja dar preferência à rolagem vertical, de modo que, assim que o usuário tocar na visualização e começar a mover o dedo (mesmo que ligeiramente), a visualização comece a se deslocar na direção vertical; mas quando o usuário move o dedo na direção horizontal longe o suficiente, você deseja cancelar a rolagem vertical e iniciar a rolagem horizontal.

Você deseja criar uma subclasse de seu UIScrollView externo (digamos, você nomeia sua classe como RemorsefulScrollView), de modo que, em vez do comportamento padrão, ele encaminhe imediatamente todos os eventos para a visualização interna e somente quando um movimento horizontal significativo for detectado ele role.

Como fazer RemorsefulScrollView se comportar dessa maneira?

  • Parece que está desativando a rolagem vertical e a configuração delaysContentTouches como NÃO deve fazer com que UIScrollViews aninhados funcionem. Infelizmente, não; UIScrollView parece fazer alguma filtragem adicional para movimentos rápidos (que não podem ser desabilitados), de forma que mesmo se UIScrollView só puder ser rolado horizontalmente, ele sempre consumirá (e ignorará) movimentos verticais rápidos o suficiente.

    O efeito é tão grave que a rolagem vertical dentro de uma visualização de rolagem aninhada é inutilizável. (Parece que você tem exatamente esta configuração, então tente: segure um dedo por 150 ms e, em seguida, mova-o na direção vertical - o UIScrollView aninhado funciona conforme o esperado então!)

  • Isso significa que você não pode usar o código do UIScrollView para manipulação de eventos; você tem que substituir todos os quatro métodos de manipulação de toque em RemorsefulScrollView e fazer seu próprio processamento primeiro, apenas encaminhando o evento para super(UIScrollView) se você decidiu usar a rolagem horizontal.

  • No entanto, você deve passar touchesBeganpara UIScrollView, porque você deseja que ele se lembre de uma coordenada de base para rolagem horizontal futura (se mais tarde você decidir que é uma rolagem horizontal). Você não poderá enviar touchesBeganpara UIScrollView mais tarde, porque você não pode armazenar o touchesargumento: ele contém objetos que sofrerão mutação antes do próximo touchesMovedevento e você não pode reproduzir o estado antigo.

    Portanto, você deve passar touchesBeganpara o UIScrollView imediatamente, mas ocultará quaisquer touchesMovedeventos posteriores até decidir rolar horizontalmente. Não touchesMovedsignifica que não há rolagem, então esta inicial touchesBegannão fará mal. Mas defina delaysContentTouchescomo NÃO, de modo que nenhum temporizador surpresa adicional interfira.

    (Offtopic - ao contrário de você, UIScrollView pode armazenar toques corretamente e pode reproduzir e encaminhar o touchesBeganevento original posteriormente. Ele tem uma vantagem injusta de usar APIs não publicadas, então pode clonar objetos de toque antes que eles sofram mutação.)

  • Como você sempre encaminha touchesBegan, você também deve encaminhar touchesCancellede touchesEnded. Você tem que virar touchesEndedem touchesCancelled, no entanto, porque UIScrollView iria interpretar touchesBegan, touchesEndedseqüência como um toque de cliques, e iria enviá-lo para o ponto de vista interno. Você já está encaminhando os eventos apropriados, então nunca deseja que o UIScrollView encaminhe nada.

Basicamente, aqui está um pseudocódigo para o que você precisa fazer. Para simplificar, nunca permito a rolagem horizontal após a ocorrência do evento multitoque.

// RemorsefulScrollView.h

@interface RemorsefulScrollView : UIScrollView {
  CGPoint _originalPoint;
  BOOL _isHorizontalScroll, _isMultitouch;
  UIView *_currentChild;
}
@end

// RemorsefulScrollView.m

// the numbers from an example in Apple docs, may need to tune them
#define kThresholdX 12.0f
#define kThresholdY 4.0f

@implementation RemorsefulScrollView

- (id)initWithFrame:(CGRect)frame {
  if (self = [super initWithFrame:frame]) {
    self.delaysContentTouches = NO;
  }
  return self;
}

- (id)initWithCoder:(NSCoder *)coder {
  if (self = [super initWithCoder:coder]) {
    self.delaysContentTouches = NO;
  }
  return self;
}

- (UIView *)honestHitTest:(CGPoint)point withEvent:(UIEvent *)event {
  UIView *result = nil;
  for (UIView *child in self.subviews)
    if ([child pointInside:point withEvent:event])
      if ((result = [child hitTest:point withEvent:event]) != nil)
        break;
  return result;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event]; // always forward touchesBegan -- there's no way to forward it later
    if (_isHorizontalScroll)
      return; // UIScrollView is in charge now
    if ([touches count] == [[event touchesForView:self] count]) { // initial touch
      _originalPoint = [[touches anyObject] locationInView:self];
    _currentChild = [self honestHitTest:_originalPoint withEvent:event];
    _isMultitouch = NO;
    }
  _isMultitouch |= ([[event touchesForView:self] count] > 1);
  [_currentChild touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  if (!_isHorizontalScroll && !_isMultitouch) {
    CGPoint point = [[touches anyObject] locationInView:self];
    if (fabsf(_originalPoint.x - point.x) > kThresholdX && fabsf(_originalPoint.y - point.y) < kThresholdY) {
      _isHorizontalScroll = YES;
      [_currentChild touchesCancelled:[event touchesForView:self] withEvent:event]
    }
  }
  if (_isHorizontalScroll)
    [super touchesMoved:touches withEvent:event]; // UIScrollView only kicks in on horizontal scroll
  else
    [_currentChild touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  if (_isHorizontalScroll)
    [super touchesEnded:touches withEvent:event];
    else {
    [super touchesCancelled:touches withEvent:event];
    [_currentChild touchesEnded:touches withEvent:event];
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
  [super touchesCancelled:touches withEvent:event];
  if (!_isHorizontalScroll)
    [_currentChild touchesCancelled:touches withEvent:event];
}

@end

Eu não tentei executar ou mesmo compilar isso (e digitei a classe inteira em um editor de texto simples), mas você pode começar com o descrito acima e, com sorte, fazê-lo funcionar.

O único problema oculto que vejo é que, se você adicionar visualizações filho não UIScrollView ao RemorsefulScrollView, os eventos de toque que você encaminha para um filho podem chegar de volta a você via cadeia de resposta, se o filho nem sempre lida com toques como o UIScrollView faz. Uma implementação RemorsefulScrollView à prova de balas protegeria contra touchesXxxreentrada.

Segunda estratégia: se por algum motivo UIScrollViews aninhados não funcionarem ou se mostrarem muito difíceis de acertar, você pode tentar se dar bem com apenas um UIScrollView, trocando sua pagingEnabledpropriedade em tempo real de seu scrollViewDidScrollmétodo delegado.

Para evitar a rolagem diagonal, você deve primeiro tentar lembrar o contentOffset in scrollViewWillBeginDragginge verificar e redefinir o contentOffset interno scrollViewDidScrollse detectar um movimento diagonal. Outra estratégia a ser tentada é redefinir contentSize para permitir a rolagem apenas em uma direção, depois de decidir para qual direção o dedo do usuário está indo. (UIScrollView parece bastante tolerante quanto a mexer com contentSize e contentOffset em seus métodos delegados.)

Se isso também não funcionar ou resultar em visuais desleixados, você deve substituir touchesBegan, touchesMovedetc. , e não encaminhar eventos de movimento diagonal para UIScrollView. (A experiência do usuário será subótima neste caso, no entanto, porque você terá que ignorar os movimentos diagonais em vez de forçá-los em uma única direção. Se você estiver se sentindo realmente aventureiro, pode escrever seu próprio UITouch parecido com algo como RevengeTouch. Objetivo -C é o C simples e antigo, e não há nada mais complicado no mundo do que C; contanto que ninguém verifique a classe real dos objetos, o que eu acredito que ninguém faz, você pode fazer qualquer classe se parecer com qualquer outra. Isso abre a possibilidade de sintetizar qualquer toque que você quiser, com as coordenadas que você quiser.)

Estratégia de backup: há TTScrollView, uma reimplementação sã do UIScrollView na biblioteca Three20 . Infelizmente, parece muito pouco natural e não-iphonish para o usuário. Mas se todas as tentativas de usar UIScrollView falharem, você pode voltar para uma visualização de rolagem com código personalizado. Eu não recomendo, se possível; o uso do UIScrollView garante que você obtenha a aparência e o comportamento nativos, não importa como ele evolua nas futuras versões do iPhone OS.

Ok, este pequeno ensaio ficou um pouco longo demais. Ainda estou interessado nos jogos UIScrollView depois de trabalhar no ScrollingMadness há vários dias.

PS: Se você conseguir algum desses funcionando e quiser compartilhar, envie-me um e-mail com o código relevante para [email protected]. Eu o incluiria em meu pacote de truques do ScrollingMadness .

PPS Adicionando este pequeno ensaio ao README de ScrollingMadness.

Andrey Tarantsov
fonte
7
Observe que esse comportamento de visualização de rolagem aninhado funciona imediatamente no SDK, sem necessidade de
diversão
1
Marcou o comentário de andygeers com +1 e percebeu que não é preciso; você ainda pode "quebrar" o UIScrollView regular arrastando diagonalmente a partir do ponto inicial e então você "puxou o scroller perdendo" e ele irá ignorar o bloqueio direcional até que você solte e termine sabe Deus onde.
Kalle
Obrigado pela ótima explicação dos bastidores! Eu escolhi a solução de Mattias Wadman abaixo, que parece um pouco menos invasiva IMO.
Ortwin Gentz
Seria ótimo ver essa resposta atualizada agora que o UIScrollView expõe seu reconhecedor de gesto de panorâmica. (Mas talvez isso esteja fora do escopo original.)
jtbandes
Existe a possibilidade de este post ser atualizado? Excelente postagem e obrigado por sua contribuição.
Pavan de
56

Isso funciona para mim sempre ...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);
Tonetel
fonte
20
Para qualquer pessoa que esteja se deparando com esta pergunta: Acho que esta resposta foi postada antes da edição que esclareceu a questão. Isso permitirá que você role apenas horizontalmente, não aborda o problema de ser capaz de rolar verticalmente ou horizontalmente, mas não diagonalmente.
andygeers
Funciona como um encanto para mim. Eu tenho um scrollView horizontal muito longo com paginação e um UIWebView em cada página, que lida com a rolagem vertical de que preciso.
Tom Redman
Excelente! Mas alguém pode explicar como isso funciona (definindo contentSize height para 1)!
Praveen de
Realmente funciona, mas por que e como funciona? Alguém consegue entender isso?
Marko Francekovic
26

Para as pessoas preguiçosas como eu:

Definir scrollview.directionalLockEnabledparaYES

- (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}

- (void) scrollViewDidScroll: (UIScrollView *) scrollView
{
    if (scrollView.contentOffset.x != self.oldContentOffset.x)
    {
        scrollView.pagingEnabled = YES;
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x,
                                               self.oldContentOffset.y);
    }
    else
    {
        scrollView.pagingEnabled = NO;
    }
}

- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}
icompot
fonte
16

Usei o seguinte método para resolver este problema, espero que ajude você.

Primeiro defina as seguintes variáveis ​​no arquivo de cabeçalho de seus controladores.

CGPoint startPos;
int     scrollDirection;

startPos manterá o valor contentOffset quando seu delegado receber a mensagem scrollViewWillBeginDragging . Portanto, neste método, fazemos isso;

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    startPos = scrollView.contentOffset;
    scrollDirection=0;
}

em seguida, usamos esses valores para determinar a direção de rolagem pretendida pelos usuários na mensagem scrollViewDidScroll .

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{

   if (scrollDirection==0){//we need to determine direction
       //use the difference between positions to determine the direction.
       if (abs(startPos.x-scrollView.contentOffset.x)<abs(startPos.y-scrollView.contentOffset.y)){          
          NSLog(@"Vertical Scrolling");
          scrollDirection=1;
       } else {
          NSLog(@"Horitonzal Scrolling");
          scrollDirection=2;
       }
    }
//Update scroll position of the scrollview according to detected direction.     
    if (scrollDirection==1) {
       [scrollView setContentOffset:CGPointMake(startPos.x,scrollView.contentOffset.y) animated:NO];
    } else if (scrollDirection==2){
       [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x,startPos.y) animated:NO];
    }
 }

finalmente, temos que parar todas as operações de atualização quando o usuário termina de arrastar;

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (decelerate) {
       scrollDirection=3;
    }
 }
Yildiray Meric
fonte
Hmm ... Na verdade, após mais investigação - funciona muito bem, mas o problema é que perde o controle da direção durante a fase de desaceleração - então, se você começar a mover o dedo diagonalmente, ele parecerá se mover apenas horizontal ou verticalmente até que você o solte , ponto em que ele desacelerará em uma direção diagonal por um tempo
andygeers
@andygeers, pode funcionar melhor se - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (decelerate) { scrollDirection=3; } }for alterado para - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (!decelerate) { scrollDirection=0; } }e- (void)scrollViewDidEndDecelerating{ scrollDirection=0; }
cduck
Não funcionou para mim. Definir o contentOffset em andamento parece destruir o comportamento de paginação. Eu escolhi a solução de Mattias Wadman.
Ortwin Gentz
13

Posso estar gravedigging aqui, mas me deparei com este post hoje enquanto tentava resolver o mesmo problema. Aqui está minha solução, parece estar funcionando muito bem.

Desejo exibir uma tela cheia de conteúdo, mas permitir que o usuário role para uma das outras quatro telas, para cima, para baixo, à esquerda e à direita. Eu tenho um único UIScrollView com tamanho 320x480 e contentSize de 960x1440. O deslocamento de conteúdo começa em 320.480. Em scrollViewDidEndDecelerating :, eu redesenho as 5 visualizações (visualizações centrais e as 4 ao redor delas) e redefino o deslocamento de conteúdo para 320.480.

Aqui está a parte principal da minha solução, o método scrollViewDidScroll :.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{   
    if (!scrollingVertically && ! scrollingHorizontally)
    {
        int xDiff = abs(scrollView.contentOffset.x - 320);
        int yDiff = abs(scrollView.contentOffset.y - 480);

        if (xDiff > yDiff)
        {
            scrollingHorizontally = YES;
        }
        else if (xDiff < yDiff)
        {
            scrollingVertically = YES;
        }
    }

    if (scrollingHorizontally)
    {
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, 480);
    }
    else if (scrollingVertically)
    {
        scrollView.contentOffset = CGPointMake(320, scrollView.contentOffset.y);
    }
}
James J
fonte
Você provavelmente descobrirá que esse código não "desacelera" bem quando o usuário solta a visualização de rolagem - ele para de funcionar. Se você não quer desaceleração, isso pode ser bom para você.
andygeers
4

Pessoal, é tão simples quanto fazer a altura da subvisualização igual à altura da visualização do scroll e do tamanho do conteúdo. Em seguida, a rolagem vertical é desativada.

julianlubenov
fonte
2
Para qualquer pessoa que esteja se deparando com esta pergunta: Acho que esta resposta foi postada antes da edição que esclareceu a questão. Isso permitirá que você role apenas horizontalmente, não aborda o problema de ser capaz de rolar verticalmente ou horizontalmente, mas não diagonalmente.
andygeers
3

Aqui está minha implementação de um paginado UIScrollViewque só permite rolagens ortogonais. Certifique-se de definir pagingEnabledcomo YES.

PagedOrthoScrollView.h

@interface PagedOrthoScrollView : UIScrollView
@end

PagedOrthoScrollView.m

#import "PagedOrthoScrollView.h"

typedef enum {
  PagedOrthoScrollViewLockDirectionNone,
  PagedOrthoScrollViewLockDirectionVertical,
  PagedOrthoScrollViewLockDirectionHorizontal
} PagedOrthoScrollViewLockDirection; 

@interface PagedOrthoScrollView ()
@property(nonatomic, assign) PagedOrthoScrollViewLockDirection dirLock;
@property(nonatomic, assign) CGFloat valueLock;
@end

@implementation PagedOrthoScrollView
@synthesize dirLock;
@synthesize valueLock;

- (id)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self == nil) {
    return self;
  }

  self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  self.valueLock = 0;

  return self;
}

- (void)setBounds:(CGRect)bounds {
  int mx, my;

  if (self.dirLock == PagedOrthoScrollViewLockDirectionNone) {
    // is on even page coordinates, set dir lock and lock value to closest page 
    mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
    if (mx != 0) {
      self.dirLock = PagedOrthoScrollViewLockDirectionHorizontal;
      self.valueLock = (round(CGRectGetMinY(bounds) / CGRectGetHeight(self.bounds)) *
                        CGRectGetHeight(self.bounds));
    } else {
      self.dirLock = PagedOrthoScrollViewLockDirectionVertical;
      self.valueLock = (round(CGRectGetMinX(bounds) / CGRectGetWidth(self.bounds)) *
                        CGRectGetWidth(self.bounds));
    }

    // show only possible scroll indicator
    self.showsVerticalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionVertical;
    self.showsHorizontalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionHorizontal;
  }

  if (self.dirLock == PagedOrthoScrollViewLockDirectionHorizontal) {
    bounds.origin.y = self.valueLock;
  } else {
    bounds.origin.x = self.valueLock;
  }

  mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
  my = abs((int)CGRectGetMinY(bounds) % (int)CGRectGetHeight(self.bounds));

  if (mx == 0 && my == 0) {
    // is on even page coordinates, reset lock
    self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  }

  [super setBounds:bounds];
}

@end
Mattias Wadman
fonte
1
Funciona muito bem, também no iOS 7. Obrigado!
Ortwin Gentz
3

Eu também tive que resolver esse problema e, embora a resposta de Andrey Tarantsov definitivamente tenha muitas informações úteis para entender como UIScrollViewsfunciona, acho que a solução é um pouco complicada demais. Minha solução, que não envolve nenhuma subclasse ou encaminhamento de toque, é a seguinte:

  1. Crie duas visualizações de rolagem aninhadas, uma para cada direção.
  2. Crie dois bonecos UIPanGestureRecognizers, novamente um para cada direção.
  3. Crie dependências de falha entre as UIScrollViewspropriedades 'panGestureRecognizer e o manequim UIPanGestureRecognizercorrespondente à direção perpendicular à direção de rolagem desejada de seu UIScrollView usando o UIGestureRecognizer's -requireGestureRecognizerToFail:seletor.
  4. Nos -gestureRecognizerShouldBegin:retornos de chamada para seus UIPanGestureRecognizers fictícios, calcule a direção inicial da panorâmica usando o seletor -translationInView: do gesto (seuUIPanGestureRecognizers não chamará este retorno de chamada delegado até que seu toque tenha traduzido o suficiente para registrar como um pan). Permitir ou proibir que o reconhecedor de gestos comece, dependendo da direção calculada, que por sua vez deve controlar se o reconhecedor de gestos panorâmicos UIScrollView perpendicular terá permissão para iniciar ou não.
  5. Em -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:, não permita que seu manequim UIPanGestureRecognizersseja executado junto com a rolagem (a menos que você tenha algum comportamento extra que queira adicionar).
Kurt Horimoto
fonte
2

O que fiz foi criar um UIScrollView de paginação com um quadro do tamanho da tela de visualização e definir o tamanho do conteúdo para a largura necessária para todo o meu conteúdo e uma altura de 1 (obrigado Tonetel). Então criei as páginas do menu UIScrollView com frames definidos para cada página, o tamanho da tela de visualização e defina o tamanho do conteúdo de cada uma para a largura da tela e a altura necessária para cada uma. Em seguida, simplesmente adicionei cada uma das páginas do menu à exibição de paginação. Certifique-se de que a paginação esteja habilitada na visualização de rolagem de paginação, mas desabilitada nas visualizações de menu (deve ser desabilitada por padrão) e você deve estar pronto para continuar.

A visualização de rolagem de paginação agora rola horizontalmente, mas não verticalmente por causa da altura do conteúdo. Cada página rola verticalmente, mas não horizontalmente por causa de suas restrições de tamanho de conteúdo também.

Aqui está o código se essa explicação deixou algo a desejar:

UIScrollView *newPagingScrollView =  [[UIScrollView alloc] initWithFrame:self.view.bounds]; 
[newPagingScrollView setPagingEnabled:YES];
[newPagingScrollView setShowsVerticalScrollIndicator:NO];
[newPagingScrollView setShowsHorizontalScrollIndicator:NO];
[newPagingScrollView setDelegate:self];
[newPagingScrollView setContentSize:CGSizeMake(self.view.bounds.size.width * NumberOfDetailPages, 1)];
[self.view addSubview:newPagingScrollView];

float pageX = 0;

for (int i = 0; i < NumberOfDetailPages; i++)
{               
    CGRect pageFrame = (CGRect) 
    {
        .origin = CGPointMake(pageX, pagingScrollView.bounds.origin.y), 
        .size = pagingScrollView.bounds.size
    };
    UIScrollView *newPage = [self createNewPageFromIndex:i ToPageFrame:pageFrame]; // newPage.contentSize custom set in here        
    [pagingScrollView addSubview:newPage];

    pageX += pageFrame.size.width;  
}           
Jon Conner
fonte
2

Obrigado Tonetel. Modifiquei ligeiramente sua abordagem, mas era exatamente o que eu precisava para evitar a rolagem horizontal.

self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 1);
Jeremy
fonte
2

Esta é uma solução melhorada de icompot, que era bastante instável para mim. Este funciona bem e é fácil de implementar:

- (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}

- (void) scrollViewDidScroll: (UIScrollView *) scrollView
{    

    float XOffset = fabsf(self.oldContentOffset.x - scrollView.contentOffset.x );
    float YOffset = fabsf(self.oldContentOffset.y - scrollView.contentOffset.y );

        if (scrollView.contentOffset.x != self.oldContentOffset.x && (XOffset >= YOffset) )
        {

            scrollView.pagingEnabled = YES;
            scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x,
                                                   self.oldContentOffset.y);

        }
        else
        {
            scrollView.pagingEnabled = NO;
               scrollView.contentOffset = CGPointMake( self.oldContentOffset.x,
                                                   scrollView.contentOffset.y);
        }

}


- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}
Maciej
fonte
2

Para referência futura, aproveitei a resposta de Andrey Tanasov e encontrei a seguinte solução que funciona bem.

Primeiro defina uma variável para armazenar o contentOffset e configure-a para coordenadas 0,0

var positionScroll = CGPointMake(0, 0)

Agora implemente o seguinte no delegado scrollView:

override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    // Note that we are getting a reference to self.scrollView and not the scrollView referenced passed by the method
    // For some reason, not doing this fails to get the logic to work
    self.positionScroll = (self.scrollView?.contentOffset)!
}

override func scrollViewDidScroll(scrollView: UIScrollView) {

    // Check that contentOffset is not nil
    // We do this because the scrollViewDidScroll method is called on viewDidLoad
    if self.scrollView?.contentOffset != nil {

        // The user wants to scroll on the X axis
        if self.scrollView?.contentOffset.x > self.positionScroll.x || self.scrollView?.contentOffset.x < self.positionScroll.x {

            // Reset the Y position of the scrollView to what it was BEFORE scrolling started
            self.scrollView?.contentOffset = CGPointMake((self.scrollView?.contentOffset.x)!, self.positionScroll.y);
            self.scrollView?.pagingEnabled = true
        }

        // The user wants to scroll on the Y axis
        else {
            // Reset the X position of the scrollView to what it was BEFORE scrolling started
            self.scrollView?.contentOffset = CGPointMake(self.positionScroll.x, (self.scrollView?.contentOffset.y)!);
            self.collectionView?.pagingEnabled = false
        }

    }

}

Espero que isto ajude.

Benjamin
fonte
1

Dos documentos:

"o valor padrão é NÃO, o que significa que a rolagem é permitida nas direções horizontal e vertical. Se o valor for SIM e o usuário começar a arrastar em uma direção geral (horizontal ou vertical), a visualização de rolagem desativa a rolagem na outra direção. "

Acho que a parte importante é "se ... o usuário começar arrastar em uma direção geral". Portanto, se eles começarem a arrastar diagonalmente, isso não funcionará. Não que esses documentos sejam sempre confiáveis ​​quando se trata de interpretação - mas isso parece se adequar ao que você está vendo.

Isso realmente parece sensato. Eu tenho que perguntar - por que você quer restringir apenas horizontal ou vertical a qualquer momento? Talvez o UIScrollView não seja a ferramenta para você?

philsquared
fonte
1

Minha visualização consiste em 10 visualizações horizontais, e algumas dessas visualizações consistem em múltiplas visualizações verticais, então você obterá algo assim:


1.0   2.0   3.1    4.1
      2.1   3.1
      2.2
      2.3

Com paginação habilitada nos eixos horizontal e vertical

A visualização também possui os seguintes atributos:

[self setDelaysContentTouches:NO];
[self setMultipleTouchEnabled:NO];
[self setDirectionalLockEnabled:YES];

Agora, para evitar a rolagem diagonal, faça o seguinte:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    int pageWidth = 768;
    int pageHeight = 1024;
    int subPage =round(self.contentOffset.y / pageHeight);
    if ((int)self.contentOffset.x % pageWidth != 0 && (int)self.contentOffset.y % pageHeight != 0) {
        [self setContentOffset:CGPointMake(self.contentOffset.x, subPage * pageHeight];
    } 
}

Funciona como um encanto para mim!

user422756
fonte
1
não entendo como isso pode estar funcionando para você ... você poderia enviar um projeto de amostra?
hfossli
1

É necessário redefinir _isHorizontalScrollcomo NÃO em touchesEndede touchesCancelled.

Praveen Matanam
fonte
0

Se você ainda não tentou fazer isso no 3.0 beta, recomendo que experimente. No momento, estou usando uma série de visualizações de tabela dentro de uma visualização de rolagem e funciona exatamente como esperado. (Bem, a rolagem sim, de qualquer maneira. Essa pergunta foi encontrada ao procurar uma solução para um problema totalmente diferente ...)

Tim Keating
fonte
0

Para aqueles que procuram em Swift a segunda solução @AndreyTarantsov, é assim no código:

    var positionScroll:CGFloat = 0

    func scrollViewWillBeginDragging(scrollView: UIScrollView) {
        positionScroll = self.theScroll.contentOffset.x
    }

    func scrollViewDidScroll(scrollView: UIScrollView) {
        if self.theScroll.contentOffset.x > self.positionScroll || self.theScroll.contentOffset.x < self.positionScroll{
             self.theScroll.pagingEnabled = true
        }else{
            self.theScroll.pagingEnabled = false
        }
    }

o que estamos fazendo aqui é manter a posição atual antes de a visualização de rolagem ser arrastada e, em seguida, verificar se x aumentou ou diminuiu em comparação com a posição atual de x. Se sim, defina pagingEnabled como true, se não (y aumentou / diminuiu), defina pagingEnabled como false

Espero que seja útil para os recém-chegados!

Jonprasetyo
fonte
0

Consegui resolver isso implementando scrollViewDidBeginDragging (_ :) e observando a velocidade no UIPanGestureRecognizer subjacente.

NB: Com esta solução, cada panela que teria sido diagonal será ignorada pelo scrollView.

override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    super.scrollViewWillBeginDragging(scrollView)
    let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
    if velocity.x != 0 && velocity.y != 0 {
        scrollView.panGestureRecognizer.isEnabled = false
        scrollView.panGestureRecognizer.isEnabled = true
    }
}
Morten Saabye Kristensen
fonte
-3

Se você estiver usando o construtor de interface para projetar sua interface - isso pode ser feito facilmente.

No construtor de interface, basta clicar no UIScrollView e selecionar a opção (no inspetor) que o limita a apenas uma direção de rolagem.

zpesk
fonte
1
Não há opções no IB para fazer isso. As duas opções às quais você pode se referir têm a ver com a exibição da barra de rolagem, não com a rolagem real. Além disso, o Bloqueio de direção apenas bloqueia a direção uma vez que a rolagem tenha começado.
Sophtware de