线性 CABasicAnimation 在容器视图中不是线性执行的
Linear CABasicAnimation isn't executed linearly in Container View
即使将 timingFunction 显式设置为 linear 动画也不会线性执行。
我使用下面的代码来初始化动画。
再往下就是整个class的实现以及ViewController是如何在InterfaceBuilder
中设置的
private func timeLayerAnimation() {
let animation = CABasicAnimation()
animation.keyPath = "strokeEnd"
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 240.0
animation.toValue = 0
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
timeLayer.add(animation, forKey: nil)
}
视图如下所示。
动画总时长为 240 秒。
但在 30 秒后。已经只有 75% 的圆仍然可见。
停止次数如下:
75 % (1.5 π): 30 sec. (∆ 30 sec.)
50 % (1 π): 70 sec. (∆ 40 sec.)
25 % (0.5 π): 120 sec. (∆ 50 sec.)
// 13 % (0.25 π): 155 sec.
0 % (0 π): 240 sec. (∆ 120 sec.)
更新
我发现当负责动画的 ViewController 位于容器视图中时会出现问题。
我的猜测是它可能与默认的 UIViewAnimationCurve 有关,但我不确定,也不知道从哪里开始测试它: (
容器视图的所有边都固定到安全区域。
MainVC 的实现是空的,EmbeddedVC 如下所示:
import UIKit
class EmbeddedVC: UIViewController {
// MARK: - Properties
let timeLayer = CAShapeLayer()
// MARK: - View Lifecycle
override func viewDidLayoutSubviews() {
setupTimerLayout()
}
}
// MARK: - Timer Layout Setup
extension EmbeddedVC {
func setupTimerLayout() {
let circularPath = UIBezierPath.init(arcCenter: .zero,
radius: view.frame.width * 0.36,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
// Configure time layer
timeLayer.path = circularPath.cgPath
timeLayer.fillColor = UIColor.clear.cgColor
timeLayer.lineCap = kCALineCapRound
timeLayer.strokeColor = UIColor.red.cgColor
timeLayer.strokeEnd = 1
timeLayer.lineWidth = 10
timeLayer.position = view.center
view.layer.addSublayer(timeLayer)
animateTimeLayer()
}
private func animateTimeLayer() {
let animation = CABasicAnimation()
animation.keyPath = "strokeEnd"
animation.duration = 240.0
animation.toValue = 0
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
timeLayer.add(animation, forKey: nil)
}
}
问题是 viewDidLayoutSubviews
可以调用多次而您没有指定 fromValue
所以这两个动画相互干扰。您可以通过以下任意组合解决此问题:
添加检查以查看您是否已经开始播放动画:
var hasStarted = false
private func startAnimation() {
guard !hasStarted else { return }
hasStarted = true
...
}
(注意,我建议您在覆盖这些方法时始终调用 super
。)
在开始另一个动画之前删除动画。
在你的动画中指定一个fromValue
:
private func startAnimation() {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = ...
animation.fromValue = 1 // by setting this, it won't get confused if you start the animation again
animation.toValue = 0
...
shapeLayer.add(animation, forKey: nil)
}
推迟到 viewDidAppear
:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// start your animation here
}
就个人而言,我会同时执行#3 和#4,但您可以选择最适合您的方式。
@Rob 非常感谢你的回答这个真的让我发疯了......:)
现在一切正常。我只想为那些使用扩展并且不想在基础 class 上使用局部变量或无法访问基础 class.[=11= 的人添加一些东西到 #1 ]
private func animateTimeLayer() {
let animationKey = "animateTimeLayerKey"
guard nil == timeLayer.animation(forKey: animationKey) else { return }
let animation = CABasicAnimation()
...
timeLayer.add(animation, forKey: animationKey)
}
即使将 timingFunction 显式设置为 linear 动画也不会线性执行。
我使用下面的代码来初始化动画。
再往下就是整个class的实现以及ViewController是如何在InterfaceBuilder
中设置的private func timeLayerAnimation() {
let animation = CABasicAnimation()
animation.keyPath = "strokeEnd"
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 240.0
animation.toValue = 0
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
timeLayer.add(animation, forKey: nil)
}
视图如下所示。
动画总时长为 240 秒。
但在 30 秒后。已经只有 75% 的圆仍然可见。
停止次数如下:
75 % (1.5 π): 30 sec. (∆ 30 sec.)
50 % (1 π): 70 sec. (∆ 40 sec.)
25 % (0.5 π): 120 sec. (∆ 50 sec.)
// 13 % (0.25 π): 155 sec.
0 % (0 π): 240 sec. (∆ 120 sec.)
更新
我发现当负责动画的 ViewController 位于容器视图中时会出现问题。
我的猜测是它可能与默认的 UIViewAnimationCurve 有关,但我不确定,也不知道从哪里开始测试它: (
容器视图的所有边都固定到安全区域。 MainVC 的实现是空的,EmbeddedVC 如下所示:
import UIKit
class EmbeddedVC: UIViewController {
// MARK: - Properties
let timeLayer = CAShapeLayer()
// MARK: - View Lifecycle
override func viewDidLayoutSubviews() {
setupTimerLayout()
}
}
// MARK: - Timer Layout Setup
extension EmbeddedVC {
func setupTimerLayout() {
let circularPath = UIBezierPath.init(arcCenter: .zero,
radius: view.frame.width * 0.36,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
// Configure time layer
timeLayer.path = circularPath.cgPath
timeLayer.fillColor = UIColor.clear.cgColor
timeLayer.lineCap = kCALineCapRound
timeLayer.strokeColor = UIColor.red.cgColor
timeLayer.strokeEnd = 1
timeLayer.lineWidth = 10
timeLayer.position = view.center
view.layer.addSublayer(timeLayer)
animateTimeLayer()
}
private func animateTimeLayer() {
let animation = CABasicAnimation()
animation.keyPath = "strokeEnd"
animation.duration = 240.0
animation.toValue = 0
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
timeLayer.add(animation, forKey: nil)
}
}
问题是 viewDidLayoutSubviews
可以调用多次而您没有指定 fromValue
所以这两个动画相互干扰。您可以通过以下任意组合解决此问题:
添加检查以查看您是否已经开始播放动画:
var hasStarted = false private func startAnimation() { guard !hasStarted else { return } hasStarted = true ... }
(注意,我建议您在覆盖这些方法时始终调用
super
。)在开始另一个动画之前删除动画。
在你的动画中指定一个
fromValue
:private func startAnimation() { let animation = CABasicAnimation(keyPath: "strokeEnd") animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) animation.duration = ... animation.fromValue = 1 // by setting this, it won't get confused if you start the animation again animation.toValue = 0 ... shapeLayer.add(animation, forKey: nil) }
推迟到
viewDidAppear
:override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // start your animation here }
就个人而言,我会同时执行#3 和#4,但您可以选择最适合您的方式。
@Rob 非常感谢你的回答这个真的让我发疯了......:)
现在一切正常。我只想为那些使用扩展并且不想在基础 class 上使用局部变量或无法访问基础 class.[=11= 的人添加一些东西到 #1 ]
private func animateTimeLayer() {
let animationKey = "animateTimeLayerKey"
guard nil == timeLayer.animation(forKey: animationKey) else { return }
let animation = CABasicAnimation()
...
timeLayer.add(animation, forKey: animationKey)
}