Изменение anchorPoint моего CALayer перемещает представление

Я хочу изменить anchorPoint, но держать вид в том же месте. я пробовал NSLog-ную self.layer.position а также self.center и оба они остаются неизменными независимо от изменений в anchorPoint. И все же мой взгляд движется!

Любые советы о том, как это сделать?

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

Выход:

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

13 ответов

Решение

Раздел " Геометрия и преобразования слоев" в Руководстве по программированию базовой анимации объясняет взаимосвязь между положением CALayer и свойствами anchorPoint. По сути, положение слоя определяется с точки зрения местоположения anchorPoint слоя. По умолчанию точка привязки слоя (0.5, 0.5) находится в центре слоя. Когда вы устанавливаете положение слоя, вы затем устанавливаете положение центра слоя в системе координат его суперслоя.

Поскольку положение относительно точки привязки слоя, изменение этой точки привязки при сохранении той же позиции перемещает слой. Чтобы предотвратить это движение, вам нужно настроить положение слоя, чтобы учесть новую точку привязки. Один из способов сделать это - захватить границы слоя, умножить ширину и высоту границ на нормализованные значения старого и нового anchorPoint, взять разницу двух точек привязки и применить эту разницу к позиции слоя.

Вы могли бы даже иметь возможность учитывать вращение таким образом, используя CGPointApplyAffineTransform() с вашим UIView CGAffineTransform.

У меня такая же проблема. Решение Брэда Ларсона работало отлично, даже когда вид повернут. Вот его решение переведено в код.

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

И быстрый эквивалент:

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
}

Ключом к решению этой проблемы было использование свойства frame, которое, как ни странно, единственное, что меняется.

Swift 2

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

Свифт 3

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

Затем я делаю мой размер, где он масштабируется от anchorPoint. Затем я должен восстановить старую anchorPoint;

Swift 2

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

Свифт 3

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

РЕДАКТИРОВАТЬ: это прекращается, если представление поворачивается, поскольку свойство кадра не определено, если применено преобразование CGAffineTransform.

Для меня понимание position а также anchorPoint было проще всего, когда я начал сравнивать его с моим пониманием frame.origin в UIView. UIView с frame.origin = (20,30) означает, что UIView находится на 20 точек слева и на 30 точек сверху его родительского представления. Это расстояние рассчитывается с какой точки UIView? Его рассчитывается из верхнего левого угла UIView.

В слое anchorPoint отмечает точку (в нормализованной форме, т.е. от 0 до 1), из которой рассчитывается это расстояние, например, layer.position = (20, 30) означает, что слой anchorPoint 20 точек слева и 30 точек сверху его родительского слоя. По умолчанию значение anchorPoint для слоя составляет (0,5, 0,5), поэтому точка вычисления расстояния находится прямо в центре слоя. Следующий рисунок поможет прояснить мою точку зрения:

anchorPoint также бывает точка, вокруг которой произойдет вращение, если вы примените преобразование к слою.

Есть такое простое решение. Это основано на ответе Кенни. Но вместо применения старого фрейма используйте его источник и новые для вычисления перехода, затем примените этот переход к центру. Работает с повернутым видом тоже! Вот код, намного более простой, чем другие решения:

- (void) setAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)view {
   CGPoint oldOrigin = view.frame.origin;
   view.layer.anchorPoint = anchorPoint;
   CGPoint newOrigin = view.frame.origin;

   CGPoint transition;
   transition.x = newOrigin.x - oldOrigin.x;
   transition.y = newOrigin.y - oldOrigin.y;

   view.center = CGPointMake (view.center.x - transition.x, view.center.y - transition.y);
}

И версия Swift:

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

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

   view.center = CGPoint(x: view.center.x - transition.x, y: view.center.y - transition.y)
}

Для тех, кто в этом нуждается, вот решение Магнуса в 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
}

