Как программно увеличить масштаб UIScrollView? - PullRequest
28 голосов
/ 05 октября 2008

Я хотел бы увеличивать и уменьшать масштаб таким образом, чтобы базовый класс не поддерживал.

Например, при получении двойного нажатия.

Ответы [ 5 ]

14 голосов
/ 07 октября 2008

Я отвечаю на свой вопрос, поиграв с вещами и заставив их работать.

Apple имеет очень простой пример этого в своей документации о том, как обрабатывать двойные касания.

Основной подход к выполнению программных масштабов - это сделать это самостоятельно, а затем сообщить UIScrollView, что вы это сделали.

  • Настройка рамки и границ внутреннего вида.
  • Пометить внутренний вид как необходимый дисплей.
  • Сообщите UIScrollView о новом размере контента.
  • Рассчитайте часть своего внутренний взгляд, который должен быть отображается после увеличения и панорамирование UIScrollView к этому место.

Также ключ: как только вы сообщаете UIScrollView о вашем новом размере содержимого, оно, похоже, сбрасывает концепцию текущего уровня масштабирования. Теперь у вас новый коэффициент масштабирования 1,0. Так что вы почти наверняка захотите сбросить минимальный и максимальный коэффициенты масштабирования.

13 голосов
/ 07 октября 2010

Хватит изобретать велосипед! Посмотрите, как это делает яблоко!

ScrollViewSuite -> Страница документации Apple

Прямая ссылка ScrollViewSuite -> XcodeProject

Это именно то, что вы ищете.

Ура! * * 1013

13 голосов
/ 08 мая 2009

ПРИМЕЧАНИЕ: это ужасно устарело. Это примерно с 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
4 голосов
/ 21 марта 2009

Кажется, я понял, на какую документацию ссылался Даррон. В документе «Руководство по программированию iPhone OS» есть раздел «Обработка событий Multi-Touch». Содержит список 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 голосов
/ 18 октября 2008

Даррен, не могли бы вы дать ссылку на пример Apple? Или название, чтобы я мог его найти? Я вижу http://developer.apple.com/iphone/library/samplecode/Touches/index.html, но это не распространяется на увеличение.

Проблема, с которой я сталкиваюсь после программного увеличения, заключается в том, что жест-масштабирование возвращает масштаб обратно к тому, который был до того, как происходило программное увеличение. Кажется, что UIScrollView хранит внутреннее состояние о коэффициенте / уровне масштабирования, но у меня нет убедительных доказательств.

Спасибо, -Эндрю

РЕДАКТИРОВАТЬ: я только что понял, вы работаете вокруг того факта, что у вас мало контроля над внутренним коэффициентом масштабирования UIScrollView путем изменения размера и изменения значения коэффициента масштабирования 1,0. Немного взлома, но кажется, что все Apple оставил нас. Возможно, пользовательский класс может инкапсулировать этот трюк ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...