一个头像图片滚动轮播组件(React实现)
遇到一个效果,组件库里没有现成能用的组件,于是手搓了一个,传入图片url列表,和其他配置项即可直接使用。
轮播效果
实现思路
假设共有10张图片轮流滚动,轮播图展示3张图片。给正在轮播的图片绑定visible
类,轮播过的图片绑定left
类,待轮播的下一个图片绑定right
。其中,绑定了visible
类的图片以此从左往右排开,绑定了left
类的在左侧并且隐藏,绑定了right
的在右侧并隐藏(分左右是为了实现在右侧出现、在左侧消失的动画效果)。
.visible {transform: translateX(calc(var(--index) * @image-width * (1 - var(--overlap))));z-index: calc(var(--index) + 1);opacity: 1;}.left {transform: translateX(-30px);z-index: 1;opacity: 0;}.right {transform: translateX(calc(var(--number) * @image-width * (1 - var(--overlap))));z-index: 1;opacity: 0;}
初始时,给10张图片分别绑定上不同的类,每次将这些类依次向后调整一格,最后的类挪到最前面。只有绑定了visible类的才会显示。
图片 | 图片1 | 图片2 | 图片3 | 图片4 | 图片5 | 图片6 | … | 图片10 |
---|---|---|---|---|---|---|---|---|
初始时 | visible | visible | visible | right | left | left | … | left |
第二 | left | visible | visible | visible | right | left | … | left |
第三 | left | left | visible | visible | visible | right | … | left |
代码
核心代码
新建一个组件,编写tsx代码
import { useCallback, useEffect, useRef, useState } from 'react';
import './index.less';interface ImageBannerProps {imagesData: string[];visibleNumber?: number; // 可以看见的数量overlapRate?: number; // 重叠率,默认值0.6interval?: number; // 间隔时间imageSize?: string; // 尺寸
}const ImageBanner: React.FC<ImageBannerProps> = (props) => {const { visibleNumber = 3, overlapRate = 0.3, interval = 1000, imageSize = '40px' } = props;let { imagesData } = props;// 复制到长度>=5,轮播组件最小长度要求while (imagesData.length && imagesData.length < 5) {imagesData = imagesData.concat(imagesData);}let initClassList = Array.from({ length: visibleNumber - 1 },(_, index) => `visible-${index + 1}`,);initClassList = [...initClassList, 'right', 'visible-0'];// 例如当visibleNumber=3时,初始值为["visible-1", "visible-2", "right", "visible-0"]const [classList, setClassList] = useState<string[]>(initClassList); // 每个图片的class,通过改变class更改样式const imgListRef = useRef<HTMLDivElement>(null);const initialize = useCallback((classList: string[]) => {if (imgListRef.current) {const imgList = Array.from(imgListRef.current.children);for (let i = 0; i < imgList.length; i++) {if (classList[i].includes('visible')) {const match = classList[i].match(/visible-(\d+)/); // 提取数字imgList[i].className = 'imgBannerItem visible';if (match) {(imgList[i] as HTMLDivElement).style.setProperty('--index', match[1]);}} else {imgList[i].className = 'imgBannerItem ' + classList[i];}}}}, []);const next = useCallback(() => {setClassList((prev) => {const newClassList = [...prev];newClassList.unshift(newClassList.pop()!);initialize(newClassList);return newClassList;});}, [initialize]);useEffect(() => {if (imagesData.length < 1) {return;}let timer;if (imgListRef.current) {const imgList = imgListRef.current.children;for (let i = 0; i < visibleNumber - 1; i++) {imgList[i].className = 'imgBannerItem ' + classList[i];}imgList[visibleNumber].className = 'imgBannerItem ' + classList[visibleNumber];const fillLeft = Array(imagesData.length - visibleNumber - 1).fill('left'); // 填入class为leftconst newClassList = [...classList.slice(0, visibleNumber),...fillLeft,...classList.slice(visibleNumber),];setClassList(newClassList);initialize(newClassList);timer = setInterval(next, interval);}return () => {if (timer) {clearInterval(timer);}};}, []);if (imagesData.length < 1) {return null;}return (<divclassName="imgBannerContainer"ref={imgListRef}style={{'--number': visibleNumber,'--overlap': overlapRate,'--size': imageSize,} as React.CSSProperties}>{imagesData.map((item, idx) => (<img className="imgBannerItem" src={item} alt="" key={idx} />))}</div>);
};export default ImageBanner;
样式代码
less代码
@image-width: var(--size);
@image-height: var(--size);.imgBannerContainer {width: 100%;height: 100%;position: relative;.visible {transform: translateX(calc(var(--index) * @image-width * (1 - var(--overlap))));z-index: calc(var(--index) + 1);opacity: 1;}.left {transform: translateX(-30px);z-index: 1;opacity: 0;}.right {transform: translateX(calc(var(--number) * @image-width * (1 - var(--overlap))));z-index: 1;opacity: 0;}
}.imgBannerItem {width: @image-width;height: @image-height;position: absolute;top: 0;transition: 0.3s;border-radius: 50%;border: 2px solid #fff;
}
使用
引入组件,传入图片列表进行使用
import ImageBanner from "../ImageBanner";const urlList = ["url1","url2","url3","url4","url5",
];const Index = () => {return ( <><div style={{width: 300, height: 40, marginLeft: 20}}><ImageBanner imagesData={urlList} /></div></>);
}export default Index;