You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何获取Core Animation中CABasicAnimation的中间计算值?

获取CABasicAnimation动画过程中的中间值

方法一:通过CADisplayLink读取presentationLayer的实时值

动画运行时,图层的presentationLayer会同步展示当前动画的中间状态,我们可以用和屏幕刷新率同步的CADisplayLink定时读取这个图层的属性值:

var displayLink: CADisplayLink?

// 初始化动画
let animation = CABasicAnimation()
animation.keyPath = "position.x"
animation.fromValue = 0
animation.toValue = 300
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.duration = 2
// 可选:保持动画结束后的状态
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false

// 创建并启动CADisplayLink
displayLink = CADisplayLink(target: self, selector: #selector(updateAnimationValue))
displayLink?.add(to: .current, forMode: .common)

// 添加动画到图层
nsView.layer?.add(animation, forKey: "basic")

// 定时更新并获取中间值
@objc func updateAnimationValue() {
    guard let presentationLayer = nsView.layer?.presentation() else { return }
    let currentX = presentationLayer.position.x
    print("当前position.x:\(currentX)")
    
    // 动画接近结束时停止定时器(处理浮点精度误差)
    if currentX >= 299.9 {
        displayLink?.invalidate()
        displayLink = nil
    }
}

方法二:手动计算时序进度与插值

如果不需要实际展示动画,或者想提前模拟动画值,可以通过动画代理监听开始时间,结合定时器计算已运行时长占比,再通过时序函数的控制点计算缓动后的插值:

var animationTimer: Timer?
var startTime: CFTimeInterval = 0

// 初始化动画
let animation = CABasicAnimation()
animation.keyPath = "position.x"
animation.fromValue = 0
animation.toValue = 300
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.duration = 2
animation.delegate = self

nsView.layer?.add(animation, forKey: "basic")

// 实现CAAnimationDelegate
extension YourViewController: CAAnimationDelegate {
    func animationDidStart(_ anim: CAAnimation) {
        startTime = CACurrentMediaTime()
        // 以60fps频率计算值
        animationTimer = Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { [weak self] timer in
            guard let self = self else { return }
            let elapsedTime = CACurrentMediaTime() - self.startTime
            
            // 动画结束后停止定时器
            guard elapsedTime <= anim.duration else {
                timer.invalidate()
                print("最终值:300.0")
                return
            }
            
            // 计算原始进度
            let rawProgress = elapsedTime / anim.duration
            // 计算缓动后的进度
            let easedProgress = self.calculateEasedProgress(rawProgress, timingFunction: anim.timingFunction as! CAMediaTimingFunction)
            // 插值计算当前值
            let currentValue = 0 + (300 - 0) * easedProgress
            print("当前计算值:\(currentValue)")
        }
    }
    
    // 根据时序函数的贝塞尔控制点计算缓动进度
    func calculateEasedProgress(_ progress: Double, timingFunction: CAMediaTimingFunction) -> Double {
        var controlPoints = [CGPoint]()
        // 获取时序函数的4个控制点
        for index in 0..<4 {
            var x: Float = 0, y: Float = 0
            timingFunction.getControlPoint(at: index, values: &x, &y)
            controlPoints.append(CGPoint(x: CGFloat(x), y: CGFloat(y)))
        }
        
        let t = CGFloat(progress)
        let p0 = controlPoints[0]
        let p1 = controlPoints[1]
        let p2 = controlPoints[2]
        let p3 = controlPoints[3]
        
        // 三次贝塞尔曲线插值公式
        let easedY = pow(1-t,3)*p0.y + 3*pow(1-t,2)*t*p1.y + 3*(1-t)*pow(t,2)*p2.y + pow(t,3)*p3.y
        return Double(easedY)
    }
}

两种方法对比

  • 方法一直接读取动画的实际运行值,精度更高,适合需要和视觉效果完全同步的场景。
  • 方法二是手动模拟计算,不需要依赖图层的presentationLayer,适合预计算动画序列或者脱离图层场景使用。

内容的提问来源于stack exchange,提问作者swiftPunk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 05:03:34