43 votes

Échelle Max/Min du zoom par pincement dans UIPinchGestureRecognizer - iPhone iOS

Comment pourrais-je limiter l'échelle du UIPinchGestureRecognizer à un niveau min et max ? La propriété d'échelle ci-dessous semble être relative à la dernière échelle connue (le delta du dernier état) et je n'arrive pas à trouver comment fixer une limite à la taille/hauteur de l'objet zoomé.

-(void)scale:(id)sender {

[self.view bringSubviewToFront:[(UIPinchGestureRecognizer*)sender view]];

if([(UIPinchGestureRecognizer*)sender state] == UIGestureRecognizerStateEnded) {
    lastScale = 1.0;
    return;
}

CGFloat pinchscale = [(UIPinchGestureRecognizer*)sender scale];
CGFloat scale = 1.0 - (lastScale - pinchscale);
CGAffineTransform currentTransform = [(UIPinchGestureRecognizer*)sender view].transform;
CGAffineTransform holderTransform = holderView.transform;
CGAffineTransform newTransform = CGAffineTransformScale(currentTransform, scale, scale);
[[(UIPinchGestureRecognizer*)sender view] setTransform:newTransform];

lastScale = [(UIPinchGestureRecognizer*)sender scale];

}

0 votes

Regardez cet échantillon sur le pinch zoom cocoabugs.blogspot.com/2011/03/

110voto

Paul Solt Points 2641

Voici la solution que j'ai trouvée après avoir utilisé la réponse d'Anomie comme point de départ.

- (void)handlePinchGesture:(UIPinchGestureRecognizer *)gestureRecognizer {

    if([gestureRecognizer state] == UIGestureRecognizerStateBegan) {
        // Reset the last scale, necessary if there are multiple objects with different scales
        lastScale = [gestureRecognizer scale];
    }

    if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || 
        [gestureRecognizer state] == UIGestureRecognizerStateChanged) {

        CGFloat currentScale = [[[gestureRecognizer view].layer valueForKeyPath:@"transform.scale"] floatValue];

        // Constants to adjust the max/min values of zoom
        const CGFloat kMaxScale = 2.0;
        const CGFloat kMinScale = 1.0;

        CGFloat newScale = 1 -  (lastScale - [gestureRecognizer scale]); 
        newScale = MIN(newScale, kMaxScale / currentScale);   
        newScale = MAX(newScale, kMinScale / currentScale);
        CGAffineTransform transform = CGAffineTransformScale([[gestureRecognizer view] transform], newScale, newScale);
        [gestureRecognizer view].transform = transform;

        lastScale = [gestureRecognizer scale];  // Store the previous scale factor for the next pinch gesture call  
    }
}

2 votes

Excellente solution ! Je l'ai cherché depuis toujours ! Pourquoi n'est-elle pas en tête de cette question ? Merci

2 votes

MERCI ! - Merci ! - et merci à vous ! . . . . :D

1 votes

Ajoutez cette ligne après avoir défini la transformation de la vue gestureRecognizer : [recognizer setScale:1.0] ;

18voto

Anomie Points 43759

Il n'y a pas de moyen de limiter l'échelle d'une UIPinchGestureRecognizer . Pour limiter la hauteur dans votre code, vous devriez pouvoir faire quelque chose comme ceci :

CGFloat scale = 1.0 - (lastScale - pinchscale);
CGRect bounds = [(UIPinchGestureRecognizer*)sender view].bounds;
scale = MIN(scale, maximumHeight / CGRectGetHeight(bounds));
scale = MAX(scale, minimumHeight / CGRectGetHeight(bounds));

Pour limiter la largeur, remplacez "Height" par "Width" dans les deux dernières lignes.

0 votes

C'est incorrect. Si vous transformez la vue, le cadre n'est pas valide. Par exemple, si la vue subit une rotation de 45 degrés, le cadre est en fait beaucoup plus grand que l'uiview réelle, de sorte que le facteur d'échelle change lorsque vous effectuez une rotation et que vous essayez ensuite de le fixer.

0 votes

Cela fonctionne, ou vous pouvez travailler avec des valeurs d'échelle au lieu de valeurs spécifiques de hauteur/largeur comme dans ma réponse.

4voto

unsynchronized Points 2576

J'ai pris quelques informations glanées dans les réponses de Paul Solt et d'Anoime, et je les ai ajoutées à une catégorie existante que j'ai créée pour UIViewController afin de permettre de rendre n'importe quel UIView traînable, pour le rendre maintenant pinçable en utilisant des gestes et des transformations.

