¿Cómo ajusto el punto de anclaje de un CALayer, cuando se utiliza el diseño automático?

Nota : las cosas han cambiado desde que se hizo esta pregunta; mira aquí para una buena visión general reciente.


Antes del diseño automático, puede cambiar el punto de anclaje de la capa de una vista sin mover la vista almacenando el marco, configurando el punto de anclaje y restaurando el marco.

En un mundo de diseño automático, no establecemos marcos más, pero las restricciones no parecen estar a la altura de la tarea de ajustar la posición de una vista a donde queremos. Puede hackear las restricciones para reubicar su vista, pero en la rotación u otros eventos de cambio de tamaño, estos vuelven a ser inválidos.

La siguiente idea shiny no funciona, ya que crea un “Emparejamiento no válido de atributos de diseño (izquierdo y ancho)”:

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

Mi intención aquí era establecer el borde izquierdo de layerView , la vista con el punto de anclaje ajustado, a la mitad de su ancho más 20 (la distancia que quiero insertar desde el borde izquierdo de la supervista).

¿Es posible cambiar el punto de anclaje, sin cambiar la ubicación de una vista, en una vista que se presenta con diseño automático? ¿Debo usar valores codificados y editar la restricción en cada rotación? Espero que no.

Necesito cambiar el punto de anclaje para que cuando aplique una transformación a la vista, obtenga el efecto visual correcto.

[EDITAR: Advertencia: toda la discusión posterior será posiblemente obsoleta o al menos muy mitigada por iOS 8, que ya no puede cometer el error de activar el diseño en el momento en que se aplica una transformación de vista.]

Transformaciones de Autolayout vs. View

Autolayout no funciona bien con las transformaciones de vista. El motivo, en la medida en que puedo discernir, es que no debe interferir con el marco de una vista que tiene una transformación (que no sea la transformación de identidad predeterminada), pero eso es exactamente lo que hace el ajuste automático. La forma en que funciona el autolayout es que en layoutSubviews el tiempo de ejecución layoutSubviews través de todas las restricciones y establece los marcos de todas las vistas en consecuencia.

En otras palabras, las restricciones no son mágicas; son solo una lista de cosas para hacer. layoutSubviews es donde se realiza la lista de tareas pendientes. Y lo hace al establecer marcos.

No puedo evitar esto como un error. Si aplico esta transformación a una vista:

 v.transform = CGAffineTransformMakeScale(0.5,0.5); 

Espero ver la vista aparecer con su centro en el mismo lugar que antes y en la mitad del tamaño. Pero dependiendo de sus limitaciones, eso puede no ser lo que veo en absoluto.

[En realidad, hay una segunda sorpresa aquí: aplicar una transformación a una vista activa el diseño de inmediato. Esto me parece ser otro error. O tal vez es el corazón del primer error. Lo que esperaría es poder salir con una transformación al menos hasta el tiempo de diseño, por ejemplo, el dispositivo se gira, del mismo modo que puedo salirme con una animación de cuadros hasta el momento del diseño. Pero, de hecho, el tiempo de diseño es inmediato, lo que parece simplemente incorrecto].

Solución 1: sin restricciones

Una solución actual es, si voy a aplicar una transformación semipermanente a una vista (y no meramente menearlo temporalmente de alguna manera), para eliminar todas las restricciones que la afectan. Desafortunadamente, esto generalmente hace que la vista se desvanezca de la pantalla, ya que todavía se lleva a cabo el diseño automático, y ahora no hay restricciones para decirnos dónde colocar la vista. Por lo tanto, además de eliminar las restricciones, configuro la translatesAutoresizingMaskIntoConstraints de la vistaAutoresizingMaskIntoConstraints en SÍ. La vista ahora funciona de la manera antigua, sin ser afectada por el diseño automático. (Esto se ve afectado por el ajuste automático, obviamente, pero las restricciones implícitas de la máscara de aumento automático hacen que su comportamiento sea exactamente como antes de la ejecución automática).

