一文學會用 react-spring 做彈簧動畫

網頁中經常會見到一些動畫,動畫可以讓產品的交互體驗更好。

一般的動畫我們會用 css 的 animation 和 transition 來做,但當涉及到多個元素的時候,事情就會變得複雜。

比如下面這個動畫:

橫線和豎線依次做動畫,最後是笑臉的動畫。

這麼多個元素的動畫如何來安排順序呢?

如果用 css 動畫來做,那要依次設置不同的動畫開始時間,就很麻煩。

這種就需要用到專門的動畫庫了,比如 react-spring。

我們創建個 react 項目:

npx create-react-app --template=typescript react-spring-test

安裝 react-spring 的包:

npm install --save @react-spring/web

然後改下 App.tsx

import { useSpringValue, animated, useSpring } from '@react-spring/web'
import { useEffect } from 'react';
import './App.css';

export default function App() {
  const width = useSpringValue(0, {
    config: {
      duration: 2000
    }
  });

  useEffect(() ={
    width.start(300);
  }[]);

  return <animated.div class style={{ width }}></animated.div>
}

還有 App.css

.box {
  background: blue;
  height: 100px;
}

跑一下開發服務:

npm run start

可以看到,box 會在 2s 內完成 width 從 0 到 300 的動畫:

此外,你還可以不定義 duration,而是定義摩擦力等參數:

const width = useSpringValue(0, {
    config: {
      // duration: 2000
      mass: 2,
      friction: 10,
      tension: 200
    }
});

先看效果:

是不是像彈簧一樣?

彈簧的英文是 spring,這也是爲什麼這個庫叫做 react-spring

以及爲什麼 logo 是這樣的:

它主打的就是這種彈簧動畫。

當然,你不想做這種動畫,直接指定 duration 也行,那就是常規的動畫了。

回過頭來看下這三個參數:

這些參數設置不同的值,彈簧動畫的效果就不一樣:

tension: 400

tension: 100

可以看到,確實 tension(彈簧張力)越大,彈簧越緊,回彈速度越快。

mass: 2

mass: 20

可以看到,mass(質量越大),慣性越大,回彈距離和次數越大。

friction: 10

friction: 80

可以看到,firction(摩擦力)越大,tension 和 mass 的效果抵消的越多。

這就是彈簧動畫的 3 個參數。

回過頭來,我們繼續看其它的 api。

如果有多個 style 都要變化呢?

這時候就不要用 useSpringValue 了,而是用 useSpring:

import { useSpring, animated } from '@react-spring/web'
import './App.css';

export default function App() {
  const styles = useSpring({
    from: {
      width: 0,
      height: 0
    },
    to: {
      width: 200,
      height: 200
    },
    config: {
      duration: 2000
    }
  });

  return <animated.div class style={{ ...styles }}></animated.div>
}

用 useSpring 指定 from 和 to,並指定 duration。

動畫效果如下:

當然,也可以不用 duration 的方式:

而是用彈簧動畫的效果:

useSpring 還有另外一種傳入函數的重載,這種重載會返回 [styles, api] 兩個參數:

import { useSpring, animated } from '@react-spring/web'
import './App.css';

export default function App() {
  const [styles, api] = useSpring(() ={
    return {
      from: {
        width: 100,
        height: 100
      },
      config: {
        // duration: 2000
        mass: 2,
        friction: 10,
        tension: 400
      }
    }
  });

  function clickHandler() {
    api.start({
      width: 200,
      height: 200
    });
  }

  return <animated.div class style={{ ...styles }} onClick={clickHandler}></animated.div>
}

可以用返回的 api 來控制動畫的開始。

那如果有多個元素都要同時做動畫呢?

這時候就用 useSprings:

import { useSprings, animated } from '@react-spring/web'
import './App.css';

export default function App() {
  const [springs, api] = useSprings(
    3,
    () =({
      from: { width: 0 },
      to: { width: 300 },
      config: {
        duration: 1000
      }
    })
  )

  return <div>
    {springs.map(styles =(
      <animated.div style={styles} className='box'></animated.div>
    ))}
  </div>
}