Remarque : cette opération modifie la propriété de la balise de la vue que vous rendez glissante/rapide. Donc si vous avez besoin de la balise pour autre chose, vous pouvez envisager de placer cette valeur dans le NSMutableDictionary utilisé par cette technique. Celui-ci est disponible sous la forme [self dictForView:theView]

Mise en œuvre dans votre projet :

Vous pouvez faire en sorte que n'importe quelle vue secondaire de la "vue" des contrôleurs de vue puisse être glissée ou pincée (ou les deux). placez une seule ligne de code dans votre viewDidLoad (par exemple :)

[self makeView:mySubView draggable:YES pinchable:YES minPinchScale:0.75 maxPinchScale:1.0];

le désactiver dans viewDidUnload (libère les invitations et le dictionnaire) :

[self makeView:mySubView draggable:NO pinchable:NO minPinchScale:1.0 maxPinchScale:1.0];

Fichier DragAndPinchScale.h

#import <UIKit/UIKit.h>

@interface UIViewController (DragAndPinchScale)

-(void) makeView:(UIView*)aView 
       draggable:(BOOL)draggable 
       pinchable:(BOOL)pinchable 
   minPinchScale:(CGFloat)minPinchScale
   maxPinchScale:(CGFloat)maxPinchScale;

-(NSMutableDictionary *) dictForView:(UIView *)theView;
-(NSMutableDictionary *) dictForViewGuestures:(UIGestureRecognizer *)guesture;

@end

Fichier DragAndPinchScale.m

#import "DragAndPinchScale.h"

@implementation UIViewController (DragAndPinchScale)

-(NSMutableDictionary *) dictForView:(UIView *)theView{
    NSMutableDictionary *dict = (NSMutableDictionary*) (void*) theView.tag;
    if (!dict) {
        dict = [[NSMutableDictionary dictionary ] retain];
        theView.tag = (NSInteger) (void *) dict;
    }

    return dict;

}

-(NSMutableDictionary *) dictForViewGuestures:(UIGestureRecognizer *)guesture {
    return [self dictForView:guesture.view];
}

- (IBAction)fingersDidPinchInPinchableView:(UIPinchGestureRecognizer *)fingers {
    NSMutableDictionary *dict = [self dictForViewGuestures:fingers];
    UIView *viewToZoom = fingers.view;
    CGFloat lastScale;
    if([fingers state] == UIGestureRecognizerStateBegan) {
        // Reset the last scale, necessary if there are multiple objects with different scales
        lastScale = [fingers scale];
    } else {
        lastScale = [[dict objectForKey:@"lastScale"] floatValue];
    }

    if ([fingers state] == UIGestureRecognizerStateBegan || 
        [fingers state] == UIGestureRecognizerStateChanged) {

        CGFloat currentScale = [[[fingers view].layer valueForKeyPath:@"transform.scale"] floatValue];

        // limits to adjust the max/min values of zoom
        CGFloat maxScale = [[dict objectForKey:@"maxScale"] floatValue];
        CGFloat minScale = [[dict objectForKey:@"minScale"] floatValue];

        CGFloat newScale = 1 -  (lastScale - [fingers scale]); 
        newScale = MIN(newScale, maxScale / currentScale);   
        newScale = MAX(newScale, minScale / currentScale);
        CGAffineTransform transform = CGAffineTransformScale([[fingers view] transform], newScale, newScale);
        viewToZoom.transform = transform;

        lastScale = [fingers scale];  // Store the previous scale factor for the next pinch gesture call  
    }

    [dict setObject:[NSNumber numberWithFloat:lastScale] 
             forKey:@"lastScale"];

}

