使用React动画库——react-spring
为了让后台系统视觉体验更好,决定增加过渡动画效果。
React官网中提到的动画库有3个:React Transition Group、React Motion 以及 React Spring。·
我选择用react-spring的原因,无非就是文档是3个中看的最顺眼的,所以就选它了。
react-spring 是一个基于弹簧物理学的动画库,满足大多数与UI相关的动画需求,提供了足够灵活的工具,可以自信地将想法投射到不断变化的界面中。
该库代表了一种现代动画方法。它的灵感很大程度上来自克里斯托弗·切多(Christopher Chedeau)的 animated 和成娄(Cheng Lou)的 react-motion 。它继承了animated强大的插值和性能,以及react-motion的易用性。
虽然animated主要是命令式的,而react-motion主要是声明式的,但react-spring却在两者之间架起了桥梁。
react-spring库提供了2种API,一种是 Hooks api(就是 React 16.8 的新增特性——Hook),一种是 Render-props api。
下面是我加到系统后台的过渡动画(以后增加再写出来慢慢补充):
当值改变时,数字的过渡动画:
// 用 Hooks api 写的话,就是重新写一个函数组件
import { useSpring, animated } from 'react-spring'
function Number(props) {
const props = useSpring({ number: props.number ? props.number : 0 });
return (
<animated.span>
{props.number.interpolate(x => (x * 100).toFixed(0))}
</animated.span>
)
}
// 如果要再已有的Class组件内直接加的话,用 Render-props api
import { Spring, animated } from 'react-spring/renderprops'
<Spring native to={{ number: classWrongScore ? classWrongScore : 0 }}>
{props => (<animated.span>
{props.number.interpolate(x => (x * 100).toFixed(0))}
</animated.span>)}
</Spring>
当值改变时,环形图的过渡动画:
下面的是从我之前用css写的环形图改过来的。
import { Spring, animated } from 'react-spring/renderprops'
<div className={style.annulusBasics}>
<div className={style.centerCircle}></div>
<div className={style.annulusOuter}></div>
<Spring native to={{ number: classWrongScore ? classWrongScore : 0 }}>
{props => <>
<animated.div className={style.leftRectangle} style={{
transform: props.number.interpolate(x => (x > 0.5 ? `rotate(${180 * x}deg)` : 'rotate(0deg)'))
}}></animated.div>
<animated.div className={style.rightRectangle} style={{
transform: props.number.interpolate(x => (x <= 0.5 ? `rotate(${360 * x}deg)` : 'rotate(0deg)')),
background: props.number.interpolate(x => (x > 0.5 ? '#FF7F69' : '#EDEDED'))
}} ></animated.div>
</>}
</Spring>
{/* 加下面一个div是因为hidde在移动端失效导致样式不对 */}
<div className={style.repairAnnulus}></div>
</div>
下面的是从我用canvas写的环形图demo改过来的,我是写成函数组件的形式。
import React, { useEffect } from "react";
export default function canvasDonut(props) {
useEffect(() => {
const ctx = document.getElementById("myCanvas").getContext("2d");
//外圆环
ctx.beginPath();
ctx.arc(50, 50, 47, 0, 2 * Math.PI);
ctx.lineWidth = 1;
ctx.strokeStyle = "#fff";
ctx.fillStyle = "#EDEDED";
ctx.fill();
ctx.stroke();
//内圆环
ctx.beginPath();
ctx.arc(50, 50, 35, 0, 2 * Math.PI);
ctx.lineWidth = 1;
ctx.strokeStyle = "#fff";
ctx.fillStyle = "#fff";
ctx.fill();
ctx.stroke();
// //圆环图的进度条
ctx.beginPath();
ctx.arc(50, 50, 41, -Math.PI / 2, -Math.PI / 2 + props.percent * (Math.PI * 2), false);
ctx.lineWidth = props.percent!==0 && 12;
ctx.lineCap = "round";
ctx.strokeStyle = "rgb(255, 127, 105)";
ctx.stroke();
});
return (
<canvas id="myCanvas" width="100" height="100" />
);
}
<Spring to={{ number: classWrongScore ? classWrongScore : 0 }}>
{props => <CanvasDonut percent={props.number}/>}
</Spring>
定时上下滚动:
//定时上下滚动
function useTimingRolling(time = 1000, distance) {
const [scrolling, setScrolling] = useSpring(() => ({ from: { scroll: 0 } }))
const ref = useRef(null)
useEffect(() => {
if (!distance) { distance = ref.current.clientHeight }
}, [])
useEffect(() => {
let intervalID = setInterval(() => {
setScrolling({ scroll: ref.current.scrollTop + distance });
if (Math.ceil(ref.current.scrollTop + ref.current.clientHeight) >= ref.current.scrollHeight) {
setScrolling({ scroll: 0 });
}
}, time);
return () => { clearInterval(intervalID); }
}, [])
return [scrolling.scroll, ref]
}
function chestnut() {
const [timeTable, timeTableRef] = useTimingRolling(10000);
return (
<animated.div scrollTop={timeTable} ref={timeTableRef} >
{new Array(10).fill('chestnut').map((item, i) => (
<animated.div key={i}> {item}{i} </animated.div>
))}
</animated.div>
)
}
逐条滚动:
function chestnut() {
const [delayTable, setDelayTable] = useState(new Array(10).fill('chestnut'))
useEffect(() => {
let intervalID;
intervalID = setInterval(() => {
new Promise(resolve => {
let ago;
setDelayTable(delayTable => { ago = delayTable.shift(); return delayTable })
resolve(ago)
}).then((res) => {
setDelayTable(delayTable => { delayTable.push(res); return delayTable })
})
}, 5000)
return () => { clearInterval(intervalID); clearTimeout(dingshi) }
}, [])
const transitions = useTransition(delayTable, (item, i) => i, {
from: { transform: 'rotateX(0deg)' },
enter: { transform: 'rotateX(0deg)' },
leave: { transform: 'rotateX(90deg)' },
})
return (
<animated.div>
{transitions.map(({ item, props, key }) =>
<animated.div style={props} key={key}>
<animated.span>{item}{i}</animated.span>
</animated.div>
)}
</animated.div>
)
}
使用 Hooks api注意点:
- Hooks api 中的 useSpring 只能写在函数组件中,写在Class组件中会报错。
使用 Render-props api 注意点:(renderprops是:Spring,Transition,Trail,Keyframes,Parallax)
- 如果需要使用 interpolate插值的话,需要在 renderprops 中加一个 native 标志,不然会报错。
- 如果在 renderprops 加一个 native 标志,接收元素需要变成 animated.[elementName],(例如div变为animated.div),不然会报错。
这个库在打包的时候可能会有个坑:
我是用roadhog,所以使用了这个库之后编译打包,会报错 Failed to minify the bundle. Error: 2.async.js from UglifyJs。原因是这个 npm 包没有遵守约定,没有转成 es5 就发上去,可以看下这个文章。
解决方案就是可以在.webpackrc里加入以下代码,让 babel 编译 node_modules 下指定的文件
{
"extraBabelIncludes": [
"node_modules/react-spring",
]
}