Solución 2: Use solo restricciones apropiadas

Si eso parece un poco drástico, otra solución es establecer las restricciones para que funcionen correctamente con una transformación prevista. Si una vista tiene un tamaño puramente por su ancho y alto fijos internos, y está posicionado puramente por su centro, por ejemplo, mi transformación de escala funcionará como espero. En este código, otherView las restricciones existentes en una subvista ( otherView ) y las reemplazo con esas cuatro restricciones, dándole un ancho y una altura fijos y fijándolo puramente por su centro. Después de eso, mi transformación de escala funciona:

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

El resultado es que si no tiene restricciones que afecten el marco de una vista, la configuración automática no tocará el marco de la vista, que es exactamente lo que busca cuando se trata de una transformación.

Solución 3: use una subvista

El problema con ambas soluciones anteriores es que perdemos los beneficios de las restricciones para posicionar nuestra vista. Así que aquí hay una solución que resuelve eso. Comience con una vista invisible cuyo trabajo es únicamente actuar como un host, y use restricciones para posicionarlo. Dentro de eso, ponga la vista real como una subvista. Use restricciones para posicionar la subvista dentro de la vista del host, pero limite esas restricciones a las restricciones que no se verán afectadas cuando apliquemos una transformación.

Aquí hay una ilustración:

enter image description here

La vista blanca es la vista de host; se supone que debes pretender que es transparente y por lo tanto invisible. La vista roja es su subvista, posicionada al fijar su centro en el centro de la vista de host. Ahora podemos escalar y rotar la vista roja alrededor de su centro sin ningún problema, y ​​de hecho la ilustración muestra que lo hemos hecho:

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

Y mientras tanto, las restricciones en la vista del host lo mantienen en el lugar correcto mientras rotamos el dispositivo.

Solución 4: use transformaciones de capa en su lugar

En lugar de ver transformaciones, use transformaciones de capa, que no desencadenan el diseño y, por lo tanto, no causan un conflicto inmediato con las restricciones.

Por ejemplo, esta simple animación de vista “pulsátil” puede romperse bajo el diseño automático:

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

A pesar de que al final no hubo cambios en el tamaño de la vista, simplemente se configuró el diseño de las causas de transform , y las restricciones pueden hacer que la vista salte. (¿Esto se siente como un error o qué?) Pero si hacemos lo mismo con Core Animation (usando CABasicAnimation y aplicando la animación a la capa de la vista), el diseño no sucede, y funciona 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]; 

Tuve un Isuue similar y escuché Back from the Autolayout Team en Apple. Sugieren usar el enfoque de vista de contenedor que matt sugiere, pero crean una subclase de UIView para sobrescribir las vistas de diseño y aplicar el código de diseño personalizado allí. Funciona como un encanto.

El archivo de encabezado se ve así para que pueda vincular su subvista de elección directamente desde el generador de interfaz

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

y el archivo m aplica el código especial así:

 #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 

Como puede ver, toma el punto central de la Vista cuando se le llama por primera vez y reutiliza esa Posición en llamadas adicionales para colocar la Vista en consecuencia. Esto sobrescribe el código de Autolayout en ese sentido, que tiene lugar después de [superdispositivoSubviews]; que contiene el código de autolayout.

Al igual que ya no es necesario evitar Autolayout, pero puede crear su propio diseño automático cuando los comportamientos predeterminados ya no sean apropiados. Por supuesto, puedes aplicar cosas mucho más complicadas que las incluidas en ese ejemplo, pero esto fue todo lo que necesité, ya que mi aplicación solo puede usar el modo de retrato.

Encuentro una manera simple. Y funciona en iOS 8 e iOS 9.

Como ajustar anchorPoint cuando usa el diseño basado en marcos:

 let oldFrame = layerView.frame layerView.layer.anchorPoint = newAnchorPoint layerView.frame = oldFrame 