Вот ответ пользователя 945711, настроенный для NSView на OS X. Кроме NSView, не имеющего .center Свойство, кадр NSView не изменяется (вероятно, потому что NSViews не идут с CALayer по умолчанию), но происхождение кадра CALayer изменяется, когда 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)
}

Отредактируйте и увидите опорную точку UIView прямо на раскадровке (Swift 3)

Это альтернативное решение, которое позволяет изменить точку привязки через инспектор атрибутов и имеет еще одно свойства, чтобы просмотреть точку привязки для подтверждения.

Создать новый файл для включения в ваш проект

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

Добавить вид в раскадровку и установить пользовательский класс

Пользовательский класс

Теперь установите новую опорную точку для UIView

демонстрация

Включение "Показать опорную точку" покажет красную точку, чтобы вы могли лучше видеть, где опорная точка будет визуально. Вы всегда можете выключить его позже.

Это действительно помогло мне при планировании преобразований в UIViews.

Если вы измените anchorPoint, его позиция тоже изменится, ЕСЛИ БЕЗ вашего происхождения не будет нулевой точкой CGPointZero,

position.x == origin.x + anchorPoint.x;
position.y == origin.y + anchorPoint.y;

Для 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
}

iOS CALayer якорная точка и позиция

[iOS CALayer]

является источником дляUIView.anchorPointиспользуетUnit coordinate space Anout для указания внутренней точки, которая будет использоваться в геометрических манипуляциях (например, преобразованиях).

является источником дляUIView.centerиспользуетPoint-based coordinate systemsдля отражения в системах координат супервизора

Простые правила:

  • когда вы меняете, и на основе (и дополнительных параметров, таких какCALayer.bounds, трансформируется...) контент будет перерисован
  • при изменении контента будет перенесен в новую точку

Пример:

  • шаг 1. У нас есть UIViewframe: CGRect(origin: CGPoint(x: 50, y: 60), size: CGSize(width: 200, height: 300))иanchorPoint = CGPoint(x: 0.5, y: 0.5)// по умолчанию
  • шаг 2. Давайте поменяемсяanchorPoint = CGPoint(x: 0, y: 0)

исходный код:

      //step 1
let viewA = UIView(
    frame: CGRect(
        origin: CGPoint(x: 50, y: 60),
        size: CGSize(width: 200, height: 300)
    )
)
viewA.anchorPoint = CGPoint(x: 0.5, y: 0.5) //by default

viewA.translatesAutoresizingMaskIntoConstraints = false
viewA.backgroundColor = .gray
self.view.addSubview(viewA)

//step 2
viewA.anchorPoint = CGPoint(x: 0, y: 0)

На основании этой информации и ответа на вопрос - следует произвести перерасчет.CALayer.positionпосле измененияCALayer.anchorPoint.

Развивая отличный и полный ответ Магнуса, я создал версию, которая работает на подслоях:

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

Пусть a будет центром слоя с точки зрения координат anchorPoint (0,5, 0,5). Если вы знакомы с Metal/Vulkan/OpenGL/DirectX, подумайте о координатах юнитов/или координатах uv. Пусть n будет новой позицией привязки (x, y). Пусть d — размеры границ слоя (ширина, высота).

Затем, чтобы вернуть слой в исходное положение после изменения положения AnchorPoint слоя, вычислите вектор смещения от a до n, умноженный на d: v = d ( n - a ). Итак, чтобы вернуть слой в исходное положение, добавьте вектор смещения к layer.position: layer.position += v

Свифт/iOS

      let vx = (layer.anchorPoint.x - 0.5) * layer.bounds.width
let vy = (layer.anchorPoint.y - 0.5) * layer.bounds.height
layer.position.x += vx
layer.position.y += vy

Ваш view.center перемещается, когда изменяется layer.position(и наоборот), если слой является резервным слоем представления. Если бы это был sublayer.position, то view.center не сдвинулся бы. Итак, если бы в приведенном выше коде слой был вспомогательным слоем представления, я мог бы использовать смещение view.center вместо layer.position, оба варианта сработали бы.

Другие вопросы по тегам