ПРИМЕЧАНИЕ: это ужасно устарело. Это примерно с iOS 2.x раз, и фактически было исправлено вокруг iOS 3.x.
Хранение здесь только в исторических целях.
Я думаю, что нашел чистое решение для этого, и я создал подкласс UIScrollView для его инкапсуляции.
Пример кода, который иллюстрирует как программное масштабирование (+ обработка двойным касанием), так и пейджинг в стиле библиотеки фотографий + масштабирование + прокрутка вместе с классом ZoomScrollView, доступно по адресу github.com / andreyvit / ScrollingMadness .
В двух словах, мое решение состоит в том, чтобы вернуть новое фиктивное представление из viewForZoomingInScrollView:
, временно сделав ваше представление содержимого (UIImageView, независимо от того, что это) его дочерним. В scrollViewDidEndZooming:
мы изменим это, избавившись от фиктивного представления и переместив ваше представление контента обратно в представление прокрутки.
Почему это помогает? Это способ победить постоянную шкалу представлений, которую мы не можем изменить программно. UIScrollView не сохраняет сам текущий масштаб представления. Вместо этого каждый UIView способен сохранять свой текущий масштаб представления (внутри объекта UIGestureInfo, на который указывает поле _gestureInfo). Предоставляя новый UIView для каждой операции масштабирования, мы всегда начинаем с масштабирования 1,00.
И как это помогает? Мы сохраняем текущую шкалу масштабирования самостоятельно и применяем ее вручную к нашему представлению содержимого, например, contentView.transform = CGAffineTransformMakeScale(zoomScale, zoomScale)
. Это, однако, конфликтует с UIScrollView, желающим сбросить преобразование, когда пользователь сжимает представление. Предоставляя UIScrollView другое представление с преобразованием идентичности для увеличения, мы больше не боремся за преобразование того же представления. UIScrollView может с уверенностью полагать, что он начинается с масштаба 1,00 каждый раз и масштабирует представление, начиная с преобразования идентичности, а к его внутреннему виду применяется преобразование, соответствующее нашему текущему текущему масштабу масштабирования.
Теперь ZoomScrollView инкапсулирует все это. Вот его код для полноты картины, однако я действительно рекомендую скачать пример проекта с GitHub (вам не нужно использовать Git, там есть кнопка «Скачать»). Если вы хотите получать уведомления об обновлениях примеров кода (и вы должны - я планирую поддерживать и обновлять этот класс!), Либо следите за проектом на GitHub, либо отправьте мне электронное письмо по адресу andreyvit@gmail.com.
Интерфейс:
/*
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
Реализация:
@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