Estás en una situación muy difícil, debo decir.
Tenga en cuenta que debe usar un UIScrollView con pagingEnabled=YES
para cambiar entre páginas, pero debe pagingEnabled=NO
desplazarse 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 hitTest
método y siempre se devuelve a sí mismo, de modo que todos los eventos táctiles van a UIScrollView. Luego touchesBegan
, adentro , touchesMoved
etc., 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 touchesCancelled
en 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
delaysContentTouches
es 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
cancelsTouches
es 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
, touchesEnded
y touchesCanceled
eventos de Cocoa Touch (debido a que su hitTest
dice 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 delaysContentTouches
en 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 touchesBegan
a UIScrollView, porque desea que recuerde una coordenada base para el desplazamiento horizontal futuro (si luego decide que es un desplazamiento horizontal). No podrá enviar touchesBegan
a UIScrollView más tarde, porque no puede almacenar el touches
argumento: contiene objetos que serán mutados antes del próximo touchesMoved
evento y no puede reproducir el estado anterior.
Por lo tanto, debe pasar touchesBegan
a UIScrollView de inmediato, pero ocultará más touchesMoved
eventos hasta que decida desplazarse horizontalmente. No touchesMoved
significa que no haya desplazamiento, por lo que esta inicial touchesBegan
no hará ningún daño. Pero configure delaysContentTouches
en NO, para que no interfieran temporizadores sorpresa adicionales.
(Offtopic: a diferencia de usted, UIScrollView puede almacenar toques correctamente y puede reproducir y reenviar el touchesBegan
evento 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 touchesCancelled
y touchesEnded
. Se debe girar touchesEnded
en touchesCancelled
, sin embargo, porque UIScrollView interpretaría touchesBegan
, touchesEnded
secuencia 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.
@interface RemorsefulScrollView : UIScrollView {
CGPoint _originalPoint;
BOOL _isHorizontalScroll, _isMultitouch;
UIView *_currentChild;
}
@end
#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];
if (_isHorizontalScroll)
return;
if ([touches count] == [[event touchesForView:self] count]) {
_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];
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 touchesXxx
reentrada.
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 pagingEnabled
propiedad sobre la marcha desde su scrollViewDidScroll
método delegado.
Para evitar el desplazamiento diagonal, primero debe intentar recordar contentOffset adentro scrollViewWillBeginDragging
y verificar y restablecer contentOffset adentro scrollViewDidScroll
si 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
, touchesMoved
etc. 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 andreyvit@gmail.com. Con mucho gusto lo incluiría en mi bolsa de trucos ScrollingMadness .
PPS Añadiendo este pequeño ensayo a ScrollingMadness README.