UIScrollView: paginación horizontal, desplazamiento vertical?

88

¿Cómo puedo forzar un UIScrollViewen el que la paginación y el desplazamiento están activados para que solo se muevan vertical u horizontalmente en un momento dado?

Tengo entendido que la directionalLockEnabledpropiedad debería lograr esto, pero un deslizamiento diagonal aún hace que la vista se desplace en diagonal en lugar de restringir el movimiento a un solo eje.

Editar: para ser más claro, me gustaría permitir que el usuario se desplace horizontal O verticalmente, pero no ambos simultáneamente.

Cœur
fuente
¿Desea restringir una vista para que se desplace SÓLO horizontal / vertical o desea que el usuario se desplace horizontal o vertical, pero no ambos simultáneamente?
Will Hartung
¿Puedes hacerme un favor y cambiar el título para que parezca un poco más interesante y no trivial? Algo como "UIScrollView: paginación horizontal, desplazamiento vertical"
Andrey Tarantsov
Desafortunadamente, publiqué la pregunta como un usuario no registrado en una computadora a la que ya no tengo acceso (antes de registrarme con el mismo nombre cuando llegué a casa), lo que significa que no puedo editarla. Esto también debería explicarme el seguimiento más abajo en lugar de editar la pregunta original. ¡Perdón por romper la etiqueta de Stack esta vez!
Nick

Respuestas:

117

Estás en una situación muy difícil, debo decir.

Tenga en cuenta que debe usar un UIScrollView con pagingEnabled=YESpara cambiar entre páginas, pero debe pagingEnabled=NOdesplazarse verticalmente.

Hay 2 estrategias posibles. No sé cuál funcionará / es más fácil de implementar, así que pruebe ambos.

Primero: UIScrollViews anidados. Francamente, todavía tengo que ver a una persona que haya logrado que esto funcione. Sin embargo, personalmente no me he esforzado lo suficiente, y mi práctica demuestra que cuando lo intentas lo suficiente, puedes hacer que UIScrollView haga lo que quieras .

Entonces, la estrategia es dejar que la vista de desplazamiento exterior solo maneje el desplazamiento horizontal y las vistas de desplazamiento interno solo manejen el desplazamiento vertical. Para lograrlo, debe saber cómo funciona UIScrollView internamente. Anula el hitTestmétodo y siempre se devuelve a sí mismo, de modo que todos los eventos táctiles van a UIScrollView. Luego touchesBegan, adentro , touchesMovedetc., verifica si está interesado en el evento y lo maneja o lo pasa a los componentes internos.

Para decidir si el toque se debe manejar o reenviar, UIScrollView inicia un temporizador cuando lo toca por primera vez:

  • Si no ha movido el dedo significativamente en 150 ms, pasa el evento a la vista interior.

  • Si ha movido el dedo significativamente en 150 ms, comienza a desplazarse (y nunca pasa el evento a la vista interior).

    Observe cómo cuando toca una tabla (que es una subclase de vista de desplazamiento) y comienza a desplazarse de inmediato, la fila que tocó nunca se resalta.

  • Si ha no movido el dedo significativamente dentro de 150ms y UIScrollView comenzó a pasar los eventos a la vista interno, pero luego ha movido el dedo lo suficiente para que el desplazamiento para empezar, UIScrollView llama touchesCancelleden la vista interior y empieza a oscilar.

    Tenga en cuenta que cuando toca una mesa, sostiene un poco el dedo y luego comienza a desplazarse, la fila que tocó se resalta primero, pero luego se quita el resaltado.

Esta secuencia de eventos se puede modificar mediante la configuración de UIScrollView:

  • Si delaysContentToucheses NO, entonces no se usa ningún temporizador: los eventos pasan inmediatamente al control interno (pero luego se cancelan si mueve el dedo lo suficiente)
  • Si cancelsToucheses NO, una vez que los eventos se envían a un control, el desplazamiento nunca ocurrirá.

Tenga en cuenta que es UIScrollView que recibe todo touchesBegin , touchesMoved, touchesEndedy touchesCanceledeventos de Cocoa Touch (debido a que su hitTestdice que lo haga). Luego los reenvía a la vista interior si así lo desea, siempre que lo desee.

Ahora que sabe todo sobre UIScrollView, puede modificar su comportamiento. Puedo apostar que quiere dar preferencia al desplazamiento vertical, de modo que una vez que el usuario toque la vista y comience a mover el dedo (aunque sea ligeramente), la vista comience a desplazarse en dirección vertical; pero cuando el usuario mueve su dedo en dirección horizontal lo suficiente, desea cancelar el desplazamiento vertical y comenzar el desplazamiento horizontal.

