react实现4种轮播效果

4,751 阅读5分钟

完整demo 链接:github.com/webfamer/re…

轮播效果一

轮播1.gif

实现思路:主要使用CSSTransition 实现淡入淡出效果,效果变换的同时更新一下列表的数据,从而表现出轮播图的效果

主要代码如下:

animatiom.css

//定义淡入淡出效果供cssTransiton组件使用
.tableScale-enter {
  opacity: 0;
  transform: scale(0.9);
}
.tableScale-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: opacity 300ms, transform 300ms;
}
.tableScale-exit {
  opacity: 1;
}
.tableScale-exit-active {
  opacity: 0;
  transform: scale(0.9);
  transition: opacity 300ms, transform 300ms;
}

index.tsx

import { waitTime } from '@/utils/utils';
import './animate.less';

//自定义假数据
const personInfoData = [{
  subjectName: '课目一',
  finalTime: '11:10.12',
  violationTime: 2,
  tRank: 1,
},
{
  subjectName: '课目二',
  finalTime: '12:11.12',
  violationTime: 3,
  tRank: 2,
},
{
  subjectName: '课目三',
  finalTime: '13:11.10',
  violationTime: 0,
  tRank: 3,
}]

const PersonDetail = () => {
  const [visible, setvisible] = useState(true); //触发cssTransiotion动画效果
  const [subjectInfo, setSubjectInfo] = useState<IsubjectResponseList>({}); //存储单条展示的数据

  //最主要的函数 控制淡入淡出并且更换显示数据
  const groupChangeTimer = () => {
    let count = 1;
    //当数据只有1条时不轮播
    if (personInfoData.length === 1) return;
    let timer = setInterval(async () => {
      if (personInfoData.length > 0 &&count < personInfoData.length) 
      {
        setSubjectInfo(personInfoData[count]); //更新展示的数据
      }
      setvisible(false);
      await waitTime(1000); //延迟函数,淡入后延迟1s淡出
      setvisible(true);
      count = count >= personInfoData.length - 1 ? 0 : count + 1;
    }, 5000); //5s轮换一次
    return timer;
  };

  useEffect(() => {
    let timer = groupChangeTimer();
    return () => {
      clearInterval(timer);
    };
  }, []);


  return (
    <div>
      <CSSTransition
        in={visible} //显示隐藏控制的变量
        timeout={300}
        classNames="tableScale" //对应animate.css的类名
        unmountOnExit
      >
        <div className={styles.tableContent}>
          <div className={styles.tableItem}>
            <label>课目用时</label>
            <span>{subjectInfo?.finalTime}</span>
          </div>
          <div className={styles.tableItem}>
            <label>判罚次数</label>
            <span>{subjectInfo?.violationTime}</span>
          </div>
          <div className={styles.tableItem}>
            <label>排名</label>
            <span>{subjectInfo.tRank}</span>
          </div>
        </div>
      </CSSTransition>
    </div>
  )
}

utils.ts

export const waitTime = (time: number = 500) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(true);
    }, time);
  });
};

轮播效果二

轮播2.gif

实现思路:

不间断滚动思路:准备两个div,里面填充一样的内容,当滚动的距离等于第一个盒子的高度(offsetHeight,与height的区别是多了border)时,重置回0,从头开始滚动。

定时停止重复滚动实现:当盒子向上滚动的距离达到需要的距离后,停止定时器,隔5s后又重新启动定时器,实现了该效果。

代码如下:

index.less

.tableContent {
  height: 712px;
  overflow: hidden;

  .table {
    .tableItem {
      height: 89px;
      background: rgba(79, 173, 223, 0.14);
    }
  }
}

.hiddenTableItem { //把填充的项设置成透明
  opacity: 0;
}

data.ts

