通过将.speed设置为-1来向后恢复CABasicAnimation [英] Resume CABasicAnimation backwards by setting .speed equal to -1

查看:196
本文介绍了通过将.speed设置为-1来向后恢复CABasicAnimation的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

编辑:我对问题进行了一些重构,并解决了部分问题,现在的问题归结为为什么恢复动画时演示层会出现毛刺/闪烁.在这一点上,我接受任何使动画可以随意向前和向后恢复的答案.我不确定我使用的方法是否正确,对于Swift来说我还是很新.

EDIT: i've refactored the question a bit and solved part of the issue, now the question comes down to why does the presentation layer glitches/flashes when the animation is resumed. At this point tho i'm accepting any answer that makes the animation resume both forwards and backwards at will with no issue. I'm not sure the approach i'm using is the right one, i'm still pretty new to Swift.

注意:底部的示例项目,是为了更好地理解问题.

Note: Sample project at the bottom, for having a better understanding of the issue.

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

In my project i'm pausing a CABasicAnimation by setting the layer .speed property to 0, then i'm changing the animation value interactively by setting the layer's .timeOffset property equal to a UISlider .value property whenever the user scrolls the slider. By code:

layer.speed = 0 

然后,当用户滑动时:

layer.timeOffset = CFTimeInterval(sender.value)

现在,我希望每当滑块上的用户手势结束时都可以随意向后或向前恢复动画,因此从与当前动画值相关的起点开始.我发现,唯一可以顺利运行的解决方案是以下解决方案,但只能向前使用:

Now i want to resume the animation backwards or forwards at will whenever the user gesture on the slider ends, so from the starting point related to the current animation value. The only viable solution i've found which runs smoothly is the following, but it works only going forwards:

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

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

Then i can simply pause it again at the completion of the animation:

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

据我所知, .speed 不仅定义了与 .duration 属性结合的动画的实际速度,而且还定义了动画的方向:如果我设置了层的速度等于 -1 ,然后动画向后完成.关于 CAMediaTiming 的工作方式,参考答案,我试图更改上面的内容片段的参数来恢复动画,但是没有运气.我认为这会起作用:

From my understanding, .speed not only defines the actual speed of the animation combined with the .duration property, but also the direction of the animation: if i set a layer's speed equal to -1 then the animation completes backwards. Referring to this answer in regards to how CAMediaTiming works, i was trying to change the up above snippet's parameters to resume the animation going backwards with no luck. I thought this would work:

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 方法有关.

but the layer is never animated like so. The issue seems to be related to the convertTime method.

然后我发现了这个问题,与我的问题基本相同,并且唯一的答案是一个体面的解决方案.重构一下代码,我只能说:

Then i found this question which is basically the same of mine, and the only answer has a decent solution. Refactoring a bit the code, i can just say:

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

但是,当向后播放动画时,会出现一些小故障,特别是表示层在继续播放和完成播放时都会闪烁.我已经尝试过各种解决方案,但没有运气,我做出了一些推测:

However, when the animation is played backwards is very glitchy, in particular the presentation layer flashes both at the resume and on completion. I've tried various solutions with no luck, some speculations i've made:

  • 这可能是与 CAMediaTimingFillMode 相关的问题,因为我可以将其设置为 .back .forwards ,但是当恢复动画时既不是最终状态也不是初始状态,因此不会渲染初始帧;
  • 这是由于我没有使模态树和表示树保持同步这一事实造成的.
  • it may be an issue related to CAMediaTimingFillMode, as i can set it .back or .forwards but when it resumes the animation is neither in it's final nor in it's initial state and thus the initial frame is not rendered;
  • it is caused by the fact that i'm not keeping the modal tree and the presentation tree synchronized.

但是,在恢复和完成时闪烁/闪烁时,这两种方法都无法解释.另外,在我看来,动画在向前恢复时的持续时间可能为1,而在向后恢复时的持续时间仅为1-timeOffset.

Both of these however doesn't explain while it flickers/flashes both on resume and on completion. Additionally, it seems to me that the animation may have a duration of 1 when resumed forwards, but only of 1-timeOffset when resumed backwards, not sure tho.

真的不确定实际问题是什么以及如何解决此问题.所有建议都值得欢迎.

Really not sure what's the actual problem and how to fix this mess. All suggestions are more than welcomed.

对于感兴趣的人,这是一个类似于我的示例项目,受到另一个问题的启发(动画正在向前运行,向后运行并捕捉故障,只需调用 resumeLayerBackwards()).我知道应该对代码进行重构,但出于此目的,它还是可以的.只需复制,粘贴并运行:

For anyone interested, here's a sample project similar to mine, inspired by another question (animation is running forward, to run it backwards and catch the glitch just call resumeLayerBackwards()). I know the code should be refactored, but still for the purpose it's fine. Just copy, paste and run:

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

推荐答案

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

I managed to remove the glitch for resumeLayerBackwards(layer:) in the sample project. Two problems there in fact:

  1. 动画视觉上结束后,屏幕空白
  2. 空白屏幕可见 1-.timeOffset
  1. there is an empty screen after animation has visually finished
  2. the empty screen is visible for 1 - .timeOffset seconds

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

So, seems like the problem is that animation in fact plays not just for .timeOffset period, but for the whole .duration period. And the empty screen appears because there is no animation defined for 1 - .timeOffset block.

回想一下: CALayer 也采用了 CAMediaTiming 协议,就像 CAAnimation 一样(定义了所有属性:尽管其中一些属性似乎没有非常清楚如何应用于图层.

Just to recall: CALayer also adopts CAMediaTiming protocol, as CAAnimation does (with all the properties defined: although some of them seem not be very clear how to be applied to a layer).

speed = -1 的情况下—属性 .timeOffset 等于零.这意味着动画已经开始,因此(以负速度)完成了.尽管不是那么明显-似乎由于 .fillMode 属性而被删除了.为了解决这个问题,我在 animate()方法中添加了 perspectiveLayer.fillMode = .forwards .

With speed = -1 after .timeOffset seconds passed — the property .timeOffset becomes equal to zero. It means that animation has reached its beginning and therefore (with negative speed) it is finished. Though it is not that obvious — seems like it is removed because of the .fillMode property. To fix this I've added perspectiveLayer.fillMode = .forwards to animate() method.

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

To have animation completed exactly after .timeOffset seconds instead of the whole .duration — use .repeatDuration property. I've added layer.repeatDuration = layer.timeOffset to your resumeLayerBackwards(layer:) method.

该项目仅适用于添加了两行的情况.

The project works only with both lines added.

尽管可以解决,但我不能说该解决方案对我而言确实是合乎逻辑的.负速度对我来说有点不可预测.在我的项目中,我曾经通过在克隆的动画对象中交换开始值和结束值来反转动画.

I can't say that the solution is really logical for me, although it works. Negative speed works a bit unpredictable as for me. In my project I used to reverse animation by swapping begin and end values in cloned animation object.

这篇关于通过将.speed设置为-1来向后恢复CABasicAnimation的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