Desea subclasificar su UIScrollView externo (digamos, nombra su clase RemorsefulScrollView), de modo que en lugar del comportamiento predeterminado, reenvíe inmediatamente todos los eventos a la vista interna, y solo cuando se detecta un movimiento horizontal significativo, se desplaza.

¿Cómo hacer que RemorsefulScrollView se comporte de esa manera?

  • Parece que deshabilitar el desplazamiento vertical y establecer delaysContentTouchesen NO debería hacer que UIScrollViews anidadas funcionen. Desafortunadamente, no es así; UIScrollView parece hacer un filtrado adicional para movimientos rápidos (que no se pueden deshabilitar), de modo que incluso si UIScrollView solo se puede desplazar horizontalmente, siempre consumirá (e ignorará) los movimientos verticales lo suficientemente rápidos.

    El efecto es tan severo que el desplazamiento vertical dentro de una vista de desplazamiento anidada es inutilizable. (Parece que tiene exactamente esta configuración, así que inténtelo: mantenga un dedo durante 150 ms y luego muévalo en dirección vertical; ¡UIScrollView anidado funciona como se esperaba entonces!)

  • Esto significa que no puede usar el código de UIScrollView para el manejo de eventos; debe anular los cuatro métodos de manejo táctil en RemorsefulScrollView y hacer su propio procesamiento primero, solo reenviando el evento a super(UIScrollView) si ha decidido ir con el desplazamiento horizontal.

  • Sin embargo, debe pasar touchesBegana UIScrollView, porque desea que recuerde una coordenada base para el desplazamiento horizontal futuro (si luego decide que es un desplazamiento horizontal). No podrá enviar touchesBegana UIScrollView más tarde, porque no puede almacenar el touchesargumento: contiene objetos que serán mutados antes del próximo touchesMovedevento y no puede reproducir el estado anterior.

    Por lo tanto, debe pasar touchesBegana UIScrollView de inmediato, pero ocultará más touchesMovedeventos hasta que decida desplazarse horizontalmente. No touchesMovedsignifica que no haya desplazamiento, por lo que esta inicial touchesBeganno hará ningún daño. Pero configure delaysContentTouchesen NO, para que no interfieran temporizadores sorpresa adicionales.

    (Offtopic: a diferencia de usted, UIScrollView puede almacenar toques correctamente y puede reproducir y reenviar el touchesBeganevento original más tarde. Tiene la ventaja injusta de usar API no publicadas, por lo que puede clonar objetos táctiles antes de que se muten).

  • Dado que siempre reenvías touchesBegan, también tienes que reenviar touchesCancelledy touchesEnded. Se debe girar touchesEndeden touchesCancelled, sin embargo, porque UIScrollView interpretaría touchesBegan, touchesEndedsecuencia como un toque del ratón, y le transmitirá a la vista interior. Ya está reenviando los eventos adecuados usted mismo, por lo que nunca desea que UIScrollView reenvíe nada.

Básicamente, aquí hay un pseudocódigo para lo que necesita hacer. Para simplificar, nunca permito el desplazamiento horizontal después de que se haya producido un evento multitáctil.

// 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

No he intentado ejecutar o incluso compilar esto (y escribí toda la clase en un editor de texto sin formato), pero puede comenzar con lo anterior y, con suerte, hacerlo funcionar.

El único problema oculto que veo es que si agrega vistas secundarias que no sean UIScrollView a RemorsefulScrollView, los eventos táctiles que reenvía a un niño pueden llegar a usted a través de la cadena de respuesta, si el niño no siempre maneja los toques como lo hace UIScrollView. Una implementación de RemorsefulScrollView a prueba de balas protegería contra la touchesXxxreentrada.

Segunda estrategia: si por alguna razón los UIScrollViews anidados no funcionan o resultan demasiado difíciles de hacerlo bien, puede intentar llevarse bien con un solo UIScrollView, cambiando su pagingEnabledpropiedad sobre la marcha desde su scrollViewDidScrollmétodo delegado.

Para evitar el desplazamiento diagonal, primero debe intentar recordar contentOffset adentro scrollViewWillBeginDraggingy verificar y restablecer contentOffset adentro scrollViewDidScrollsi detecta un movimiento diagonal. Otra estrategia que puede probar es restablecer contentSize para permitir el desplazamiento en una sola dirección, una vez que decida en qué dirección se dirige el dedo del usuario. (UIScrollView parece bastante indulgente a la hora de jugar con contentSize y contentOffset desde sus métodos delegados).

