提问 发文

easyv组件开发者——如何实现一些简单的组件动画

赵炎飞

| 2023-05-15 11:18 839 1 3

        介绍几种组件开发常用的动画以及实现思路,这里主要是 react-hooks 的动画实现思路,没用 react-spring 等动画库,所以并不是最优解,对动画库熟练的话用库会更快一点。

    吐槽一下,这个文本编辑器实在是太辣鸡啦!居然不能用快捷键生成代码块,而且代码块还不支持javascript,我创建代码块+复制代码就花了好长时间,心累~~

轮播动画:

实现效果:


实现思路:

如上图所示,每隔一秒选项就会进行状态切换,这其中,状态的切换离不开useState这个hooks,而每隔一秒的过程可以有多种方式实现(setInterval, setTimeout, requestAnimationFrame, requestIdleCallback)。这里我们用requestAnimationFrame来实现,setInterval和setTimeout会在浏览器进入后台运行后依旧执行,浪费性能。

1.首先用useState创建一个index变量,用于记录当前选中的选项卡,默认为0(表示选中第一个)。再用useRef创建一个变量timer,用于记录计时器id。

2.创建一个启动动画的函数,startLoop,在startLoop中创建一个变量initTime,用于记录第一次执行动画的时间戳。

3.startLoop中再创建一个函数loop,这个函数将被作为requestAnimationFrame的回调函数,在每一帧浏览器渲染网页前被调用。requestAnimationFrame在执行回调函数loop时,会给loop传入一个时间戳参数(timeStamp),表示动画执行的时长(ms级)。

4.loop函数中,我们用timeStamp - initTime >= interval来判断是否需要切换状态,其中Interval为时间间隔(ms级)。切换状态时需要用setIndex的函数式参数,因为在react中,原生事件和异步函数中如果需要获取最新的状态值,需要用函数式参数的方式来获取,比如:setIndex( pre=>pre+1 ),其中pre就是最新的状态值。

5.在startLoop中执行loop函数,再创建一个useEffect,添加所需的依赖,执行startLoop。最后记得在useEffect中添加卸载函数,执行cancelAnimationFrame,用于清除对应的计时器。

demo:

data.main.json:

{
"data": [
{
"text": "Tab A"
},
{
"text": "Tab B"
},
{
"text": "Tab C"
},
{
"text": "Tab D"
}
],
"fields": [
{
"name": "text",
"value": "text",
"desc": "文本"
}
]
}

index.jsx:

import React,{useState, useEffect, useRef} from "react";


export default function (props) {
const { left, top, width, height, id, data } = props;

const interval = 1000; //轮播间隔,单位为ms

const [index, setIndex] = useState(0); //当前项下标
const timer = useRef(); //计时器实例

//初始化时启动轮播动画
useEffect(()=>{
startLoop();
return ()=>{
cancelAnimationFrame(timer.current)
}
},[]);

// 样式
const styles = {
position: "absolute",left,top,width,height,
display:"grid",
gridTemplateColumns:`repeat(${data.length},1fr)`,
columnGap:10
};

return (
<div className="__easyv-component" style={styles} id={id}>
{
data.map((d,i)=>{
return <div key={i} style={{
background:i==index?"red":"green",
}}>{d.text}</div>
})
}
</div>
);
function startLoop(){
let initTime;
const loop=(timeStamp)=>{
if(!initTime) initTime=timeStamp;
if(timeStamp - initTime > interval){
initTime += Math.floor((timeStamp-initTime)/interval)*interval;
setIndex(pre=>(pre+1)%data.length);
}
timer.current = requestAnimationFrame(loop);
}
loop();
}
}


颜色过渡动画:

实现效果:

CSS.registerProperty,chrome 78开始支持


实现思路:

    如上图所示,我们的选项卡在切换状态时,多了一个颜色渐变的过渡动画,这使得它看上去更加舒适。在前一个demo中,我们已经完成了轮播动画的效果,接下来我们将在其基础上,完成这个过渡动画。

demo:

index.jsx:

import React,{useState, useEffect, useRef} from "react";

export default function (props) {
const { left, top, width, height, id, data } = props;

const interval = 1000; //轮播间隔,单位为ms

const [index, setIndex] = useState(0); //当前项下标
const timer = useRef(); //计时器实例

//初始化时启动轮播动画
useEffect(()=>{
startLoop();
return ()=>{
cancelAnimationFrame(timer.current)
}
},[]);

// 样式
const styles = {
position: "absolute",left,top,width,height,
display:"grid",
gridTemplateColumns:`repeat(${data.length},1fr)`,
columnGap:10
};

return (
<div className="__easyv-component" style={styles} id={id}>
{
data.map((d,i)=>{
return <div key={i} style={{
background:i==index?"red":"green",
transition:"background 0.5s linear"
}}>{d.text}</div>
})
}
</div>
);
function startLoop(){
let initTime;
const loop=(timeStamp)=>{
if(!initTime) initTime=timeStamp;
if(timeStamp - initTime > interval){
initTime += Math.floor((timeStamp-initTime)/interval)*interval;
setIndex(pre=>(pre+1)%data.length);
}
timer.current = requestAnimationFrame(loop);
}
loop();
}
}

淡入淡出效果:

实现效果:

Animation、KeyframeEffect,chrome 75开始支持;

另外,animte 的隐式from/to关键帧在chrome 84支持,animte本身在chrome 36就支持了

5g8zo-v29fx.gif

