Carousel

147 阅读1分钟
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();
      }}
    />
  );
};

记录一下,下一次用自己的轮子