Si eso tampoco funciona o da como resultado imágenes descuidadas, debe anular touchesBegan, touchesMovedetc. y no reenviar eventos de movimiento diagonal a UIScrollView. (Sin embargo, la experiencia del usuario no será óptima en este caso, ya que tendrá que ignorar los movimientos diagonales en lugar de forzarlos en una sola dirección. Si se siente realmente aventurero, puede escribir su propio UITouch similar, algo como RevengeTouch. Objetivo -C es simplemente C antiguo, y no hay nada más tipo pato en el mundo que C; siempre que nadie verifique la clase real de los objetos, lo que creo que nadie hace, puedes hacer que cualquier clase se vea como cualquier otra clase. Esto abre la posibilidad de sintetizar los toques que desee, con las coordenadas que desee).

Estrategia de copia de seguridad: hay TTScrollView, una nueva implementación sensata de UIScrollView en la biblioteca Three20 . Desafortunadamente, el usuario lo siente muy poco natural y poco honesto. Pero si cada intento de usar UIScrollView falla, puede recurrir a una vista de desplazamiento con codificación personalizada. Recomiendo no hacerlo si es posible; El uso de UIScrollView garantiza que obtenga el aspecto y la sensación nativos, sin importar cómo evolucione en las futuras versiones del iPhone OS.

De acuerdo, este pequeño ensayo se ha vuelto un poco largo. Todavía estoy en los juegos de UIScrollView después del trabajo en ScrollingMadness hace varios días.

PD: Si alguno de estos funciona y tiene ganas de compartirlo, envíeme un correo electrónico con el código correspondiente a [email protected]. Con mucho gusto lo incluiría en mi bolsa de trucos ScrollingMadness .

PPS Añadiendo este pequeño ensayo a ScrollingMadness README.

Andrey Tarantsov
fuente
7
Tenga en cuenta que este comportamiento de vista de desplazamiento anidado funciona de inmediato en el SDK, no se necesitan negocios divertidos
andygeers
1
Hizo +1 en el comentario de andygeers, luego se dio cuenta de que no era exacto; todavía puede "romper" el UIScrollView normal arrastrando diagonalmente desde el punto de inicio y luego "ha tirado el scroller para perder" e ignorará el bloqueo direccional hasta que lo sueltes y termines Dios sabe dónde.
Kalle
¡Gracias por la gran explicación detrás de escena! Fui con la solución de Mattias Wadman a continuación, que parece un poco menos invasiva en mi opinión.
Ortwin Gentz
Sería genial ver esta respuesta actualizada ahora que UIScrollView expone su reconocedor de gestos panorámicos. (Pero quizás eso esté fuera del alcance original.)
jtbandes
¿Existe la posibilidad de que se actualice esta publicación? Gran publicación y gracias por tu aporte.
Pavan
56

Esto funciona para mí cada vez ...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);
Tonetel
fuente
20
Para cualquier otra persona que se encuentre con esta pregunta: creo que esta respuesta se publicó antes de la edición que aclaró la pregunta. Esto le permitirá desplazarse solo horizontalmente, no aborda el problema de poder desplazarse vertical u horizontalmente pero no diagonalmente.
andygeers
Funciona como un encanto para mí. Tengo un scrollView horizontal muy largo con paginación y un UIWebView en cada página, que maneja el desplazamiento vertical que necesito.
Tom Redman
¡Excelente! Pero, ¿alguien puede explicar cómo funciona esto (estableciendo contentSize height en 1)?
Praveen
Realmente funciona, pero ¿por qué y cómo funciona? ¿Alguien puede encontrarle algún sentido?
Marko Francekovic
27

Para la gente perezosa como yo:

Establecer scrollview.directionalLockEnabledenYES

- (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
fuente
16

Utilicé el siguiente método para resolver este problema, espero que te ayude.

Primero defina las siguientes variables en el archivo de encabezado de sus controladores.

CGPoint startPos;
int     scrollDirection;

startPos mantendrá el valor contentOffset cuando su delegado reciba el mensaje scrollViewWillBeginDragging . Entonces en este método hacemos esto;

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

luego usamos estos valores para determinar la dirección de desplazamiento deseada por los usuarios en el mensaje 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 tenemos que detener todas las operaciones de actualización cuando el usuario termina de arrastrar;

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (decelerate) {
       scrollDirection=3;
    }
 }
