I have a view with an image which responds to pinch, rotation and pan gestures. I want that pinching and rotation of the image would be done with respect to the anchor point placed in the middle of the screen, exactly as it is done using Xcode iPhone simulator by pressing the options key. How can I place the anchor point in the middle of the screen if the centre of the image might be scaled and panned to a different location?
Here's my scale and rotate gesture functions:
@IBAction func pinchGesture(_ gestureRecognizer: UIPinchGestureRecognizer) {
// Move the achor point of the view's layer to the touch point
// so that scaling the view and the layer becames simpler.
self.adjustAnchorPoint(gestureRecognizer: gestureRecognizer)
// Scale the view by the current scale factor.
if(gestureRecognizer.state == .began) {
// Reset the last scale, necessary if there are multiple objects with different scales
lastScale = gestureRecognizer.scale
}
if (gestureRecognizer.state == .began || gestureRecognizer.state == .changed) {
let currentScale = gestureRecognizer.view!.layer.value(forKeyPath:"transform.scale")! as! CGFloat
// Constants to adjust the max/min values of zoom
let kMaxScale:CGFloat = 15.0
let kMinScale:CGFloat = 1.0
var newScale = 1 - (lastScale - gestureRecognizer.scale)
newScale = min(newScale, kMaxScale / currentScale)
newScale = max(newScale, kMinScale / currentScale)
let transform = (gestureRecognizer.view?.transform)!.scaledBy(x: newScale, y: newScale);
gestureRecognizer.view?.transform = transform
lastScale = gestureRecognizer.scale // Store the previous scale factor for the next pinch gesture call
scale = currentScale // Save current scale for later use
}
}
@IBAction func rotationGesture(_ gestureRecognizer: UIRotationGestureRecognizer) {
// Move the achor point of the view's layer to the center of the
// user's two fingers. This creates a more natural looking rotation.
self.adjustAnchorPoint(gestureRecognizer: gestureRecognizer)
// Apply the rotation to the view's transform.
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
gestureRecognizer.view?.transform = (gestureRecognizer.view?.transform.rotated(by: gestureRecognizer.rotation))!
// Set the rotation to 0 to avoid compouding the
// rotation in the view's transform.
angle += gestureRecognizer.rotation // Save rotation angle for later use
gestureRecognizer.rotation = 0.0
}
}
func adjustAnchorPoint(gestureRecognizer : UIGestureRecognizer) {
if gestureRecognizer.state == .began {
let view = gestureRecognizer.view
let locationInView = gestureRecognizer.location(in: view)
let locationInSuperview = gestureRecognizer.location(in: view?.superview)
// Move the anchor point to the the touch point and change the position of the view
view?.layer.anchorPoint = CGPoint(x: (locationInView.x / (view?.bounds.size.width)!), y: (locationInView.y / (view?.bounds.size.height)!))
view?.center = locationInSuperview
}
}
EDIT
I see that people aren't eager to get into this. Let me help by sharing some progress I've made in the past few days.
Firstly, I wrote a function centerAnchorPoint
which correctly places the anchor point of an image to the centre of the screen regardless of where that anchor point was previously. However the image must not be scaled or rotated for it to work.
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
}
func centerAnchorPoint(gestureRecognizer : UIGestureRecognizer) {
if gestureRecognizer.state == .ended {
view?.layer.anchorPoint = CGPoint(x: (photo.bounds.midX / (view?.bounds.size.width)!), y: (photo.bounds.midY / (view?.bounds.size.height)!))
}
}
func centerAnchorPoint() {
// Position of the current anchor point
let currentPosition = photo.layer.anchorPoint
self.setAnchorPoint(CGPoint(x: 0.5, y: 0.5), forView: photo)
// Center of the image
let imageCenter = CGPoint(x: photo.center.x, y: photo.center.y)
self.setAnchorPoint(currentPosition, forView: photo)
// Center of the screen
let screenCenter = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)
// Distance between the centers
let distanceX = screenCenter.x - imageCenter.x
let distanceY = screenCenter.y - imageCenter.y
// Find new anchor point
let newAnchorPoint = CGPoint(x: (imageCenter.x+2*distanceX)/(UIScreen.main.bounds.size.width), y: (imageCenter.y+2*distanceY)/(UIScreen.main.bounds.size.height))
//photo.layer.anchorPoint = newAnchorPoint
self.setAnchorPoint(newAnchorPoint, forView: photo)
let dotPath = UIBezierPath(ovalIn: CGRect(x: photo.layer.position.x-2.5, y: photo.layer.position.y-2.5, width: 5, height: 5))
layer.path = dotPath.cgPath
}
Function setAchorPoint
is used here to set anchor point to a new position without moving an image.
Then I updated panGesture
function by inserting this at the end of it:
if gestureRecognizer.state == .ended {
self.centerAnchorPoint()
}
EDIT 2
Ok, so I'll try to simply explain the code above.
What I am doing is:
newAnchorPointX = (imageCenter.x-distanceX)/screenWidth + distanceX/screenWidth
Then do the same for y position.
setAnchorPoint
functionAs I said this works great if the image is not scaled. If it is, then the anchor point does not stay at the center.
Strangely enough distanceX or distanceY doesn't exactly depend on scale value, so something like this doesn't quite work:
newAnchorPointX = (imageCenter.x-distanceX)/screenWidth + distanceX/(scaleValue*screenWidth)
EDIT 3
I figured out the scaling. It appears that the correct scale factor has to be:
scaleFactor = photo.frame.size.width/photo.layer.bounds.size.width
I used this instead of scaleValue
and it worked splendidly.
So panning and scaling are done. The only thing left is rotation, but it appears that it's the hardest.
First thing I thought is to apply rotation matrix to increments in X and Y directions, like this:
let incrementX = (distanceX)/(screenWidth)
let incrementY = (distanceY)/(screenHeight)
// Applying rotation matrix
let incX = incrementX*cos(angle)+incrementY*sin(angle)
let incY = -incrementX*sin(angle)+incrementY*cos(angle)
// Find new anchor point
let newAnchorPoint = CGPoint(x: 0.5+incX, y: 0.5+incY)
However this doesn't work.
Since the question is mostly answered in the edits, I don't want to repeat myself too much.
Broadly what I changed from the code posted in the original question:
adjustAnchorPoint
function in pinch and rotation gesture functions. Placed this piece of code in pan gesture function, so that the anchor point would update its position after panning the photo:
if gestureRecognizer.state == .ended { self.centerAnchorPoint() }
Updated centerAnchorPoint
function to work for rotation.
A fully working centerAnchorPoint
function (rotation included):
func centerAnchorPoint() {
// Scale factor
photo.transform = photo.transform.rotated(by: -angle)
let curScale = photo.frame.size.width / photo.layer.bounds.size.width
photo.transform = photo.transform.rotated(by: angle)
// Position of the current anchor point
let currentPosition = photo.layer.anchorPoint
self.setAnchorPoint(CGPoint(x: 0.5, y: 0.5), forView: photo)
// Center of the image
let imageCenter = CGPoint(x: photo.center.x, y: photo.center.y)
self.setAnchorPoint(currentPosition, forView: photo)
// Center of the screen
let screenCenter = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)
// Distance between the centers
let distanceX = screenCenter.x - imageCenter.x
let distanceY = screenCenter.y - imageCenter.y
// Apply rotational matrix to the distances
let distX = distanceX*cos(angle)+distanceY*sin(angle)
let distY = -distanceX*sin(angle)+distanceY*cos(angle)
let incrementX = (distX)/(curScale*UIScreen.main.bounds.size.width)
let incrementY = (distY)/(curScale*UIScreen.main.bounds.size.height)
// Find new anchor point
let newAnchorPoint = CGPoint(x: 0.5+incrementX, y: 0.5+incrementY)
self.setAnchorPoint(newAnchorPoint, forView: photo)
}
The key things to notice here is that the rotation matrix has to be applied to distanceX and distanceY. The scale factor is also updated to remain the same throughout the rotation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With