线性 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 所以这两个动画相互干扰。您可以通过以下任意组合解决此问题:

  1. 添加检查以查看您是否已经开始播放动画:

    var hasStarted = false
    
    private func startAnimation() {            
        guard !hasStarted else { return }
    
        hasStarted = true
    
        ...
    }
    

    (注意,我建议您在覆盖这些方法时始终调用 super。)

  2. 在开始另一个动画之前删除动画。

  3. 在你的动画中指定一个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)
    }
    
  4. 推迟到 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)
}