Yildiray Meric
fuente
Hmm ... En realidad, después de más investigación, funciona bastante bien, pero el problema es que pierde el control de la dirección durante la fase de desaceleración, por lo que si comienza a mover el dedo en diagonal, parecerá que solo se mueve horizontal o verticalmente. hasta que lo suelte, momento en el que se desacelerará en una dirección diagonal por un tiempo
andygeers
@andygeers, podría funcionar mejor si - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (decelerate) { scrollDirection=3; } }se cambia a - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (!decelerate) { scrollDirection=0; } }y- (void)scrollViewDidEndDecelerating{ scrollDirection=0; }
cduck
No funcionó para mí. Establecer el contentOffset en vuelo parece destruir el comportamiento de paginación. Fui con la solución de Mattias Wadman.
Ortwin Gentz
13

Puede que esté excavando aquí, pero me encontré con esta publicación hoy porque estaba tratando de resolver el mismo problema. Aquí está mi solución, parece estar funcionando muy bien.

Quiero mostrar una pantalla llena de contenido, pero permitir que el usuario se desplace a una de las otras 4 pantallas completas, arriba, abajo, izquierda y derecha. Tengo un único UIScrollView con un tamaño de 320x480 y un contentSize de 960x1440. El desplazamiento de contenido comienza en 320,480. En scrollViewDidEndDecelerating :, redibujo las 5 vistas (vistas centrales y las 4 a su alrededor) y restablezco el desplazamiento de contenido a 320,480.

Aquí está el meollo de mi solución, el 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
fuente
Probablemente encontrará que este código no se "desacelera" bien cuando el usuario suelta la vista de desplazamiento; se detendrá en seco. Si no desea la desaceleración, esto podría estar bien para usted.
andygeers
4

Chicos, es tan simple como hacer que la altura de la subvista sea la misma que la altura de la vista de desplazamiento y la altura del tamaño del contenido. Entonces el desplazamiento vertical está desactivado.

julianlubenov
fuente
2
Para cualquier otra persona que se encuentre con esta pregunta: creo que esta respuesta se publicó antes de la edición que aclaró la pregunta. Esto le permitirá desplazarse solo horizontalmente, no aborda el problema de poder desplazarse vertical u horizontalmente pero no diagonalmente.
andygeers
3

Aquí está mi implementación de un paginado UIScrollViewque solo permite pergaminos ortogonales. Asegúrese de establecerlo pagingEnableden 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
fuente
1
Funciona muy bien, también en iOS 7. ¡Gracias!
Ortwin Gentz
3

También tuve que resolver este problema, y ​​aunque la respuesta de Andrey Tarantsov definitivamente tiene mucha información útil para comprender cómo UIScrollViewsfunciona, creo que la solución es un poco complicada. Mi solución, que no implica ninguna subclase o reenvío táctil, es la siguiente:

  1. Cree dos vistas de desplazamiento anidadas, una para cada dirección.
  2. Crea dos muñecos UIPanGestureRecognizers, nuevamente uno para cada dirección.
  3. Cree dependencias de falla entre las UIScrollViewspropiedades 'panGestureRecognizer y el maniquí UIPanGestureRecognizercorrespondiente a la dirección perpendicular a la dirección de desplazamiento deseada de su UIScrollView usando el UIGestureRecognizer's -requireGestureRecognizerToFail:selector.
  4. En las -gestureRecognizerShouldBegin:devoluciones de llamada para sus UIPanGestureRecognizers ficticios, calcule la dirección inicial de la panorámica utilizando el selector -translationInView: del gesto ( UIPanGestureRecognizersno llamará a esta devolución de llamada delegada hasta que su toque se haya traducido lo suficiente para registrarse como una panorámica). Permita o no permita que su reconocedor de gestos comience dependiendo de la dirección calculada, que a su vez debería controlar si el reconocedor de gestos de panorámica UIScrollView perpendicular podrá o no comenzar.
  5. En -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:, no permita que su muñeco se UIPanGestureRecognizersejecute junto con el desplazamiento (a menos que tenga algún comportamiento adicional que desee agregar).
Kurt Horimoto
fuente
2

Lo que hice fue crear un UIScrollView de paginación con un marco del tamaño de la pantalla de visualización y establecer el tamaño del contenido al ancho necesario para todo mi contenido y una altura de 1 (gracias Tonetel). Luego creé las páginas del menú UIScrollView con marcos configurados para cada página, el tamaño de la pantalla de visualización y configuré el tamaño del contenido de cada una al ancho de la pantalla y la altura necesaria para cada una. Luego, simplemente agregué cada una de las páginas del menú a la vista de paginación. Asegúrese de que la paginación esté habilitada en la vista de desplazamiento de paginación pero deshabilitada en las vistas del menú (debería estar deshabilitada de forma predeterminada) y debería estar listo para comenzar.

