Como faço zoom programaticamente um UIScrollView?

Eu gostaria de ampliar e descompactar de maneiras que a class base não suporta.

Por exemplo, ao receber um toque duplo.

Estou respondendo a minha própria pergunta, depois de brincar com as coisas e fazê-lo funcionar.

A Apple tem um exemplo muito simples disso em sua documentação sobre como lidar com dois toques.

A abordagem básica para fazer zooms programáticos é fazer você mesmo e depois informar ao UIScrollView que você fez isso.

  • Ajuste o quadro e os limites da vista interna.
  • Marque a vista interna como precisando de exibição.
  • Diga ao UIScrollView sobre o novo tamanho do conteúdo.
  • Calcule a parte da sua visão interna que deve ser exibida após o zoom, e tenha o pan UIScrollView para esse local.

Também chave: uma vez que você diz ao UIScrollView sobre seu novo tamanho de conteúdo, ele parece redefinir seu conceito do nível de zoom atual. Você está agora no novo fator de zoom 1.0. Portanto, você certamente desejará redefinir os fatores de zoom mínimo e máximo.

Pare de reinventar a roda! Veja como a Apple faz isso!

ScrollViewSuite -> Página de documentação da Apple

Link direto do ScrollViewSuite -> XcodeProject

É exatamente o que você está procurando.

Felicidades!

NOTA: isso é terrivelmente desatualizado. Está em torno do iOS 2.x vezes e foi realmente corrigido em torno do iOS 3.x.

Mantê-lo aqui apenas para fins históricos.


Acho que encontrei uma solução limpa para isso e criei uma subclass UIScrollView para encapsulá-la.

O código de exemplo que ilustra o zoom programático (+ manipulação de toque duplo) e paginação em estilo de biblioteca de fotos + zoom + rolagem, junto com a class ZoomScrollView, está disponível em github.com/andreyvit/ScrollingMadness .

Em poucas palavras, minha solução é retornar uma nova visão fictícia do viewForZoomingInScrollView: temporariamente fazendo sua exibição de conteúdo (UIImageView, qualquer que seja) seu filho. Em scrollViewDidEndZooming: invertemos isso, descartando a visualização fictícia e movendo sua visualização de conteúdo de volta para a visualização de rolagem.

Por que isso ajuda? É uma maneira de derrotar a escala de exibição persistente que não podemos alterar programaticamente. O UIScrollView não mantém a escala de visão atual em si. Em vez disso, cada UIView é capaz de manter sua escala de visão atual (dentro do object UIGestureInfo apontado pelo campo _gestureInfo). Ao fornecer um novo UIView para cada operação de zoom, sempre começamos com a escala de zoom 1,00.

E como isso ajuda? Nós armazenamos a escala de zoom atual e a aplicamos manualmente em nossa visualização de conteúdo, por exemplo, contentView.transform = CGAffineTransformMakeScale(zoomScale, zoomScale) . No entanto, isso conflita com o UIScrollView que deseja redefinir a transformação quando o usuário aperta a exibição. Dando ao UIScrollView outra visão com transformação de identidade para zoom, não lutamos mais por transformar a mesma visão. O UIScrollView pode felizmente acreditar que ele começa com zoom de 1,00 cada vez e dimensiona uma visualização começando com uma transformação de identidade, e sua visão interna tem uma transformação aplicada correspondente à nossa escala de zoom atual real.

Agora, ZoomScrollView encapsula tudo isso. Aqui está o seu código por questão de completude, no entanto eu realmente recomendo fazer o download do projeto de amostra do GitHub (você não precisa usar o Git, existe um botão de Download lá). Se você deseja ser notificado sobre atualizações de código de amostra (e você deve – estou planejando manter e atualizar esta class!), Siga o projeto no GitHub ou envie-me um e-mail para andreyvit@gmail.com.

Interface:

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

Implementação:

 @interface ZoomScrollView (DelegateMethods)  @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)realDelegate { return _realDelegate; } - (void)setDelegate:(id)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 

Eu acho que descobri a documentação que Darron estava se referindo. No documento “iPhone OS Programming Guide” há uma seção “Manipulando events Multi-Touch”. Isso contém a listview 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]; } } } 

Darren, você pode fornecer um link para o exemplo da Apple? Ou o título para que eu possa achá-lo? Eu vejo http://developer.apple.com/iphone/library/samplecode/Touches/index.html , mas isso não cobre o zoom.

O problema que estou vendo depois de um zoom de programação é que um zoom de gesto faz com que o zoom volte ao que era antes do zoom programático ocorrer. Parece que o UIScrollView mantém o estado internamente sobre o fator / nível de zoom, mas não tenho evidências conclusivas.

Obrigado, -andrew

EDIT: Acabei de perceber, você está trabalhando em torno do fato de que você tem pouco controle sobre o fator de zoom interno do UIScrollView redimensionando e alterando o significado de fator de zoom 1.0. Um pouco de um hack, mas parece que todos os da Apple nos deixaram com. Talvez uma class personalizada possa encapsular esse truque …