如何使用 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
}
}
如何使用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
}
}