在 css 里加一下 margin:

.box {
  background: blue;
  height: 100px;
  margin: 10px;
}

渲染出來是這樣的:

當你指定了 to,那會立刻執行動畫,或者不指定 to,用 api.start 來開始動畫:

import { useSprings, animated } from '@react-spring/web'
import './App.css';
import { useEffect } from 'react';

export default function App() {
  const [springs, api] = useSprings(
    3,
    () =({
      from: { width: 0 },
      config: {
        duration: 1000
      }
    })
  )

  useEffect(() ={
    api.start({ width: 300 });
  }[])

  return <div>
    {springs.map(styles =(
      <animated.div style={styles} className='box'></animated.div>
    ))}
  </div>
}

那如果多個元素的動畫是依次進行的呢?

這時候要用 useTrail

import { animated, useTrail } from '@react-spring/web'
import './App.css';
import { useEffect } from 'react';

export default function App() {
  const [springs, api] = useTrail(
    3,
    () =({
      from: { width: 0 },
      config: {
        duration: 1000
      }
    })
  )

  useEffect(() ={
    api.start({ width: 300 });
  }[])

  return <div>
    {springs.map(styles =(
      <animated.div style={styles} className='box'></animated.div>
    ))}
  </div>
}

用起來很簡單,直接把 useSprings 換成 useTrail 就行:

可以看到,動畫會依次執行,而不是同時。

接下來我們實現下文章開頭的這個動畫效果:

橫線和豎線的動畫就是用 useTrail 實現的。

而中間的笑臉使用 useSprings 同時做動畫。

那多個動畫如何安排順序的呢?

用 useChain:

import { animated, useChain, useSpring, useSpringRef, useSprings, useTrail } from '@react-spring/web'
import './App.css';

export default function App() {

  const api1 = useSpringRef()
  
  const [springs] = useTrail(
    3,
    () =({
      ref: api1,
      from: { width: 0 },
      to: { width: 300 },
      config: {
        duration: 1000
      }
    }),
    []
  )

  const api2 = useSpringRef()
  
  const [springs2] = useSprings(
    3,
    () =({
      ref: api2,
      from: { height: 100 },
      to: { height: 50},
      config: {
        duration: 1000
      }
    }),
    []
  )

  useChain([api1, api2][0, 1], 500)

  return <div>
    {springs.map((styles1, index) =(
      <animated.div style={{...styles1, ...springs2[index]}} className='box'></animated.div>
    ))}
  </div>
}

我們用 useSpringRef 拿到兩個動畫的 api,然後用 useChain 來安排兩個動畫的順序。

useChain 的第二個參數指定了 0 和 1,第三個參數指定了 500,那就是第一個動畫在 0s 開始,第二個動畫在 500ms 開始。

如果第三個參數指定了 3000,那就是第一個動畫在 0s 開始,第二個動畫在 3s 開始。

可以看到,確實第一個動畫先執行,然後 0.5s 後第二個動畫執行。

這樣,就可以實現那個笑臉動畫了。

我們來寫一下:

import { useTrail, useChain, useSprings, animated, useSpringRef } from '@react-spring/web'
import './styles.css'
import { useEffect } from 'react'

const STROKE_WIDTH = 0.5

const MAX_WIDTH = 150
const MAX_HEIGHT = 100

export default function App() {

  const gridApi = useSpringRef()

  const gridSprings = useTrail(16, {
    ref: gridApi,
    from: {
      x2: 0,
      y2: 0,
    },
    to: {
      x2: MAX_WIDTH,
      y2: MAX_HEIGHT,
    },
  })

  useEffect(() ={
    gridApi.start();
  });

  return (
      <div className='container'>
        <svg viewBox={`0 0 ${MAX_WIDTH} ${MAX_HEIGHT}`}>
          <g>
            {gridSprings.map(({ x2 }, index) =(
              <animated.line
                x1={0}
                y1={index * 10}
                x2={x2}
                y2={index * 10}
                key={index}
                strokeWidth={STROKE_WIDTH}
                stroke="currentColor"
              />
            ))}
            {gridSprings.map(({ y2 }, index) =(
              <animated.line
                x1={index * 10}
                y1={0}
                x2={index * 10}
                y2={y2}
                key={index}
                strokeWidth={STROKE_WIDTH}
                stroke="currentColor"
              />
            ))}
          </g>
        </svg>
      </div>
  )
}