- (void)fingerDidMoveInDraggableView:(UIPanGestureRecognizer *)finger {
    NSMutableDictionary *dict = [self dictForViewGuestures:finger];
    UIView *viewToDrag =  finger.view;
    if (finger.state == UIGestureRecognizerStateBegan) {

        [dict setObject:[NSValue valueWithCGPoint:viewToDrag.frame.origin] 
                 forKey:@"startDragOffset"];

        [dict setObject:[NSValue valueWithCGPoint:[finger locationInView:self.view]] 
                 forKey:@"startDragLocation"];

    }
    else if (finger.state == UIGestureRecognizerStateChanged) {

        NSMutableDictionary *dict = (NSMutableDictionary*) (void*) viewToDrag.tag;

        CGPoint stopLocation = [finger locationInView:self.view];
        CGPoint startDragLocation = [[dict valueForKey:@"startDragLocation"] CGPointValue];
        CGPoint startDragOffset = [[dict valueForKey:@"startDragOffset"] CGPointValue];
        CGFloat dx = stopLocation.x - startDragLocation.x;
        CGFloat dy = stopLocation.y - startDragLocation.y;
        //   CGFloat distance = sqrt(dx*dx + dy*dy );
        CGRect dragFrame = viewToDrag.frame;

        CGSize selfViewSize = self.view.frame.size;
        if (!UIDeviceOrientationIsPortrait(self.interfaceOrientation)) {
            selfViewSize = CGSizeMake(selfViewSize.height,selfViewSize.width);
        }

        selfViewSize.width  -= dragFrame.size.width;
        selfViewSize.height -= dragFrame.size.height;

        dragFrame.origin.x = MIN(selfViewSize.width, MAX(0,startDragOffset.x+dx));
        dragFrame.origin.y = MIN(selfViewSize.height,MAX(0,startDragOffset.y+dy));

        viewToDrag.frame = dragFrame;
    }
    else if (finger.state == UIGestureRecognizerStateEnded) {

        [dict removeObjectForKey:@"startDragLocation"];
        [dict removeObjectForKey:@"startDragOffset"];
    }
}

-(void) makeView:(UIView*)aView 
       draggable:(BOOL)draggable 
       pinchable:(BOOL)pinchable 
   minPinchScale:(CGFloat)minPinchScale
   maxPinchScale:(CGFloat)maxPinchScale{
    NSMutableDictionary *dict = (NSMutableDictionary*) (void*) aView.tag;

    if (!(pinchable || draggable)) {

        if (dict){ 
            [dict release];
            aView.tag = 0;
        }
        return;
    }

    if (dict) {

        UIPanGestureRecognizer *pan =[dict objectForKey:@"UIPanGestureRecognizer"];
        if(pan){
            if ([aView.gestureRecognizers indexOfObject:pan]!=NSNotFound) {
                [aView removeGestureRecognizer:pan];
            }
            [dict removeObjectForKey:@"UIPanGestureRecognizer"];
        }

        UIPinchGestureRecognizer *pinch =[dict objectForKey:@"UIPinchGestureRecognizer"];
        if(pinch){
            if ([aView.gestureRecognizers indexOfObject:pinch]!=NSNotFound) {
                [aView removeGestureRecognizer:pinch];
            }
            [dict removeObjectForKey:@"UIPinchGestureRecognizer"];
        }

        [dict removeObjectForKey:@"startDragLocation"];
        [dict removeObjectForKey:@"startDragOffset"];
        [dict removeObjectForKey:@"lastScale"];
        [dict removeObjectForKey:@"minScale"];
        [dict removeObjectForKey:@"maxScale"];
    }

    if (draggable) {

        UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(fingerDidMoveInDraggableView:)];
        pan.minimumNumberOfTouches = 1;  
        pan.maximumNumberOfTouches = 1;  
        [aView addGestureRecognizer:pan];
        [pan release];

        dict = [self dictForViewGuestures:pan];
        [dict setObject:pan forKey:@"UIPanGestureRecognizer"];

    }

    if (pinchable) {

        CGAffineTransform initialTramsform = CGAffineTransformMakeScale(1.0, 1.0);
        aView.transform = initialTramsform;

        UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(fingersDidPinchInPinchableView:)];
        [aView addGestureRecognizer:pinch];
        [pinch release];
        dict = [self dictForViewGuestures:pinch];
        [dict setObject:pinch forKey:@"UIPinchGestureRecognizer"];
        [dict setObject:[NSNumber numberWithFloat:minPinchScale] forKey:@"minScale"];
        [dict setObject:[NSNumber numberWithFloat:maxPinchScale] forKey:@"maxScale"];

    }

}

@end

0 votes

Merci cela a bien fonctionné avec mon code. j'étais confronté à ce problème depuis longtemps mais votre code ci-dessus m'a aidé à le résoudre. merci encore

4voto

damian Points 1482

Le problème avec la plupart des autres réponses est qu'elles essaient de traiter l'échelle comme une valeur linéaire, alors qu'en fait, elle n'est pas linéaire en raison de la façon dont l'échelle est calculée. UIPinchGestureRecognizer calcule sa propriété d'échelle en fonction de la distance tactile. Lorsque cela n'est pas pris en compte, l'utilisateur doit utiliser une distance de pincement plus ou moins grande pour "annuler" la mise à l'échelle appliquée par un geste de pincement précédent.

