import React, {
useEffect,
useLayoutEffect,
useRef,
useState,
useMemo,
} from "react";
interface CarouselProps {
dataSource: Array<React.ReactNode>;
width: string;
height: string;
controllerPosition?: "top" | "left" | "bottom" | "right";
needAuto?: boolean;
}
const DEFAULT_HEIGHT = "240px";
const DEFAULT_WIDTH = "400px";
const concatDataSource = <T,>(dataSource: Array<T>) => {
const concattedDataSource = [
dataSource[dataSource.length - 1],
...dataSource,
dataSource[0],
];
return concattedDataSource;
};
export const Carousel: React.FC<CarouselProps> = ({
dataSource,
controllerPosition = "bottom",
width = DEFAULT_WIDTH,
height = DEFAULT_HEIGHT,
needAuto = false,
}) => {
const countOfPics = dataSource.length;
const totalWidth = (countOfPics + 2) * parseInt(width);
const [curIndex, updateCurIndex] = useState(0);
const intervalRef = useRef<NodeJS.Timer>();
// 这里主要用于重置动画
// 小把戏,但是不能用useState,因为dispatch函数会被react做合并
// 反正每次都要rerender,正好就用ref
const isLastPicRef = useRef(false);
const isFirstMountRef = useRef(false);
const concattedDataSource = useMemo(() => {
return concatDataSource(dataSource);
}, [countOfPics]);
useEffect(() => {
startAuto();
isFirstMountRef.current = true;
return () => {
resetInterval();
};
}, []);
useLayoutEffect(() => {
if (curIndex === countOfPics) {
// 先切换到concat上,然后弄个小把戏切换回第一张
isLastPicRef.current = true;
setTimeout(() => {
updateCurIndex(0);
}, 300);
}
if (curIndex === 0 && isFirstMountRef.current) {
setTimeout(() => {
// 已经切换回第一张了,改这个东西来重置动画状态
isLastPicRef.current = false;
}, 0);
}
}, [curIndex]);
const resetInterval = () => {
intervalRef.current && clearInterval(intervalRef.current);
};
const startAuto = () => {
needAuto &&
(intervalRef.current = setInterval(() => {
updateCurIndex((prev) => {
return prev + 1;
});
}, 2000));
};
return (
<div
className="carousel-wrapper"
style={{ width, height, overflow: "hidden", position: "relative" }}
>
<div
className="carousel-scroller"
style={{
height,
width: totalWidth,
position: "absolute",
left: `${(curIndex + 1) * -parseInt(width)}px`,
transition: isLastPicRef.current ? "none" : "left 0.2s ease-in",
overflow: "hidden",
}}
>
{concattedDataSource}
</div>
<CarouselController
position={controllerPosition}
height={parseInt(height)}
width={parseInt(width)}
count={countOfPics}
curIndex={curIndex}
updateCurIndex={updateCurIndex}
resetAutoInterval={resetInterval}
startAuto={startAuto}
/>
</div>
);
};
interface CarouselControllerProps {
height: number;
width: number;
position: "top" | "left" | "bottom" | "right";
count: number;
curIndex: number;
updateCurIndex: (index: number) => void;
resetAutoInterval: () => void;
startAuto: () => void;
}
const CarouselController: React.FC<CarouselControllerProps> = ({
width,
count,
curIndex,
updateCurIndex,
resetAutoInterval,
startAuto,
}) => {
const controllerWidth = width * 0.6;
return (
<div
style={{
width: `${controllerWidth}px`,
height: "10px",
position: "absolute",
bottom: "12px",
left: `${(width - controllerWidth) / 2}px`,
display: "flex",
gap: "4px",
justifyContent: "center",
}}
>
{new Array(count).fill(null).map((_, i) => (
<CarouselControllerDot
key={i}
id={i}
count={count}
updateCurIndex={updateCurIndex}
curIndex={curIndex}
resetAutoInterval={resetAutoInterval}
startAuto={startAuto}
/>
))}
</div>
);
};
const CarouselControllerDot: React.FC<{
id: number;
count: number;
updateCurIndex: (index: number) => void;
curIndex: number;
resetAutoInterval: () => void;
startAuto: () => void;
}> = ({
id,
updateCurIndex,
curIndex,
resetAutoInterval,
startAuto,
count,
}) => {
return (
<div
style={{
width: id === curIndex % count ? "25px" : "10px",
height: "5px",
backgroundColor: "#fff",
opacity: id === curIndex % count ? 1 : "0.3",
cursor: "pointer",
transition: "width 0.2s ease-in",
borderRadius: "2px",
}}
onClick={() => {
updateCurIndex(id);
resetAutoInterval();
startAuto();
}}
/>
);
};
记录一下,下一次用自己的轮子