如何获取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
相关产品推荐
相关产品推荐

