Welcome toVigges Developer Community-Open, Learning,Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
3.4k views
in Technique[技术] by (71.8m points)

ios - Unexpected behaviour in animation when i change the properties of the model layers

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:

  1. 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.
  2. 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:

enter image description here

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:

  1. 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...

  2. ...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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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()
    
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to Vigges Developer Community for programmer and developer-Open, Learning and Share
...