问题
Referring to this post, i'm trying to adapt the animations to landscape mode. Basically what i want is to rotate all layers of -90° (90° clockwise) and the animations to run horizontally instead of vertically. The author didn't bother to explain the logic under the hood, there are a dozen paper folding libraries in obj-c which are all based on the same architecture, so apparently this is the way to go for folding.
EDIT: To further clarify what i want to achieve, here you can look at three snapshots (starting point, halftime and ending point) of the animations i want. In the question from the link up above the animation collapses from bottom to top, while i want it to collapse from left to right.
Down below you can take a look at the the original project a bit tweaked:
- i changed the gray
bottomSleeve
layer final angle value, as well as the red and blue ones angle; - i paused the animations on initialization by setting the
perspectiveLayer
speed
equal to0
and added a slider, the slider value is then set equal to theperspectiveLayer
timeOffset
so that you can interactively run each frame of the animations by sliding. When the touch event on the slider ends, the animations are then resumed from the frame relative to the currenttimeOffset
to the final value. - i changed all the model layers values before running each animation added to the relative presentation layer using
CATransaction
. Also, on completion theperspectiveLayer
speed is set to0
again. - for a better visual understanding, i set the
perspectiveLayer
backgroundColor
equal tocyan
.
Just to point it out, there are two main functions:
setupLayers()
, called inviewDidLoad()
is responsible of setting up the layers positions and anchor points, as well as adding them as sublayers to themainView
layer.animate()
, called recursively insetupLayers()
, responsible of adding the animations. Here i also set the model layers values to the related animations final value before adding them.
Just copy, paste it and run:
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()
}
}
As you can see the animations run as expected, at this point in order to rotate the whole thing it should be just a matter of changing positions, anchor points and final animations values. Taken from an answer from the link above, here is a great representation of all the layers of the starting project:
Then i proceeded to refactor setupLayers()
and animate()
to run the animations horizontally, from left to right (in other words, i'm rotating of 90° clockwise the up above layers representation).
Once the code is changed to rotate the animations, i encounter two issues:
when the animations start, the
firstJointLayer
position translate from left to right along theperspectiveLayer
. To be fair to my understanding this should be an expected behaviour, as it is a sublayer ofperspectiveLayer
, actually i'm not sure why in the original project it doesn't happen. However, to fix this, i've added another animation responsible of translating it from right to left in its relative system, so that it actually appears stationary. At this point while i don't change the model layers final values (commented lines in the down below project), the animations run horizontally as expected. If i didn't have to also modify the model layers, my goal would be reached as this is the exact animation i want. However......if i then try to set the animations final values (just comment the lines out) i get an unexpected behaviour. At the initial frame of the animations, the red, blue and gray layers appear folded on each other, thus the rotations don't work as predicted anymore. Here are some snapshots at time 0.0, 0.5 and 1.0 (duration: 1.0):
The most illogical part to me is that setting the model layers values equal to the presentation layers final values causes the bug, but it only affects the presentation layers, as once the animations are over the model layers below are in the expected (and wanted) rotation/position:
The anchor points are for sure placed right as the rotations happen around the correct points. I think it may be related to issue 1., but i've tried to reposition the layers multiple times with no success. To the present day this is still unsolved, in two days i wasn't able to track down the primary issue and thus to fix it. To me the original project (up above) and the rotated project (down below) look the same in the logic under the hood.
EDIT2: i've found out a minor bug in the code, i was animating the firstJointLayer x position from a starting value equal to the perspectiveLayer x position instead of his own x position, i've fixed it but nothing changed.
EDIT3: Since setting the model layers values equal to the animation final values is what causes the bug, please note that using animation.fillMode = CAMediaTimingFillMode.forwards
and animation.isRemovedOnCompletion = false
is not a viable workaround for avoiding to touch the modal layers, as i need to revert the animation at a later time thus keeping presentation and model layers synced is required.
Any help is really appreciated. Down here the rotated project - i've also commented the blocks i've changed from the up above project:
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()
}
}
回答1:
OK - a bit of playing around...
Looks like you need to flip the animations, since they're effectively "going backward."
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()
}
来源:https://stackoverflow.com/questions/65749372/unexpected-behaviour-in-animation-when-i-change-the-properties-of-the-model-laye