react,js前端轮播图 Swipper Carousel实现无缝滚动效果

Carousel组件设计和具体代码实现

在前端中很多时候我们都需要实现图片自动播放然后图片一张一张从左往右滑动的一个效果。在组件库中我们称之为Carousel组件,我们从思路到代码来一起看看怎么封装一个属于自己的Carousel组件。

  1. 一个这样的组件首先想到的依然还是dom结构组成。
  2. 怎么保证无缝连接这个是这个组件的关键。
  3. 动画效果的实现,转场的过程中动画的衔接。

这个图大致就是我封装的时候的一个大致方向。每次移动的时候呢就是整个容器一起移动每次移动的距离每次就一个dom。
在这里插入图片描述
在可视区显示的就是当前能看到的图,超过这个可视区全部隐藏掉。在末尾我有复制了一个视图第一个元素的dom,这个就是实现无缝滚动的关键所在。当我们滑动到了最后一个视图,我们可以把整个容器的位置设置成最开始的位置,这样就可以完美实现最后一个视图和第一个视图的完美衔接了。
具体的代码逻辑就是先设置容器运动的动画正常执行,动画执行完成之后使用transition:none;然后修改transform:translate()的值。
由于没有动画效果所以这个操作对于用户是无感的。这个无缝的关键就是在动画效果取消,动画效果恢复。

Carousel代码实现逻辑
/*
 * @Date: 2023-08-23 14:06:45
 * @Auth: 463997479@qq.com
 * @LastEditors: 463997479@qq.com
 * @LastEditTime: 2023-08-29 14:26:30
 * @FilePath: \reactui\src\Carousel\index.tsx
 */
