通过将 .speed 设置为 -1 向后恢复 CABasicAnimation
Resume CABasicAnimation backwards by setting .speed equal to -1
编辑:我稍微重构了问题并解决了部分问题,现在问题归结为为什么表示层 glitches/flashes 当动画恢复。在这一点上,我接受任何使动画可以随意向前和向后恢复而没有问题的答案。我不确定我使用的方法是否正确,我对 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
然后动画向后完成。参考 this 关于 CAMediaTiming
工作原理的回答,我试图更改上面片段的参数以恢复向后播放的动画,但没有成功。我认为这会起作用:
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
但是当它恢复时动画既不是最终状态也不是初始状态因此不渲染初始帧;
- 这是因为我没有保持模态树和展示树同步。
然而,这两个都没有在简历和完成时 flickers/flashes 进行解释。另外,在我看来,动画向前恢复时的持续时间可能为 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)
}
}
我设法消除了示例项目中 resumeLayerBackwards(layer:)
的故障。
实际上有两个问题:
- 动画 视觉上 完成后出现空白屏幕
- 空白屏幕可见
1 - .timeOffset
秒
所以,问题似乎在于动画实际上不仅在 .timeOffset
期间播放,而且在整个 .duration
期间播放。并且出现空白屏幕是因为没有为 1 - .timeOffset
块定义动画。
回想一下:CALayer
也采用 CAMediaTiming
协议,正如 CAAnimation
所做的那样(定义了所有属性:尽管其中一些似乎不太清楚如何应用到一层)。
speed = -1
在 .timeOffset
秒后 — 属性 .timeOffset
变为零。这意味着动画已经开始,因此(以负速度)它结束了。尽管它不是那么明显 — 似乎是因为 .fillMode
属性 而被删除了。为了解决这个问题,我在 animate()
方法中添加了 perspectiveLayer.fillMode = .forwards
。
要让动画恰好在 .timeOffset
秒后而不是整个 .duration
秒后完成 — 使用 .repeatDuration
属性。我已将 layer.repeatDuration = layer.timeOffset
添加到您的 resumeLayerBackwards(layer:)
方法中。
该项目仅适用于添加的两条线。
我不能说这个解决方案对我来说真的合乎逻辑,尽管它确实有效。负速度对我来说有点不可预测。在我的项目中,我曾经通过交换克隆动画对象中的开始和结束值来反转动画。
编辑:我稍微重构了问题并解决了部分问题,现在问题归结为为什么表示层 glitches/flashes 当动画恢复。在这一点上,我接受任何使动画可以随意向前和向后恢复而没有问题的答案。我不确定我使用的方法是否正确,我对 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
然后动画向后完成。参考 this 关于 CAMediaTiming
工作原理的回答,我试图更改上面片段的参数以恢复向后播放的动画,但没有成功。我认为这会起作用:
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
但是当它恢复时动画既不是最终状态也不是初始状态因此不渲染初始帧; - 这是因为我没有保持模态树和展示树同步。
然而,这两个都没有在简历和完成时 flickers/flashes 进行解释。另外,在我看来,动画向前恢复时的持续时间可能为 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)
}
}
我设法消除了示例项目中 resumeLayerBackwards(layer:)
的故障。
实际上有两个问题:
- 动画 视觉上 完成后出现空白屏幕
- 空白屏幕可见
1 - .timeOffset
秒
所以,问题似乎在于动画实际上不仅在 .timeOffset
期间播放,而且在整个 .duration
期间播放。并且出现空白屏幕是因为没有为 1 - .timeOffset
块定义动画。
回想一下:CALayer
也采用 CAMediaTiming
协议,正如 CAAnimation
所做的那样(定义了所有属性:尽管其中一些似乎不太清楚如何应用到一层)。
speed = -1
在 .timeOffset
秒后 — 属性 .timeOffset
变为零。这意味着动画已经开始,因此(以负速度)它结束了。尽管它不是那么明显 — 似乎是因为 .fillMode
属性 而被删除了。为了解决这个问题,我在 animate()
方法中添加了 perspectiveLayer.fillMode = .forwards
。
要让动画恰好在 .timeOffset
秒后而不是整个 .duration
秒后完成 — 使用 .repeatDuration
属性。我已将 layer.repeatDuration = layer.timeOffset
添加到您的 resumeLayerBackwards(layer:)
方法中。
该项目仅适用于添加的两条线。
我不能说这个解决方案对我来说真的合乎逻辑,尽管它确实有效。负速度对我来说有点不可预测。在我的项目中,我曾经通过交换克隆动画对象中的开始和结束值来反转动画。