通过将 .speed 设置为 -1 向后恢复 CABasicAnimation

Posted

技术标签:

【中文标题】通过将 .speed 设置为 -1 向后恢复 CABasicAnimation【英文标题】:Resume CABasicAnimation backwards by setting .speed equal to -1 【发布时间】:2021-01-12 11:50:20 【问题描述】:

编辑:我已经稍微重构了问题并解决了部分问题,现在问题归结为为什么在动画恢复时表示层会出现故障/闪烁。在这一点上,我接受了任何使动画可以随意向前和向后恢复而没有问题的答案。我不确定我使用的方法是否正确,我对 Swift 还是很陌生。

注意:底部有示例项目,以便更好地理解问题。

在我的项目中,我通过将层.speed 属性设置为0 来暂停CABasicAnimation,然后我通过将层的.timeOffset 属性设置为等于UISlider 以交互方式更改动画值每当用户滚动滑块时,.value 属性。通过代码:

layer.speed = 0 

那么当用户滑动时:

layer.timeOffset = CFTimeInterval(sender.value)

现在我想在滑块上的用户手势结束时随意向后或向前恢复动画,因此从与当前动画值相关的起点开始。我发现运行顺利的唯一可行的解​​决方案如下,但它只能继续前进:

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

然后我可以在动画完成时再次暂停它:

 DispatchQueue.main.asyncAfter(deadline: .now()+1) 
    layer.timeOffset = 0
    layer.speed = 0
 

据我了解,.speed 不仅定义了结合.duration 属性的动画的实际速度,还定义了动画的方向:如果我将图层的速度设置为等于-1,则动画完成向后。关于CAMediaTiming 的工作原理,请参考this 的答案,我试图更改上面sn-p 的参数以恢复动画倒退而没有运气。我认为这会起作用:

let pausedTime = layer.timeOffset
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
layer.timeOffset = pausedTime*2
layer.speed = -1.0

但是图层从来没有像这样设置动画。该问题似乎与convertTime 方法有关。

然后我发现this 的问题与我的基本相同,唯一的答案是一个不错的解决方案。重构一下代码,我只能说:

   layer.beginTime = CACurrentMediaTime()
   layer.speed = -1
   DispatchQueue.main.asyncAfter(deadline: .now()+1) 
       layer.timeOffset = 0
       layer.speed = 0
   

但是,当动画向后播放时非常有问题,特别是表示层在恢复时和完成时都会闪烁。我尝试了各种解决方案,但都没有运气,我做了一些猜测:

这可能是与CAMediaTimingFillMode 相关的问题,因为我可以将其设置为.back.forwards,但是当它恢复时,动画既不是最终状态也不是初始状态,因此不会渲染初始帧; 这是因为我没有保持模态树和表示树同步。

但是,当它在恢复和完成时闪烁/闪烁时,这两者都没有解释。此外,在我看来,动画在向前恢复时可能有 1 的持续时间,但在向后恢复时只有 1-timeOffset,不确定。

真的不确定真正的问题是什么以及如何解决这个烂摊子。我们非常欢迎所有建议。

对于任何感兴趣的人,这里有一个与我类似的示例项目,灵感来自另一个问题(动画正在向前运行,要向后运行并捕捉故障,只需调用 resumeLayerBackwards())。我知道代码应该重构,但仍然可以。只需复制、粘贴并运行:

import UIKit

class ViewController: UIViewController 

var perspectiveLayer: CALayer = 
    let perspectiveLayer = CALayer()
    perspectiveLayer.speed = 0.0
    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)
    animate()


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
    DispatchQueue.main.asyncAfter(deadline: .now()+1) 
        layer.timeOffset = 1.0
        layer.speed = 0.0
    


private func resumeLayerBackwards(layer: CALayer)   
        layer.beginTime = CACurrentMediaTime()
        layer.speed = -1
        DispatchQueue.main.asyncAfter(deadline: .now()+1) 
            layer.timeOffset = 0
            layer.speed = 0
        



