Zmiana punktu kotwicy CALayer powoduje przesunięcie widoku

132

Chcę zmienić anchorPoint, ale zachować widok w tym samym miejscu. Próbowałem NSLog-ing self.layer.positioni self.centeri obaj pozostają takie same, niezależnie od zmian w Anchorpoint. Jednak mój pogląd się porusza!

Jakieś wskazówki, jak to zrobić?

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

Wynik to:

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
źródło

Odpowiedzi:

140

Sekcja Layer Geometry and Transforms w Core Animation Programming Guide wyjaśnia związek między pozycją CALayer a właściwościami anchorPoint. Zasadniczo położenie warstwy jest określane na podstawie położenia jej punktu zakotwiczenia. Domyślnie punkt zakotwiczenia warstwy to (0,5, 0,5), który znajduje się w środku warstwy. Kiedy ustawiasz położenie warstwy, ustawiasz następnie położenie środka warstwy w układzie współrzędnych jej superwarstwy.

Ponieważ położenie jest zależne od punktu kotwicy warstwy, zmiana tego punktu kotwicy przy zachowaniu tej samej pozycji powoduje przesunięcie warstwy. Aby zapobiec temu ruchowi, należałoby dostosować położenie warstwy, aby uwzględnić nowy punkt kotwicy. Jednym ze sposobów, w jaki to zrobiłem, jest złapanie granic warstwy, pomnożenie szerokości i wysokości granic przez stare i nowe znormalizowane wartości anchorPoint, pobranie różnicy między dwoma anchorPoints i zastosowanie tej różnicy do położenia warstwy.

Możesz nawet w ten sposób uwzględnić rotację, używając CGPointApplyAffineTransform()z CGAffineTransform swojego UIView.

Brad Larson
źródło
4
Zobacz przykładową funkcję Magnusa, aby zobaczyć, jak odpowiedź Brada może zostać zaimplementowana w Objective-C.
Jim Jeffers
Dzięki, ale nie rozumiem, jak te anchorPointi positionsą ze sobą powiązane?
dumbfingers
6
@ ss1271 - Jak opisałem powyżej, anchorPoint warstwy jest podstawą jej układu współrzędnych. Jeśli jest ustawiony na (0,5, 0,5), to kiedy ustawiasz położenie warstwy, ustalasz, gdzie będzie jej środek w układzie współrzędnych jej superwarstwy. Jeśli ustawisz anchorPoint na (0,0, 0,5), ustawienie pozycji spowoduje, że będzie znajdować się środek jego lewej krawędzi. Ponownie, Apple ma kilka ładnych obrazów w powyższej dokumentacji (do której zaktualizowałem odniesienie).
Brad Larson
Użyłem metody dostarczonej przez Magnusa. Kiedy I NSLog pozycja i kadr, wyglądają na poprawne, ale mój widok jest nadal przesunięty. Każdy pomysł, dlaczego? To tak, jakby nowa pozycja nie została uwzględniona.
Alexis
W moim przypadku chcę dodać animację na warstwie przy użyciu metody videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer klasy AVVideoCompositionCoreAnimationTool. Dzieje się tak, że połowa mojej warstwy znika, gdy obracam ją wokół osi y.
nr5
205

Miałem ten sam problem. Rozwiązanie Brada Larsona działało świetnie, nawet gdy widok jest obrócony. Oto jego rozwiązanie przetłumaczone na kod.

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

I szybki odpowiednik:

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
źródło
2
Idealne i ma sens, kiedy mogłem zobaczyć tutaj twój przykład. Dzięki za to!
Jim Jeffers
2
Używam tego kodu w moich metodach awakeFromNib / initWithFrame, ale nadal nie działa, czy muszę zaktualizować wyświetlacz? edit: setNeedsDisplay nie działa
Adam Carter
2
Dlaczego używasz view.bounds? Nie powinieneś używać layer.bounds?
zakdances
1
w moim przypadku ustawienie wartości na: view.layer.position nic nie zmienia
AndrewK
5
FWIW, musiałem zawinąć metodę setAnchor w blok dispatch_async, aby działała. Nie wiem, dlaczego, ponieważ nazywam to wewnątrz viewDidLoad. Czy viewDidLoad nie znajduje się już w kolejce głównego wątku?
John Fowler
46

Kluczem do rozwiązania tego problemu było użycie właściwości frame, która jest dziwną jedyną rzeczą, która się zmienia.

Szybki 2

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

Szybki 3

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

Następnie zmieniam rozmiar, w którym skaluje się od punktu kotwiczenia. Następnie muszę przywrócić stary anchorPoint;

Szybki 2

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

Szybki 3

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

EDYTUJ: ta opcja znika, jeśli widok jest obrócony, ponieważ właściwość ramki jest niezdefiniowana, jeśli zastosowano CGAffineTransform.

Kenny Winker
źródło
10
Chciałem tylko dodać, dlaczego to działa i wydaje się być najłatwiejszym sposobem: zgodnie z dokumentacją właściwość frame jest właściwością „złożoną”: „Wartość frame pochodzi z właściwości bounds, anchorPoint i position”. Dlatego też właściwość „frame” nie jest animowana.
DarkDust,
2
Szczerze mówiąc, jest to moje preferowane podejście. Jeśli nie próbujesz samodzielnie manipulować granicami i pozycją lub animować żadnego z nich, zwykle wystarczy przechwycić klatkę przed zmianą punktu kontrolnego. Nie potrzeba matematyki.
CIFilter
28

