166 votes

Comment puis-je ajuster le point d'ancrage d'un CALayer, lorsque la mise en page automatique est utilisée ?

Note : Les choses ont évolué depuis que cette question a été posée ; voir aquí pour un bon aperçu récent.


Avant la mise en page automatique, vous pouviez modifier le point d'ancrage du calque d'une vue sans déplacer la vue en stockant le cadre, en définissant le point d'ancrage et en restaurant le cadre.

Dans un monde de mise en page automatique, nous ne définissons plus de cadres, mais les contraintes ne semblent pas être à la hauteur de la tâche consistant à ajuster la position d'une vue pour la remettre là où nous le souhaitons. Vous pouvez modifier les contraintes pour repositionner votre vue, mais en cas de rotation ou d'autres événements de redimensionnement, elles redeviennent invalides.

L'idée brillante suivante ne fonctionne pas car elle crée un message "Invalid pairing of layout attributes (left and width)" :

layerView.layer.anchorPoint = CGPointMake(1.0, 0.5);
// Some other size-related constraints here which all work fine...
[self.view addConstraint:
    [NSLayoutConstraint constraintWithItem:layerView
                                 attribute:NSLayoutAttributeLeft
                                 relatedBy:NSLayoutRelationEqual 
                                    toItem:layerView 
                                 attribute:NSLayoutAttributeWidth 
                                multiplier:0.5 
                                  constant:20.0]];

Mon intention ici était de définir le bord gauche du layerView la vue avec le point d'ancrage ajusté, à la moitié de sa largeur plus 20 (la distance que je veux insérer à partir du bord gauche de la vue supérieure).

Est-il possible de changer le point d'ancrage, sans changer l'emplacement d'une vue, dans une vue qui est mise en page avec la mise en page automatique ? Dois-je utiliser des valeurs codées en dur et modifier la contrainte à chaque rotation ? J'espère que non.

J'ai besoin de changer le point d'ancrage de sorte que lorsque j'applique une transformation à la vue, j'obtienne l'effet visuel correct.

0 votes

Compte tenu de ce que vous avez ici, il semble que vous allez vous retrouver avec des mises en page ambiguës de toute façon, même si vous avez réussi à faire fonctionner le code ci-dessus. Comment layerView connaître sa largeur ? Est-ce que son côté droit est collé à quelque chose d'autre ?

0 votes

C'est couvert dans le //Size-related constraints that work fine - la largeur et la hauteur de la vue en couches sont dérivées de celles de la vue principale.

365voto

matt Points 60113

