Cambiar el punto de anclaje de mi CALayer mueve la vista

131

Quiero alterar el anchorPoint, pero mantener la vista en el mismo lugar. He intentado NSLog-ing self.layer.positiony self.centerambos permanecen igual independientemente de los cambios en el AnchorPoint. ¡Sin embargo, mi vista se mueve!

¿Algunos consejos de como hacer esto?

self.layer.anchorPoint = CGPointMake(0.5, 0.5);
NSLog(@"center point: %f %f", self.layer.position.x, self.layer.position.y);
self.layer.anchorPoint = CGPointMake(1, 1);
NSLog(@"center point: %f %f", self.layer.position.x, self.layer.position.y);

El resultado es:

2009-12-27 20:43:24.161 Type[11289:207] center point: 272.500000 242.500000
2009-12-27 20:43:24.162 Type[11289:207] center point: 272.500000 242.500000
Kenny Winker
fuente

Respuestas:

139

La sección de Geometría de capa y transformaciones de la Guía de programación de animación central explica la relación entre la posición de CALayer y las propiedades de anchorPoint. Básicamente, la posición de una capa se especifica en términos de la ubicación del punto de anclaje de la capa. Por defecto, el AnchorPoint de una capa es (0.5, 0.5), que se encuentra en el centro de la capa. Cuando establece la posición de la capa, establece la ubicación del centro de la capa en el sistema de coordenadas de su supercapa.

Debido a que la posición es relativa al punto de anclaje de la capa, al cambiar ese punto de anclaje mientras se mantiene la misma posición se mueve la capa. Para evitar este movimiento, necesitará ajustar la posición de la capa para tener en cuenta el nuevo punto de anclaje. Una forma en que he hecho esto es tomar los límites de la capa, multiplicar el ancho y la altura de los límites por los valores normalizados viejos y nuevos de anchorPoint, tomar la diferencia de los dos puntos de anclaje y aplicar esa diferencia a la posición de la capa.

Es posible que incluso pueda dar cuenta de la rotación de esta manera si la usa CGPointApplyAffineTransform()con el CGAffineTransform de su UIView.

Brad Larson
fuente
44
Vea la función de ejemplo de Magnus para ver cómo se puede implementar la respuesta de Brad en Objective-C.
Jim Jeffers
Gracias pero no entienden cómo el anchorPointy positionestán relacionados entre sí?
dumbfingers
66
@ ss1271 - Como describí anteriormente, el punto de anclaje de una capa es la base de su sistema de coordenadas. Si se establece en (0.5, 0.5), cuando establece la posición de una capa, establece dónde se ubicará su centro en el sistema de coordenadas de su supercapa. Si establece el punto de anclaje en (0.0, 0.5), establecer la posición establecerá dónde estará el centro de su borde izquierdo. Nuevamente, Apple tiene algunas imágenes agradables en la documentación vinculada anteriormente (a la que he actualizado la referencia).
Brad Larson
Usé el método proporcionado por Magnus. Cuando NSLog posición y marco se ven correctos pero mi vista todavía se mueve. ¿Alguna idea de por qué? Es como si la nueva posición no se tuviera en cuenta.
Alexis
En mi caso, quiero agregar una animación en la capa usando el método videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer de la clase AVVideoCompositionCoreAnimationTool. Lo que sucede es que la mitad de mi capa sigue desapareciendo mientras la giro alrededor del eje y.
nr5
205

Yo tuve el mismo problema. La solución de Brad Larson funcionó de maravilla incluso cuando se gira la vista. Aquí está su solución traducida en código.

-(void)setAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)view
{
    CGPoint newPoint = CGPointMake(view.bounds.size.width * anchorPoint.x, 
                                   view.bounds.size.height * anchorPoint.y);
    CGPoint oldPoint = CGPointMake(view.bounds.size.width * view.layer.anchorPoint.x, 
                                   view.bounds.size.height * view.layer.anchorPoint.y);

    newPoint = CGPointApplyAffineTransform(newPoint, view.transform);
    oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform);

    CGPoint position = view.layer.position;

    position.x -= oldPoint.x;
    position.x += newPoint.x;

    position.y -= oldPoint.y;
    position.y += newPoint.y;

    view.layer.position = position;
    view.layer.anchorPoint = anchorPoint;
}

Y el equivalente rápido:

func setAnchorPoint(anchorPoint: CGPoint, forView view: UIView) {
    var newPoint = CGPointMake(view.bounds.size.width * anchorPoint.x, view.bounds.size.height * anchorPoint.y)
    var oldPoint = CGPointMake(view.bounds.size.width * view.layer.anchorPoint.x, view.bounds.size.height * view.layer.anchorPoint.y)

    newPoint = CGPointApplyAffineTransform(newPoint, view.transform)
    oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform)

    var position = view.layer.position
    position.x -= oldPoint.x
    position.x += newPoint.x

    position.y -= oldPoint.y
    position.y += newPoint.y

    view.layer.position = position
    view.layer.anchorPoint = anchorPoint
}

SWIFT 4.x

func setAnchorPoint(anchorPoint: CGPoint, forView view: UIView) {
    var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x,
                           y: view.bounds.size.height * anchorPoint.y)


    var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x,
                           y: view.bounds.size.height * view.layer.anchorPoint.y)

    newPoint = newPoint.applying(view.transform)
    oldPoint = oldPoint.applying(view.transform)

    var position = view.layer.position
    position.x -= oldPoint.x
    position.x += newPoint.x

    position.y -= oldPoint.y
    position.y += newPoint.y

    view.layer.position = position
    view.layer.anchorPoint = anchorPoint
}
Magnus
fuente
2
Perfecto y tiene mucho sentido una vez que pude ver tu ejemplo aquí. ¡Gracias por esto!
Jim Jeffers
2
Uso este código en mis métodos awakeFromNib / initWithFrame, pero aún no funciona, ¿necesito actualizar la pantalla? editar: setNeedsDisplay no funciona
Adam Carter
2
¿Por qué estás usando view.bounds? ¿No deberías estar usando layer.bounds?
zakdances
1
en mi caso, establecer el valor en: view.layer.position no cambia nada
AndrewK
55
FWIW, tuve que ajustar el método setAnchor en un bloque dispatch_async para que funcione. No estoy seguro de por qué, ya que lo llamo dentro de viewDidLoad. ¿ViewDidLoad ya no está en la cola del hilo principal?
John Fowler
44

La clave para resolver esto fue usar la propiedad frame, que es extrañamente lo único que cambia.

Swift 2

let oldFrame = self.frame
self.layer.anchorPoint = CGPointMake(1, 1)
self.frame = oldFrame

Swift 3

let oldFrame = self.frame
self.layer.anchorPoint = CGPoint(x: 1, y: 1)
self.frame = oldFrame

Luego hago el cambio de tamaño, donde se escala desde el punto de anclaje. Luego tengo que restaurar el viejo punto de anclaje;

Swift 2

let oldFrame = self.frame
self.layer.anchorPoint = CGPointMake(0.5,0.5)
self.frame = oldFrame

Swift 3

let oldFrame = self.frame
self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.frame = oldFrame

EDITAR: esto se descarta si la vista se gira, ya que la propiedad del marco no está definida si se ha aplicado una CGAffineTransform.

Kenny Winker
fuente
9
Solo quería agregar por qué esto funciona y parece ser la forma más fácil: según los documentos, la propiedad frame es una propiedad "compuesta": "El valor de frame se deriva de los límites, anchorPoint y las propiedades de posición". Esa es también la razón por la cual la propiedad "frame" no es animable.
DarkDust
1
Este es honestamente mi enfoque preferido. Si no está tratando de manipular los límites y la posición de forma independiente o animarlos, simplemente es suficiente capturar el marco antes de cambiar el punto de anclaje. No se necesitan matemáticas.
CIFilter
28

Para mi comprension positionyanchorPoint fue más fácil cuando comencé a compararlo con mi comprensión de frame.origin en UIView. Una UIView con frame.origin = (20,30) significa que la UIView está a 20 puntos desde la izquierda y 30 puntos desde la parte superior de su vista principal. ¿Desde qué punto de una UIView se calcula esta distancia? Se calcula desde la esquina superior izquierda de una UIView.

En capa anchorPointmarca el punto (en forma normalizada, es decir, 0 a 1) desde donde se calcula esta distancia, por ejemplo, layer.position = (20, 30) significa que la capa anchorPointestá a 20 puntos desde la izquierda y 30 puntos desde la parte superior de su capa principal. Por defecto, un punto de anclaje de capa es (0.5, 0.5) por lo que el punto de cálculo de distancia está justo en el centro de la capa. La siguiente figura ayudará a aclarar mi punto:

ingrese la descripción de la imagen aquí

anchorPoint También resulta ser el punto alrededor del cual ocurrirá la rotación en caso de que aplique una transformación a la capa.

2cupsOfTech
fuente
1
Por favor, ¿puede decir cómo puedo resolver este punto de anclaje? Saltando quién está configurado UIView usando el diseño automático, significa que con el diseño automático se supone que no debemos obtener o establecer propiedades de marcos y límites.
SJ
17

Hay una solución tan simple. Esto se basa en la respuesta de Kenny. Pero en lugar de aplicar el marco anterior, use su origen y el nuevo para calcular la traducción, luego aplique esa traducción al centro. ¡Funciona con vista girada también! Aquí está el código, mucho más simple que otras soluciones:

func setAnchorPoint(anchorPoint: CGPoint, view: UIView) {
   let oldOrigin = view.frame.origin
   view.layer.anchorPoint = anchorPoint
   let newOrigin = view.frame.origin

   let translation = CGPoint(x: newOrigin.x - oldOrigin.x, y: newOrigin.y - oldOrigin.y)

   view.center = CGPoint(x: view.center.x - translation.x, y: view.center.y - translation.y)
}
Arroz frito
fuente
2
Un pequeño método agradable ... funciona bien con un par de correcciones menores: Línea 3: P mayúscula en anchorPoint. Línea 8: TRANS.Y = NUEVO - ANTIGUO
bob
2
Todavía estoy confundido, ¿cómo puedo usarlo? Estoy enfrentando el mismo problema en este momento para rotar mi vista con la rotación uigesture. Me extraña que a dónde debería llamar este método. Si estoy llamando al gestor de reconocimiento de gestos. Hace que la vista desaparezca.
JAck
3
Esta respuesta es tan dulce que ahora tengo diabetes.
GeneCode
14

Para aquellos que lo necesitan, aquí está la solución de Magnus en Swift:

func setAnchorPoint(anchorPoint: CGPoint, view: UIView) {
    var newPoint: CGPoint = CGPointMake(view.bounds.size.width * anchorPoint.x, view.bounds.size.height * anchorPoint.y)
    var oldPoint: CGPoint = CGPointMake(view.bounds.size.width * view.layer.anchorPoint.x, view.bounds.size.height * view.layer.anchorPoint.y)

    newPoint = CGPointApplyAffineTransform(newPoint, view.transform)
    oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform)

    var position: CGPoint = view.layer.position

    position.x -= oldPoint.x
    position.x += newPoint.x

    position.y -= oldPoint.y
    position.y += newPoint.y

    view.setTranslatesAutoresizingMaskIntoConstraints(true)     // Added to deal with auto layout constraints
    view.layer.anchorPoint = anchorPoint
    view.layer.position = position
}
Corey Davis
fuente
1
voto a favor para view.setTranslatesAutoresizingMaskIntoConstraints(true). gracias hombre
Oscar Yuandinata
1
view.setTranslatesAutoresizingMaskIntoConstraints(true)es necesario cuando se usan restricciones de diseño automático.
SamirChen
1
muchas gracias por esto! Esto translatesAutoresizingMaskIntoConstraintses justo lo que me faltaba, podría haber aprendido ahora
Ziga
5

Aquí está la respuesta del usuario945711 ajustada para NSView en OS X. Además de que NSView no tiene una .centerpropiedad, el marco de NSView no cambia (probablemente porque los NSView no vienen con un CALayer por defecto) pero el origen del marco de CALayer cambia cuando se cambia el anchorPoint.

func setAnchorPoint(anchorPoint: NSPoint, view: NSView) {
    guard let layer = view.layer else { return }

    let oldOrigin = layer.frame.origin
    layer.anchorPoint = anchorPoint
    let newOrigin = layer.frame.origin

    let transition = NSMakePoint(newOrigin.x - oldOrigin.x, newOrigin.y - oldOrigin.y)
    layer.frame.origin = NSMakePoint(layer.frame.origin.x - transition.x, layer.frame.origin.y - transition.y)
}
iMaddin
fuente
4

Si cambia anchorPoint, su posición también cambiará, A MENOS QUE su origen sea punto cero CGPointZero.