实现思路:

    如上图所示,这次我们不再需要用 index 来记录当前选中项了,但是我们依旧需要 index 这个变量,用于记录数据的切割起点,当 index 变化时,我们需要截取数据中索引在 index——index+4 之间的数据,如果 index+4大于数据长度,就从头部截取一部分。

    1.由于直接控制组件渲染的是截取后的数据,不再是 index了, 所以我们可以将 index 改为用useRef绑定,并重新创建一个状态 showData ,组件将根据 showData 来渲染。

    2.每触发一次轮播动画,我们就需要做以下这些事情:

        a.生成新的showData,这个showData比前一次渲染的showData多一条数据,但是起点相同,这样产生的效果就是在组件最右侧添加了一个新的选项,这个选项在画布外。

        b.最左边的选项透明度会从1变成0 。

        c.最右边新添加的选项透明度会从0变成1 。

        d.所有选项都会整体向左偏移,偏移量为 一个选项宽度+一个间隔宽度。

        e.所有动画结束后,更新 index 和 showData,showData 只需要4条数据就足够了。

    3.如果我们用替换className,setTimeout等手段来完成淡入淡出的动画,是没办法保证 "更新 index 和 showData 的代码" 一定是在动画执行完毕后执行的,所以这里我们使用了 Animation API,用 Animation 创建的动画对象,可以添加 finish 事件,在动画完成时执行 onfinish 回调函数。另外,引入KeyframeEffect也为动态创建动画效果提供了便利,再也不需要在css中预置keyframes了。

demo:

index.jsx:

import React,{useState, useEffect, useLayoutEffect, useRef} from "react";

export default function (props) {
const { left, top, width, height, id, data } = props;
const interval = 3000; //轮播间隔,单位为ms
const gap = 10; //每一项之间的间距
const count = 4; //每次需要显示的项数
const itemWidth = (width-gap*(count-1))/count; //每项的宽度

const index = useRef(0); //当前项下标
const timer = useRef(); //计时器实例
const boxRef = useRef(); //容器实例,做平移动画

const [showData, setShowData] = useState(data.length>count?[...data,...data].slice(index.current, index.current+count):data); //需要显示的数据

//初始化时启动轮播动画
useEffect(()=>{
data.length>count && startLoop(); //只有数据量大于每次显示的项数时才需要启动轮播动画
return ()=>{
cancelAnimationFrame(timer.current);
}
},[]);
useLayoutEffect(()=>{
const dom = boxRef.current;
const child = dom.childNodes;
//showData长度为5时,表示组件开始执行过渡动画
if(showData.length==5){
//平移动画
const animation = new Animation(new KeyframeEffect(
dom,
[
{ transform:"translateX(0)" },
{ transform:`translateX(-${itemWidth+gap}px)` }
],
{ duration:500, fill:"backwards" }
),document.timeline);
animation.play();
//透明度变化动画
child[0].animate({ //淡出
opacity:[1,0],
easing:["ease-out"]
},500);
child[child.length-1].animate({ //淡入
opacity:[0,1],
easing:["ease-in"]
},500);
animation.onfinish = (e)=>{
index.current = (index.current+1)%data.length;
setShowData([...data,...data].slice(index.current, index.current+count));
}
}
},[showData]);

// 样式
const styles = {
position: "absolute",left,top,width,height,
overflowX:"hidden"
};
const boxStyle = {
height:"100%",
display:"flex",
gap
}

return (
<div className="__easyv-component" style={styles} id={id}>
<div ref={boxRef} style={boxStyle}>
{
showData.map((d,i)=>{
return <div key={i} style={{
background:"green",
minWidth:itemWidth
}}>{d.text}</div>
})
}
</div>
</div>
);
/**开始动画 */
function startLoop(){
let initTime;
const loop=(timeStamp)=>{
if(!initTime) initTime=timeStamp;
if(timeStamp - initTime > interval){
initTime += Math.floor((timeStamp-initTime)/interval)*interval;
setShowData([...data,...data].slice(index.current, index.current+count+1));
}
timer.current = requestAnimationFrame(loop);
}
loop();
}
}


import React,{useState, useEffect, useRef} from "react";

export default function (props) {
const { left, top, width, height, id, data } = props;

const interval = 1000; //轮播间隔,单位为ms

const [index, setIndex] = useState(0); //当前项下标
const timer = useRef(); //计时器实例

//初始化时启动轮播动画
useEffect(()=>{
startLoop();
return ()=>{
cancelAnimationFrame(timer.current)
}
},[]);

// 样式
const styles = {
position: "absolute",left,top,width,height,
display:"grid",
gridTemplateColumns:`repeat(${data.length},1fr)`,
columnGap:10
};

return (
<div className="__easyv-component" style={styles} id={id}>
{
data.map((d,i)=>{
return <div key={i} style={{
background:i==index?"red":"green",
}}>{d.text}</div>
})
}
</div>
);
function startLoop(){
let initTime;
const loop=(timeStamp)=>{
if(!initTime) initTime=timeStamp;
if(timeStamp - initTime > interval){
initTime += Math.floor((timeStamp-initTime)/interval)*interval;
setIndex(pre=>(pre+1)%data.length);
}
timer.current = requestAnimationFrame(loop);
}
loop();








收藏 0
分享
分享方式
微信

评论

游客

全部 1条评论

福州领视 福州领视 2023-06-06 12:47
666
回复
19

文章

1.81K

人气

6

粉丝

1

关注

官方媒体

轻松设计高效搭建,减少3倍设计改稿与开发运维工作量

开始免费试用 预约演示

扫一扫关注公众号 扫一扫联系客服

©Copyrights 2016-2022 杭州易知微科技有限公司 浙ICP备2021017017号-3 浙公网安备33011002011932号

互联网信息服务业务 合字B2-20220090

400-8505-905 复制
免费试用
微信社区
易知微-数据可视化
微信扫一扫入群