[EDIT : Attention : Toute la discussion qui suit sera peut-être dépassée ou du moins fortement atténuée par iOS 8, qui ne fera peut-être plus l'erreur de déclencher la mise en page au moment où une transformation de vue est appliquée].

Autolayout et transformations de vues

Autolayout ne joue pas du tout bien avec les transformations de vues. La raison, pour autant que je puisse la discerner, est que vous n'êtes pas censé jouer avec le cadre d'une vue qui a une transformation (autre que la transformation d'identité par défaut) - mais c'est exactement ce que fait autolayout. La façon dont autolayout fonctionne est la suivante : dans la fenêtre layoutSubviews le runtime passe en revue toutes les contraintes et définit les cadres de toutes les vues en conséquence.

En d'autres termes, les contraintes ne sont pas magiques ; elles ne sont qu'une liste de choses à faire. layoutSubviews est l'endroit où la liste des choses à faire est faite. Et il le fait en fixant des cadres.

Je ne peux pas aider à considérer ceci comme un bug. Si j'applique cette transformation à une vue :

v.transform = CGAffineTransformMakeScale(0.5,0.5);

Je m'attends à ce que la vue apparaisse avec son centre au même endroit qu'avant et à la moitié de sa taille. Mais en fonction de ses contraintes, ce n'est peut-être pas ce que je vois du tout.

[En fait, il y a une deuxième surprise ici : l'application d'une transformation à une vue déclenche immédiatement la mise en page. Cela me semble être un autre bug. Ou peut-être est-ce le cœur du premier bogue. Je m'attendrais à pouvoir m'en sortir avec une transformation au moins jusqu'au moment de la mise en page, par exemple si le dispositif est tourné - tout comme je peux m'en sortir avec une animation d'image jusqu'au moment de la mise en page. Mais en fait, le moment de la mise en page est immédiat, ce qui ne semble pas correct].

Solution 1 : aucune contrainte

Une solution actuelle consiste, si je dois appliquer une transformation semi-permanente à une vue (et non pas simplement l'agiter temporairement), à supprimer toutes les contraintes qui l'affectent. Malheureusement, cela entraîne généralement la disparition de la vue de l'écran, puisque l'autolayout a toujours lieu et qu'il n'y a plus de contraintes pour nous dire où placer la vue. Donc, en plus de supprimer les contraintes, je règle le paramètre translatesAutoresizingMaskIntoConstraints à OUI. La vue fonctionne maintenant de l'ancienne manière, sans être affectée par autolayout. (Il es affectée par l'autolayout, évidemment, mais les contraintes implicites du masque d'autoréalisation font que son comportement est identique à ce qu'il était avant l'autolayout).

Solution 2 : N'utilisez que des contraintes appropriées

Si cela semble un peu radical, une autre solution consiste à définir les contraintes pour qu'elles fonctionnent correctement avec une transformation prévue. Si une vue est dimensionnée uniquement par sa largeur et sa hauteur internes fixes, et positionnée uniquement par son centre, par exemple, ma transformation d'échelle fonctionnera comme prévu. Dans ce code, je supprime les contraintes existantes sur une sous-vue ( otherView ) et les remplacer par ces quatre contraintes, en lui donnant une largeur et une hauteur fixes et en l'épinglant uniquement par son centre. Après cela, ma transformation d'échelle fonctionne :

NSMutableArray* cons = [NSMutableArray array];
for (NSLayoutConstraint* con in self.view.constraints)
    if (con.firstItem == self.otherView || con.secondItem == self.otherView)
        [cons addObject:con];

[self.view removeConstraints:cons];
[self.otherView removeConstraints:self.otherView.constraints];
[self.view addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterX relatedBy:0 toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:self.otherView.center.x]];
[self.view addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterY relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:self.otherView.center.y]];
[self.otherView addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeWidth relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.width]];
[self.otherView addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.height]];

Le résultat est que si vous n'avez pas de contraintes qui affectent le cadre d'une vue, la mise en page automatique ne touchera pas le cadre de la vue, ce qui est exactement ce que vous recherchez lorsqu'une transformation est impliquée.

Solution 3 : utiliser une vue secondaire

Le problème avec les deux solutions ci-dessus est que nous perdons les avantages des contraintes pour positionner notre vue. Voici donc une solution qui résout ce problème. Commencez par une vue invisible dont le rôle est uniquement d'agir en tant qu'hôte, et utilisez les contraintes pour la positionner. À l'intérieur de cette vue, placez la vue réelle en tant que sous-vue. Utilisez des contraintes pour positionner la vue secondaire dans la vue hôte, mais limitez ces contraintes à celles qui ne se défendront pas lorsque nous appliquerons une transformation.

En voici une illustration :

enter image description here

La vue blanche est la vue hôte ; vous êtes censé prétendre qu'elle est transparente et donc invisible. La vue rouge est sa sous-vue, positionnée en épinglant son centre à celui de la vue hôte. Nous pouvons maintenant mettre à l'échelle et faire pivoter la vue rouge autour de son centre sans aucun problème, et l'illustration montre d'ailleurs que nous l'avons fait :

self.otherView.transform = CGAffineTransformScale(self.otherView.transform, 0.5, 0.5);
self.otherView.transform = CGAffineTransformRotate(self.otherView.transform, M_PI/8.0);

Et pendant ce temps, les contraintes sur la vue hôte la maintiennent au bon endroit lorsque nous faisons pivoter le dispositif.

Solution 4 : utilisez plutôt les transformations de calque

Au lieu des transformations de vue, utilisez les transformations de couche, qui ne déclenchent pas de mise en page et n'entraînent donc pas de conflit immédiat avec les contraintes.

Par exemple, cette simple animation de vue "throb" pourrait bien se briser sous autolayout :

[UIView animateWithDuration:0.3 delay:0
                    options:UIViewAnimationOptionAutoreverse
                 animations:^{
    v.transform = CGAffineTransformMakeScale(1.1, 1.1);
} completion:^(BOOL finished) {
    v.transform = CGAffineTransformIdentity;
}];

