通过将.speed设置为-1来向后恢复CABasicAnimation [英] Resume CABasicAnimation backwards by setting .speed equal to -1
问题描述
编辑:我对问题进行了一些重构,并解决了部分问题,现在的问题归结为为什么恢复动画时演示层会出现毛刺/闪烁.在这一点上,我接受任何使动画可以随意向前和向后恢复的答案.我不确定我使用的方法是否正确,对于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-.timeOffset
秒
- there is an empty screen after animation has visually finished
- 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屋!