iphone - Desplazándose con dos dedos con un UIScrollView
objective-c (14)
Tengo una aplicación en la que mi vista principal acepta ambos touchesBegan
y touchesMoved
, y por lo tanto toma toques con un solo dedo y arrastra. Quiero implementar un UIScrollView
, y lo tengo funcionando, pero anula los arrastres, y por lo tanto mi contentView nunca los recibe. Me gustaría implementar un UIScrollview
, donde un arrastre de dos dedos indica un desplazamiento, y un evento de arrastre de un dedo pasa a mi vista de contenido, por lo que se realiza normalmente. ¿Necesito crear mi propia subclase de UIScrollView
?
Aquí está mi código de mi appDelegate
donde implemento el UIScrollView
.
@implementation MusicGridAppDelegate
@synthesize window;
@synthesize viewController;
@synthesize scrollView;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Override point for customization after app launch
//[application setStatusBarHidden:YES animated:NO];
//[window addSubview:viewController.view];
scrollView.contentSize = CGSizeMake(720, 480);
scrollView.showsHorizontalScrollIndicator = YES;
scrollView.showsVerticalScrollIndicator = YES;
scrollView.delegate = self;
[scrollView addSubview:viewController.view];
[window makeKeyAndVisible];
}
- (void)dealloc {
[viewController release];
[scrollView release];
[window release];
[super dealloc];
}
Echa un vistazo a mi solution :
#import “JWTwoFingerScrollView.h”
@implementation JWTwoFingerScrollView
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
for (UIGestureRecognizer* r in self.gestureRecognizers) {
NSLog(@“%@”,[r class]);
if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
[((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
[((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
}
}
}
return self;
}
-(void)firstTouchTimerFired:(NSTimer*)timer {
[self setCanCancelContentTouches:NO];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self setCanCancelContentTouches:YES];
if ([event allTouches].count == 1){
touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(firstTouchTimerFired:) userInfo: nil repeats:NO];
[touchesBeganTimer retain];
[touchFilter touchesBegan:touches withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[touchFilter touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@“ended %i”,[event allTouches].count);
[touchFilter touchesEnded:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@“canceled %i”,[event allTouches].count);
[touchFilter touchesCancelled:touches withEvent:event];
}
@end
No retrasa el primer toque y no se detiene cuando el usuario toca con dos dedos después de usar uno. Aún así, permite cancelar un evento de un toque recién iniciado utilizando un temporizador.
En el SDK 3.2, el manejo táctil de UIScrollView se maneja usando gestores reconocedores.
Si desea realizar una panorámica de dos dedos en lugar de la panorámica de un dedo predeterminada, puede utilizar el siguiente código:
for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *) gestureRecognizer;
panGR.minimumNumberOfTouches = 2;
}
}
En iOS 3.2+ ahora puedes lograr el desplazamiento con dos dedos con bastante facilidad. Simplemente agregue un reconocedor de gestos de paneo a la vista de desplazamiento y establezca su número máximo de Toques de Números 1. Tendrá todos los desplazamientos de un solo dedo, pero permitirá que más de 2 desplazamientos de dedo pasen por la cadena al reconocedor de gestos de pan incorporado en la vista de desplazamiento (y por lo tanto permitir el comportamiento de desplazamiento normal).
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(recognizePan:)];
panGestureRecognizer.maximumNumberOfTouches = 1;
[scrollView addGestureRecognizer:panGestureRecognizer];
[panGestureRecognizer release];
Estas respuestas son un desastre, ya que solo puedes encontrar la respuesta correcta leyendo todas las demás respuestas y los comentarios (la respuesta más cercana obtuvo la pregunta al revés). La respuesta aceptada es demasiado vaga para ser útil, y sugiere un método diferente.
Sintetizando, esto funciona.
// makes it so that only two finger scrolls go
for (id gestureRecognizer in self.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *panGR = gestureRecognizer;
panGR.minimumNumberOfTouches = 2;
panGR.maximumNumberOfTouches = 2;
}
}
Esto requiere dos dedos para un pergamino. He hecho esto en una subclase, pero si no, simplemente reemplaza self.gestureRecognizers
con myScrollView.gestureRecognizers
y listo.
Lo único que agregué es usar id
para evitar un reparto feo :)
Esto funciona, pero puede ser bastante complicado si quiere que su UIScrollView haga zoom también ... los gestos no funcionan correctamente, ya que pisar y hacer zoom para desplazarse luchan. Voy a actualizar esto si encuentro una respuesta adecuada.
Este parece ser el mejor recurso para esta pregunta en Internet. Otra solución cercana se puede encontrar aquí .
Resolví este problema de una manera muy satisfactoria de una manera diferente, esencialmente al reemplazar mi reconocedor de gestos en la ecuación. Recomiendo encarecidamente que cualquier persona que esté tratando de lograr el efecto solicitado por el póster original considere esta alternativa en comparación con la subclasificación agresiva de UIScrollView
.
El siguiente proceso proporcionará:
Un
UIScrollView
contiene su vista personalizadaZoom y
UIPinchGestureRecognizer
con dos dedos (a través deUIPinchGestureRecognizer
)Procesamiento de eventos de su vista para todos los demás toques
Primero, supongamos que tiene un controlador de vista y su vista. En IB, convierta la vista en una subvista de un scrollView y ajuste las reglas de cambio de tamaño de su vista para que no cambie de tamaño. En los atributos de la vista de desplazamiento, active cualquier elemento que diga "rebotar" y desactive " delaysContentTouches
". También debe configurar el zoom mínimo y máximo en un valor distinto al predeterminado de 1.0 para, como dicen los documentos de Apple, esto es necesario para que el zoom funcione.
Cree una subclase personalizada de UIScrollView
y haga que esta vista de desplazamiento sea esa subclase personalizada. Agregue una salida a su controlador de vista para la vista de desplazamiento y conéctelos. Ahora estás totalmente configurado.
Deberá agregar el siguiente código a la subclase UIScrollView
para que pase de manera transparente los eventos táctiles (sospecho que esto podría hacerse de manera más elegante, tal vez incluso evitando la subclase por completo):
#pragma mark -
#pragma mark Event Passing
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesEnded:touches withEvent:event];
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return NO;
}
Agregue este código a su controlador de vista:
- (void)setupGestures {
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
[self.view addGestureRecognizer:pinchGesture];
[pinchGesture release];
}
- (IBAction)handlePinchGesture:(UIPinchGestureRecognizer *)sender {
if ( sender.state == UIGestureRecognizerStateBegan ) {
//Hold values
previousLocation = [sender locationInView:self.view];
previousOffset = self.scrollView.contentOffset;
previousScale = self.scrollView.zoomScale;
} else if ( sender.state == UIGestureRecognizerStateChanged ) {
//Zoom
[self.scrollView setZoomScale:previousScale*sender.scale animated:NO];
//Move
location = [sender locationInView:self.view];
CGPoint offset = CGPointMake(previousOffset.x+(previousLocation.x-location.x), previousOffset.y+(previousLocation.y-location.y));
[self.scrollView setContentOffset:offset animated:NO];
} else {
if ( previousScale*sender.scale < 1.15 && previousScale*sender.scale > .85 )
[self.scrollView setZoomScale:1.0 animated:YES];
}
}
Tenga en cuenta que en este método hay referencias a una serie de propiedades que debe definir en los archivos de clase de su controlador de vista:
-
CGFloat previousScale;
-
CGPoint previousOffset;
-
CGPoint previousLocation;
-
CGPoint location;
¡Vale eso es todo!
Desafortunadamente, no pude obtener el scrollView para mostrar sus scrollers durante el gesto. Probé todas estas estrategias:
//Scroll indicators
self.scrollView.showsVerticalScrollIndicator = YES;
self.scrollView.showsVerticalScrollIndicator = YES;
[self.scrollView flashScrollIndicators];
[self.scrollView setNeedsDisplay];
Una cosa que realmente disfruté es que si miras la última línea, notarás que capta cualquier zoom final que sea alrededor del 100% y simplemente lo redondea. Puedes ajustar tu nivel de tolerancia; Había visto esto en el comportamiento del zoom de Pages y pensé que sería un buen toque.
La respuesta de en Swift 4
for gestureRecognizer: UIGestureRecognizer in self.gestureRecognizers! {
if (gestureRecognizer is UIPanGestureRecognizer) {
let panGR = gestureRecognizer as? UIPanGestureRecognizer
panGR?.minimumNumberOfTouches = 2
}
}
Lo que hago es hacer que mi controlador de vista configure la vista de desplazamiento:
[scrollView setCanCancelContentTouches:NO];
[scrollView setDelaysContentTouches:NO];
Y en la vista de mi hijo tengo un temporizador porque los toques con dos dedos generalmente comienzan como un dedo seguido rápidamente por dos dedos:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Hand tool or two or more touches means a pan or zoom gesture.
if ((selectedTool == kHandToolIndex) || (event.allTouches.count > 1)) {
[[self parentScrollView] setCanCancelContentTouches:YES];
[firstTouchTimer invalidate];
firstTouchTimer = nil;
return;
}
// Use a timer to delay first touch because two-finger touches usually start with one touch followed by a second touch.
[[self parentScrollView] setCanCancelContentTouches:NO];
anchorPoint = [[touches anyObject] locationInView:self];
firstTouchTimer = [NSTimer scheduledTimerWithTimeInterval:kFirstTouchTimeInterval target:self selector:@selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
firstTouchTimeStamp = event.timestamp;
}
Si aparece un segundo evento Empieza con: más de un dedo, la vista de desplazamiento puede cancelar los toques. Por lo tanto, si el usuario explora dos dedos, esta vista recibirá un mensaje touchesCanceled:
Malas noticias: iPhone SDK 3.0 y -touchesBegan:
posteriores, no pases toques a -touchesBegan:
and - touchesEnded:
** UIScrollview
** métodos de subclases más. Puede usar los métodos touchesShouldBegin
y touchesShouldCancelInContentView
que no sean los mismos.
Si realmente desea obtener estos toques, tenga un truco que lo permita.
En tu subclase de UIScrollView
reemplaza el método hitTest
esta manera:
- (UIView *)hitTest:(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;
}
Esto le pasará a su subclase estos toques, sin embargo, no puede cancelar los toques a la súper clase UIScrollView
.
Necesita subclase UIScrollView (¡por supuesto!). Entonces necesitas:
Haga eventos con un solo dedo para ir a su vista de contenido (fácil), y
haga que los eventos con dos dedos desplacen la vista de desplazamiento (puede ser fácil, puede ser difícil, puede ser imposible).
La sugerencia de Patrick generalmente está bien: infórmele a su subclase UIScrollView sobre su vista de contenido, luego en contacto con los controladores de eventos verifique el número de dedos y reenvíe el evento en consecuencia. Solo asegúrese de que (1) los eventos que envíe a la vista de contenido no vuelvan a UIScrollView a través de la cadena de respondedores (es decir, asegúrese de manejarlos todos), (2) respete el flujo habitual de eventos táctiles (es decir, tocaesBegan, que un número de {touchesBegan, touchesMoved, touchesEnded}, terminado con los toquesEnded o touchesCancelled), especialmente cuando se trata de UIScrollView. # 2 puede ser complicado.
Si decide que el evento es para UIScrollView, otro truco es hacer que UIScrollView crea que su gesto de dos dedos es en realidad un gesto de un dedo (porque UIScrollView no se puede desplazar con dos dedos). Intente pasar solo los datos de un dedo a super (filtrando el argumento de los (NSSet *)touches
, tenga en cuenta que solo contiene los toques modificados e ignorando los eventos del dedo equivocado).
Si eso no funciona, estás en problemas. En teoría, puede intentar crear toques artificiales para alimentar a UIScrollView creando una clase que se parece a la de UITouch. El código C subyacente no verifica los tipos, por lo que tal vez funcione (YourTouch *) en (UITouch *), y usted podrá engañar a UIScrollView para que maneje los toques que realmente no sucedieron.
Probablemente quieras leer mi artículo sobre trucos avanzados de UIScrollView (y ver un código de muestra de UIScrollView totalmente no relacionado allí).
Por supuesto, si no puede hacer que funcione, siempre existe la opción de controlar el movimiento de UIScrollView manualmente o usar una vista de desplazamiento completamente personalizada. Hay una clase TTScrollView en la biblioteca Three20 ; no se siente bien para el usuario, pero se siente bien para el programador.
Para iOS 5+, configurar esta propiedad tiene el mismo efecto que la respuesta de Mike Laurence:
self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;
El arrastre de un dedo es ignorado por panGestureRecognizer y así el evento de arrastre de un dedo pasa a la vista de contenido.
Puse esto en el método viewDidLoad y esto logra que la vista de desplazamiento controle el comportamiento de los dos controles táctiles y otro manejador de gestos de movimientos panorámicos que maneje el comportamiento de los controles de un solo toque
scrollView.panGestureRecognizer.minimumNumberOfTouches = 2
let panGR = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGR.minimumNumberOfTouches = 1
panGR.maximumNumberOfTouches = 1
scrollView.gestureRecognizers?.append(panGR)
y en el método handlePan que es una función adjunta al ViewController hay simplemente una declaración de impresión para verificar que el método se está ingresando ->
@IBAction func handlePan(_ sender: UIPanGestureRecognizer) {
print("Entered handlePan numberOfTuoches: /(sender.numberOfTouches)")
}
HTH
Sí, deberá subclasificar UIScrollView
y anular sus - touchesBegan:
y -touchesEnded:
métodos para pasar "toques". Esto probablemente también involucrará a la subclase que tiene una variable de miembro UIView
para que sepa lo que está destinado a pasar los retoques.
Tengo una mejora adicional al código anterior. El problema era que, incluso después de configurar setCanCancelContentTouches:NO
Tenemos el problema, que un gesto de zoom se interrumpirá con el contenido. No cancelará el toque de contenido, pero permitirá el zoom mientras tanto. Para evitar esto, bloqueo el zoom configurando la configuración mínima de zoom y la máxima de zoom con los mismos valores cada vez que se dispara el temporizador.
Un comportamiento bastante extraño es que cuando un evento de un dedo se cancela con un gesto de dos dedos dentro del período de tiempo permitido, el temporizador se retrasará. Se dispara después de que se llama el evento TouchCanceled. Así que tenemos el problema, que intentamos bloquear el zoom aunque el evento ya está cancelado y, por lo tanto, deshabilitamos el zoom para el próximo evento. Para manejar este comportamiento, el método de devolución de llamada del temporizador verifica si se llamó antes a touchesCanceled. @implementation JWTwoFingerScrollView
#pragma mark -
#pragma mark Event Passing
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
for (UIGestureRecognizer* r in self.gestureRecognizers) {
if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
[((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
[((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
zoomScale[0] = -1.0;
zoomScale[1] = -1.0;
}
timerWasDelayed = NO;
}
}
return self;
}
-(void)lockZoomScale {
zoomScale[0] = self.minimumZoomScale;
zoomScale[1] = self.maximumZoomScale;
[self setMinimumZoomScale:self.zoomScale];
[self setMaximumZoomScale:self.zoomScale];
NSLog(@"locked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
}
-(void)unlockZoomScale {
if (zoomScale[0] != -1 && zoomScale[1] != -1) {
[self setMinimumZoomScale:zoomScale[0]];
[self setMaximumZoomScale:zoomScale[1]];
zoomScale[0] = -1.0;
zoomScale[1] = -1.0;
NSLog(@"unlocked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"began %i",[event allTouches].count);
[self setCanCancelContentTouches:YES];
if ([event allTouches].count == 1){
touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
[touchesBeganTimer retain];
[touchFilter touchesBegan:touches withEvent:event];
}
}
//if one finger touch gets canceled by two finger touch, this timer gets delayed
// so we can! use this method to disable zooming, because it doesnt get called when two finger touch events are wanted; otherwise we would disable zooming while zooming
-(void)firstTouchTimerFired:(NSTimer*)timer {
NSLog(@"fired");
[self setCanCancelContentTouches:NO];
//if already locked: unlock
//this happens because two finger gesture delays timer until touch event finishes.. then we dont want to lock!
if (timerWasDelayed) {
[self unlockZoomScale];
}
else {
[self lockZoomScale];
}
timerWasDelayed = NO;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// NSLog(@"moved %i",[event allTouches].count);
[touchFilter touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"ended %i",[event allTouches].count);
[touchFilter touchesEnded:touches withEvent:event];
[self unlockZoomScale];
}
//[self setCanCancelContentTouches:NO];
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"canceled %i",[event allTouches].count);
[touchFilter touchesCancelled:touches withEvent:event];
[self unlockZoomScale];
timerWasDelayed = YES;
}
@end
logramos implementar una funcionalidad similar en nuestra aplicación de dibujo para iPhone mediante la subclasificación de UIScrollView y el filtrado de eventos en función de la cantidad de toques de forma simple y ruda:
//OCRScroller.h
@interface OCRUIScrollView: UIScrollView
{
double pass2scroller;
}
@end
//OCRScroller.mm
@implementation OCRUIScrollView
- (id)initWithFrame:(CGRect)aRect {
pass2scroller = 0;
UIScrollView* newv = [super initWithFrame:aRect];
return newv;
}
- (void)setupPassOnEvent:(UIEvent *)event {
int touch_cnt = [[event allTouches] count];
if(touch_cnt<=1){
pass2scroller = 0;
}else{
double timems = double(CACurrentMediaTime()*1000);
pass2scroller = timems+200;
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self setupPassOnEvent:event];
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self setupPassOnEvent:event];
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
pass2scroller = 0;
[super touchesEnded:touches withEvent:event];
}
- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view
{
return YES;
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
double timems = double(CACurrentMediaTime()*1000);
if (pass2scroller == 0 || timems> pass2scroller){
return NO;
}
return YES;
}
@end
ScrollView configurado de la siguiente manera:
scroll_view = [[OCRUIScrollView alloc] initWithFrame:rect];
scroll_view.contentSize = img_size;
scroll_view.contentOffset = CGPointMake(0,0);
scroll_view.canCancelContentTouches = YES;
scroll_view.delaysContentTouches = NO;
scroll_view.scrollEnabled = YES;
scroll_view.bounces = NO;
scroll_view.bouncesZoom = YES;
scroll_view.maximumZoomScale = 10.0f;
scroll_view.minimumZoomScale = 0.1f;
scroll_view.delegate = self;
self.view = scroll_view;
el toque simple no hace nada (puede manejarlo de la manera que necesite), toque con dos dedos para verlo como se espera. no se utiliza GestureRecognizer, por lo que funciona desde iOS 3.1