仿小红书轮播图实现(pc版)

最近做pc的轮播需求时,遇到了一个问题,指示点太多了,可能会超出每个轮播的宽度,于是便参考小红书轮播图的指示点效果,实现了pc版的轮播图,最多显示五个指示点,超出则移动展示。

首先实现指示点的样式,有三种情况:

  • 选中的点索引小于3时,是不需要移动的,第五个指示点开始,都是小号的指示点,条件如下:
selectedIndex < 3 && index >= 4
  • 选中的点索引大于等于3并且小于倒数第三个点的索引时,是需要进行移动的,这个时候,只有中间的三个点是大号的指示点,其它的都是小号的指示点,条件如下:
selectedIndex >= 3 &&
selectedIndex < dotTotal - 3 &&
Math.abs(index - selectedIndex) >= 2
  • 选中的点大于等于倒数第三个点的索引时,是不需要移动的,小于等于倒数第五个点的索引的指示点都是小号的,条件如下:
selectedIndex >= dotTotal - 3 && index <= dotTotal - 5

然后来处理位移:

只有指示点大于5时,才存在需要移动的情况。这种时候,又需要分为两种情况:

  • 当选中的指示点的索引大于当前指示点的索引时,需要往左移,左移的情况又需要分为两种:

    • 选中的指示点的索引小于等于2时,是不需要移动的
      if (index <= 2) {
        offset = 0;
      }
      
    • 选中的指示点的索引大于2时,才需要左移,需要计算将其移动到最中间位置所需要的位移
      if (index > 2) {
          offset = (index - middleIndex) * 16
      }
      
  • 当选中的指示点的索引小于当前指示点的索引时,需要往右移动,右移的情况也需要分为两种:

    • 选中的指示点的索引大于等于倒数第三个点的索引时,不需要移动

      if (index >= dotTotal - 3) {
          offset = 0
      }
      
    • 选中的指示点的索引小于倒数第三个点的索引时,才需要往右移动,需要计算将其移动到最中间位置所需要的位移

      if (index < dotTotal - 3) {
          offset = (index - middleIndex) * 24;
      }
      

上述中我们都需要计算将选中的指示点移动到最中间所需要的位移,所以每次移动,我们都需要记录最中间指示点的索引:

// 索引小于2时,2始终是最中间的指示点
if (index < 2) {
setMiddleIndex(2);
} else if (index >= 2 && index <= dotTotal - 3) {
// 选中的点需要移动到最中间,即它的索引为最中间的索引
setMiddleIndex(index);
} else {
// 索引大于倒数第三个点时,倒数第三个点始终是最中间的指示点
setMiddleIndex(dotTotal - 3);
}

完整代码如下:

App.jsx

import { useEffect, useRef, useState } from "react";
import { Carousel } from "antd";
import "./App.css";

export default function CustomCarousel() {
  const [dataList, setDataList] = useState([]);
  // 指示点的个数
  const [dotTotal, setDotTotal] = useState(0);
  // 当前选中的指示点的索引
  const [selectedIndex, setSelectedIndex] = useState(0);
  // 位于最中间的指示点的索引
  const [middleIndex, setMiddleIndex] = useState(0);

  const dotRef = useRef(null);
  const carouselRef = useRef(null);

  useEffect(() => {
    const dataList = [];
    for (let i = 0; i < 10; i++) {
      dataList.push(i);
    }
    setDataList(dataList);
    setDotTotal(dataList.length);
    if (dataList.length > 5) {
      setMiddleIndex(2);
    }
  }, []);

  const handleSlide = (index) => {
    // 当前选中的指示点和之前选中的指示点相同,则不做任何操作,返回
    if (index === selectedIndex) return;
    // 指示点偏移位置
    let offset = 24;
    // 只有大于5的时候,指示点才需要移动
    if (dotTotal > 5) {
      if (index > selectedIndex) {
        if (index <= 2) {
          offset = 0;
        } else {
          offset = (index - middleIndex) * 24;
        }
      } else {
        if (index < dotTotal - 3) {
          offset = (index - middleIndex) * 24;
        } else {
          offset = 0;
        }
      }

      if (index < 2) {
        setMiddleIndex(2);
      } else if (index >= 2 && index <= dotTotal - 3) {
        setMiddleIndex(index);
      } else {
        setMiddleIndex(dotTotal - 3);
      }

      // 设置平缓滑动动画
      const duration = 1000;
      const frames = (duration / 1000) * 24;
      let frame = 0; 
      const animate = () => {
        if (!dotRef.current) return;
        dotRef.current.scrollLeft += offset / frames;
        frame += 1;
        if (frame < frames) {
          window.requestAnimationFrame(animate);
        }
      };

      window.requestAnimationFrame(animate);
    }

    carouselRef.current?.goTo(index);
    setSelectedIndex(index);
  };

  return (
    <div className="listBox">
      <Carousel ref={carouselRef} dots={false}>
        {dataList.map((item) => (
          <div className="listItem">{item}</div>
        ))}
      </Carousel>
      <div className="dotWrapper">
        <div className="dotList" ref={dotRef}>
          {new Array(dotTotal).fill(" ").map((_, index) => {
            return (
              <div
                className={`dot
                       ${index === selectedIndex && "selectedDot"}
                       ${
                         dotTotal > 5 &&
                         ((selectedIndex < 3 && index >= 4) ||
                           (selectedIndex >= 3 &&
                             selectedIndex < dotTotal - 3 &&
                             Math.abs(index - selectedIndex) >= 2) ||
                           (selectedIndex >= dotTotal - 3 &&
                             index <= dotTotal - 5)) &&
                         "smallDot"
                       }`}
                key={index}
                onClick={() => {
                  handleSlide(index);
                }}
              >
                {index}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

App.css

@import '~antd/dist/reset.css';

.listBox{
  width: 300px;
  margin: 100px auto;
}
.listItem{
  width: 100%;
  height: 100px;
  border: 1px solid #ccc;
}
.dotWrapper{
  width: 100%;
  display: flex;
  justify-content: center;
}
.dotList{
  width: 108px;
  overflow: hidden;
  white-space: nowrap;
}
.dot {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 12px;
  background-color: #767684;
  margin-right: 12px;
  font-size: 12px;
  text-align: center;
  line-height: 12px;
  transition: all .2s ease;
  &:last-child {
    margin-right: 0;
  }
}
.selectedDot {
  background: #586ee0;
}
.smallDot {
  transform: scale(0.8);
}
posted @ 2023-07-11 15:43  SummerSatr  阅读(172)  评论(0编辑  收藏  举报