Dla mnie zrozumienie positioni anchorPointbyło to najłatwiejsze, gdy zacząłem porównywać to z moim rozumieniem frame.origin w UIView. UIView z frame.origin = (20,30) oznacza, że ​​UIView ma 20 punktów od lewej i 30 punktów od góry widoku nadrzędnego. Ta odległość jest obliczana z którego punktu UIView? Jest obliczany z lewego górnego rogu UIView.

W warstwie anchorPointzaznacza się punkt (w znormalizowanej formie tj. Od 0 do 1), od którego obliczana jest ta odległość, więc np. Layer.position = (20, 30) oznacza, że ​​warstwa anchorPointznajduje się 20 punktów od lewej i 30 punktów od góry jej warstwy macierzystej. Domyślnie punkt zakotwiczenia warstwy to (0,5, 0,5), więc punkt obliczania odległości znajduje się dokładnie w środku warstwy. Poniższy rysunek pomoże wyjaśnić mój punkt widzenia:

wprowadź opis obrazu tutaj

anchorPoint zdarza się również, że jest to punkt, wokół którego nastąpi obrót w przypadku zastosowania transformacji do warstwy.

2cupsOfTech
źródło
1
Czy możesz powiedzieć, w jaki sposób mogę rozwiązać ten skok punktu kotwiczenia, kto UIView jest ustawiony przy użyciu automatycznego układu, co oznacza, że ​​w przypadku automatycznego układu nie powinniśmy pobierać ani ustawiać właściwości ramek i granic.
SJ
17

Jest takie proste rozwiązanie. To jest oparte na odpowiedzi Kenny'ego. Ale zamiast stosować starą ramkę, użyj jej początku i nowej do obliczenia tłumaczenia, a następnie zastosuj to tłumaczenie do środka. Działa również z widokiem obróconym! Oto kod, dużo prostszy niż inne rozwiązania:

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)
}
Smażony ryż
źródło
2
Niezła metoda ... działa dobrze z kilkoma drobnymi poprawkami: Wiersz 3: duże P w anchorPoint. Linia 8: TRANS.Y = NOWE - STARE
bob
2
nadal jestem zdezorientowany, jak mogę go używać. W tej chwili mam ten sam problem dotyczący obracania mojego widoku przy uigesturerotation. Dziwię się, że gdzie mam nazwać tę metodę. Jeśli wzywam moduł obsługi rozpoznawania gestów. To sprawia, że ​​widok znika.
JAck
3
Ta odpowiedź jest tak słodka, że ​​teraz mam cukrzycę.
GeneCode
15

Dla tych, którzy tego potrzebują, oto rozwiązanie Magnusa w 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
źródło
1
głosuj za view.setTranslatesAutoresizingMaskIntoConstraints(true). dzięki człowiekowi
Oscar Yuandinata
1
view.setTranslatesAutoresizingMaskIntoConstraints(true)jest konieczne, gdy używane są ograniczenia automatycznego układu.
SamirChen
1
Bardzo dziękuję za to! To translatesAutoresizingMaskIntoConstraintsjest właśnie to, czego mi brakowało, do tej pory mogłem się nauczyć
Ziga
5

Oto odpowiedź użytkownika945711 dostosowana do NSView na OS X. Poza tym, że NSView nie ma .centerwłaściwości, ramka NSView nie zmienia się (prawdopodobnie dlatego, że NSViews domyślnie nie są dostarczane z CALayer), ale początek ramki CALayer zmienia się, gdy zmienia się punkt 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
źródło
4

Jeśli zmienisz anchorPoint, jego pozycja również się zmieni, chyba że twój początek jest punktem zerowym CGPointZero.

position.x == origin.x + anchorPoint.x;
position.y == origin.y + anchorPoint.y;
user3156083
źródło
1
Nie możesz porównać pozycji i anchorPoint, anchorPoint skaluje się od 0 do 1,0
Edward Anthony
4

Edytuj i zobacz punkt zakotwiczenia UIView bezpośrednio na Storyboard (Swift 3)

Jest to alternatywne rozwiązanie, które umożliwia zmianę punktu zakotwiczenia za pomocą Inspektora atrybutów i ma inną właściwość do wyświetlania punktu zakotwiczenia w celu potwierdzenia.

Utwórz nowy plik, który zostanie dołączony do projektu

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

Dodaj widok do scenorysu i ustaw klasę niestandardową

Klasa niestandardowa

Teraz ustaw nowy punkt kotwiczenia dla UIView

Demonstracja

Włączenie punktu kontrolnego Pokaż punkt zakotwiczenia spowoduje wyświetlenie czerwonej kropki, dzięki czemu będzie można lepiej zobaczyć, gdzie będzie punkt kontrolny. Zawsze możesz to wyłączyć później.

To naprawdę pomogło mi przy planowaniu zmian w UIViews.

Mark Moeykens
źródło
Żadnej kropki na UIView, użyłem Objective-C z Swift, dołączyłem niestandardową klasę UIView z twojego kodu i
włączyłem
1

Dla 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
źródło
1
Dziękuję za to :) Właśnie wkleiłem go do mojego projektu jako statyczna funkcja i działa jak marzenie: D
Kjell
0

Rozwijając świetną i dokładną odpowiedź Magnusa, stworzyłem wersję, która działa na warstwach podrzędnych:

-(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
źródło