private func animate() 
    var transform:CATransform3D = CATransform3DIdentity
    var topSleeve:CALayer
    var middleSleeve:CALayer
    var bottomSleeve:CALayer
    var topShadow:CALayer
    var middleShadow:CALayer
    let width:CGFloat = 300
    let height:CGFloat = 150
    var firstJointLayer:CALayer
    var secondJointLayer:CALayer
    
    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)
    mainView.layer.addSublayer(perspectiveLayer)
    
    firstJointLayer = CATransformLayer()
    firstJointLayer.frame = mainView.bounds
    perspectiveLayer.addSublayer(firstJointLayer)
    
    topSleeve = CALayer()
    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 = CATransformLayer()
    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)
    
    middleSleeve = CALayer()
    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 = CALayer()
    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 = CALayer()
    topSleeve.addSublayer(topShadow)
    topShadow.frame = topSleeve.bounds
    topShadow.backgroundColor = UIColor.black.cgColor
    topShadow.opacity = 0
    
    middleShadow = CALayer()
    middleSleeve.addSublayer(middleShadow)
    middleShadow.frame = middleSleeve.bounds
    middleShadow.backgroundColor = UIColor.black.cgColor
    middleShadow.opacity = 0
    
    transform.m34 = -1/700
    perspectiveLayer.sublayerTransform = transform
    
    var animation = CABasicAnimation(keyPath: "transform.rotation.x")
    animation.fillMode = CAMediaTimingFillMode.forwards
    animation.isRemovedOnCompletion = false
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = -90*Double.pi/180
    firstJointLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "transform.rotation.x")
    animation.fillMode = CAMediaTimingFillMode.forwards
    animation.isRemovedOnCompletion = false
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 180*Double.pi/180
    secondJointLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "transform.rotation.x")
    animation.fillMode = CAMediaTimingFillMode.forwards
    animation.isRemovedOnCompletion = false
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = -160*Double.pi/180
    bottomSleeve.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "bounds.size.height")
    animation.fillMode = CAMediaTimingFillMode.forwards
    animation.isRemovedOnCompletion = false
    animation.duration = 1
    animation.fromValue = perspectiveLayer.bounds.size.height
    animation.toValue = 0
    perspectiveLayer.add(animation, forKey: nil)
    
    
    animation = CABasicAnimation(keyPath: "position.y")
    animation.fillMode = CAMediaTimingFillMode.forwards
    animation.isRemovedOnCompletion = false
    animation.duration = 1
    animation.fromValue = perspectiveLayer.position.y
    animation.toValue = 0
    perspectiveLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "opacity")
    animation.fillMode = CAMediaTimingFillMode.forwards
    animation.isRemovedOnCompletion = false
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 0.5
    topShadow.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "opacity")
    animation.fillMode = CAMediaTimingFillMode.forwards
    animation.isRemovedOnCompletion = false
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 0.5
    middleShadow.add(animation, forKey: nil)


【问题讨论】:

【参考方案1】:

我设法消除了示例项目中resumeLayerBackwards(layer:) 的故障。 实际上存在两个问题:

    动画视觉上完成后有一个空白屏幕 空白屏幕可见1 - .timeOffset

所以,问题似乎在于动画实际上不仅在 .timeOffset 期间播放,而且在整个 .duration 期间播放。并且出现空白屏幕是因为没有为 1 - .timeOffset 块定义动画。

回想一下:CALayer 也采用 CAMediaTiming 协议,就像 CAAnimation 所做的那样(定义了所有属性:尽管其中一些似乎不太清楚如何应用于层)。

在经过.timeOffset 秒后,speed = -1 — 属性 .timeOffset 变为等于 0。这意味着动画已经开始,因此(以负速度)结束。虽然它不是那么明显——似乎因为 .fillMode 属性而被删除。为了解决这个问题,我将perspectiveLayer.fillMode = .forwards 添加到animate() 方法中。

要在 .timeOffset 秒之后而不是整个 .duration 之后完成动画 - 使用 .repeatDuration 属性。我已将 layer.repeatDuration = layer.timeOffset 添加到您的 resumeLayerBackwards(layer:) 方法中。

该项目仅适用于添加的两行。

我不能说这个解决方案对我来说真的合乎逻辑,尽管它确实有效。负速度对我来说有点不可预测。在我的项目中,我曾经通过在克隆的动画对象中交换开始和结束值来反转动画。

【讨论】:

是的,它现在可以工作了。我觉得我不太了解 CAMediaTiming 是如何工作的,但它确实有效,而且我很好。非常感谢,非常感谢您的帮助。 顺便说一句,经过一番测试后,似乎该解决方案开始工作时,当我多次滑动时,动画开始自行中断。我发现您关于交换值 + 在完成时更改模型层的建议更幸运。

以上是关于通过将 .speed 设置为 -1 向后恢复 CABasicAnimation的主要内容,如果未能解决你的问题,请参考以下文章

使用向后滑动手势以交互方式取消选择选定单元格

安全与加密之加密算法,CA,openssl,证书管理

将 UUID 转换为字符串表示,例如:1816 转换为 Cycling Speed 和 Cadence

jq 自动滑动轮换(向后插入小块)

Turtle常用命令

SwiftUI - navigationBarBackButtonHidden - 向后滑动手势?