web-dev-qa-db-fra.com

Comment puis-je zoomer par programme sur UIScrollView?

J'aimerais zoomer et dézoomer d'une manière que la classe de base ne prend pas en charge.

Par exemple, lors de la réception d'un double tap.

28
Darron

Je réponds à ma propre question après avoir joué avec les choses et l'avoir fait fonctionner.

Apple en a un exemple très simple dans sa documentation sur la façon de gérer les doubles tapotements.

L'approche de base pour effectuer des zooms programmatiques consiste à le faire vous-même, puis à dire à UIScrollView que vous l'avez fait.

  • Ajustez le cadre et les limites de la vue interne.
  • Marquez la vue interne comme nécessitant un affichage.
  • Indiquez à UIScrollView la nouvelle taille du contenu.
  • Calculez la partie de votre vue interne qui doit être affichée après le zoom et faites glisser UIScrollView vers cet emplacement.

Également clé: une fois que vous indiquez à UIScrollView la nouvelle taille de votre contenu, il semble réinitialiser son concept du niveau de zoom actuel. Vous êtes maintenant au nouveau facteur de zoom 1.0. Vous voudrez donc certainement réinitialiser les facteurs de zoom minimum et maximum.

14
Darron

Arrêtez de réinventer la roue! Voyez comment Apple le fait!

ScrollViewSuite -> Apple

Lien direct ScrollViewSuite -> XcodeProject

C'est exactement ce que vous recherchez.

À votre santé!

13

REMARQUE: c'est horriblement obsolète. Cela remonte à iOS 2.x et a en fait été corrigé autour d'iOS 3.x.

Gardez-le ici à des fins historiques uniquement.


Je pense avoir trouvé une solution propre à cela et j'ai créé une sous-classe UIScrollView pour l'encapsuler.

L'exemple de code qui illustre à la fois le zoom par programmation (+ gestion du double appui) et la pagination de style photothèque + zoom + défilement, ainsi que la classe ZoomScrollView, est disponible à l'adresse github.com/andreyvit/ScrollingMadness .

En quelques mots, ma solution est de renvoyer une nouvelle vue factice de viewForZoomingInScrollView:, Faisant temporairement de votre vue de contenu (UIImageView, peu importe) son enfant. Dans scrollViewDidEndZooming:, Nous inversons cela, en supprimant la vue fictive et en replaçant votre vue de contenu dans la vue de défilement.

Pourquoi ça aide? C'est un moyen de vaincre l'échelle de vue persistante que nous ne pouvons pas modifier par programme. UIScrollView ne conserve pas l'échelle de vue actuelle elle-même. Au lieu de cela, chaque UIView est capable de conserver son échelle de vue actuelle (à l'intérieur de l'objet UIGestureInfo pointé par le champ _gestureInfo). En fournissant un nouvel UIView pour chaque opération de zoom, nous commençons toujours avec une échelle de zoom de 1,00.

Et comment cela aide? Nous stockons nous-mêmes l'échelle de zoom actuelle et l'appliquons manuellement à notre vue de contenu, par ex. contentView.transform = CGAffineTransformMakeScale(zoomScale, zoomScale). Cependant, cela entre en conflit avec UIScrollView qui souhaite réinitialiser la transformation lorsque l'utilisateur pince la vue. En donnant à UIScrollView une autre vue avec transformation d'identité pour zoomer, nous ne nous battons plus pour transformer la même vue. UIScrollView peut heureusement croire qu'il commence avec un zoom de 1,00 à chaque fois et met à l'échelle une vue en commençant par une transformation d'identité, et sa vue intérieure a une transformation appliquée correspondant à notre échelle de zoom actuelle.

Maintenant, ZoomScrollView encapsule tout cela. Voici son code par souci d'exhaustivité, mais je recommande vraiment de télécharger l'exemple de projet depuis GitHub (vous n'avez pas besoin d'utiliser Git, il y a un bouton Télécharger). Si vous souhaitez être informé des mises à jour d'exemples de code (et vous devriez - je prévois de maintenir et de mettre à jour cette classe!), Suivez le projet sur GitHub ou envoyez-moi un e-mail à [email protected].

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

La mise en oeuvre:

@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
13

Je pense avoir compris à quelle documentation Darron faisait référence. Dans le document "Guide de programmation iPhone OS", il y a une section "Gestion des événements Multi-Touch". Cela contient la liste 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];
    }
 }

}
4
n8 g

Darren, pouvez-vous fournir un lien vers ledit Apple? Ou le titre pour que je puisse le trouver? Je vois http://developer.Apple.com/iphone/library /samplecode/Touches/index.html , mais cela ne couvre pas le zoom.

Le problème que je vois après un zoom programmatique est qu'un zoom gestuel ramène le zoom à ce qu'il était avant le zoom programmatique. Il semble que UIScrollView garde l'état en interne du facteur/niveau de zoom, mais je n'ai pas de preuves concluantes.

Merci, -andrew

EDIT: Je viens de réaliser que vous travaillez autour du fait que vous avez peu de contrôle sur le facteur de zoom interne d'UIScrollView en redimensionnant et en modifiant la signification du facteur de zoom 1.0. Un peu de hack, mais il semble que tout ce que Apple nous a laissé. Peut-être qu'une classe personnalisée pourrait encapsuler cette astuce ...

0