swift 如何在Core Animation中访问动画的计算小值?

xyhw6mcr  于 2023-03-16  发布在  Swift
关注(0)|答案(1)|浏览(166)

我使用下面的代码为我的动画:

let animation = CABasicAnimation()
animation.keyPath = "position.x"
animation.fromValue = 0
animation.toValue = 300
animation.timingFunction = .init(name: .easeInEaseOut)
animation.duration = 2

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

如果您查看2秒内看到的代码,我们将从0更改为300,我希望访问中间的计算值,如:0.0, 0.2, 0.3, ..., 300.0CABasicAnimation一起使用,那么我该如何执行此操作?

eit6fx6z

eit6fx6z1#

对于正在进行的动画,您可以通过访问您正在制作动画的属性来获取它在presentation()层中的进度。如果您想要动画的每一帧,您可以连接CVDisplayLink(适用于macOS)或CADisplayLink(适用于iOS)。
示例代码:

var displayLink: CVDisplayLink!
//var displayLink: CADisplayLink!

func someFunctionThatTriggersTheAnimation() {
    let animation = CABasicAnimation()
    // let's say we are animating position.x
    animation.keyPath = "position.x"
    animation.fromValue = 0
    animation.toValue = 300
    animation.timingFunction = .init(name: .easeInEaseOut)
    animation.duration = 2

    // Please conform self to CAAnimationDelegate!
    animation.delegate = self
    button.layer?.add(animation, forKey: "basic")
}

// CAAnimationDelegate methods:
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    CVDisplayLinkStop(displayLink)
}

func animationDidStart(_ anim: CAAnimation) {

    // CVDisplayLink is a bit of a hassle to use in Swift :(
    CVDisplayLinkCreateWithCGDisplay(CGMainDisplayID(), &displayLink)
    withUnsafeMutablePointer(to: &button!) { pointer in
        CVDisplayLinkSetOutputCallback(displayLink, { _, _, _, _, _, buttonPointer in
            DispatchQueue.main.async {
                print(buttonPointer!.assumingMemoryBound(to: NSButton.self).pointee.layer!.presentation()!.position.x)
            }
            return 1
        }, pointer)
    }
    CVDisplayLinkStart(displayLink)
}

/*
CADisplayLink would look like this:

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    displayLink.invalidate()
}

func animationDidStart(_ anim: CAAnimation) {
    displayLink = CADisplayLink(target: self, selector: #selector(printAnimationProgress))
    displayLink.add(to: .main, forMode: .common)
}

@objc func printAnimationProgress() {
    print(button.layer.presentation()!.position.x)
}
*/

也就是说,CAMediaTimingFunction s只是三次Bezier曲线,因此可以在不运行动画的情况下计算动画进度。正如你在维基百科页面上看到的,曲线是由四个控制点参数化定义的,我们可以使用getControlPoint获得这些控制点。
之后,我们可以得到曲线的y坐标方程,用参数t表示,但是t不是时间,曲线的x坐标是动画的时间,所以我们需要用x坐标来表示t,我们已经可以用三次方程,用控制点来表示x(见维基百科上的方程),所以我们只需要解方程。这向你展示了从哪里开始。
最后,我们只需要代入x,也就是时间,得到参数t,然后代入y坐标方程。
我们可以这样写一个函数:

func getCallableFunction(fromTimingFunction function: CAMediaTimingFunction) -> (Float) -> Float {
    // Each *pair* of elements in cps store a control point.
    // the elements at even indices store the x coordinates
    // the elements at odd indices store the y coordinates
    var cps = Array(repeating: Float(0), count: 8)
    cps.withUnsafeMutableBufferPointer { pointer in
        for i in 0..<4 {
            function.getControlPoint(at: i, values: &pointer[i * 2])
        }
    }

    return { x in
        // assuming the curve doesn't do weird things like loop around on itself, this only has one real root
        // coefficients got from https://pomax.github.io/bezierinfo/#yforx
        // implementation of cubicSolve from https://gist.github.com/kieranb662/b85aada0b1c03a06ad2f83a0291d7243
        let t = Float(cubicSolve(
            a: Double(-cps[0] + cps[2] * 3 - cps[4] * 3 + cps[6]),
            b: Double(cps[0] * 3 - cps[2] * 6 + cps[4] * 3),
            c: Double(-cps[0] * 3 + cps[2] * 3),
            d: Double(cps[0] - x)
        ).first(where: \.isReal)!.real)
        
        // getting y from t, see equation on Wikipedia
        return powf(1 - t, 3) * cps[1] +
                powf(1 - t, 2) * t * cps[3] * 3 +
                (1 - t) * t * t * cps[5] * 3 +
                t * t * t * cps[7]
    }
}

注意,我使用了this gist中的三次解算器,它计算所有的根,请随意使用另一个更快的算法。
default定时函数的用法示例:

let function = getCallableFunction(fromTimingFunction: .init(name: .default))
for i in stride(from: Float(0), through: 1, by: 0.05) {
    let result = function(i)
    print(result) // this prints a result between 0 and 1
}

在图形上绘制结果如下所示:

这与default文档中显示的图形非常相似。
要将此应用于特定的动画,只需线性缩放函数的时间和输出。例如,对于持续时间为2且动画在0到300之间的动画:

let result = function(time / 2) * 300

相关问题