一文學會用 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 也行,那就是常規的動畫了。
回過頭來看下這三個參數:
-
mass: 質量(也就是重量),質量越大,回彈慣性越大,回彈的距離和次數越多
-
tension: 張力,彈簧鬆緊程度,彈簧越緊,回彈速度越快
-
friction:摩擦力,增加點阻力可以抵消質量和張力的效果
這些參數設置不同的值,彈簧動畫的效果就不一樣:
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(摩擦力)就可以了。
-
mass 質量:決定回彈慣性,mass 越大,回彈的距離和次數越多。
-
tension 張力:彈簧鬆緊程度,彈簧越緊,回彈速度越快。
-
friction:摩擦力: 可以抵消質量和張力的效果
彈簧動畫不需要指定時間。
當然,你也可以指定 duration 來做那種普通動畫。
react-spring 有不少 api,分別用於單個、多個元素的動畫:
-
useSpringValue:指定單個屬性的變化。
-
useSpring:指定多個屬性的變化
-
useSprings:指定多個元素的多個屬性的變化,動畫並行執行
-
useTrial:指定多個元素的多個屬性的變化,動畫依次執行
-
useSpringRef:用來拿到每個動畫的 ref,可以用來控制動畫的開始、暫停等
-
useChain:串行執行多個動畫,每個動畫可以指定不同的開始時間
掌握了這些,就足夠基於 react-spring 做動畫了。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/So8OVR1x9W4y8BUwkxg1xg