Cuando ajusta el ancla de la vista con el diseño automático, hace lo mismo pero de forma restringida. Cuando anchorPoint cambie de (0.5, 0.5) a (1, 0.5), el layerView se moverá hacia la izquierda con una distancia de la mitad del ancho de la vista, por lo que deberá compensarlo.

No entiendo la restricción en la pregunta. Por lo tanto, suponga que agrega una restricción centerX relativa a superView centerX con una constante: layerView.centerX = superView.centerX + constante

 layerView.layer.anchorPoint = CGPoint(1, 0.5) let centerXConstraint = ..... centerXConstraint.constant = centerXConstraint.constant + layerView.bounds.size.width/2 

Si está utilizando el diseño automático, entonces no veo cómo la posición de configuración manual le servirá a largo plazo porque eventualmente el diseño automático reducirá el valor de posición que ha establecido cuando calcula su propio diseño.

Más bien, lo que se necesita es modificar las propias restricciones de diseño para compensar los cambios producidos al establecer el punto de anclaje. La siguiente función hace eso para vistas no transformadas.

 /** 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 (eg, {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]; } 

Admito que probablemente esto no sea todo lo que esperabas, ya que generalmente la única razón por la que querrías modificar el anchorPoint es establecer una transformación. Eso requeriría una función más compleja que actualice las restricciones de diseño para reflejar todos los cambios de ttwig que podrían ser causados ​​por la propiedad de transformación en sí. Esto es complicado porque las transformaciones pueden hacer mucho para el marco. Una transformación de escala o rotación haría que el marco fuera más grande, por lo que necesitaríamos actualizar cualquier restricción de ancho o altura, etc.

Si solo está utilizando la transformación para una animación temporal, entonces lo que está arriba puede ser suficiente, ya que no creo que el diseño automático impida que la animación en vuelo presente imágenes que representen violaciones puramente transitorias de las restricciones.

tl: dr: puede crear una salida para una de las restricciones para que se pueda eliminar y volver a agregar.


Creé un nuevo proyecto y agregué una vista con un tamaño fijo en el centro. Las restricciones se muestran en la imagen a continuación.

Las limitaciones para el menor ejemplo en el que podría pensar.

Luego agregué una salida para la vista que va a rotar y para la restricción de alineación x central.

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

Más tarde en viewDidAppear calculo el nuevo punto de anclaje

 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; 

Luego elimino la restricción para la que tengo una salida, creo una nueva que está desfasada y la vuelvo a agregar. Después de eso le digo a la vista con la nueva restricción que necesita para actualizar las restricciones.

 [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]; 

Finalmente, solo agrego la animación de rotación a la vista giratoria.

 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"]; 

La capa giratoria parece que permanece centrada (lo que debería) incluso cuando se gira el dispositivo o hace que actualice las restricciones. La nueva restricción y el punto de anclaje modificado se cancelan visualmente entre sí.

Mi solución actual es ajustar manualmente la posición de la capa en viewDidLayoutSubviews . Este código también se puede usar en layoutSubviews para una subclase de vista, pero en mi caso mi vista es una vista de nivel superior dentro de un controlador de vista, por lo que no tuve que crear una subclase UIView.

Parece demasiado esfuerzo para que otras respuestas sean bienvenidas.

 -(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; } } } 

Inspirado en la respuesta de mi mate, decidí probar un enfoque diferente. Se puede usar una vista de contenedor, con restricciones aplicadas apropiadamente. La vista con el punto de anclaje modificado se puede colocar dentro de la vista del contenedor, utilizando máscaras de autocalibración y configuración explícita de marcos como en los viejos tiempos malos.

Funciona de maravilla, para mi situación de todos modos. Las vistas se configuran aquí en viewDidLoad:

 - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. UIView *redView = [UIView new]; redView.translatesAutoresizingMaskIntoConstraints = NO; redView.backgroundColor = [UIColor redColor]; [self.view addSubview:redView]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[redView]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(redView)]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[redView]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(redView)]]; self.redView = redView; UIView *greenView = [UIView new]; greenView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; greenView.layer.anchorPoint = CGPointMake(1.0, 0.5); greenView.frame = redView.bounds; greenView.backgroundColor = [UIColor greenColor]; [redView addSubview:greenView]; self.greenView = greenView; CATransform3D perspective = CATransform3DIdentity; perspective.m34 = 0.005; self.redView.layer.sublayerTransform = perspective; } 

No importa que los marcos para la vista roja sean cero en este punto, debido a la máscara de aumento automático en la vista verde.

Agregué una transformación de rotación en un método de acción, y este fue el resultado:

enter image description here

Pareció perderse durante la rotación del dispositivo, así que lo agregué al método viewDidLayoutSubviews:

 -(void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; [CATransaction begin]; [CATransaction setDisableActions:YES]; CATransform3D transform = self.greenView.layer.transform; self.greenView.layer.transform = CATransform3DIdentity; self.greenView.frame = self.redView.bounds; self.greenView.layer.transform = transform; [CATransaction commit]; } 

Creo que estás derrotando el propósito del autodiseño con ese método. Mencionaste que el ancho y el borde derecho dependen de la supervista, entonces ¿por qué no solo agregar restricciones a lo largo de esa línea de pensamiento?

Pierda el paradigma anchorPoint / transform e intente:

 [self.view addConstraint: [NSLayoutConstraint constraintWithItem:layerView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:1.0f constant:-somePadding]]; [self.view addConstraint: [NSLayoutConstraint constraintWithItem:layerView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:someViewWeDependTheWidthOn attribute:NSLayoutAttributeWidth multiplier:0.5f // because you want it to be half of someViewWeDependTheWidthOn constant:-20.0f]]; // your 20pt offset from the left 

La restricción NSLayoutAttributeRight significa exactamente como anchorPoint = CGPointMake(1.0, 0.5) y la restricción NSLayoutAttributeWidth es más o menos equivalente al NSLayoutAttributeLeft su código anterior.

Esta pregunta y respuestas me inspiraron para resolver mis propios problemas con el autodesplazamiento y la escala, pero con las vistas de desplazamiento. Creé un ejemplo de mi solución en github:

https://github.com/hansdesmedt/AutoLayout-scrollview-scale

Este es un ejemplo de UIScrollView con paginación personalizada hecha completamente en AutoLayout y escalable (CATransform3DMakeScale) con presionar y tocar para hacer zoom. iOS 6 y 7 compatibles.

Es un gran tema y no he leído todos los comentarios, pero estaba enfrentando el mismo problema.

Tenía una vista de XIB con autolayout. Y quería actualizar su propiedad de transformación. Incrustar la vista en una vista de contenedor no resuelve mi problema porque el diseño automático estaba actuando de forma extraña en la vista de contenedor. Es por eso que acabo de agregar la segunda vista de contenedor para contener la vista de contenedor que contiene mi vista y estaba aplicando transformaciones en ella.

tl; dr Digamos que cambiaste el punto de anclaje a (0, 0). El punto de anclaje ahora está arriba a la izquierda. Cada vez que vea el centro de palabra en el diseño automático, debería pensar en la parte superior izquierda .

Cuando ajusta su punto de ancla, simplemente cambia la semántica de AutoLayout. El diseño automático no interferirá con su anclaje ni viceversa. Si no entiendes esto, vas a tener un mal momento .


Ejemplo:

Figura A. Sin modificaciones del punto de anclaje

 #Before changing anchor point to top-left view.size == superview.size view.center == superview.center 

Figura B. Punto de anclaje cambiado a la esquina superior izquierda

 view.layer.anchorPoint = CGPointMake(0, 0) view.size == superview.size view.center == superview.topLeft <----- L0-0K, center is now top-left 

La Figura A y la Figura B se ven exactamente iguales. Nada ha cambiado. Solo la definición de a qué centro se refiere cambió.