Même si, en fin de compte, la taille de la vue n'a pas été modifiée, le simple fait de définir son transform provoque la mise en page, et les contraintes peuvent faire sauter la vue. (Est-ce que cela ressemble à un bug ou quoi ?) Mais si nous faisons la même chose avec Core Animation (en utilisant une CABasicAnimation et en appliquant l'animation au calque de la vue), le layout ne se produit pas, et cela fonctionne bien :

CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:@"transform"];
ba.autoreverses = YES;
ba.duration = 0.3;
ba.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1, 1.1, 1)];
[v.layer addAnimation:ba forKey:nil];

3 votes

Ce n'est pas la transformation qui me pose problème, mais la définition du nouveau point d'ancrage. Je vais essayer de supprimer les contraintes et d'utiliser le masque de redimensionnement automatique.

0 votes

Votre réponse est la plus complète et la plus exhaustive, ayez une tique verte. Pour être honnête, depuis que j'ai posé cette question, j'ai arrêté d'utiliser autolayout pour cette circonstance particulière mais j'étais quand même intéressé par une réponse.

0 votes

À mon avis, la solution 4 est vraiment la meilleure, voire la seule façon de progresser. AutoLayout est génial, donc ne pas l'utiliser n'est pas une solution. L'ajout de sous-vues ou de vues de conteneurs est un gonflement inutile de l'interface utilisateur.

41voto

sensslen Points 455

J'ai eu une question similaire et je viens d'avoir un retour de l'équipe Autolayout d'Apple. Ils suggèrent d'utiliser l'approche de la vue conteneur que Matt suggère, mais ils créent une sous-classe de UIView pour écraser les sous-vues de mise en page et y appliquer le code de mise en page personnalisé.

Le fichier d'en-tête ressemble à cela afin que vous puissiez lier la vue secondaire de votre choix directement à partir de l'Interface Builder.

#import <UIKit/UIKit.h>

@interface BugFixContainerView : UIView
@property(nonatomic,strong) IBOutlet UIImageView *knobImageView;
@end

et le fichier m applique le code spécial comme ça :

#import "BugFixContainerView.h"

@implementation BugFixContainerView
- (void)layoutSubviews
{
    static CGPoint fixCenter = {0};
    [super layoutSubviews];
    if (CGPointEqualToPoint(fixCenter, CGPointZero)) {
        fixCenter = [self.knobImageView center];
    } else {
        self.knobImageView.center = fixCenter;
    }
}
@end

Comme vous pouvez le voir, il saisit le point central de la vue lors du premier appel et réutilise cette position dans les appels suivants afin de placer la vue en conséquence. Cela écrase le code de mise en page automatique dans le sens où cela se passe après [super layoutSubviews] ; qui contient le code de mise en page automatique.

Comme cela, il n'est plus nécessaire d'éviter l'autolayout, mais vous pouvez créer votre propre autolayout lorsque les comportements par défaut ne sont plus appropriés. Bien sûr, vous pouvez appliquer des choses beaucoup plus compliquées que ce qui est dans cet exemple, mais c'est tout ce dont j'avais besoin puisque mon application ne peut utiliser que le mode portrait.

5 votes

Merci d'avoir posté ce message. Depuis que j'ai écrit ma réponse, j'ai définitivement compris l'intérêt de remplacer layoutSubviews . Je voudrais juste ajouter, cependant, qu'Apple ne fait ici que faire usted faire ce que je pense ils aurait dû faire (dans leur mise en œuvre d'Autolayout) depuis le début.

0 votes

Vous avez raison, mais je suppose que cela n'est pas vraiment lié au problème lui-même. Puisqu'ils ont fourni le code ci-dessus, j'en suis heureux !

4 votes

Il est triste et absurde que l'équipe d'autolayout elle-même recommande de créer une sous-classe de conteneur personnalisée juste pour contourner la façon dont autolayout brise un comportement intuitif et de longue date dans le système de vue.

4voto

algal Points 4298

Si vous utilisez la mise en page automatique, je ne vois pas en quoi le réglage manuel de la position est utile à long terme, car la mise en page automatique finira par écraser la valeur de position que vous avez définie lorsqu'elle calculera sa propre mise en page.

Il s'agit plutôt de modifier les contraintes de mise en page elles-mêmes pour compenser les changements produits par le réglage du point d'ancrage. La fonction suivante fait cela pour les vues non transformées.

