Reputation: 12087
I want to alter the anchorPoint
, but keep the view in the same place.
I've tried NSLog
-ing self.layer.position
and self.center
and they both stay the same regardless of changes to the anchorPoint. Yet my view moves!
Any tips on how to do this?
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);
The output is:
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
Upvotes: 136
Views: 74346
Reputation: 944
Basically, CALayer.position
is the coordinates of the layers’s anchorPoint
in its superlayers’s coordinate system. That is why changing anchorPoint
doesn't change layer's position
.
The coordinates of anchorPoint
in layer's own coordinate system can be calculated as:
anchorPointCoordinate.x = layer.anchorPoint.x * layer.bounds.size.width
anchorPointCoordinate.y = layer.anchorPoint.y * layer.bounds.size.height
Based on this knowledge you can do all the adjustments needed for your layout.
Upvotes: 0
Reputation: 34175
iOS CALayer anchorPoint and position
CALayer.anchorPoint
is a source for UIView.anchorPoint
uses Unit coordinate space
Anout to specify internal point which will be used in geometric manipulations(like transsformations)
CALayer.position
is a source for UIView.center
uses Point-based coordinate systems
to reflect a CALayer.anchorPoint
in superview's coordinate systems
Simple rules:
CALayer.anchorPoint
, and based on CALayer.position
(and additional parameters like CALayer.bounds
, transforms...) content will be redrawnCALayer.position
content will be moved to a new pointExample:
frame: CGRect(origin: CGPoint(x: 50, y: 60), size: CGSize(width: 200, height: 300))
and anchorPoint = CGPoint(x: 0.5, y: 0.5)
// by defaultanchorPoint = CGPoint(x: 0, y: 0)
sorce code:
//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)
Based on this information and answering for the question - you should recalculate CALayer.position
after changing CALayer.anchorPoint
.
Upvotes: 0
Reputation: 410
Let a be the center of the layer in terms of anchorPoint coords (0.5, 0.5). Think unit coordinates/or uv coords if you are familiar with Metal/Vulkan/OpenGL/DirectX. Let n be the new anchor position (x, y). Let d be the dimensions of the bounds of the layer, (width, height).
Then to move back the layer to its original position after a layer anchorPoint position change, compute the offset vector from a to n multiplied by d: v = d(n-a). So then, to get your layer back to it's original position, add offset vector to the layer.position: layer.position += v
Swift/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
Your view.center moves when the layer.position changes(and vice versa) if the layer is the view's backing layer. If it was a sublayer.position, then that would not move view.center. So if in the above code, the layer was the view's backing layer, I could have offset view.center instead of layer.position, both would have worked.
Upvotes: 0
Reputation: 3693
There is such a simple solution. This is based on Kenny's answer. But instead of applying the old frame, use it's origin and the new one to calculate the translation, then apply that translation to the center. It works with rotated view too! Here's the code, a lot simpler than other solutions:
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)
}
Upvotes: 18
Reputation: 2137
I had the same problem. Brad Larson's solution worked great even when the view is rotated. Here is his solution translated into code.
-(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;
}
And the swift equivalent:
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
}
Upvotes: 211
Reputation: 12087
The key to solving this was to use the frame property, which is weirdly the only thing that changes.
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
Then I do my resize, where it scales from the anchorPoint. Then I have to restore the old anchorPoint;
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
EDIT: this flakes out if the view is rotated, as the frame property is undefined if a CGAffineTransform has been applied.
Upvotes: 48
Reputation: 992
Here is user945711's answer adjusted for NSView on OS X. Besides NSView not having a .center
property, the NSView's frame doesn't change (probably because NSViews do not come with a CALayer by default) but the CALayer frame origin changes when the anchorPoint is changed.
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)
}
Upvotes: 6
Reputation: 17560
This is an alternate solution which allows you to change the anchor point through the Attributes Inspector and has another property to view the anchor point for confirmation.
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
}
}
Turning on the Show Anchor Point will show a red dot so you can better see where the anchor point will be visually. You can always turn it off later.
This really helped me when planning transforms on UIViews.
Upvotes: 4
Reputation: 1820
Expanding on Magnus' great & thorough answer, I have created a version that works on sub layers:
-(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;
}
Upvotes: 0
Reputation: 1276
For 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
}
Upvotes: 1
Reputation: 39
If you change anchorPoint, its position will change too, UNLESS you origin is zero point CGPointZero
.
position.x == origin.x + anchorPoint.x;
position.y == origin.y + anchorPoint.y;
Upvotes: 4
Reputation: 170309
The Layer Geometry and Transforms section of the Core Animation Programming Guide explains the relationship between a CALayer's position and anchorPoint properties. Basically, the position of a layer is specified in terms of the location of the layer's anchorPoint. By default, a layer's anchorPoint is (0.5, 0.5), which lies at the center of the layer. When you set the position of the layer, you are then setting the location of the center of the layer in its superlayer's coordinate system.
Because the position is relative to the anchorPoint of the layer, changing that anchorPoint while maintaining the same position moves the layer. In order to prevent this movement, you would need to adjust the layer's position to account for the new anchorPoint. One way I've done this is to grab the layer's bounds, multiply the bounds' width and height by the old and new anchorPoint's normalized values, take the difference of the two anchorPoints, and apply that difference to the position of the layer.
You might even be able to account for rotation this way by using CGPointApplyAffineTransform()
with your UIView's CGAffineTransform.
Upvotes: 143
Reputation: 193
For those who need it, here is Magnus's solution in 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
}
Upvotes: 17
Reputation: 6073
For me understanding position
and anchorPoint
was easiest when I started comparing it with my understanding of frame.origin in UIView. A UIView with frame.origin = (20,30) means that the UIView is 20 points from left and 30 points from top of its parent view. This distance is calculated from which point of a UIView? Its calculated from top-left corner of a UIView.
In layer anchorPoint
marks the point (in normalized form i.e. 0 to 1) from where this distance is calculated so e.g. layer.position = (20, 30) means that the layer anchorPoint
is 20 points from left and 30 points from top of its parent layer. By default a layer anchorPoint is (0.5, 0.5) so the distance calculation point is right in the center of the layer. The following figure will help clarify my point:
anchorPoint
also happens to be the point around which rotation will happen in case you apply a transform to the layer.
Upvotes: 28