JS实现无限轮播

3,773 阅读3分钟

背景:

产品小姐姐提的一个需求,希望在顶部的banner实现商品图的无限轮播,每次轮播只展示三张图片,每张图片停留1.5s后滑动,小于4张则不轮播。

需求拆分

接下来就是拆分需求了,根据小姐姐的需求,这次轮播需要实现三个功能点:

  1. 图片滚动,且每张图片停留1.5s。
  2. 无限轮播。所谓无限轮播,也就是最后一张展示完之后,要继续从第一张展示,就像一个环形一样(头咬尾)
  3. 每次只展示三张照片,小于三张则不滚动。

功能实现

拆分需求之后就好说了,接下来就是一个个来实现了

图片滚动

  • 首先,JS要控制元素的移动,则需要通过控制style属性来实现。在这里,我是通过控制marginLeft值。
  • 其次,要想实现平滑式的滚动,则需要设置transition来让用户感知滚动的过程,而不是立即跳转。
# 获取需要滚动的元素
const pages = document.querySelector<HTMLElement>('.carousel-contents');
# 设置marginLeft、transition来实现平滑滚动
pages.style.marginLeft = `${nowPositionLeft}px`;
pages.style.transition = 'margin-left 0.7s linear';
  • 然后,每次滚动都只滑动一张图片的距离,并且保证图片要停留展示1.5s后再滑动。这里就需要定时器来帮我们解决了,我们只需要一个1.5s的setTimeout,每隔1.5s后再执行下一张图片的滚动(这里可以用一个函数封装下)。
const PAGE_WIDTH = 23; // 每一张图片的宽度
const TRANSLATION_DELAY = 1500;  // 轮播每一张图片停留的时长
const translatePage = () => {
    const pages = document.querySelector<HTMLElement>('.carousel-contents'); // 获取需要滚动的元素
    let nowPositionLeft = 0;  // 初始化当前滚动元素的marginLeft
    const run = () => {
        setTimeout(() => {
            # 设置marginLeft、transition来实现平滑滚动
            pages.style.marginLeft = `${nowPositionLeft}px`;
            pages.style.transition = 'margin-left 0.7s linear';
            run(); // 持续滚动
        }, TRANSLATION_DELAY);
    }
    run(); // 执行滚动
}
    

无限轮播

以上都不难想到,真正的问题在于当元素滚动到最后一张图片时(这里举例向左👈滚动),怎么又切到第一张图片,也就是实现无限轮播 你肯定能想到,当滚动最后一张图片时,通过设置marginLeft: 0px,又可以重新回到第一张,这确实是解决的办法,我们来看效果;

屏幕录制2021-06-28 下午10.gif

当从最后一张滚回到第一张的时候,用户很明显的看到元素“滚回去”的过程,即向右滑动的过程,这里是我们所不能接受的,我们希望的是能够始终向一个方向滚动。

细心分析的话可以发现,原因在于我们设置marginLeft: 0px的时候,transition依然保持着‘0.7s linear’的动画效果,此时如果我们将transition: 'none',则去掉了动画,瞬间回到第一张,也就让用户感知不到往回滑动的过程。但此时瞬间回到第一张的实现,也没办法让用户感知从4——>1的滑动过程,也是不符合要求的。

想来想去,怎么都不能实现4——>1的平滑滚动,那为什么不在4后面加个1,模拟4——>1的滑动。考虑视口需要展示三张图片,那就需要在4后面加1和2(同理可推,如果只是展示两张照片,只需要在4后面多加一个就好)。既然在后面加了,前面肯定也是要加的,才能切换回去,因此在滚动元素的前面加上最后一张,即4。当4滚动到视口的第一张图片位置时,瞬间切换到元素的头部(用户感知不到什么变化),再接着滚动,这样解决元素瞬间回到第一张,没有持续向一个方向滚动了。

const PAGE_WIDTH = 23; // 每一张图片的宽度
const TRANSLATION_DELAY = 1500;  // 轮播每一张图片停留的时长
const translatePage = () => {
    const pages = document.querySelector<HTMLElement>('.carousel-contents'); // 获取需要滚动的元素
    let nowPositionLeft = 0;  // 初始化当前滚动元素的marginLeft
    const lastChild = pages.lastElementChild;
    const firstChild = pages.firstElementChild;
    const secondChild = pages.childNodes[1];
    // 在 pages 容器的首尾分别添加第一个和最后两个
    lastChild && pages.insertBefore(lastChild.cloneNode(true), firstChild);
    firstChild && pages.appendChild(firstChild.cloneNode(true));
    secondChild && pages.appendChild(secondChild.cloneNode(true));
    const run = () => {
        setTimeout(() => {
            # 设置marginLeft、transition来实现平滑滚动
            if (nowPositionLeft + ((pages.childNodes.length - 3) * PAGE_WIDTH) <= 0) {
                nowPositionLeft = 0;
                pages.style.transition = 'none';  // 去掉动画,让用户感知不到滚动回至最左端
                pages.style.marginLeft = `${nowPositionLeft}px`;
            } else {
                nowPositionLeft = nowPositionLeft - PAGE_WIDTH;
                pages.style.transition = 'margin-left 0.7s linear';
                pages.style.marginLeft = `${nowPositionLeft}px`;
            }
            run();
        }, TRANSLATION_DELAY);
    }
    run(); // 执行滚动
}

走到这里,差不多已经能够实现无限滚动了,但是细致的小伙伴一定能发现,这样实现存在一个问题,你会发现4、1、2在视口停留2个TRANSLATION_DELAY的时长,那是应为当瞬间切换到头部时(这里用户感知不到,以为仍然在4、1、2),定时器还是保持着TRANSLATION_DELAY计时,即等待TRANSLATION_DELAY后再进行下一步操作。因此这里需要优化下,即切换到头部位置时,定时器间隔为0,如下

image.png

图片个数不够时限制滚动

综合以上三个功能点,最后实现如下

const PAGE_WIDTH = 23; // 每一张图片的宽度
const TRANSLATION_DELAY = 1500;  // 轮播每一张图片停留的时长
const translatePage = () => {
    const pages = document.querySelector<HTMLElement>('.carousel-contents'); // 获取需要滚动的元素
    // 小于4张则不轮播
    if (pages.childNodes.length < 4) return;
    let nowPositionLeft = 0;  // 初始化当前滚动元素的marginLeft
    const lastChild = pages.lastElementChild;
    const firstChild = pages.firstElementChild;
    const secondChild = pages.childNodes[1];
    // 在 pages 容器的首尾分别添加第一个和最后两个
    lastChild && pages.insertBefore(lastChild.cloneNode(true), firstChild);
    firstChild && pages.appendChild(firstChild.cloneNode(true));
    secondChild && pages.appendChild(secondChild.cloneNode(true));
    const run = (time = TRANSLATION_DELAY) => {
        setTimeout(() => {
            # 设置marginLeft、transition来实现平滑滚动
            if (nowPositionLeft + ((pages.childNodes.length - 3) * PAGE_WIDTH) <= 0) {
                nowPositionLeft = 0;
                pages.style.transition = 'none';  // 去掉动画,让用户感知不到滚动回至最左端
                pages.style.marginLeft = `${nowPositionLeft}px`;
                run(0);  // 这里不设计setTime间隔,避免该状态连续出现两次
            } else {
                nowPositionLeft = nowPositionLeft - PAGE_WIDTH;
                pages.style.transition = 'margin-left 0.7s linear';
                pages.style.marginLeft = `${nowPositionLeft}px`;
                run();
            }
        }, time);
    }
    run(); // 执行滚动
}

1.gif (注释: 视频没有截全)