当我更改模型层的属性时动画中的意外行为

问题描述

参考 this 帖子,我正在尝试将动画调整为横向模式。基本上我想要的是旋转 -90°(顺时针 90°)的所有图层,并使动画水平而不是垂直运行。作者没有费心解释引擎盖下的逻辑,obj-c 中有十几个折纸库都是基于相同的架构,所以显然这是折叠的方式。

编辑:为了进一步阐明我想要实现的目标,here 您可以查看我想要的动画的三个快照(起点、中场时间和终​​点)。在上面链接的问题中,动画从下到上折叠,而我希望它从左到右折叠。

在下面,您可以查看稍微调整的原始项目:

  • 我更改了灰色 bottomSleeve 层的最终角度值,以及红色和蓝色的角度;
  • 我在初始化时暂停了动画,方法是将 perspectiveLayer speed 设置为 0添加一个滑块,然后将滑块值设置为等于 perspectiveLayer {{ 1}} 以便您可以通过滑动以交互方式运行动画的每一帧。当滑块上的触摸事件结束时,动画将从相对于当前 timeOffset 的帧恢复到最终值。
  • 在运行添加到相关表示层的每个动画之前,我使用 timeOffset 更改了所有模型层的值。此外,完成后,CATransaction 速度再次设置为 perspectiveLayer
  • 为了更好的视觉理解,我将 0 perspectiveLayer 设置为 backgroundColor

简单说一下,主要有两个作用:

  1. cyan,在 setupLayers()调用,负责设置图层位置和锚点,并将它们作为子图层添加viewDidLoad() 图层。
  2. mainView,在animate()中递归调用,负责添加动画。在这里,我还将模型层值设置为相关动画的最终值,然后再添加

只需复制、粘贴并运行:

setupLayers()

如您所见,动画按预期运行,此时为了旋转整个事物,只需更改位置、锚点和最终动画值即可。 从上面链接的答案中获取,这里是起始项目所有层的一个很好的表示:

enter image description here

然后我继续重构 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,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,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,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,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,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) secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180),0) bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180),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() } } setupLayers() 以从左到右水平运行动画(换句话说,我顺时针旋转 90° 以上层表示)。

更改代码以旋转动画后,我遇到两个问题:

  1. 当动画开始时,animate() 位置沿 firstJointLayer 从左到右平移。根据我的理解,这应该是一种预期行为,因为它是 perspectiveLayer 的子层,实际上我不确定为什么在原始项目中它不会发生。但是,为了解决这个问题,我添加了另一个动画,负责在其相关系统中将其从右向左平移,因此它实际上看起来是静止的。此时,虽然我没有更改模型层的最终值(项目下方的注释行),但动画按预期水平运行。如果我不必也修改模型层,我的目标就会达到,因为这是我想要的确切动画。不过……

  2. ...如果我然后尝试设置动画最终值(只需注释掉这些行),我就会得到一个意想不到的行为。在动画的初始帧,红色、蓝色和灰色层看起来相互折叠,因此旋转不再像预测的那样工作。以下是时间 0.0、0.5 和 1.0(持续时间:1.0)的一些快照:

对我来说最不合逻辑的部分是将模型层的值设置为等于表示层的最终值会导致错误,但它只会影响表示层,因为一旦动画在下面的模型层上处于预期状态(并想要)旋转/位置:

enter image description here

当旋转发生在正确的点周围时,锚点肯定放在正确的位置。我认为这可能与问题 1.有关,但我尝试多次重新定位图层,但均未成功。直到今天,这仍然没有解决,在两天内我无法找到主要问题并因此解决它。对我来说,原始项目(上方)和旋转项目(下方)在引擎盖下的逻辑看起来相同。

EDIT2:我发现了代码中的一个错误,我正在从等于透视层 x 位置而不是他自己的 x 位置的起始值为 firstJointLayer x 位置设置动画,我已修复,但没有任何改变。

EDIT3:由于将模型层值设置为动画最终值是导致错误的原因,请注意使用 perspectiveLayeranimation.fillMode = camediatimingFillMode.forwards 不是可行的解决方法避免触摸模态层,因为我需要稍后恢复动画,因此需要保持展示层和模型层同步。

非常感谢任何帮助。下面是旋转的项目 - 我还评论了我从上面的项目中更改的块:

animation.isRemovedOnCompletion = false

解决方法

好的 - 有点玩...

看起来您需要翻转动画,因为它们实际上是在“倒退”。

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)
    secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180),0)
    bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180),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()
    
}