/**
  Set the anchorPoint of view without changing is perceived position.

 @param view view whose anchorPoint we will mutate
 @param anchorPoint new anchorPoint of the view in unit coords (e.g., {0.5,1.0})
 @param xConstraint an NSLayoutConstraint whose constant property adjust's view x.center
 @param yConstraint an NSLayoutConstraint whose constant property adjust's view y.center

  As multiple constraints can contribute to determining a view's center, the user of this
 function must specify which constraint they want modified in order to compensate for the
 modification in anchorPoint
 */
void SetViewAnchorPointMotionlesslyUpdatingConstraints(UIView * view,CGPoint anchorPoint,
                                                       NSLayoutConstraint * xConstraint,
                                                       NSLayoutConstraint * yConstraint)
{
  // assert: old and new anchorPoint are in view's unit coords
  CGPoint const oldAnchorPoint = view.layer.anchorPoint;
  CGPoint const newAnchorPoint = anchorPoint;

  // Calculate anchorPoints in view's absolute coords
  CGPoint const oldPoint = CGPointMake(view.bounds.size.width * oldAnchorPoint.x,
                                 view.bounds.size.height * oldAnchorPoint.y);
  CGPoint const newPoint = CGPointMake(view.bounds.size.width * newAnchorPoint.x,
                                 view.bounds.size.height * newAnchorPoint.y);

  // Calculate the delta between the anchorPoints
  CGPoint const delta = CGPointMake(newPoint.x-oldPoint.x, newPoint.y-oldPoint.y);

  // get the x & y constraints constants which were contributing to the current
  // view's position, and whose constant properties we will tweak to adjust its position
  CGFloat const oldXConstraintConstant = xConstraint.constant;
  CGFloat const oldYConstraintConstant = yConstraint.constant;

  // calculate new values for the x & y constraints, from the delta in anchorPoint
  // when autolayout recalculates the layout from the modified constraints,
  // it will set a new view.center that compensates for the affect of the anchorPoint
  CGFloat const newXConstraintConstant = oldXConstraintConstant + delta.x;
  CGFloat const newYConstraintConstant = oldYConstraintConstant + delta.y;

  view.layer.anchorPoint = newAnchorPoint;
  xConstraint.constant = newXConstraintConstant;
  yConstraint.constant = newYConstraintConstant;
  [view setNeedsLayout];
}

J'admets que ce n'est probablement pas tout ce que vous espériez, car en général, la seule raison pour laquelle vous souhaitez modifier le point d'ancrage est de définir une transformation. Cela nécessiterait une fonction plus complexe qui mettrait à jour les contraintes de mise en page afin de refléter les modifications de l'image. todo les modifications du cadre qui pourraient être causées par la propriété de transformation elle-même. C'est délicat car les transformations peuvent faire beaucoup de choses à l'image. Une transformation de mise à l'échelle ou de rotation rendrait le cadre plus grand, il faudrait donc mettre à jour les contraintes de largeur ou de hauteur, etc.

Si vous n'utilisez la transformation que pour une animation temporaire, alors ce qui précède peut suffire, car je ne pense pas que la mise en page automatique empêchera l'animation en vol de présenter des images représentant des violations purement transitoires des contraintes.

0 votes

Malheureusement, j'utilise des transformations. Le réglage de la position est correct car la méthode est appelée à chaque fois que la vue est mise en page, donc elle n'est pas bloquée. Je pense que surcharger layoutRect pourrait être une meilleure solution (mentionné dans une de vos réponses précédentes) mais je ne l'ai pas encore essayé.

0 votes

Au fait, voici un banc d'essai que j'ai utilisé. Ce serait génial si quelqu'un trouvait la solution : github.com/algal/AutolayoutAnchorPointTestRig

0 votes

Ceci devrait être la réponse acceptée. La réponse de @matt ne concerne même pas le point d'ancrage.

3voto

David Rönnqvist Points 25290

tl:dr : Vous pouvez créer une sortie pour l'une des contraintes afin qu'elle puisse être retirée et ajoutée à nouveau.


J'ai créé un nouveau projet et ajouté une vue avec une taille fixe au centre. Les contraintes sont montrées dans l'image ci-dessous.

The constraints for the smallest example I could think of.

Ensuite, j'ai ajouté une sortie pour la vue qui va pivoter et pour la contrainte d'alignement centre x.

