如何使用 UIPanGestureRecognizer 以 180 度角 3Drotate 和 3Dtranslate CALayer?

How to 3Drotate and 3Dtranslate CALayer with 180 degree angle using UIPanGestureRecognizer?

如何使用UIPanGestureRecognizer一起旋转和变换180度,就像图层在半球上移动,我试过做一些事情,我可以在各个方向变换它,但方向之间的过渡不平滑

简而言之,我想做到like in this video

我只是简单地编码了这些 class,它适用于所有方向,但结果不尽如人意:

//
//  MoveCircleToolViewController.swift
//
//  Created by Coder ACJHP on 17.06.2020.
//  Copyright © 2020 Coder ACJHP. All rights reserved.
//

import UIKit

class MoveCircleToolViewController: UIViewController {

    var currentAngleX: CGFloat = 0
    var currentOffsetX: CGFloat = 0
    var currentAngleY: CGFloat = 0
    var currentOffsetY: CGFloat = 0
    var cardSize: CGSize = .zero
    let transformLayer = CATransformLayer()
    var directionsFrames = Array<CGRect>()

    override func viewDidLoad() {
        super.viewDidLoad()

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        view.addGestureRecognizer(panGesture)

        transformLayer.frame = view.bounds
        view.layer.addSublayer(transformLayer)

        /// Add simple CALayer (circle shape)
        addCircleView()

        /** Calculate 4 corners TR, TL, BR, BL and store them in array list
         to use them inside pan gesture event */
        calculateCorners()
    }

    private func degreeToRadians(degree: CGFloat) -> CGFloat {
        return (degree * CGFloat.pi) / 180
    }

    private func addCircleView() {

        let singleSideSize = self.view.bounds.width * 0.18
        cardSize = CGSize(width: singleSideSize, height: singleSideSize)
        let imageLayer = CALayer()
        let origin = CGPoint(x: (view.bounds.width / 2) - (cardSize.width / 2),
                             y: (view.bounds.height / 2) - (cardSize.height / 2))
        imageLayer.frame = CGRect(origin: origin, size: cardSize)

        imageLayer.contentsGravity = .resizeAspectFill
        imageLayer.borderColor = UIColor.cyan.cgColor
        imageLayer.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
        imageLayer.borderWidth = 3.0
        imageLayer.cornerRadius = cardSize.width / 2
        imageLayer.masksToBounds = true
        imageLayer.isDoubleSided = true
        transformLayer.addSublayer(imageLayer)
    }

    private func calculateCorners() {
        let quarterW = self.view.bounds.width / 2
        let quarterH = self.view.bounds.height / 2

        let topLeftRect = CGRect(x: 0,
                             y: 0,
                             width: quarterW - cardSize.width / 2,
                             height: quarterH - cardSize.height / 2)

        let topRightRect = CGRect(x: topLeftRect.width + cardSize.width,
                             y: 0,
                             width: quarterW - cardSize.width / 2,
                             height: quarterH - cardSize.height / 2)

        let bottomLeftRect = CGRect(x: 0,
                             y: topLeftRect.height + cardSize.height,
                             width: quarterW - cardSize.width / 2,
                             height: quarterH - cardSize.height / 2)

        let bottomRightRect = CGRect(x: bottomLeftRect.width + cardSize.height,
                             y: topRightRect.height + cardSize.height,
                             width: quarterW - cardSize.width / 2,
                             height: quarterH - cardSize.height / 2)

        directionsFrames.append(topLeftRect)
        directionsFrames.append(topRightRect)
        directionsFrames.append(bottomLeftRect)
        directionsFrames.append(bottomRightRect)
    }