La vista de desplazamiento de paginación ahora se desplaza horizontalmente, pero no verticalmente debido a la altura del contenido. Cada página se desplaza verticalmente pero no horizontalmente debido a sus limitaciones de tamaño de contenido también.

Aquí está el código si esa explicación dejó algo que desear:

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
fuente
2

Gracias Tonetel. Modifiqué ligeramente su enfoque, pero era exactamente lo que necesitaba para evitar el desplazamiento horizontal.

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

Esta es una solución mejorada de icompot, que era bastante inestable para mí. Este funciona bien y es 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
fuente
2

Para referencia futura, me basé en la respuesta de Andrey Tanasov y se me ocurrió la siguiente solución que funciona bien.

Primero defina una variable para almacenar el contentOffset y configúrelo en coordenadas 0,0

var positionScroll = CGPointMake(0, 0)

Ahora implemente lo siguiente en el 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 esto ayude.

Benjamín
fuente
1

De los documentos:

"el valor predeterminado es NO, lo que significa que se permite el desplazamiento tanto en dirección horizontal como vertical. Si el valor es SÍ y el usuario comienza a arrastrar en una dirección general (horizontal o verticalmente), la vista de desplazamiento deshabilita el desplazamiento en la otra dirección. "

Creo que la parte importante es "si ... el usuario comienza a arrastrar en una dirección general". Entonces, si comienzan a arrastrar en diagonal, esto no se activa. No es que estos documentos sean siempre confiables en lo que respecta a la interpretación, pero eso parece encajar con lo que está viendo.

Esto realmente parece sensato. Tengo que preguntar: ¿por qué quiere restringirse solo a horizontal o solo vertical en cualquier momento? ¿Quizás UIScrollView no es la herramienta para usted?

philsquared
fuente
1

Mi vista consta de 10 vistas horizontales, y algunas de esas vistas consisten en múltiples vistas verticales, por lo que obtendrá algo como esto:


1.0   2.0   3.1    4.1
      2.1   3.1
      2.2
      2.3

Con la paginación habilitada tanto en el eje horizontal como en el vertical

La vista también tiene los siguientes atributos:

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

Ahora, para evitar el desplazamiento diagonal, haga esto:

-(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 un encanto para mí!

user422756
fuente
1
No entiendo cómo esto podría funcionar para usted ... ¿podría cargar un proyecto de muestra?
hfossli
1

Necesita restablecer _isHorizontalScrolla NO en touchesEndedy touchesCancelled.

Praveen Matanam
fuente
0

Si no ha intentado hacer esto en la versión 3.0 beta, le recomiendo que lo pruebe. Actualmente estoy usando una serie de vistas de tabla dentro de una vista de desplazamiento y funciona como se esperaba. (Bueno, el desplazamiento lo hace, de todos modos. Encontré esta pregunta cuando buscaba una solución a un problema totalmente diferente ...)

Tim Keating
fuente
0

Para aquellos que buscan en Swift en @AndreyTarantsov, la segunda solución es así en el 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
        }
    }

lo que estamos haciendo aquí es mantener la posición actual justo antes de que se arrastre scrollview y luego verificar si x ha aumentado o disminuido en comparación con la posición actual de x. Si es así, establezca pagingEnabled en verdadero, si no (y ha aumentado / disminuido), establezca pagingEnabled en falso

¡Espero que sea útil para los recién llegados!

jonprasetyo
fuente
0

Logré resolver esto implementando scrollViewDidBeginDragging (_ :) y mirando la velocidad en el UIPanGestureRecognizer subyacente.

NB: Con esta solución, scrollView ignorará cada pan que hubiera sido diagonal.

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
fuente
-3

Si está utilizando el generador de interfaces para diseñar su interfaz, esto se puede hacer con bastante facilidad.

En el generador de interfaces, simplemente haga clic en UIScrollView y seleccione la opción (en el inspector) que lo limita a un solo desplazamiento en una dirección.

zpesk
fuente
1
No hay opciones en IB para hacer esto. Las dos opciones a las que puede referirse tienen que ver con la visualización de la barra de desplazamiento, no con el desplazamiento real. Además, el bloqueo de dirección solo bloquea la dirección una vez que ha comenzado el desplazamiento.
Sophtware