Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Place anchor point at the centre of the screen while doing gestures

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:

  1. Finding the distance between the center of the photo and the center of the screen
  2. Apply this formula to find the new position of anchor point:

newAnchorPointX = (imageCenter.x-distanceX)/screenWidth + distanceX/screenWidth

Then do the same for y position.

  1. Set this point as a new anchor point without moving the photo using setAnchorPoint function

As 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.

like image 951
Kaspis245 Avatar asked Aug 22 '17 06:08

Kaspis245


Video Answer


1 Answers

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:

  1. Deleted calls to adjustAnchorPoint function in pinch and rotation gesture functions.
  2. 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() }

  3. 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.

like image 191
Kaspis245 Avatar answered Oct 05 '22 07:10

Kaspis245