    @objc
    private func handlePan(_ gestureRecognier: UIPanGestureRecognizer) {

        let translationPoint = gestureRecognier.translation(in: view)
        let location = gestureRecognier.location(in: view)

        /// Calculate X and Y offset for animation
        let xOffset = gestureRecognier.translation(in: view).x
        let yOffset = gestureRecognier.translation(in: view).y

        /// Reset offsets
        if gestureRecognier.state == .began {
            currentOffsetX = 0
            currentOffsetY = 0
        }

        /// Calculate angle for rotation X
        let xDifference = xOffset * 0.6 - currentOffsetX
        currentOffsetX += xDifference
        currentAngleX += xDifference
        let angleOffsetX = currentAngleX

        /// Calculate angle for rotation Y
        let yDifference = yOffset * 0.6 + currentOffsetY
        currentOffsetY -= yDifference
        currentAngleY -= yDifference
        let angleOffsetY = currentAngleY

        /// Create transform object
        var transform = CATransform3DIdentity
        transform.m34 = -1 / self.view.bounds.width

        // Top Left
        if directionsFrames[0].contains(location) {
            transform = CATransform3DRotate(transform, degreeToRadians(degree: 30), 1, 0, 0)
            transform = CATransform3DTranslate(transform, translationPoint.x, translationPoint.y, 200)

        // Top Right
        } else if directionsFrames[1].contains(location) {
            transform = CATransform3DRotate(transform, degreeToRadians(degree: 30), 1, 0, 0)
            transform = CATransform3DTranslate(transform, translationPoint.x, translationPoint.y, 200)

        // Bottom Left
        } else if directionsFrames[2].contains(location) {
            transform = CATransform3DRotate(transform, degreeToRadians(degree: -30), 1, 0, 0)
            transform = CATransform3DTranslate(transform, translationPoint.x, translationPoint.y, 200)

        // Bottom Right
        } else if directionsFrames[3].contains(location) {
            transform = CATransform3DRotate(transform, degreeToRadians(degree: -30), 1, 0, 0)
            transform = CATransform3DTranslate(transform, translationPoint.x, translationPoint.y, 200)

        } else {

            if let direction = gestureRecognier.direction {
                switch direction {
                case .Left, .Right:
                    transform = CATransform3DRotate(transform, degreeToRadians(degree: angleOffsetX), 0, 1, 0)
                    transform = CATransform3DTranslate(transform, 0, 0, 200)
                case .Up, .Down:
                    transform = CATransform3DRotate(transform, degreeToRadians(degree: angleOffsetY), 1, 0, 0)
                    transform = CATransform3DTranslate(transform, 0, 0, 200)
                }
            }
        }
        CATransaction.setAnimationDuration(0)
        transformLayer.transform = transform
    }
}

public extension UIPanGestureRecognizer {

    enum PanDirection: Int {
        case Up, Down, Left, Right
        public var isVertical: Bool { return [.Up, .Down].contains(self) }
        public var isHorizontal: Bool { return !isVertical }
    }

    var direction: PanDirection? {
        let translation = self.translation(in: view)
        let isVertical = abs(translation.y) > abs(translation.x)
        switch (isVertical, translation.x, translation.y) {
        case (true, _, let y) where y < 0: return .Up
        case (true, _, let y) where y > 0: return .Down
        case (false, let x, _) where x > 0: return .Right
        case (false, let x, _) where x < 0: return .Left
        default: return nil
        }
    }
}

提前致谢

看起来你只允许用户在一个或另一个轴上旋转,并且可能使事情过于复杂。

给定一些包含我们想要旋转的 "primaryView" 子视图的 UIView,以及我们想要在 3D space 中旋转和向前移动的 "secondaryView" 子视图(作为您链接的视频中的圆),并给定一些二维点和该点的距离 "in front",我们可以计算从该点到该二维投影点的 X 和 Y 欧拉角(以弧度为单位):

// Note, "self" is some UIView. "primaryView" and "secondaryView"s are subviews of self in the below example:

let somePoint: CGPoint = CGPoint(...)
let distanceInfront: CGFloat = 10

// First we need to convert this point to a coordinate relative to the "center" of what we're trying to orbit around. For your case, this center would be the center of the view itself:

let center = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let offset = CGPoint(x: center.x - somePoint, y: center.y - somePoint)