//自定义假数据
export const rankData = [{
  delegation: '代表队1',
  tRank: 1,
  staffName: '队员1',
  score: 98,
  departmentName: '杭州代表队'
}, {
  delegation: '代表队2',
  tRank: 1,
  staffName: '队员1',
  score: 96,
  departmentName: '温州代表队'
}, {
  delegation: '代表队3',
  tRank: 1,
  staffName: '队员1',
  score: 94,
  departmentName: '嘉兴代表队'
}, {
  delegation: '代表队4',
  tRank: 1,
  staffName: '队员1',
  score: 86,
  departmentName: '湖州代表队'
}, {
  delegation: '代表队5',
  tRank: 1,
  staffName: '队员1',
  score: 84,
  departmentName: '衢州代表队'
}, {
  delegation: '代表队6',
  tRank: 1,
  staffName: '队员1',
  score: 82,
  departmentName: '绍兴代表队'
}, {
  delegation: '代表队7',
  tRank: 1,
  staffName: '队员1',
  score: 80,
  departmentName: '金华代表队'
}, {
  delegation: '代表队8',
  tRank: 1,
  staffName: '队员1',
  score: 70,
  departmentName: '丽水代表队'
}, {
  delegation: '代表队9',
  tRank: 1,
  staffName: '队员1',
  score: 66,
  departmentName: '义乌代表队'
}, {
  delegation: '代表队10',
  tRank: 1,
  staffName: '队员1',
  score: 64,
  departmentName: '嘉兴代表队'
}, {
  delegation: '代表队11',
  tRank: 1,
  staffName: '队员1',
  score: 62,
  departmentName: '诸暨代表队'
}, {
  delegation: '代表队12',
  tRank: 1,
  staffName: '队员1',
  score: 60,
  departmentName: '湖州代表队'
}]

index.tsx

import { rankData } from './data'
import styles from './index.less';
import { useEffect, useState, useRef } from 'react';

const PersonRank = () => {
  const [listData, setListData] = useState<IallRank[]>([]); //存储列表数据
  const tableDiv = useRef<HTMLDivElement>(null); //盒子1
  const tableDiv2 = useRef<HTMLDivElement>(null); //盒子2
  const tableContainer = useRef<HTMLDivElement>(null); //主盒子
  const timerRef = useRef<NodeJS.Timer | null>(null); //存储定时器

  /**处理数据,补全成8的倍数**/
  useEffect(() => {
    if (rankData) {
      //这里对假数据进行处理,因为我需要8条数据一版,所以我处理成8的倍数
      const personAllRank = rankData;
      if (personAllRank?.length > 0) {
        if (personAllRank.length % 8 !== 0) {
          let restNum = 8 - (personAllRank.length % 8);
          for (let i = 0; i < restNum; i++) {
            personAllRank.push({
              delegation: '',
              tRank: '',
              score: '',
              departmentName: '',
              staffName: '',
            });
          }
        }
        setListData(rankData);
      }
    }
  }, [rankData]);
/**滚动函数**/
  const startScroll = () => {
    let count = 0;
    let value = 0;
    //把第一个盒子的内容复制给第二个盒子,方便实现无缝滚动
    setTimeout(() => {
      if (tableDiv.current && tableDiv2.current)
        tableDiv2.current.innerHTML = tableDiv.current.innerHTML;
    }, 1000);

    function scrollTable() { //最主要的滚动函数
      timerRef.current = setInterval(() => {
        count -= 4; //4像素滚动
        if (tableContainer.current && tableDiv.current) {
          tableContainer.current.style.transform = `translateY(${count}px)`;
          value = Number(
            //正则匹配获取translateY的值
            tableContainer.current.style.transform.match(/-?[0-9]+/g),
          );
          //向上滚动的距离等于第一个盒子高度时,重置
          if (parseInt(String(value)) <= -tableDiv.current?.offsetHeight) {
            count = 0;
          }
        }
        //这里设置的8条数据滚动一版,8条数据的高度正好是712
        //8条数据滚完时,暂停5s,然后继续滚动
        if (parseInt(String(value)) % 712 === 0 && timerRef.current) {
          clearInterval(timerRef.current);
          setTimeout(() => {
            scrollTable();
          }, 5000);
        }
      }, 10);
    }
    scrollTable();
    //存储一下定时器,方便其他地方调用
    return timerRef.current;
  };
/**初始化时开始滚动函数**/
  useEffect(() => {
    let timer = setTimeout(() => {
      startScroll();
    }, 5000);
    return () => {
      clearInterval(timer);
    };
  }, [tableDiv.current]);
  return (
    <div className={styles.personRank}>
      <div className={styles.tableTitle}>
        <span>排名</span>
        <span>姓名</span>
        <span style={{ width: 300 }}>所属单位</span>
        <span style={{ marginLeft: 30, width: 164 }}>总分</span>
        <span>荣誉</span>
      </div>
      <div className={styles.tableContent}>
        <div ref={tableContainer}>
          <div
            className={styles.table}
            ref={tableDiv}
          >
            //补全的数据是空数据,这里判断没有delegation这个字段就设置成透明
            {listData.map((item, index) => {
              return (
                <div
                  className={`${styles.tableItem} ${item.delegation === '' ? styles.hiddenTableItem : ''
                    }`}
                  key={index}
                >
                  <span>{item.tRank}</span>
                  <span>{item.staffName}</span>
                  <span style={{ width: 300 }}>{item.departmentName}</span>
                  <span style={{ marginLeft: 30, width: 164 }}>
                    {item.score}
                  </span>
                </div>
              );
            })}
          </div>
          <div className={styles.table} ref={tableDiv2}></div>
        </div>
      </div>
    </div>
  );
}