當用 useSpringRef 拿到動畫引用時,需要手動調用 start 來開始動畫。

用 useTrail 來做從 0 到指定 width、height 的動畫。

然後分別遍歷它,拿到 x、y 的值,來繪製橫線和豎線。

用 svg 的 line 來畫線,設置 x1、y1、x2、y2 就是一條線。

效果是這樣的:

當你註釋掉橫線或者豎線,會更明顯一點:

然後再做笑臉的動畫,這個就是用 rect 在不同畫幾個方塊,做一個 scale 從 0 到 1 的動畫:

動畫用彈簧動畫的方式,指定 mass(質量) 和 tension(張力),並且每個 box 都有不同的 delay:

並用 useChain 來把兩個動畫串聯執行。

import { useTrail, useChain, useSprings, animated, useSpringRef } from '@react-spring/web'
import './styles.css'

const COORDS = [
  [50, 30],
  [90, 30],
  [50, 50],
  [60, 60],
  [70, 60],
  [80, 60],
  [90, 50],
]

const STROKE_WIDTH = 0.5

const MAX_WIDTH = 150
const MAX_HEIGHT = 100

export default function App() {

  const gridApi = useSpringRef()

  const gridSprings = useTrail(16, {
    ref: gridApi,
    from: {
      x2: 0,
      y2: 0,
    },
    to: {
      x2: MAX_WIDTH,
      y2: MAX_HEIGHT,
    },
  })

  const boxApi = useSpringRef()

  const [boxSprings] = useSprings(7, i =({
    ref: boxApi,
    from: {
      scale: 0,
    },
    to: {
      scale: 1,
    },
    delay: i * 200,
    config: {
      mass: 2,
      tension: 220,
    },
  }))

  useChain([gridApi, boxApi][0, 1], 1500)

  return (
      <div className='container'>
        <svg viewBox={`0 0 ${MAX_WIDTH} ${MAX_HEIGHT}`}>
          <g>
            {gridSprings.map(({ x2 }, index) =(
              <animated.line
                x1={0}
                y1={index * 10}
                x2={x2}
                y2={index * 10}
                key={index}
                strokeWidth={STROKE_WIDTH}
                stroke="currentColor"
              />
            ))}
            {gridSprings.map(({ y2 }, index) =(
              <animated.line
                x1={index * 10}
                y1={0}
                x2={index * 10}
                y2={y2}
                key={index}
                strokeWidth={STROKE_WIDTH}
                stroke="currentColor"
              />
            ))}
          </g>
          {boxSprings.map(({ scale }, index) =(
            <animated.rect
              key={index}
              width={10}
              height={10}
              fill="currentColor"
              style={{
                transform: `translate(${COORDS[index][0]}px, ${COORDS[index][1]}px)`,
                transformOrigin: `5px 5px`,
                scale,
              }}
            />
          ))}
        </svg>
      </div>
  )
}

這樣,整個動畫就完成了:

這個動畫,我們綜合運用了 useSprings、useTrail、useSpringRef、useChain 這些 api。

把這個動畫寫一遍,react-spring 就算是掌握的可以了。

案例代碼上傳了 github:https://github.com/QuarkGluonPlasma/react-course-code/tree/main/react-spring-test

總結

我們學了用 react-spring 來做動畫。

react-spring 主打的是彈簧動畫,就是類似彈簧那種回彈效果。

只要指定 mass(質量)、tension(張力)、friction(摩擦力)就可以了。

彈簧動畫不需要指定時間。

當然,你也可以指定 duration 來做那種普通動畫。

react-spring 有不少 api,分別用於單個、多個元素的動畫:

掌握了這些,就足夠基於 react-spring 做動畫了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/So8OVR1x9W4y8BUwkxg1xg