// If we picture the problem as if we're looking at it "from the side", we're essentially trying to calculate a 2D angle between a horizontal line and some 2D point to obtain the x rotation angle. The point we're trying to calculate the angle to has an x value of our desired distance, and a y value of the calculated offset's y value:

let xP = CGPoint(x: distance, y: offset.y)
let xAngle = atan2(xP.y, xP.x)

// We can do the same to calculate the y angle, picturing the problem "from above":

let yP = CGPoint(x: distance, y: offset.x)
let yAngle = atan2(yP.y, yP.x)

// Now we can use our calculated x and y angles to compute our transform:

var primaryTransform = CATransform3DIdentity
primaryTransform.m34 = 1 / self.bounds.width
primaryTransform = CATransform3DRotate(primaryTransform, yAngle, 0, 1, 0)
primaryTransform = CATransform3DRotate(primaryTransform, -xAngle, 1, 0, 0)
primaryView.layer.transform = primaryTransform

// Our primary view is now "looking at" the 2D point we've provided, at a distance "distance" in front of our view.

// We can then take that same transform and shift it forwards by our desired distance to compute the transform of the secondaryView (the circle view):
let secondaryTransform = CATransform3DTranslate(primaryTransform, 0, 0, -distance)
secondaryView.layer.transform = secondaryTransform

这是一个 Swift 游乐场,将所有内容打包在一起进行演示:

import UIKit
import PlaygroundSupport

class OrbitView: UIView {

    let primaryView = UIView()
    let secondaryView = UIView()

    public init(primaryRadius: CGFloat, secondaryRadius: CGFloat) {
        super.init(frame: .zero)
        primaryView.backgroundColor = .blue
        primaryView.layer.cornerRadius = primaryRadius/2.0
        primaryView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(primaryView)
        NSLayoutConstraint.activate([
            primaryView.centerXAnchor.constraint(equalTo: centerXAnchor),
            primaryView.centerYAnchor.constraint(equalTo: centerYAnchor),
            primaryView.widthAnchor.constraint(equalToConstant: primaryRadius),
            primaryView.heightAnchor.constraint(equalTo: primaryView.widthAnchor),
        ])

        secondaryView.backgroundColor = .red
        secondaryView.layer.cornerRadius = secondaryRadius/2.0
        secondaryView.translatesAutoresizingMaskIntoConstraints = false
        primaryView.addSubview(secondaryView)
        NSLayoutConstraint.activate([
            secondaryView.centerXAnchor.constraint(equalTo: centerXAnchor),
            secondaryView.centerYAnchor.constraint(equalTo: centerYAnchor),
            secondaryView.widthAnchor.constraint(equalToConstant: secondaryRadius),
            secondaryView.heightAnchor.constraint(equalTo: secondaryView.widthAnchor),
        ])
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public func lookAt(_ location: CGPoint, distanceInfront distance: CGFloat) {

        // Compute how far the location is from the center of our view
        let center = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
        let offset = CGPoint(x: center.x - location.x, y: center.y - location.y)

        // Calculate the x angle to the point "infront" of us
        let xP = CGPoint(x: distance, y: offset.y)
        let xAngle = atan2(xP.y, xP.x)

        // Calculate the y angle to the point "infront" of us
        let yP = CGPoint(x: distance, y: offset.x)
        let yAngle = atan2(yP.y, yP.x)


        // Construct a transform that rotates our primary subview's layer to point to the location in 3D space
        var primaryTransform = CATransform3DIdentity
        primaryTransform.m34 = 1 / self.bounds.width
        primaryTransform = CATransform3DRotate(primaryTransform, yAngle, 0, 1, 0)
        primaryTransform = CATransform3DRotate(primaryTransform, -xAngle, 1, 0, 0)

        // Set our primary layer's transform
        primaryView.layer.transform = primaryTransform

        // Now, shift this primary transform forward by the distance infront of our view we're "looking",
        // and apply this transform to our secondary subview
        let secondaryTransform = CATransform3DTranslate(primaryTransform, 0, 0, -distance)
        secondaryView.layer.transform = secondaryTransform

    }

}


class MyViewController : UIViewController {

