当我更改模型层的属性时动画中的意外行为
Unexpected behaviour in animation when i change the properties of the model layers
参考 this post,我正在尝试使动画适应横向模式。基本上我想要的是将所有层 -90°(顺时针 90°)和动画旋转到 运行 水平而不是垂直。作者懒得解释背后的逻辑,obj-c 中有十几个折纸库,它们都是基于相同的架构,所以显然这就是折叠的方式。
编辑:为了进一步阐明我想要实现的目标,here您可以查看我的动画的三个快照(起点、中场休息和终点)想。在上面 link 的问题中,动画从下到上折叠,而我希望它从左到右折叠。
您可以在下方查看对原始项目进行了一些调整:
- 我改变了灰色
bottomSleeve
图层的最终角度值,以及红色和蓝色的角度;
- 我通过将
perspectiveLayer
speed
设置为 0
来暂停初始化动画并添加一个滑块,然后将滑块值设置为 perspectiveLayer
timeOffset
这样您就可以通过滑动来交互 运行 动画的每一帧。当滑块上的触摸事件结束时,动画将从相对于当前 timeOffset
的帧恢复到最终值。
- 我在 运行 使用
CATransaction
将每个动画添加到相关表示层之前更改了所有模型层值。此外,完成后 perspectiveLayer
速度再次设置为 0
。
- 为了更好的视觉理解,我将
perspectiveLayer
backgroundColor
设置为 cyan
。
顺便提一下,主要有两个功能:
setupLayers()
,在viewDidLoad()
中调用,负责设置图层位置和锚点,并将它们作为子图层添加到mainView
图层。
animate()
,在setupLayers()
中递归调用,负责添加动画。在这里,我还将模型层值设置为相关动画的最终值,然后再添加它们。
只需复制粘贴即可运行:
class ViewController: UIViewController {
var transform: CATransform3D = CATransform3DIdentity
var topSleeve: CALayer = CALayer()
var middleSleeve: CALayer = CALayer()
var bottomSleeve: CALayer = CALayer()
var topShadow: CALayer = CALayer()
var middleShadow: CALayer = CALayer()
let width: CGFloat = 300
let height: CGFloat = 150
var firstJointLayer: CATransformLayer = CATransformLayer()
var secondJointLayer:CATransformLayer = CATransformLayer()
var sizeHeight: CGFloat = 0
var positionY: CGFloat = 0
var perspectiveLayer: CALayer = {
let perspectiveLayer = CALayer()
perspectiveLayer.speed = 0.0
perspectiveLayer.fillMode = .removed
return perspectiveLayer
}()
var mainView: UIView = {
let view = UIView()
return view
}()
private let slider: UISlider = {
let slider = UISlider()
slider.addTarget(self, action: #selector(slide(sender:event:)) , for: .valueChanged)
return slider
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(slider)
setupLayers()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
slider.frame = CGRect(x: view.bounds.size.width/3,
y: view.bounds.size.height/10*8,
width: view.bounds.size.width/3,
height: view.bounds.size.height/10)
}
@objc private func slide(sender: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .ended:
resumeLayer(layer: perspectiveLayer)
default:
perspectiveLayer.timeOffset = CFTimeInterval(sender.value)
}
}
}
private func resumeLayer(layer: CALayer) {
let pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
}
private func setupLayers() {
mainView = UIView(frame:CGRect(x: 50, y: 50, width: width, height: height*3))
mainView.backgroundColor = UIColor.yellow
view.addSubview(mainView)
perspectiveLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
perspectiveLayer.backgroundColor = UIColor.cyan.cgColor
mainView.layer.addSublayer(perspectiveLayer)
firstJointLayer.fillMode = .removed
firstJointLayer.frame = mainView.bounds
perspectiveLayer.addSublayer(firstJointLayer)
topSleeve.fillMode = .removed
topSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
topSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
topSleeve.backgroundColor = UIColor.red.cgColor
topSleeve.position = CGPoint(x: width/2, y: 0)
firstJointLayer.addSublayer(topSleeve)
topSleeve.masksToBounds = true
secondJointLayer.fillMode = .removed
secondJointLayer.frame = mainView.bounds
secondJointLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
secondJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
secondJointLayer.position = CGPoint(x: width/2, y: height)
firstJointLayer.addSublayer(secondJointLayer)
secondJointLayer.fillMode = .removed
middleSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
middleSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
middleSleeve.backgroundColor = UIColor.blue.cgColor
middleSleeve.position = CGPoint(x: width/2, y: 0)
secondJointLayer.addSublayer(middleSleeve)
middleSleeve.masksToBounds = true
bottomSleeve.fillMode = .removed
bottomSleeve.frame = CGRect(x: 0, y: height, width: width, height: height)
bottomSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
bottomSleeve.backgroundColor = UIColor.gray.cgColor
bottomSleeve.position = CGPoint(x: width/2, y: height)
secondJointLayer.addSublayer(bottomSleeve)
firstJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
firstJointLayer.position = CGPoint(x: width/2, y: 0)
topShadow.fillMode = .removed
topSleeve.addSublayer(topShadow)
topShadow.frame = topSleeve.bounds
topShadow.backgroundColor = UIColor.black.cgColor
topShadow.opacity = 0
middleShadow.fillMode = .removed
middleSleeve.addSublayer(middleShadow)
middleShadow.frame = middleSleeve.bounds
middleShadow.backgroundColor = UIColor.black.cgColor
middleShadow.opacity = 0
transform.m34 = -1/700
perspectiveLayer.sublayerTransform = transform
sizeHeight = perspectiveLayer.bounds.size.height
positionY = perspectiveLayer.position.y
animate()
}
private func animate() {
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setCompletionBlock{ [weak self] in
if self == nil { return }
self?.perspectiveLayer.speed = 0
}
firstJointLayer.transform = CATransform3DMakeRotation(CGFloat(-85*Double.pi/180), 1, 0, 0)
secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180), 1, 0, 0)
bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180), 1, 0, 0)
perspectiveLayer.bounds.size.height = 0
perspectiveLayer.position.y = 0
topShadow.opacity = 0.5
middleShadow.opacity = 0.5
var animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = -85*Double.pi/180
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 170*Double.pi/180
secondJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = -165*Double.pi/180
bottomSleeve.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "bounds.size.height")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = sizeHeight
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "position.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = positionY
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
topShadow.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
middleShadow.add(animation, forKey: nil)
CATransaction.commit()
}
}
如您所见,动画 运行 符合预期,此时为了旋转整个对象,只需更改位置、锚点和最终动画值即可。
摘自上面 link 的回答,这里很好地表示了起始项目的所有层:
然后我开始重构 setupLayers()
和 animate()
到 运行 水平动画,从左到右(换句话说,我顺时针旋转 90°以上层表示)。
更改代码以旋转动画后,我遇到了两个问题:
当动画开始时,firstJointLayer
位置沿 perspectiveLayer
从左向右平移。公平地说,根据我的理解,这应该是一种预期行为,因为它是 perspectiveLayer
的子层,实际上我不确定为什么在原始项目中它没有发生。然而,为了解决这个问题,我添加了另一个动画,负责在其相关系统中将它从右向左平移,这样它实际上看起来是静止的。此时,虽然我没有更改模型层的最终值(下面项目中的注释行),但动画 运行 水平如预期。如果我不必也修改模型层,我的目标就会达到,因为这正是我想要的动画。然而...
...如果我随后尝试设置动画的最终值(只需将行注释掉),我会得到意想不到的行为。在动画的初始帧,红色、蓝色和灰色层看起来相互折叠,因此旋转不再像预期的那样工作。以下是时间 0.0、0.5 和 1.0(持续时间:1.0)的一些快照:
对我来说最不合逻辑的部分是将模型层值设置为等于表示层最终值会导致错误,但它只会影响表示层,因为一旦动画在模型层之上,下面的模型层就在预期(和想要)rotation/position:
锚点肯定是正确的,因为旋转发生在正确的点周围。我认为这可能与问题 1 有关,但我已多次尝试重新定位图层,但均未成功。直到今天,这仍然没有解决,在两天内我无法找到主要问题并因此修复它。对我来说,原始项目(上图)和旋转后的项目(下图)在底层逻辑上看起来是一样的。
EDIT2: 我在代码中发现了一个小错误,我正在从等于 perspectiveLayer x 位置的起始值而不是他自己的起始值动画 firstJointLayer x 位置x 位置,我已经修复了,但没有任何改变。
EDIT3:由于将模型层值设置为等于动画最终值是导致错误的原因,请注意使用 animation.fillMode = CAMediaTimingFillMode.forwards
和 animation.isRemovedOnCompletion = false
不是避免接触模态层的可行解决方法,因为我需要稍后恢复动画,因此需要保持表示层和模型层同步。
非常感谢您的帮助。在这里旋转的项目 - 我还评论了我从上面的项目更改的块:
class ViewController: UIViewController {
var transform: CATransform3D = CATransform3DIdentity
var topSleeve: CALayer = CALayer()
var middleSleeve: CALayer = CALayer()
var bottomSleeve: CALayer = CALayer()
var topShadow: CALayer = CALayer()
var middleShadow: CALayer = CALayer()
let width: CGFloat = 200
let height: CGFloat = 300
var firstJointLayer: CALayer = CATransformLayer()
var secondJointLayer: CALayer = CATransformLayer()
var sizeWidth: CGFloat = 0
var positionX: CGFloat = 0
var firstJointLayerPositionX: CGFloat = 0
var perspectiveLayer: CALayer = {
let perspectiveLayer = CALayer()
perspectiveLayer.speed = 0.0
perspectiveLayer.fillMode = .removed
return perspectiveLayer
}()
var mainView: UIView = {
let view = UIView()
return view
}()
private let slider: UISlider = {
let slider = UISlider()
slider.addTarget(self, action: #selector(slide(sender:event:)) , for: .valueChanged)
return slider
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(slider)
setupLayers()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
slider.frame = CGRect(x: view.bounds.size.width/3,
y: view.bounds.size.height/10*8,
width: view.bounds.size.width/3,
height: view.bounds.size.height/10)
}
@objc private func slide(sender: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .ended:
resumeLayer(layer: perspectiveLayer)
default:
perspectiveLayer.timeOffset = CFTimeInterval(sender.value)
}
}
}
private func resumeLayer(layer: CALayer) {
let pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
}
private func setupLayers() {
// Changing all anchor points and positions here, in order to rotate the whole thing of -90°
mainView = UIView(frame:CGRect(x: 50, y: 50, width: width*3, height: height))
mainView.backgroundColor = UIColor.yellow
view.addSubview(mainView)
perspectiveLayer.frame = CGRect(x: width, y: 0, width: width*2, height: height)
perspectiveLayer.backgroundColor = UIColor.cyan.cgColor
mainView.layer.addSublayer(perspectiveLayer)
firstJointLayer.fillMode = .removed
firstJointLayer.frame = mainView.bounds
firstJointLayer.anchorPoint = CGPoint(x: 1, y: 0.5)
firstJointLayer.position = CGPoint(x: width*2, y: height/2)
perspectiveLayer.addSublayer(firstJointLayer)
topSleeve.fillMode = .removed
topSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
topSleeve.anchorPoint = CGPoint(x: 1, y: 0.5)
topSleeve.backgroundColor = UIColor.red.cgColor
topSleeve.position = CGPoint(x: width*3, y: height/2)
firstJointLayer.addSublayer(topSleeve)
topSleeve.masksToBounds = true
secondJointLayer.fillMode = .removed
secondJointLayer.frame = mainView.bounds
secondJointLayer.frame = CGRect(x: 0, y: 0, width: width*2, height: height)
secondJointLayer.anchorPoint = CGPoint(x: 1, y: 0.5)
secondJointLayer.position = CGPoint(x: width*2, y: height/2)
firstJointLayer.addSublayer(secondJointLayer)
secondJointLayer.fillMode = .removed
middleSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
middleSleeve.anchorPoint = CGPoint(x: 1, y: 0.5)
middleSleeve.backgroundColor = UIColor.blue.cgColor
middleSleeve.position = CGPoint(x: width*2, y: height/2)
secondJointLayer.addSublayer(middleSleeve)
middleSleeve.masksToBounds = true
bottomSleeve.fillMode = .removed
bottomSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
bottomSleeve.anchorPoint = CGPoint(x: 1, y: 0.5)
bottomSleeve.backgroundColor = UIColor.gray.cgColor
bottomSleeve.position = CGPoint(x: width, y: height/2)
secondJointLayer.addSublayer(bottomSleeve)
topShadow.fillMode = .removed
topSleeve.addSublayer(topShadow)
topShadow.frame = topSleeve.bounds
topShadow.backgroundColor = UIColor.black.cgColor
topShadow.opacity = 0
middleShadow.fillMode = .removed
middleSleeve.addSublayer(middleShadow)
middleShadow.frame = middleSleeve.bounds
middleShadow.backgroundColor = UIColor.black.cgColor
middleShadow.opacity = 0
transform.m34 = -1/700
perspectiveLayer.sublayerTransform = transform
sizeWidth = perspectiveLayer.bounds.size.width
positionX = perspectiveLayer.position.x
firstJointLayerPositionX = firstJointLayer.position.x
animate()
}
private func animate() {
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setCompletionBlock{ [weak self] in
if self == nil { return }
self?.perspectiveLayer.speed = 0
}
// firstJointLayer.transform = CATransform3DMakeRotation(CGFloat(-85*Double.pi/180), 0, 1, 0)
// secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180), 0, 1, 0)
// bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180), 0, 1, 0)
// perspectiveLayer.bounds.size.width = 0
// perspectiveLayer.position.x = 600
// firstJointLayer.position.x = 0
// topShadow.opacity = 0.5
// middleShadow.opacity = 0.5
var animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = -85*Double.pi/180
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 170*Double.pi/180
secondJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = -165*Double.pi/180
bottomSleeve.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "bounds.size.width")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = sizeWidth
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "position.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = positionX
animation.toValue = 600
perspectiveLayer.add(animation, forKey: nil)
// As said above, i added this animation which is not included in the original project, as the firstJointLayer was translating his position from left to right along with the perspectiveLayer position, so i make a reverse translation in its relative system so that it is stationary in the mainView system
animation = CABasicAnimation(keyPath: "position.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = firstJointLayerPositionX
animation.toValue = 0
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
topShadow.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
middleShadow.add(animation, forKey: nil)
CATransaction.commit()
}
}
好的 - 稍微玩一下...
看来您需要翻转动画,因为它们实际上是在“倒退”。
private func animate() {
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setCompletionBlock{ [weak self] in
if self == nil { return }
//self?.perspectiveLayer.speed = 0
}
firstJointLayer.transform = CATransform3DMakeRotation(CGFloat(-85*Double.pi/180), 0, 1, 0)
secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180), 0, 1, 0)
bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180), 0, 1, 0)
perspectiveLayer.bounds.size.width = 0
perspectiveLayer.position.x = 600
firstJointLayer.position.x = 0
topShadow.opacity = 0.5
middleShadow.opacity = 0.5
var animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = -85*Double.pi/180
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
// flip 180 degrees
animation.fromValue = 180*Double.pi/180
// to 180 - 170
animation.toValue = 10*Double.pi/180
secondJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
// flip -180 degrees
animation.fromValue = -180*Double.pi/180
// to 180 - 165
animation.toValue = -15*Double.pi/180
bottomSleeve.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "bounds.size.width")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = sizeWidth
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "position.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = positionX
animation.toValue = 600
perspectiveLayer.add(animation, forKey: nil)
// As said above, i added this animation which is not included in the original project, as the firstJointLayer was translating his position from left to right along with the perspectiveLayer position, so i make a reverse translation in its relative system so that it is stationary in the mainView system
animation = CABasicAnimation(keyPath: "position.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = firstJointLayerPositionX
animation.toValue = 0
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
topShadow.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
middleShadow.add(animation, forKey: nil)
CATransaction.commit()
}
参考 this post,我正在尝试使动画适应横向模式。基本上我想要的是将所有层 -90°(顺时针 90°)和动画旋转到 运行 水平而不是垂直。作者懒得解释背后的逻辑,obj-c 中有十几个折纸库,它们都是基于相同的架构,所以显然这就是折叠的方式。
编辑:为了进一步阐明我想要实现的目标,here您可以查看我的动画的三个快照(起点、中场休息和终点)想。在上面 link 的问题中,动画从下到上折叠,而我希望它从左到右折叠。
您可以在下方查看对原始项目进行了一些调整:
- 我改变了灰色
bottomSleeve
图层的最终角度值,以及红色和蓝色的角度; - 我通过将
perspectiveLayer
speed
设置为0
来暂停初始化动画并添加一个滑块,然后将滑块值设置为perspectiveLayer
timeOffset
这样您就可以通过滑动来交互 运行 动画的每一帧。当滑块上的触摸事件结束时,动画将从相对于当前timeOffset
的帧恢复到最终值。 - 我在 运行 使用
CATransaction
将每个动画添加到相关表示层之前更改了所有模型层值。此外,完成后perspectiveLayer
速度再次设置为0
。 - 为了更好的视觉理解,我将
perspectiveLayer
backgroundColor
设置为cyan
。
顺便提一下,主要有两个功能:
setupLayers()
,在viewDidLoad()
中调用,负责设置图层位置和锚点,并将它们作为子图层添加到mainView
图层。animate()
,在setupLayers()
中递归调用,负责添加动画。在这里,我还将模型层值设置为相关动画的最终值,然后再添加它们。
只需复制粘贴即可运行:
class ViewController: UIViewController {
var transform: CATransform3D = CATransform3DIdentity
var topSleeve: CALayer = CALayer()
var middleSleeve: CALayer = CALayer()
var bottomSleeve: CALayer = CALayer()
var topShadow: CALayer = CALayer()
var middleShadow: CALayer = CALayer()
let width: CGFloat = 300
let height: CGFloat = 150
var firstJointLayer: CATransformLayer = CATransformLayer()
var secondJointLayer:CATransformLayer = CATransformLayer()
var sizeHeight: CGFloat = 0
var positionY: CGFloat = 0
var perspectiveLayer: CALayer = {
let perspectiveLayer = CALayer()
perspectiveLayer.speed = 0.0
perspectiveLayer.fillMode = .removed
return perspectiveLayer
}()
var mainView: UIView = {
let view = UIView()
return view
}()
private let slider: UISlider = {
let slider = UISlider()
slider.addTarget(self, action: #selector(slide(sender:event:)) , for: .valueChanged)
return slider
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(slider)
setupLayers()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
slider.frame = CGRect(x: view.bounds.size.width/3,
y: view.bounds.size.height/10*8,
width: view.bounds.size.width/3,
height: view.bounds.size.height/10)
}
@objc private func slide(sender: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .ended:
resumeLayer(layer: perspectiveLayer)
default:
perspectiveLayer.timeOffset = CFTimeInterval(sender.value)
}
}
}
private func resumeLayer(layer: CALayer) {
let pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
}
private func setupLayers() {
mainView = UIView(frame:CGRect(x: 50, y: 50, width: width, height: height*3))
mainView.backgroundColor = UIColor.yellow
view.addSubview(mainView)
perspectiveLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
perspectiveLayer.backgroundColor = UIColor.cyan.cgColor
mainView.layer.addSublayer(perspectiveLayer)
firstJointLayer.fillMode = .removed
firstJointLayer.frame = mainView.bounds
perspectiveLayer.addSublayer(firstJointLayer)
topSleeve.fillMode = .removed
topSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
topSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
topSleeve.backgroundColor = UIColor.red.cgColor
topSleeve.position = CGPoint(x: width/2, y: 0)
firstJointLayer.addSublayer(topSleeve)
topSleeve.masksToBounds = true
secondJointLayer.fillMode = .removed
secondJointLayer.frame = mainView.bounds
secondJointLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
secondJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
secondJointLayer.position = CGPoint(x: width/2, y: height)
firstJointLayer.addSublayer(secondJointLayer)
secondJointLayer.fillMode = .removed
middleSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
middleSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
middleSleeve.backgroundColor = UIColor.blue.cgColor
middleSleeve.position = CGPoint(x: width/2, y: 0)
secondJointLayer.addSublayer(middleSleeve)
middleSleeve.masksToBounds = true
bottomSleeve.fillMode = .removed
bottomSleeve.frame = CGRect(x: 0, y: height, width: width, height: height)
bottomSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
bottomSleeve.backgroundColor = UIColor.gray.cgColor
bottomSleeve.position = CGPoint(x: width/2, y: height)
secondJointLayer.addSublayer(bottomSleeve)
firstJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
firstJointLayer.position = CGPoint(x: width/2, y: 0)
topShadow.fillMode = .removed
topSleeve.addSublayer(topShadow)
topShadow.frame = topSleeve.bounds
topShadow.backgroundColor = UIColor.black.cgColor
topShadow.opacity = 0
middleShadow.fillMode = .removed
middleSleeve.addSublayer(middleShadow)
middleShadow.frame = middleSleeve.bounds
middleShadow.backgroundColor = UIColor.black.cgColor
middleShadow.opacity = 0
transform.m34 = -1/700
perspectiveLayer.sublayerTransform = transform
sizeHeight = perspectiveLayer.bounds.size.height
positionY = perspectiveLayer.position.y
animate()
}
private func animate() {
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setCompletionBlock{ [weak self] in
if self == nil { return }
self?.perspectiveLayer.speed = 0
}
firstJointLayer.transform = CATransform3DMakeRotation(CGFloat(-85*Double.pi/180), 1, 0, 0)
secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180), 1, 0, 0)
bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180), 1, 0, 0)
perspectiveLayer.bounds.size.height = 0
perspectiveLayer.position.y = 0
topShadow.opacity = 0.5
middleShadow.opacity = 0.5
var animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = -85*Double.pi/180
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 170*Double.pi/180
secondJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = -165*Double.pi/180
bottomSleeve.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "bounds.size.height")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = sizeHeight
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "position.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = positionY
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
topShadow.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
middleShadow.add(animation, forKey: nil)
CATransaction.commit()
}
}
如您所见,动画 运行 符合预期,此时为了旋转整个对象,只需更改位置、锚点和最终动画值即可。 摘自上面 link 的回答,这里很好地表示了起始项目的所有层:
然后我开始重构 setupLayers()
和 animate()
到 运行 水平动画,从左到右(换句话说,我顺时针旋转 90°以上层表示)。
更改代码以旋转动画后,我遇到了两个问题:
当动画开始时,
firstJointLayer
位置沿perspectiveLayer
从左向右平移。公平地说,根据我的理解,这应该是一种预期行为,因为它是perspectiveLayer
的子层,实际上我不确定为什么在原始项目中它没有发生。然而,为了解决这个问题,我添加了另一个动画,负责在其相关系统中将它从右向左平移,这样它实际上看起来是静止的。此时,虽然我没有更改模型层的最终值(下面项目中的注释行),但动画 运行 水平如预期。如果我不必也修改模型层,我的目标就会达到,因为这正是我想要的动画。然而......如果我随后尝试设置动画的最终值(只需将行注释掉),我会得到意想不到的行为。在动画的初始帧,红色、蓝色和灰色层看起来相互折叠,因此旋转不再像预期的那样工作。以下是时间 0.0、0.5 和 1.0(持续时间:1.0)的一些快照:
对我来说最不合逻辑的部分是将模型层值设置为等于表示层最终值会导致错误,但它只会影响表示层,因为一旦动画在模型层之上,下面的模型层就在预期(和想要)rotation/position:
锚点肯定是正确的,因为旋转发生在正确的点周围。我认为这可能与问题 1 有关,但我已多次尝试重新定位图层,但均未成功。直到今天,这仍然没有解决,在两天内我无法找到主要问题并因此修复它。对我来说,原始项目(上图)和旋转后的项目(下图)在底层逻辑上看起来是一样的。
EDIT2: 我在代码中发现了一个小错误,我正在从等于 perspectiveLayer x 位置的起始值而不是他自己的起始值动画 firstJointLayer x 位置x 位置,我已经修复了,但没有任何改变。
EDIT3:由于将模型层值设置为等于动画最终值是导致错误的原因,请注意使用 animation.fillMode = CAMediaTimingFillMode.forwards
和 animation.isRemovedOnCompletion = false
不是避免接触模态层的可行解决方法,因为我需要稍后恢复动画,因此需要保持表示层和模型层同步。
非常感谢您的帮助。在这里旋转的项目 - 我还评论了我从上面的项目更改的块:
class ViewController: UIViewController {
var transform: CATransform3D = CATransform3DIdentity
var topSleeve: CALayer = CALayer()
var middleSleeve: CALayer = CALayer()
var bottomSleeve: CALayer = CALayer()
var topShadow: CALayer = CALayer()
var middleShadow: CALayer = CALayer()
let width: CGFloat = 200
let height: CGFloat = 300
var firstJointLayer: CALayer = CATransformLayer()
var secondJointLayer: CALayer = CATransformLayer()
var sizeWidth: CGFloat = 0
var positionX: CGFloat = 0
var firstJointLayerPositionX: CGFloat = 0
var perspectiveLayer: CALayer = {
let perspectiveLayer = CALayer()
perspectiveLayer.speed = 0.0
perspectiveLayer.fillMode = .removed
return perspectiveLayer
}()
var mainView: UIView = {
let view = UIView()
return view
}()
private let slider: UISlider = {
let slider = UISlider()
slider.addTarget(self, action: #selector(slide(sender:event:)) , for: .valueChanged)
return slider
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(slider)
setupLayers()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
slider.frame = CGRect(x: view.bounds.size.width/3,
y: view.bounds.size.height/10*8,
width: view.bounds.size.width/3,
height: view.bounds.size.height/10)
}
@objc private func slide(sender: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .ended:
resumeLayer(layer: perspectiveLayer)
default:
perspectiveLayer.timeOffset = CFTimeInterval(sender.value)
}
}
}
private func resumeLayer(layer: CALayer) {
let pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
}
private func setupLayers() {
// Changing all anchor points and positions here, in order to rotate the whole thing of -90°
mainView = UIView(frame:CGRect(x: 50, y: 50, width: width*3, height: height))
mainView.backgroundColor = UIColor.yellow
view.addSubview(mainView)
perspectiveLayer.frame = CGRect(x: width, y: 0, width: width*2, height: height)
perspectiveLayer.backgroundColor = UIColor.cyan.cgColor
mainView.layer.addSublayer(perspectiveLayer)
firstJointLayer.fillMode = .removed
firstJointLayer.frame = mainView.bounds
firstJointLayer.anchorPoint = CGPoint(x: 1, y: 0.5)
firstJointLayer.position = CGPoint(x: width*2, y: height/2)
perspectiveLayer.addSublayer(firstJointLayer)
topSleeve.fillMode = .removed
topSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
topSleeve.anchorPoint = CGPoint(x: 1, y: 0.5)
topSleeve.backgroundColor = UIColor.red.cgColor
topSleeve.position = CGPoint(x: width*3, y: height/2)
firstJointLayer.addSublayer(topSleeve)
topSleeve.masksToBounds = true
secondJointLayer.fillMode = .removed
secondJointLayer.frame = mainView.bounds
secondJointLayer.frame = CGRect(x: 0, y: 0, width: width*2, height: height)
secondJointLayer.anchorPoint = CGPoint(x: 1, y: 0.5)
secondJointLayer.position = CGPoint(x: width*2, y: height/2)
firstJointLayer.addSublayer(secondJointLayer)
secondJointLayer.fillMode = .removed
middleSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
middleSleeve.anchorPoint = CGPoint(x: 1, y: 0.5)
middleSleeve.backgroundColor = UIColor.blue.cgColor
middleSleeve.position = CGPoint(x: width*2, y: height/2)
secondJointLayer.addSublayer(middleSleeve)
middleSleeve.masksToBounds = true
bottomSleeve.fillMode = .removed
bottomSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
bottomSleeve.anchorPoint = CGPoint(x: 1, y: 0.5)
bottomSleeve.backgroundColor = UIColor.gray.cgColor
bottomSleeve.position = CGPoint(x: width, y: height/2)
secondJointLayer.addSublayer(bottomSleeve)
topShadow.fillMode = .removed
topSleeve.addSublayer(topShadow)
topShadow.frame = topSleeve.bounds
topShadow.backgroundColor = UIColor.black.cgColor
topShadow.opacity = 0
middleShadow.fillMode = .removed
middleSleeve.addSublayer(middleShadow)
middleShadow.frame = middleSleeve.bounds
middleShadow.backgroundColor = UIColor.black.cgColor
middleShadow.opacity = 0
transform.m34 = -1/700
perspectiveLayer.sublayerTransform = transform
sizeWidth = perspectiveLayer.bounds.size.width
positionX = perspectiveLayer.position.x
firstJointLayerPositionX = firstJointLayer.position.x
animate()
}
private func animate() {
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setCompletionBlock{ [weak self] in
if self == nil { return }
self?.perspectiveLayer.speed = 0
}
// firstJointLayer.transform = CATransform3DMakeRotation(CGFloat(-85*Double.pi/180), 0, 1, 0)
// secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180), 0, 1, 0)
// bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180), 0, 1, 0)
// perspectiveLayer.bounds.size.width = 0
// perspectiveLayer.position.x = 600
// firstJointLayer.position.x = 0
// topShadow.opacity = 0.5
// middleShadow.opacity = 0.5
var animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = -85*Double.pi/180
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 170*Double.pi/180
secondJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = -165*Double.pi/180
bottomSleeve.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "bounds.size.width")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = sizeWidth
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "position.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = positionX
animation.toValue = 600
perspectiveLayer.add(animation, forKey: nil)
// As said above, i added this animation which is not included in the original project, as the firstJointLayer was translating his position from left to right along with the perspectiveLayer position, so i make a reverse translation in its relative system so that it is stationary in the mainView system
animation = CABasicAnimation(keyPath: "position.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = firstJointLayerPositionX
animation.toValue = 0
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
topShadow.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
middleShadow.add(animation, forKey: nil)
CATransaction.commit()
}
}
好的 - 稍微玩一下...
看来您需要翻转动画,因为它们实际上是在“倒退”。
private func animate() {
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setCompletionBlock{ [weak self] in
if self == nil { return }
//self?.perspectiveLayer.speed = 0
}
firstJointLayer.transform = CATransform3DMakeRotation(CGFloat(-85*Double.pi/180), 0, 1, 0)
secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180), 0, 1, 0)
bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180), 0, 1, 0)
perspectiveLayer.bounds.size.width = 0
perspectiveLayer.position.x = 600
firstJointLayer.position.x = 0
topShadow.opacity = 0.5
middleShadow.opacity = 0.5
var animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = -85*Double.pi/180
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
// flip 180 degrees
animation.fromValue = 180*Double.pi/180
// to 180 - 170
animation.toValue = 10*Double.pi/180
secondJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.y")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
// flip -180 degrees
animation.fromValue = -180*Double.pi/180
// to 180 - 165
animation.toValue = -15*Double.pi/180
bottomSleeve.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "bounds.size.width")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = sizeWidth
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "position.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = positionX
animation.toValue = 600
perspectiveLayer.add(animation, forKey: nil)
// As said above, i added this animation which is not included in the original project, as the firstJointLayer was translating his position from left to right along with the perspectiveLayer position, so i make a reverse translation in its relative system so that it is stationary in the mainView system
animation = CABasicAnimation(keyPath: "position.x")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = firstJointLayerPositionX
animation.toValue = 0
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
topShadow.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.removed
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
middleShadow.add(animation, forKey: nil)
CATransaction.commit()
}