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 to 0
and added a slider, the slider value is then set equal to the perspectiveLayer
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 current timeOffset
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 the perspectiveLayer
speed is set to 0
again.
- for a better visual understanding, i set the
perspectiveLayer
backgroundColor
equal to cyan
.
Just to point it out, there are two main functions:
setupLayers()
, called in viewDidLoad()
is responsible of setting up the layers positions and anchor points, as well as adding them as sublayers to the mainView
layer.
animate()
, called recursively in setupLayers()
, 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 the perspectiveLayer
. To be fair to my understanding this should be an expected behaviour, as it is a sublayer of perspectiveLayer
, 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): <a href="https://i.stack.imgur.com/3neXN.png" rel="nofollow