轮播效果三

轮播3.gif

实现思路:给列表每一项都设置上动画,动画内容为从右边进入,从左边退出,并且动画结束时,保留结束时候的状态,这时我们在把原来数组项数据,一条一条替换,替换的时候加一点延迟,就能实现这种效果。

代码如下:

index.css

@keyframes fadeInOut {
  0% {
    transform: translateX(100%);
  }

  10% {
    transform: translateX(0);
  }
  90% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(-100%);
  }
}
.tableContent {
    height: 712px;
    overflow: hidden;
    .table {
      .tableItem {
        height: 89px;
        background: rgba(79, 173, 223, 0.14);
        border: 1px solid #173048;
      }
      .fadeInOut {
        animation: fadeInOut ease-in-out 10s;
        animation-fill-mode: forwards;
      }
      .hideTable {
        opacity: 0;
      }
    }
}

index.tsx

import styles from './index.less';
import { getGroupingData } from '@/utils/utils';
import { defalutData } from './data' //导入默认数据

const PersonRank = () => {
  const [storeData, setStoreData] = useState([]); //列表数据
  const listGroupData = useRef([[]]); //8个一组把数据分组
  const timerRef = useRef<Nodejs.Timer>(null);
  const Firstflag = useRef<boolean>(false); //首次加载判断

  //把数据划分成8个一组
  useEffect(() => {
    const filterData = defalutData.filter(
      (item) => item.score,
    );
    if (filterData.length % 8 !== 0) {
      let restNum = 8 - (filterData.length % 8);
      for (let i = 0; i < restNum; i++) {
        filterData.push({
          staffName: '',
          departmentName: '',
          tRank: '',
          score: '',
          staffId: String(i),
        });
      }
    }
    //存储划分后的分组
    const tournameGroupData = getGroupingData(filterData, 8);
    listGroupData.current = tournameGroupData;
  }, []);
  //首次加载后标记一下,反正多个定时器,导致动画效果混乱
  useEffect(() => {
    Firstflag.current = true;
  }, []);

  //开始进行滚动轮播
  useEffect(() => {
    reduceGroupData(listGroupData.current);
  }, []);
  const startAnimation = (data: IhitResponseList[]) => {
    let count = 0;
    if (data && data.length > 0) {
      //每0.25s更新一条数据
      let timer = setInterval(async () => {
        setStoreData((val) => {
          let newVal = JSON.parse(JSON.stringify(val));
          if (data[count]) {
            newVal[count] = data[count];
          }
          return newVal;
        });
        count++;
        if (count > 7) {
          //当数据大于8条时停止
          clearInterval(timer);
          count === 0;
        }
      }, 250);
    }
  };
  const reduceGroupData = (groupArr: IhitResponseList[][]) => {
    let count = 0;
    if (groupArr.length > 0) {
      //第一次加载的时候进行一下动画
      if (Firstflag.current) {
        startAnimation(groupArr[count]);
      }
      //如果数据只有一组,就不用进行数据源切换
      if (groupArr.length === 1) return;
      if (timerRef.current) {
        clearInterval(timerRef.current); //每次进来先清除一下定时器,防止多个定时器影响
      }
      //每10s替换一下列表数据源
      timerRef.current = setInterval(() => {
        count = count === groupArr.length - 1 ? 0 : count + 1;
        startAnimation(groupArr[count]);
      }, 10000);
    }
  return (
    <div className={styles.personRank}>
      <div className={styles.tableTitle}>
        <span>排名</span>
        <span>姓名</span>
        <span style={{ width: 300 }}>所属单位</span>
        <span>得分</span>
      </div>
      <div className={styles.tableContent}>
        <div>
          <div className={styles.table}>
            {storeData.map((item, index) => {
              return (
                //补全的空数据条数直接设置成透明隐藏
                <div
                  className={`${styles.tableItem} ${item.staffName ? '' : styles.hideTable
                    } ${styles.fadeInOut}`}
                  key={item?.staffId}
                >
                  <span className={styles.rankLogoBg}>{item?.tRank}</span>
                  <span>{item.staffName}</span>
                  <span style={{ width: 300 }}>{item.departmentName}</span>
                  <span>{item.score}</span>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
  };
}

data.ts

export const defalutData = [{
    delegation: '代表队1',
    tRank: 1,
    staffName: '队员1',
    score: 98,
    staffId: '100',
    departmentName: '杭州代表队'
  }, {
    delegation: '代表队2',
    tRank: 2,
    staffName: '队员2',
    score: 96,
    staffId: '101',
    departmentName: '温州代表队'
  }, {
    delegation: '代表队3',
    tRank: 3,
    staffName: '队员3',
    score: 94,
    staffId: '102',
    departmentName: '嘉兴代表队'
  }, {
    delegation: '代表队4',
    tRank: 4,
    staffName: '队员4',
    score: 86,
    staffId: '103',
    departmentName: '湖州代表队'
  }, {
    delegation: '代表队5',
    tRank: 5,
    staffName: '队员5',
    score: 84,
    staffId: '104',
    departmentName: '衢州代表队'
  }, {
    delegation: '代表队6',
    tRank: 6,
    staffName: '队员6',
    score: 82,
    staffId: '105',
    departmentName: '绍兴代表队'
  }, {
    delegation: '代表队7',
    tRank: 7,
    staffName: '队员7',
    score: 80,
    staffId: '106',
    departmentName: '金华代表队'
  }, {
    delegation: '代表队8',
    tRank: 8,
    staffName: '队员8',
    score: 70,
    staffId: '107',
    departmentName: '丽水代表队'
  }, {
    delegation: '代表队9',
    tRank: 9,
    staffName: '队员9',
    score: 66,
    staffId: '108',
    departmentName: '义乌代表队'
  }, {
    delegation: '代表队10',
    tRank: 10,
    staffName: '队员11',
    score: 64,
    staffId: '109',
    departmentName: '嘉兴代表队'
  }, {
    delegation: '代表队11',
    tRank: 11,
    staffName: '队员12',
    score: 62,
    staffId: '110',
    departmentName: '诸暨代表队'
  }, {
    delegation: '代表队12',
    tRank: 12,
    staffName: '队员13',
    score: 60,
    staffId: '111',
    departmentName: '湖州代表队'
  }]

utils.ts

//将数组划分为n个一组
export const getGroupingData = (data: any[], val: number) => {
  let groupData: any[] = [];
  for (let i = 0; i < data.length; i += Number(val)) {
    const newData = data.slice(i, i + Number(val));
    groupData.push(newData);
  }
  return groupData;
};

轮播效果四

实现思路:

轮播实现:给div动态的加上样式,让其左中右排列,然后改变样式,实现轮播

无缝轮播:将第0份复制一下放到最后,然后当从最后一份切换到第0份到时候,马上让他跳到第1份(从最后1分切换到第0份第时候,取消过渡效果)

轮播4.gif 代码如下:

index.less

  .flexCenter {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: 120px;
  }

  .transiton {
    transition: all 1s ease-in-out;
  }

  .center {
    position: relative;
    width: 100%;
    left: 0;
  }

  .right {
    position: absolute;
    left: 100%;
    width: 100%;
    top: 0;
  }

  .left {
    position: absolute;
    left: -100%;
    width: 100%;
    top: 0;
  }

index.tsx

import {personData} from './data' //引入默认数据
import styles from './index.less';

const Person = () => {
  //列表数据
    const [staffInfo, setStaffInfo] = useState<IstaffInfoResponses[]>([
    {
      teamName: '',
      staffInfoList: [],
    },
  ]);
   const [step, setStep] = useState(0); //控制div样式
  	//样式判断函数
    const getClassName = (index: number) => {
    if (index <= step - 1) {
      return styles.left;
    } else if (index === step) {
      return styles.center;
    } else if (index >= step + 1) {
      return styles.right;
    }
  };
  //轮播函数
    const startSwiper = () => {
    let count = 1;
    let timer = setInterval(async () => {
      console.log(count,'切换count')
      setStep(count);
      //无缝切换,缩短0-1的切换间隔
      if (count === 0) {
        count = 1;
        setTimeout(() => {
          setStep(1);
        }, 500);
      }
      //到最后一个到时候切换到第0个
      count = count > personData.length - 1 ? 0 : count + 1; 
    }, 3000);
    return timer;
  };
	//启动切换函数
  useEffect(() => {
    let timer = startSwiper();
    return () => {
      clearInterval(timer);
    };
  }, []);
  //对数据进行处理,把第一份复制到最后
 useEffect(() => {
    setStaffInfo([...personData,personData[0]])
  }, []);
  //根据数据结构渲染div
    return (
    <div className={`page-container ${styles.personBg}`}>
      {staffInfo.map((staffInfoItem, index) => {
        return (
          <div
            dataSet={index}

            //当step ==0,也就是从最后一个刚切换到第0个这一过程,不添加过渡函数
            className={`${getClassName(index)} ${styles.flexCenter} ${
              step === 0 ? '' : styles.transiton
            }`}
          >
            <div className={styles.title}>
              <p>{staffInfoItem?.teamName}</p>
              <img src={line} alt="" />
            </div>
            <div className={styles.personContent}>
              {staffInfoItem?.staffInfoList?.map((item) => {
                return (
                  <div
                    className={`${styles.personBox} ${
                      staffInfoItem?.staffInfoList?.length > 8
                        ? styles.ninePersonBox
                        : styles.eightPersonBox
                    }`}
                  >
                    <div>
                      <img
                        src={`${item.picture ? item.picture : headLogo}`}
                        alt=""
                        style={{ objectFit: 'contain' }}
                      />
                    </div>
                    <div className={styles.info}>
                      <div className={styles.infoItem}>
                        <span className={styles.point}></span>
                        <span>
                          {item?.staffName}&emsp;{item?.age}岁
                        </span>
                      </div>
                      <div className={styles.infoItem}>
                        <span className={styles.point}></span>
                        <span>{item?.departmentName}</span>
                      </div>
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
        );
      })}
    </div>
  );
}
export default Person;