React Spring 源码解析 基于弹簧的声明式动画库

React Spring是一个基于弹簧的动画,弹簧的弹性轨迹符合人脑对真实物理的建模。因此,相比于css基于函数插值的动画,基于弹簧轨迹的动画更有助于人们在直觉上感觉“更平滑”。这就是为什么iOS的动画饱受好评的原因,因为在iOS中,大量使用了弹簧动画。

目前,React Spring已经成为了我“御用”的动画框架,其声明式语法降低了心智负担,在正确的封装后,可以大大提高开发动画效率,增强UI界面的活泼性。

项目地址:pmndrs/react-spring: ✌️ A spring physics based React animation library (github.com)

基本结构分析

React Spring使用lerna框架实现Monorepo架构,所有依赖的包都放在/packages目录中。

在React Spring中,/packages有这些包:animated core parallax rafz react-spring shared types

其中,core包是弹簧动画核心代码所在位置;rafz用于requestAnimationFrame调度;react-spring是外部调用的入口。

React Spring是平台无关(platform-agnostic)的,一套语法可以同时用于多种不同的平台。在/targets中,React Spring对不同平台进行了适配,包括web、three fiber、react native等。也就是说,在dom、3D场景和原生app中,都可以使用React Spring实现弹簧动画。

动画核心

SpringValue中的advance方法是计算每一帧动画的核心算法,以下是主要逻辑(省略部分代码),弹簧部分其实就是高中物理:

/** 每一帧requestAnimationFrame时调用的函数,参数为距离上一帧的间隔时间 */
advance(dt: number) {
    let idle = true
    let changed = false

    //省略部分代码

    anim.values.forEach((node, i) => {
        //省略初始化代码
        if (!finished) {

            //省略

            if (!is.und(config.duration)) {
                //省略,如果config中有duration值,则使用普通的函数插值计算动画(类似 css animation)
                //is.und是工具函数,判断对应值是否为undefined
            }
            else if (config.decay) {
                //省略,如果config中有decay值,则使用decay动画
            }
            else {
                //使用基于弹簧的动画,关键部分
                velocity = node.lastVelocity == null ? v0 : node.lastVelocity

                /** The smallest distance from a value before being treated like said value. */
                const precision =
                    config.precision ||
                    (from == to ? 0.005 : Math.min(1, Math.abs(to - from) * 0.001))

                /** The velocity at which movement is essentially none */
                const restVelocity = config.restVelocity || precision / 10

                // Bouncing is opt-in (not to be confused with overshooting)
                const bounceFactor = config.clamp ? 0 : config.bounce!
                const canBounce = !is.und(bounceFactor)

                /** When `true`, the value is increasing over time */
                const isGrowing = from == to ? node.v0 > 0 : from < to

                /** When `true`, the velocity is considered moving */
                let isMoving!: boolean

                /** When `true`, the velocity is being deflected or clamped */
                let isBouncing = false

                //根据距离上一帧的时间进行循环,以1毫秒为单位
                const step = 1 // 1ms
                const numSteps = Math.ceil(dt / step)
                for (let n = 0; n < numSteps; ++n) {
                    //判断速度是否小于最小速度,如果小于,且已经到达目标位置(小于precision精度),则标记为finished
                    isMoving = Math.abs(velocity) > restVelocity

                    if (!isMoving) {
                        finished = Math.abs(to - position) <= precision
                        if (finished) {
                            break
                        }
                    }

                    //是否反弹(达到目标值后,按照惯性继续来回移动,就像弹簧来回跳动一样)
                    if (canBounce) {
                        isBouncing = position == to || position > to == isGrowing

                        // Invert the velocity with a magnitude, or clamp it.
                        if (isBouncing) {
                            velocity = -velocity * bounceFactor
                            position = to
                        }
                    }

                    //计算弹簧的弹力
                    const springForce = -config.tension * 0.000001 * (position - to)
                    //计算用于衰减速度的阻力
                    const dampingForce = -config.friction * 0.001 * velocity
                    //计算加速度,a=F/m
                    const acceleration = (springForce + dampingForce) / config.mass // pt/ms^2

                    //计算速度
                    velocity = velocity + acceleration * step // pt/ms
                    //根据速度计算位移
                    position = position + velocity * step
                }
            }

            node.lastVelocity = velocity

            if (Number.isNaN(position)) {
                console.warn(`Got NaN while animating:`, this)
                finished = true
            }
        }
 
    })

    //省略额外后续操作
}

在GitHub中源码对应的位置:https://github.com/pmndrs/react-spring/blob/c4c7de9f75eb59e9d3e24dc7f8e188abccf93c71/packages/core/src/SpringValue.ts#L304

欢迎来到Yari的网站:yar2001 » React Spring 源码解析 基于弹簧的声明式动画库