2008-10-05 12 views

Respuesta

14

Estoy respondiendo a mi propia pregunta, después de jugar con las cosas y ponerlo en funcionamiento.

Apple tiene un ejemplo muy simple de esto en su documentación sobre cómo manejar los dobles toques.

El enfoque básico para hacer zooms programáticos es hacerlo usted mismo, y luego decirle al UIScrollView que lo hizo.

  • Ajuste el marco y los límites de la vista interna.
  • Marque la vista interna como que necesita visualización.
  • Dígale a UIScrollView acerca del nuevo tamaño del contenido.
  • Calcular la parte de su vista interna que debe ser muestra después del zoom, y tienen la sartén UIScrollView a que ubicación.

También clave: una vez que le dice al UIScrollView acerca de su nuevo tamaño de contenido, parece restablecer su concepto del nivel de zoom actual. Ahora se encuentra en el nuevo factor de zoom 1.0. Por lo tanto, seguramente querrá restablecer los factores de zoom mínimo y máximo.

+3

Sería genial lanzar algunos códigos de ejemplo en github. – tcurdt

+0

[Cezar Alexandru Vancea] (http://stackoverflow.com/users/1996969/cezar-alexandru-vancea) tiene la respuesta: ¡Deje de reinventar la rueda! ¡Mira cómo lo hace Apple! – Gonen

0

Darren, ¿puede proporcionar un enlace a dicho ejemplo de Apple? ¿O el título para que pueda encontrarlo? Veo http://developer.apple.com/iphone/library/samplecode/Touches/index.html, pero eso no cubre el zoom.

El problema que estoy viendo después de un zoom programático es que un gesto-zoom ajusta el zoom de nuevo a lo que era antes de que ocurriera el zoom programático. Parece que UIScrollView mantiene el estado internamente sobre el factor/nivel de zoom, pero no tengo evidencia concluyente.

Gracias, -Andrew

EDIT: Me acabo de dar cuenta, que están trabajando en torno al hecho de que tiene poco control sobre el factor de zoom interno de UIScrollView cambiando el tamaño y cambiar el significado de zoom-factor de 1,0. Un poco hackeo, pero parece que todos los Apple nos dejaron. Tal vez una clase personalizada podría encapsular este truco ...

4

Creo que descubrí a qué documentación se refería Darron. En el documento "Guía de programación de iPhone OS" hay una sección "Manejo de eventos Multi-Touch". Que contiene el listado 7-1:

- (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { 
UIScrollView *scrollView = (UIScrollView*)[self superview]; 
UITouch  *touch = [touches anyObject]; 
CGSize  size; 
CGPoint  point; 

if([touch tapCount] == 2) { 
    if(![_viewController _isZoomed]) { 
     point = [touch locationInView:self]; 
     size = [self bounds].size; 
     point.x /= size.width; 
     point.y /= size.height; 

     [_viewController _setZoomed:YES]; 

     size = [scrollView contentSize]; 
     point.x *= size.width; 
     point.y *= size.height; 
     size = [scrollView bounds].size; 
     point.x -= size.width/2; 
     point.y -= size.height/2; 
     [scrollView setContentOffset:point animated:NO]; 
    } 
     else 
     [_viewController _setZoomed:NO]; 
    } 
} 

} 
+0

Sí, eso es todo. Lo siento, nunca lo volví a perseguir y lo publiqué yo mismo. – Darron

13

NOTA: esto está terriblemente desactualizado. Se trata de iOS 2.x veces, y se ha corregido en iOS 3.x.

Conservarlo aquí solo con fines históricos.


creo que he encontrado una solución limpia para esto, y me hizo una subclase UIScrollView a encapsular.

El código de ejemplo que ilustra el zoom programático (+ manejo de doble toque) y el paginado estilo Biblioteca de fotos + acercamiento + desplazamiento, junto con la clase ZoomScrollView, está disponible en github.com/andreyvit/ScrollingMadness.

En pocas palabras, mi solución es devolver una nueva vista ficticia desde viewForZoomingInScrollView:, convirtiendo temporalmente su vista de contenido (UIImageView, whatever) en hija. En scrollViewDidEndZooming: invertimos eso, eliminando la vista ficticia y moviendo su vista de contenido nuevamente a la vista de desplazamiento.

¿Por qué es útil? Es una forma de derrotar la escala de visión persistente que no podemos cambiar programáticamente. UIScrollView no mantiene la escala de la vista actual. En cambio, cada UIView es capaz de mantener su escala de vista actual (dentro del objeto UIGestureInfo apuntado por el campo _gestureInfo). Al proporcionar una nueva UIView para cada operación de acercamiento, siempre comenzamos con la escala de zoom 1.00.

¿Y cómo funciona que ayuda? Almacenamos la escala de zoom actual nosotros mismos, y la aplicamos de forma manual a nuestra vista de contenido, p. contentView.transform = CGAffineTransformMakeScale(zoomScale, zoomScale). Sin embargo, esto entra en conflicto con UIScrollView que desea restablecer la transformación cuando el usuario pellizca la vista. Al dar a UIScrollView otra vista con la transformación de identidad para ampliar, ya no luchamos por transformar la misma vista. UIScrollView puede creer felizmente que comienza con zoom 1.00 cada vez y escala una vista que comienza con una transformación de identidad, y su vista interna tiene una transformación aplicada correspondiente a nuestra escala actual de zoom actual.

Ahora, ZoomScrollView encapsula todo esto. Aquí está su código para completar, sin embargo, realmente recomiendo descargar el proyecto de muestra de GitHub (no es necesario que use Git, allí hay un botón de Descargar). Si desea que se le notifique sobre actualizaciones de código de muestra (¡y debe - estoy planeando mantener y actualizar esta clase!), Siga el proyecto en GitHub o envíeme un correo electrónico a [email protected]

Interfaz:

/* 
ZoomScrollView makes UIScrollView easier to use: 

- ZoomScrollView is a drop-in replacement subclass of UIScrollView 

- ZoomScrollView adds programmatic zooming 
    (see `setZoomScale:centeredAt:animated:`) 

- ZoomScrollView allows you to get the current zoom scale 
    (see `zoomScale` property) 

- ZoomScrollView handles double-tap zooming for you 
    (see `zoomInOnDoubleTap`, `zoomOutOnDoubleTap`) 

- ZoomScrollView forwards touch events to its delegate, allowing to handle 
    custom gestures easily (triple-tap? two-finger scrolling?) 

Drop-in replacement: 

You can replace `[UIScrollView alloc]` with `[ZoomScrollView alloc]` or change 
class in Interface Builder, and everything should continue to work. The only 
catch is that you should not *read* the 'delegate' property; to get your delegate, 
please use zoomScrollViewDelegate property instead. (You can set the delegate 
via either of these properties, but reading 'delegate' does not work.) 

Zoom scale: 

Reading zoomScale property returns the scale of the last scaling operation. 
If your viewForZoomingInScrollView can return different views over time, 
please keep in mind that any view you return is instantly scaled to zoomScale. 

Delegate: 

The delegate accepted by ZoomScrollView is a regular UIScrollViewDelegate, 
however additional methods from `NSObject(ZoomScrollViewDelegateMethods)` category 
will be called on your delegate if defined. 

Method `scrollViewDidEndZooming:withView:atScale:` is called after any 'bounce' 
animations really finish. UIScrollView often calls it earlier, violating 
the documented contract of UIScrollViewDelegate. 

Instead of reading 'delegate' property (which currently returns the scroll 
view itself), you should read 'zoomScrollViewDelegate' property which 
correctly returns your delegate. Setting works with either of them (so you 
can still set your delegate in the Interface Builder). 

*/ 

@interface ZoomScrollView : UIScrollView { 
@private 
    BOOL _zoomInOnDoubleTap; 
    BOOL _zoomOutOnDoubleTap; 
    BOOL _zoomingDidEnd; 
    BOOL _ignoreSubsequentTouches;        // after one of delegate touch methods returns YES, subsequent touch events are not forwarded to UIScrollView 
    float _zoomScale; 
    float _realMinimumZoomScale, _realMaximumZoomScale;   // as set by the user (UIScrollView's min/maxZoomScale == our min/maxZoomScale divided by _zoomScale) 
    id _realDelegate;      // as set by the user (UIScrollView's delegate is set to self) 
    UIView *_realZoomView;      // the view for zooming returned by the delegate 
    UIView *_zoomWrapperView;    // the disposable wrapper view actually used for zooming 
} 

// if both are enabled, zoom-in takes precedence unless the view is at maximum zoom scale 
@property(nonatomic, assign) BOOL zoomInOnDoubleTap; 
@property(nonatomic, assign) BOOL zoomOutOnDoubleTap; 

@property(nonatomic, assign) id<UIScrollViewDelegate> zoomScrollViewDelegate; 

@end 

@interface ZoomScrollView (Zooming) 

@property(nonatomic, assign) float zoomScale;      // from minimumZoomScale to maximumZoomScale 

- (void)setZoomScale:(float)zoomScale animated:(BOOL)animated; // centerPoint == center of the scroll view 
- (void)setZoomScale:(float)zoomScale centeredAt:(CGPoint)centerPoint animated:(BOOL)animated; 

@end 

@interface NSObject (ZoomScrollViewDelegateMethods) 

// return YES to stop processing, NO to pass the event to UIScrollView (mnemonic: default is to pass, and default return value in Obj-C is NO) 
- (BOOL)zoomScrollView:(ZoomScrollView *)zoomScrollView touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; 
- (BOOL)zoomScrollView:(ZoomScrollView *)zoomScrollView touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; 
- (BOOL)zoomScrollView:(ZoomScrollView *)zoomScrollView touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; 
- (BOOL)zoomScrollView:(ZoomScrollView *)zoomScrollView touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; 

@end 

Implementación:

@interface ZoomScrollView (DelegateMethods) <UIScrollViewDelegate> 
@end 

@interface ZoomScrollView (ZoomingPrivate) 
- (void)_setZoomScaleAndUpdateVirtualScales:(float)zoomScale;   // set UIScrollView's minimumZoomScale/maximumZoomScale 
- (BOOL)_handleDoubleTapWith:(UITouch *)touch; 
- (UIView *)_createWrapperViewForZoomingInsteadOfView:(UIView *)view; // create a disposable wrapper view for zooming 
- (void)_zoomDidEndBouncing; 
- (void)_programmaticZoomAnimationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(UIView *)context; 
- (void)_setTransformOn:(UIView *)view; 
@end 


@implementation ZoomScrollView 

@synthesize zoomInOnDoubleTap=_zoomInOnDoubleTap, zoomOutOnDoubleTap=_zoomOutOnDoubleTap; 
@synthesize zoomScrollViewDelegate=_realDelegate; 

- (id)initWithFrame:(CGRect)frame { 
    if (self = [super initWithFrame:frame]) { 
     _zoomScale = 1.0f; 
     _realMinimumZoomScale = super.minimumZoomScale; 
     _realMaximumZoomScale = super.maximumZoomScale; 
     super.delegate = self; 
    } 
    return self; 
} 

- (id)initWithCoder:(NSCoder *)aDecoder { 
    if (self = [super initWithCoder:aDecoder]) { 
     _zoomScale = 1.0f; 
     _realMinimumZoomScale = super.minimumZoomScale; 
     _realMaximumZoomScale = super.maximumZoomScale; 
     super.delegate = self; 
    } 
    return self; 
} 

- (id<UIScrollViewDelegate>)realDelegate { 
    return _realDelegate; 
} 
- (void)setDelegate:(id<UIScrollViewDelegate>)delegate { 
    _realDelegate = delegate; 
} 

- (float)minimumZoomScale { 
    return _realMinimumZoomScale; 
} 
- (void)setMinimumZoomScale:(float)value { 
    _realMinimumZoomScale = value; 
    [self _setZoomScaleAndUpdateVirtualScales:_zoomScale]; 
} 

- (float)maximumZoomScale { 
    return _realMaximumZoomScale; 
} 
- (void)setMaximumZoomScale:(float)value { 
    _realMaximumZoomScale = value; 
    [self _setZoomScaleAndUpdateVirtualScales:_zoomScale]; 
} 

@end 


@implementation ZoomScrollView (Zooming) 

- (void)_setZoomScaleAndUpdateVirtualScales:(float)zoomScale { 
    _zoomScale = zoomScale; 
    // prevent accumulation of error, and prevent a common bug in the user's code (comparing floats with '==') 
    if (ABS(_zoomScale - _realMinimumZoomScale) < 1e-5) 
     _zoomScale = _realMinimumZoomScale; 
    else if (ABS(_zoomScale - _realMaximumZoomScale) < 1e-5) 
     _zoomScale = _realMaximumZoomScale; 
    super.minimumZoomScale = _realMinimumZoomScale/_zoomScale; 
    super.maximumZoomScale = _realMaximumZoomScale/_zoomScale; 
} 

- (void)_setTransformOn:(UIView *)view { 
    if (ABS(_zoomScale - 1.0f) < 1e-5) 
     view.transform = CGAffineTransformIdentity; 
    else 
     view.transform = CGAffineTransformMakeScale(_zoomScale, _zoomScale); 
} 

- (float)zoomScale { 
    return _zoomScale; 
} 

- (void)setZoomScale:(float)zoomScale { 
    [self setZoomScale:zoomScale animated:NO]; 
} 

- (void)setZoomScale:(float)zoomScale animated:(BOOL)animated { 
    [self setZoomScale:zoomScale centeredAt:CGPointMake(self.frame.size.width/2, self.frame.size.height/2) animated:animated]; 
} 

- (void)setZoomScale:(float)zoomScale centeredAt:(CGPoint)centerPoint animated:(BOOL)animated { 
    if (![_realDelegate respondsToSelector:@selector(viewForZoomingInScrollView:)]) { 
     NSLog(@"setZoomScale called on ZoomScrollView, however delegate does not implement viewForZoomingInScrollView"); 
     return; 
    } 

    // viewForZoomingInScrollView may change contentOffset, and centerPoint is relative to the current one 
    CGPoint origin = self.contentOffset; 
    centerPoint = CGPointMake(centerPoint.x - origin.x, centerPoint.y - origin.y); 

    UIView *viewForZooming = [_realDelegate viewForZoomingInScrollView:self]; 
    if (viewForZooming == nil) 
     return; 

    if (animated) { 
     [UIView beginAnimations:nil context:viewForZooming]; 
     [UIView setAnimationDuration: 0.2]; 
     [UIView setAnimationDelegate: self]; 
     [UIView setAnimationDidStopSelector: @selector(_programmaticZoomAnimationDidStop:finished:context:)]; 
    } 

    [self _setZoomScaleAndUpdateVirtualScales:zoomScale]; 
    [self _setTransformOn:viewForZooming]; 

    CGSize zoomViewSize = viewForZooming.frame.size; 
    CGSize scrollViewSize = self.frame.size; 
    viewForZooming.frame = CGRectMake(0, 0, zoomViewSize.width, zoomViewSize.height); 
    self.contentSize = zoomViewSize; 
    self.contentOffset = CGPointMake(MAX(MIN(zoomViewSize.width*centerPoint.x/scrollViewSize.width - scrollViewSize.width/2, zoomViewSize.width - scrollViewSize.width), 0), 
            MAX(MIN(zoomViewSize.height*centerPoint.y/scrollViewSize.height - scrollViewSize.height/2, zoomViewSize.height - scrollViewSize.height), 0)); 

    if (animated) { 
     [UIView commitAnimations]; 
    } else { 
     [self _programmaticZoomAnimationDidStop:nil finished:nil context:viewForZooming]; 
    } 
} 

- (void)_programmaticZoomAnimationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(UIView *)context { 
    if ([_realDelegate respondsToSelector:@selector(scrollViewDidEndZooming:withView:atScale:)]) 
     [_realDelegate scrollViewDidEndZooming:self withView:context atScale:_zoomScale]; 
} 

- (BOOL)_handleDoubleTapWith:(UITouch *)touch { 
    if (!_zoomInOnDoubleTap && !_zoomOutOnDoubleTap) 
     return NO; 
    if (_zoomInOnDoubleTap && ABS(_zoomScale - _realMaximumZoomScale) > 1e-5) 
     [self setZoomScale:_realMaximumZoomScale centeredAt:[touch locationInView:self] animated:YES]; 
    else if (_zoomOutOnDoubleTap && ABS(_zoomScale - _realMinimumZoomScale) > 1e-5) 
     [self setZoomScale:_realMinimumZoomScale animated:YES]; 
    return YES; 
} 

// the heart of the zooming technique: zooming starts here 
- (UIView *)_createWrapperViewForZoomingInsteadOfView:(UIView *)view { 
    if (_zoomWrapperView != nil) // not sure this is really possible 
     [self _zoomDidEndBouncing]; // ...but just in case cleanup the previous zoom op 

    _realZoomView = [view retain]; 
    [view removeFromSuperview]; 
    [self _setTransformOn:_realZoomView]; // should be already set, except if this is a different view 
    _realZoomView.frame = CGRectMake(0, 0, _realZoomView.frame.size.width, _realZoomView.frame.size.height); 
    _zoomWrapperView = [[UIView alloc] initWithFrame:view.frame]; 
    [_zoomWrapperView addSubview:view]; 
    [self addSubview:_zoomWrapperView]; 

    return _zoomWrapperView; 
} 

// the heart of the zooming technique: zooming ends here 
- (void)_zoomDidEndBouncing { 
    _zoomingDidEnd = NO; 
    [_realZoomView removeFromSuperview]; 
    [self _setTransformOn:_realZoomView]; 
    _realZoomView.frame = _zoomWrapperView.frame; 
    [self addSubview:_realZoomView]; 

    [_zoomWrapperView release]; 
    _zoomWrapperView = nil; 

    if ([_realDelegate respondsToSelector:@selector(scrollViewDidEndZooming:withView:atScale:)]) 
     [_realDelegate scrollViewDidEndZooming:self withView:_realZoomView atScale:_zoomScale]; 
    [_realZoomView release]; 
    _realZoomView = nil; 
} 

@end 


@implementation ZoomScrollView (DelegateMethods) 

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { 
    if ([_realDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) 
     [_realDelegate scrollViewWillBeginDragging:self]; 
} 

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { 
    if ([_realDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) 
     [_realDelegate scrollViewDidEndDragging:self willDecelerate:decelerate]; 
} 

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView { 
    if ([_realDelegate respondsToSelector:@selector(scrollViewWillBeginDecelerating:)]) 
     [_realDelegate scrollViewWillBeginDecelerating:self]; 
} 

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { 
    if ([_realDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]) 
     [_realDelegate scrollViewDidEndDecelerating:self]; 
} 

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView { 
    if ([_realDelegate respondsToSelector:@selector(scrollViewDidEndScrollingAnimation:)]) 
     [_realDelegate scrollViewDidEndScrollingAnimation:self]; 
} 

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { 
    UIView *viewForZooming = nil; 
    if ([_realDelegate respondsToSelector:@selector(viewForZoomingInScrollView:)]) 
     viewForZooming = [_realDelegate viewForZoomingInScrollView:self]; 
    if (viewForZooming != nil) 
     viewForZooming = [self _createWrapperViewForZoomingInsteadOfView:viewForZooming]; 
    return viewForZooming; 
} 

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale { 
    [self _setZoomScaleAndUpdateVirtualScales:_zoomScale * scale]; 

    // often UIScrollView continues bouncing even after the call to this method, so we have to use delays 
    _zoomingDidEnd = YES; // signal scrollViewDidScroll to schedule _zoomDidEndBouncing call 
    [self performSelector:@selector(_zoomDidEndBouncing) withObject:nil afterDelay:0.1]; 
} 

- (void)scrollViewDidScroll:(UIScrollView *)scrollView { 
    if (_zoomWrapperView != nil && _zoomingDidEnd) { 
     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_zoomDidEndBouncing) object:nil]; 
     [self performSelector:@selector(_zoomDidEndBouncing) withObject:nil afterDelay:0.1]; 
    } 

    if ([_realDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) 
     [_realDelegate scrollViewDidScroll:self]; 
} 

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView { 
    if ([_realDelegate respondsToSelector:@selector(scrollViewShouldScrollToTop:)]) 
     return [_realDelegate scrollViewShouldScrollToTop:self]; 
    else 
     return YES; 
} 

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView { 
    if ([_realDelegate respondsToSelector:@selector(scrollViewDidScrollToTop:)]) 
     [_realDelegate scrollViewDidScrollToTop:self]; 
} 

@end 


@implementation ZoomScrollView (EventForwarding) 

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 
    id delegate = self.delegate; 
    if ([delegate respondsToSelector:@selector(zoomScrollView:touchesBegan:withEvent:)]) 
     _ignoreSubsequentTouches = [delegate zoomScrollView:self touchesBegan:touches withEvent:event]; 
    if (_ignoreSubsequentTouches) 
     return; 
    if ([touches count] == 1 && [[touches anyObject] tapCount] == 2) 
     if ([self _handleDoubleTapWith:[touches anyObject]]) 
      return; 
    [super touchesBegan:touches withEvent:event]; 
} 

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 
    id delegate = self.delegate; 
    if ([delegate respondsToSelector:@selector(zoomScrollView:touchesMoved:withEvent:)]) 
     if ([delegate zoomScrollView:self touchesMoved:touches withEvent:event]) { 
      _ignoreSubsequentTouches = YES; 
      [super touchesCancelled:touches withEvent:event]; 
     } 
    if (_ignoreSubsequentTouches) 
     return; 
    [super touchesMoved:touches withEvent:event]; 
} 

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 
    id delegate = self.delegate; 
    if ([delegate respondsToSelector:@selector(zoomScrollView:touchesEnded:withEvent:)]) 
     if ([delegate zoomScrollView:self touchesEnded:touches withEvent:event]) { 
      _ignoreSubsequentTouches = YES; 
      [super touchesCancelled:touches withEvent:event]; 
     } 
    if (_ignoreSubsequentTouches) 
     return; 
    [super touchesEnded:touches withEvent:event]; 
} 

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { 
    id delegate = self.delegate; 
    if ([delegate respondsToSelector:@selector(zoomScrollView:touchesCancelled:withEvent:)]) 
     if ([delegate zoomScrollView:self touchesCancelled:touches withEvent:event]) 
      _ignoreSubsequentTouches = YES; 
    [super touchesCancelled:touches withEvent:event]; 
} 

@end 
+0

Amigo, no puedo encontrar tu proyecto en github..puedes subirlo otra vez ?? –

Cuestiones relacionadas