Considérons : supposons que transform.scale = 1.0 et je place mes doigts à 6 cm de distance sur l'écran, puis je les pince vers l'intérieur jusqu'à ce qu'ils soient à 3 cm de distance. gestureRecognizer.scale es 0.5 y 0.5-1.0 es -0.5 donc transform.scale deviendra 1.0+(-0.5) = 0.5 . Maintenant, je soulève mes doigts, je les replace à 3 cm de distance et je les pince vers l'extérieur sur 6 cm. Le résultat gestureRecognizer.scale sera 2.0 y 2.0-1.0 es 1.0 donc transform.scale deviendra 0.5+1.0 = 1.5 . Pas ce que je voulais qu'il arrive.

La solution consiste à calculer l'échelle du delta de pincement comme une proportion de sa valeur précédente. Je place mes doigts vers le bas à 6cm de distance, et je pince vers l'intérieur à 3cm, donc gestureRecognizer.scale es 0.5 . 0.5/1.0 es 0.5 donc ma nouvelle transform.scale es 1.0*0.5 = 0.5 . Ensuite, je place mes doigts vers le bas à 3cm de distance, et je pince vers l'extérieur à 6cm. gestureRecognizer.scale est alors 2.0 y 2.0/1.0 es 2.0 donc ma nouvelle transform.scale es 0.5*2.0 = 1.0 ce qui est exactement ce que je voulais qu'il se passe.

Le voici en code :

sur -(void)viewDidLoad :

self.zoomGestureCurrentZoom = 1.0f;

sur -(void)onZoomGesture:(UIPinchGestureRecognizer*)gestureRecognizer :

if ( gestureRecognizer.state == UIGestureRecognizerStateBegan )
{
    self.zoomGestureLastScale = gestureRecognizer.scale;
}
else if ( gestureRecognizer.state == UIGestureRecognizerStateChanged )
{
    // we have to jump through some hoops to clamp the scale in a way that makes the UX intuitive
    float scaleDeltaFactor = gestureRecognizer.scale/self.zoomGestureLastScale;
    float currentZoom = self.zoomGestureCurrentZoom;
    float newZoom = currentZoom * scaleDeltaFactor;
    // clamp
    float kMaxZoom = 4.0f;
    float kMinZoom = 0.5f;
    newZoom = MAX(kMinZoom,MIN(newZoom,kMaxZoom));    
    self.view.transform = CGAffineTransformScale([[gestureRecognizer view] transform], newZoom, newZoom);

    // store for next time
    self.zoomGestureCurrentZoom = newZoom;
    self.zoomGestureLastScale = gestureRecognizer.scale;
}

0 votes

Cela n'a pas fonctionné pour moi lorsque je l'ai réglé sur une vue spécifique, la vue gestureRecognizer.view. J'ai rencontré des problèmes où la "vitesse" n'était pas la même pour plusieurs pincements, et je pense que c'est ce que vous voulez dire.

0 votes

@PaulSolt hmm, ouais Je suppose que là où j'ai été [[gestureRecognizer view] transform] qui pourrait peut-être par CGAffineTransformIdentity ? Je pense que dans le code sur lequel je me suis basé, self.view était censé être différent de gestureRecognizer.view . Je ne m'en souviens pas, désolé.

2voto

Shagun Points 21

Merci, un extrait de code très utile au-dessus de la fixation d'une échelle minimale et maximale.

J'ai trouvé ça quand j'ai retourné la vue en utilisant la première fois :

CGAffineTransformScale(gestureRecognizer.view.transform, -1.0, 1.0); 

cela provoquait un scintillement lors de la mise à l'échelle de la vue.

Faites-moi savoir ce que vous en pensez, mais la solution pour moi a été de mettre à jour l'exemple de code ci-dessus, et si la vue a été retournée (drapeau activé via la propriété), alors inverser la valeur de l'échelle :

if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer     state] == UIGestureRecognizerStateChanged)
{
    CGFloat currentScale = [[[gestureRecognizer view].layer valueForKeyPath:@"transform.scale"] floatValue];

    if(self.isFlipped) // (inverting)
    {
        currentScale *= -1;
    }

    CGFloat newScale = 1 -  (self.lastScale - [gestureRecognizer scale]);

    newScale = MIN(newScale, self.maximumScaleFactor / currentScale);
    newScale = MAX(newScale, self.minimumScaleFactor / currentScale);

    CGAffineTransform transform = CGAffineTransformScale([[gestureRecognizer view] transform], newScale, newScale);
    gestureRecognizer.view.transform = transform;

    self.lastScale = [gestureRecognizer scale];  // Store the previous scale factor for the next pinch gesture call

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X