@property (weak, nonatomic) IBOutlet UIView *rotatingView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *xAlignmentContstraint;

Plus tard dans viewDidAppear Je calcule le nouveau point d'ancrage

UIView *view = self.rotatingView;

CGPoint rotationPoint = // The point I'm rotating around... (only X differs)
CGPoint anchorPoint = CGPointMake((rotationPoint.x-CGRectGetMinX(view.frame))/CGRectGetWidth(view.frame),
                                  (rotationPoint.y-CGRectGetMinY(view.frame))/CGRectGetHeight(view.bounds));

CGFloat xCenterDifference = rotationPoint.x-CGRectGetMidX(view.frame);

view.layer.anchorPoint = anchorPoint;

Ensuite, je supprime la contrainte pour laquelle j'ai une sortie, j'en crée une nouvelle qui est décalée et je la rajoute à nouveau. Après cela, j'indique à la vue dont la contrainte a été modifiée qu'elle doit mettre à jour les contraintes.

[self.view removeConstraint:self.xAlignmentContstraint];
self.xAlignmentContstraint = [NSLayoutConstraint constraintWithItem:self.rotatingView
                                                          attribute:NSLayoutAttributeCenterX
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:self.view
                                                          attribute:NSLayoutAttributeCenterX
                                                         multiplier:1.0
                                                           constant:xDiff];
[self.view addConstraint:self.xAlignmentContstraint];
[self.view needsUpdateConstraints];

Enfin, j'ajoute simplement l'animation de rotation à la vue en rotation.

CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotate.toValue = @(-M_PI_2);
rotate.autoreverses = YES;
rotate.repeatCount = INFINITY;
rotate.duration = 1.0;
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 

[view.layer addAnimation:rotate forKey:@"myRotationAnimation"];

Le calque rotatif semble rester centré (ce qui devrait être le cas), même lorsque l'on fait pivoter le dispositif ou que l'on modifie les contraintes. La nouvelle contrainte et le point d'ancrage modifié s'annulent visuellement.

1 votes

Cela fonctionnerait pour ma situation spécifique, mais il ne semble pas qu'une solution générale soit possible.

1voto

jrturton Points 64875

Ma solution actuelle est d'ajuster manuellement la position du calque dans viewDidLayoutSubviews . Ce code pourrait également être utilisé dans layoutSubviews pour une sous-classe de vue, mais dans mon cas, ma vue est une vue de haut niveau à l'intérieur d'un contrôleur de vue, ce qui signifie que je n'ai pas eu à créer une sous-classe UIView.

Cela semble demander trop d'efforts, les autres réponses sont donc les bienvenues.

-(void)viewDidLayoutSubviews
{
    for (UIView *view in self.view.subviews)
    {
        CGPoint anchorPoint = view.layer.anchorPoint;
        // We're only interested in views with a non-standard anchor point
        if (!CGPointEqualToPoint(CGPointMake(0.5, 0.5),anchorPoint))
        {
            CGFloat xDifference = anchorPoint.x - 0.5;
            CGFloat yDifference = anchorPoint.y - 0.5;
            CGPoint currentPosition = view.layer.position;

            // Use transforms if we can, otherwise manually calculate the frame change
            // Assuming a transform is in use since we are changing the anchor point. 
            if (CATransform3DIsAffine(view.layer.transform))
            {
                CGAffineTransform current = CATransform3DGetAffineTransform(view.layer.transform);
                CGAffineTransform invert = CGAffineTransformInvert(current);
                currentPosition = CGPointApplyAffineTransform(currentPosition, invert);
                currentPosition.x += (view.bounds.size.width * xDifference);
                currentPosition.y += (view.bounds.size.height * yDifference);
                currentPosition = CGPointApplyAffineTransform(currentPosition, current);
            }
            else
            {
                CGFloat transformXRatio = view.bounds.size.width / view.frame.size.width;

                if (xDifference < 0)
                    transformXRatio = 1.0/transformXRatio;

                CGFloat transformYRatio = view.bounds.size.height / view.frame.size.height;
                if (yDifference < 0)
                    transformYRatio = 1.0/transformYRatio;

                currentPosition.x += (view.bounds.size.width * xDifference) * transformXRatio;
                currentPosition.y += (view.bounds.size.height * yDifference) * transformYRatio;
            }
            view.layer.position = currentPosition;
        }

    }
}

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