import { LeftCircleOutlined, RightCircleOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { debounce } from '../until';
type CarouselType = {
  children: any;
  autoplay?: boolean;
};

const Carousel: React.FC<CarouselType> = (props) => {
  const { children, autoplay = false } = props;
  const contentClass = classNames('carousel-content');
  const childrenRef = useRef();
  const [contentWidth, setContentWidth] = useState<number>();
  const [width, setWidth] = useState<number>(0);
  const [activeIndex, setActiveIndex] = useState<number>(0);
  const contentRef = useRef();
  const [contentStyle, setContentStyle] = useState<any>({});
  const [timer, setTimer] = useState<any>(null);
  const [flag, setFlag] = useState<boolean>(true);

  useLayoutEffect(() => {
    let index = activeIndex;
    //清除当前任务
    if (timer) {
      clearInterval(timer);
    }

    setTimer(
      setInterval(() => {
        if (flag) {
          return;
        }
        if (index <= children.length - 1) {
          index += 1;
          console.log(index);
          setContentStyle({
            transform: `translate3d(-${width * Math.abs(index)}px, 0px, 0px)`,
            transition: 'all .2s ease',
          });
          if (index === children.length) {
            index = 0;
            setTimeout(() => {
              setContentStyle({
                transform: `translate3d(-${width * index}px, 0px, 0px)`,
                transition: 'none',
              });
            }, 200);
          }

          setActiveIndex(index);
        }
      }, 2000),
    );
    return () => {
      clearInterval(timer);
    };
  }, [flag, width, activeIndex]);

  useEffect(() => {
    if (childrenRef.current) {
      const _width = childrenRef.current.offsetWidth;
      setContentWidth(_width * children.length);

      setWidth(_width);
    }
  }, []);

  const handlePre = debounce(() => {
    let index = activeIndex - 1;
    console.log(index);

    if (index === -1) {
      index = children.length - 1;
      setContentStyle({
        transform: `translate3d(-${width * (children.length + 1)}px, 0px, 0px)`,
        transition: 'none',
      });
      setTimeout(() => {
        setContentStyle({
          transform: `translate3d(-${width * index}px, 0px, 0px)`,
          transition: 'all 0.2s ease',
        });
      }, 0);
    } else {
      setContentStyle({
        transform: `translate3d(-${width * Math.abs(index)}px, 0px, 0px)`,
        transition: 'all 0.2s ease',
      });
    }

    setActiveIndex(index);
  }, 200);

  const handleNext = debounce(() => {
    let index = activeIndex + 1;
    setContentStyle({
      transform: `translate3d(-${width * index}px, 0px, 0px)`,
      transition: 'all 0.2s ease',
    });

    if (index === children.length) {
      index = 0;

      //到达末尾之后取消动画效果
      setTimeout(() => {
        setContentStyle({
          transform: `translate3d(-${width * index}px, 0px, 0px)`,
          transition: 'none',
        });
      }, 200);
    }
    setActiveIndex(index);
  }, 200);
  return (
    <>
      <div className={contentClass}>
        <div
          onMouseEnter={() => {
            if (autoplay) {
              setFlag(true);
            }
          }}
          onMouseLeave={() => {
            if (autoplay) {
              setFlag(false);
            }
          }}
          className="carousel-content-wrapper"
          ref={childrenRef}
        >
          <ul className="carousel-content-dots">
            {new Array(children.length).fill('').map((item, index) => (
              <li
                onClick={() => {
                  setContentStyle({
                    transform: `translate3d(-${width * index}px, 0px, 0px)`,
                    transition: 'all 0.2s ease',
                  });
                  setActiveIndex(index);
                }}
                className={
                  index === activeIndex ? 'carousel-content-dots-active' : ''
                }
                key={index}
              ></li>
            ))}
          </ul>
          <div className="carousel-step-content">
            <span
              onClick={() => handlePre()}
              className="carousel-step-content-left"
            >
              <LeftCircleOutlined />
            </span>

            <span
              onClick={() => handleNext()}
              className="carousel-step-content-right"
            >
              <RightCircleOutlined />
            </span>
          </div>

          <div
            style={{ ...contentStyle, width: contentWidth * 2 + 'px' }}
            className="carousel-content-list"
            ref={contentRef}
          >
           
            {React.Children.map([...children], (child) => {
              return (
                contentWidth &&
                React.cloneElement(child, {
                  className: 'carousel-content-item',
                  style: {
                    width: contentWidth / children.length + 'px',
                  },
                })
              );
            })}
            {React.Children.map([children[0]], (child) => {
              return (
                contentWidth &&
                React.cloneElement(child, {
                  className: 'carousel-content-item-clone',
                  style: {
                    width: contentWidth / children.length + 'px',
                  },
                })
              );
            })}
          </div>
        </div>
      </div>
    </>
  );
};
export default Carousel;

主要就是判断临界值在每个临界值的时候处理的逻辑有一点区别,其次就是在动画的取消还有动画的效果上面,什么时候应该滑动的时候不能加动画,什么时候有需要把动画效果加上。还有一个容器的大小还有各个子容器的大小都需要自己通过获取dom获取。

CSS
.carousel-content {
  width: 100%;
  .carousel-content-wrapper {
    width: 100%;
    height: 100%;

    position: relative;
    display: block;
    margin: 0;
    padding: 0;
    overflow: hidden;
    .carousel-content-dots {
      position: absolute;
      display: flex;
      z-index: 1;
      bottom: 10px;

      left: 50%;
      transform: translateX(-50%);

      li {
        cursor: pointer;
        position: relative;
        list-style: none;
        width: 16px;
        height: 3px;
        margin: 0 5px;
        box-sizing: border-box;
        transition: all 0.3s;
        padding: 0;
        background-color: #ffffff;
        opacity: 0.3;

        text-align: center;
        &::after {
          position: absolute;
          content: '';

          display: block;
        }
      }
      li.carousel-content-dots-active {
        width: 24px;
        opacity: 1;
      }
      li:hover {
        opacity: 0.5;
      }
    }
    .carousel-content-list {
      position: relative;
      top: 0;

      left: 0;
      display: block;
      &::after {
        display: block;
        clear: both;
        content: '';
      }
      .carousel-content-item {
        height: 100%;
        float: left;
      }
      .carousel-content-item-clone {
        height: 100%;
        float: left;
      }
    }
    .carousel-step-content {
      position: absolute;
      width: 100%;
      z-index: 2;
      top: 50%;
      transform: translateY(-50%);
      box-sizing: border-box;
      padding: 0 20px;
      font-size: 30px;

      opacity: 0;
      transition: all 0.3s;
      &::after {
        content: '';
        display: block;
        clear: both;
      }
      .carousel-step-content-left {
        color: #ffffff;
        float: left;
        opacity: 0.5;
        transition: all 0.3s;
        cursor: pointer;

        &:hover {
          opacity: 1;
        }
      }
      .carousel-step-content-right {
        color: #ffffff;
        float: right;
        opacity: 0.5;
        transition: all 0.3s;
        cursor: pointer;

        &:hover {
          opacity: 1;
        }
      }
    }
    &:hover {
      .carousel-step-content {
        opacity: 1;
      }
    }
  }
}

demo效果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值