position.x == origin.x + anchorPoint.x;
position.y == origin.y + anchorPoint.y;
usuario3156083
fuente
1
No se puede comparar la posición y el punto de anclaje, el punto de anclaje se escala entre 0 y 1.0
Edward Anthony
4

Edite y vea el punto de anclaje de UIView a la derecha en Storyboard (Swift 3)

Esta es una solución alternativa que le permite cambiar el punto de anclaje a través del Inspector de atributos y tiene otra propiedad para ver el punto de anclaje para su confirmación.

Crea un nuevo archivo para incluir en tu proyecto

import UIKit

@IBDesignable
class UIViewAnchorPoint: UIView {

    @IBInspectable var showAnchorPoint: Bool = false
    @IBInspectable var anchorPoint: CGPoint = CGPoint(x: 0.5, y: 0.5) {
        didSet {
            setAnchorPoint(anchorPoint: anchorPoint)
        }
    }

    override func draw(_ rect: CGRect) {
        if showAnchorPoint {
            let anchorPointlayer = CALayer()
            anchorPointlayer.backgroundColor = UIColor.red.cgColor
            anchorPointlayer.bounds = CGRect(x: 0, y: 0, width: 6, height: 6)
            anchorPointlayer.cornerRadius = 3

            let anchor = layer.anchorPoint
            let size = layer.bounds.size

            anchorPointlayer.position = CGPoint(x: anchor.x * size.width, y: anchor.y * size.height)
            layer.addSublayer(anchorPointlayer)
        }
    }

    func setAnchorPoint(anchorPoint: CGPoint) {
        var newPoint = CGPoint(x: bounds.size.width * anchorPoint.x, y: bounds.size.height * anchorPoint.y)
        var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y)

        newPoint = newPoint.applying(transform)
        oldPoint = oldPoint.applying(transform)

        var position = layer.position
        position.x -= oldPoint.x
        position.x += newPoint.x

        position.y -= oldPoint.y
        position.y += newPoint.y

        layer.position = position
        layer.anchorPoint = anchorPoint
    }
}

Agregar vista al guión gráfico y establecer la clase personalizada

Clase personalizada

Ahora establezca el Nuevo Punto de Anclaje para la UIView

Demostración

Al activar Mostrar punto de anclaje se mostrará un punto rojo para que pueda ver mejor dónde estará visualmente el punto de anclaje. Siempre puedes apagarlo más tarde.

Esto realmente me ayudó a la hora de planificar transformaciones en UIViews.

Mark Moeykens
fuente
Ningún punto en UIView, utilicé Objective-C con Swift, adjunté la clase personalizada de UIView de su código y activé Mostrar ancla ... pero ningún punto
Genevios
1

Para Swift 3:

func setAnchorPoint(_ anchorPoint: CGPoint, forView view: UIView) {
    var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x, y: view.bounds.size.height * anchorPoint.y)
    var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x, y: view.bounds.size.height * view.layer.anchorPoint.y)

    newPoint = newPoint.applying(view.transform)
    oldPoint = oldPoint.applying(view.transform)

    var position = view.layer.position
    position.x -= oldPoint.x
    position.x += newPoint.x

    position.y -= oldPoint.y
    position.y += newPoint.y

    view.layer.position = position
    view.layer.anchorPoint = anchorPoint
}
AJ9
fuente
1
Gracias por esto :) Acabo de pegarlo en mi proyecto como una función estática y funciona a las mil maravillas: D
Kjell
0

Ampliando la excelente y exhaustiva respuesta de Magnus, he creado una versión que funciona en subcapas:

-(void)setAnchorPoint:(CGPoint)anchorPoint forLayer:(CALayer *)layer
{
    CGPoint newPoint = CGPointMake(layer.bounds.size.width * anchorPoint.x, layer.bounds.size.height * anchorPoint.y);
    CGPoint oldPoint = CGPointMake(layer.bounds.size.width * layer.anchorPoint.x, layer.bounds.size.height * layer.anchorPoint.y);
    CGPoint position = layer.position;
    position.x -= oldPoint.x;
    position.x += newPoint.x;
    position.y -= oldPoint.y;
    position.y += newPoint.y;
    layer.position = position;
    layer.anchorPoint = anchorPoint;
}
Charles Robertson
fuente