    let orbitView = OrbitView(primaryRadius: 200, secondaryRadius: 50)

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .white
        self.view = view
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Construct an orbit view for demonstration purposes, and embed it in our view
        orbitView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(orbitView)
        NSLayoutConstraint.activate([
            orbitView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            orbitView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            orbitView.widthAnchor.constraint(equalToConstant: 200),
            orbitView.heightAnchor.constraint(equalTo: orbitView.widthAnchor),
        ])

        // We'll use a pan gesture recognizer to update our orbit view
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panRecognized(_:)))
        view.addGestureRecognizer(panGesture)
    }

    @objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
        // Tell our orbit view to "look" at a point in 3D space relative to where we are currently touching
        let location = recognizer.location(in: self.orbitView)
        orbitView.lookAt(location, distanceInfront: 100)
    }

}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

注意在上面的 GIF 中,红色圆圈 "pops" 在我们第一次开始拖动时向前。您可能希望首先将红色圆圈 (secondaryView) 的变换设置为在首次创建时向前移动您想要的距离以避免这种情况。

@Adam Eisfeld 向我展示了正确的计算方式,所以最后我解决了我的问题,最终代码如下所示:

//
//  MoveCircleToolViewController.swift
//
//  Created by Coder ACJHP on 17.06.2020.
//  Copyright © 2020 Coder ACJHP. All rights reserved.
//

import UIKit

class MoveCircleToolViewController: UIViewController {

    var cardSize: CGSize = .zero
    let transformLayer = CATransformLayer()

    override func viewDidLoad() {
        super.viewDidLoad()

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        view.addGestureRecognizer(panGesture)

        transformLayer.frame = view.bounds
        view.layer.addSublayer(transformLayer)

        /// Add simple CALayer (circle shape)
        addCircleView()
    }

    private func addCircleView() {

        let singleSideSize = self.view.bounds.width * 0.18
        cardSize = CGSize(width: singleSideSize, height: singleSideSize)
        let imageLayer = CALayer()
        let origin = CGPoint(x: (view.bounds.width / 2) - (cardSize.width / 2),
                             y: (view.bounds.height / 2) - (cardSize.height / 2))
        imageLayer.frame = CGRect(origin: origin, size: cardSize)

        imageLayer.contentsGravity = .resizeAspectFill
        imageLayer.borderColor = UIColor.cyan.cgColor
        imageLayer.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
        imageLayer.borderWidth = 3.0
        imageLayer.cornerRadius = cardSize.width / 2
        imageLayer.masksToBounds = true
        imageLayer.isDoubleSided = true
        transformLayer.addSublayer(imageLayer)
    }

    @objc
    private func handlePan(_ gestureRecognier: UIPanGestureRecognizer) {

        let locationOnView = gestureRecognier.location(in: view)

        let distance: CGFloat = -300
        // Compute how far the location is from the center of our view
        let center = CGPoint(x: view.frame.size.width / 2, y: view.frame.size.height / 2)
        let offset = CGPoint(x: center.x - locationOnView.x, y: center.y - locationOnView.y)

        // Calculate the x angle to the point "infront" of us
        let xP = CGPoint(x: distance, y: offset.y)
        let xAngle = atan2(xP.y, xP.x)

        // Calculate the y angle to the point "infront" of us
        let yP = CGPoint(x: distance, y: offset.x)
        let yAngle = atan2(yP.y, yP.x)

        /// Create transform object
        var transform = CATransform3DIdentity
        transform.m34 = -1 / self.view.bounds.width
        transform = CATransform3DRotate(transform, -xAngle, 1, 0, 0)
        transform = CATransform3DRotate(transform, -yAngle, 0, 1, 0)
        transform = CATransform3DTranslate(transform, 0, 0, -distance)
        CATransaction.setAnimationDuration(0)
        transformLayer.transform = transform
    }
}