无缝轮播,有点名堂

前言

最近要开发一个五一问卷调查活动,这个功能用到了轮播图,翻阅了一下项目文件,看到项目中别的地方轮播图用的是react-id-swiper工具包, 本着已有功能就要充分复用的宗旨思想,依葫芦画瓢,把别处的react-id-swiper轮播代码功能搬了过来,改了轮播内容区域,发现运行不正常,第一页跳转到第二页的时候,需要点击两次才能跳转成功。查阅react-id-swiper组件库的文档,没有找到合适的解决方案。后面发现是第一次跳转的时候,dom元素还未创建,所以调用swiper的跳转方法执行无效所致。这件事让我对轮播图的实现原理产生了兴趣,决定抽空研究一下,现在我们进入正题。

效果展示

这是最终的效果图,可以看到无论是自动轮播,还是手动向前,向后翻,翻到头都可以做到平滑的过渡,分页器的切换也是OK的,将这个轮播组件应用于生产实践,一点问题也没有。接下来说说它的实现原理。

轮播图基础原理

  1. 首先创建一个容器元素,用于放置轮播的内容。设置容器的宽高,以及 overflow 属性为 hidden,以隐藏超出部分的轮播项。
  2. 如果轮播方向为水平方向,要设置所有的轮播项显示在一行,不能换行。每个轮播项的宽度等于容器宽度,方便控制轮播顺序。
  3. 通过设置容器元素的transform:translateX(移动距离)属性控制显示哪一个轮播项。

先用纯CSS实现轮播效果

轮播图的html结构如下所示:slider-box区域是轮播内容区,slider-spot区域是分页区域,控制轮播图的页码切换。

html 复制代码
    <div class="slider-container">
      <ul class="slider-box">
        <li class="slider-item"><img src="./imgs/1.jpg" alt="" /></li>
        <li class="slider-item"><img src="./imgs/2.webp" alt="" /></li>
        <li class="slider-item"><img src="./imgs/3.webp" alt="" /></li>
      </ul>

      <div class="slider-spot">
        <input type="radio" name="pager" class="spot-item" id="page1" checked />
        <input type="radio" name="pager" class="spot-item" id="page2" />
        <input type="radio" name="pager" class="spot-item" id="page3" />
      </div>
    </div>

通过设置容器slider-containeroverflow: hidden;属性,隐藏掉不需要显示的轮播项。设置轮播父元素slider-boxdisplay属性为flex让轮播元素展示为一行,这时你会发现,每个轮播元素被压扁了。小问题,只需给轮播元素slider-item添加flex: 0 0 100%;,不让其扩展和压缩即可。借助radio单选按钮组中每个单选按钮的checked状态,就可知道要切换到哪一个轮播项。接着用has伪类反向选择轮播项父元素slider-container,计算好X轴方向的偏移量,偏移到指定位置,就实现了轮播图的切换效果。

css 复制代码
      ul {
        margin: 0;
        padding: 0;
        list-style: none;
      }

      .slider-container {
        position: relative;
        width: 375px;
        height: 250px;
        margin: 0 auto;
        overflow: hidden;

        .slider-box {
          height: 100%;
          display: flex;

          .slider-item {
            height: 100%;
            flex: 0 0 100%;

            img {
              width: 100%;
              height: 100%;
            }
          }
        }
      }

      .slider-container:has(#page1:checked) > .slider-box,
      .slider-container:has(#page2:checked) > .slider-box,
      .slider-container:has(#page3:checked) > .slider-box {
        transition: 1s ease-in-out;
      }

      .slider-container:has(#page1:checked) > .slider-box {
        transform: translateX(0);
      }

      .slider-container:has(#page2:checked) > .slider-box {
        transform: translateX(-375px);
      }

      .slider-container:has(#page3:checked) > .slider-box {
        transform: translateX(-750px);
      }

      .slider-spot {
        display: flex;
        justify-content: center;
        align-items: center;

        position: absolute;
        bottom: 10px;

        width: 100%;
        height: 15px;

        .spot-item {
          width: 15px;
          height: 15px;

          & + & {
            margin-left: 10px;
          }
        }
      }

效果演示:

再用JS实现轮播图

上面通过用纯CSS实现轮播效果,是为了理解轮播图的原理。纯CSS实现的轮播图,在轮播项数量不确定的条件下,每一项的偏移位置不好计算。还有无法实现左右滑动切换轮播项。现在我们根据轮播图的原理,用js实现一下轮播图。

把轮播图修改成支持参数配置(配置参数参见下面),动态生成轮播项。其它的参数都比较好理解,有一个参数要说明一下。配置参数中有一项是容器id,这个参数是考虑到一个页面中可能会有多个轮播图,为了防止相互之间产生串扰。页面中引入了一个slider.js的文件,这个文件是轮播图功能的核心功能实现,接下来会重点讲解。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>轮播图</title>
    <style>
      .slider-container {
        position: relative;
        width: 375px;
        height: 250px;
        margin: 0 auto;
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <div id="my-slider" class="slider-container"></div>
  </body>
  <script src="./slider.js"></script>
  <script>
    let imgList = [
      {
        imgPath: "./imgs/1.jpg",
        jumpUrl: "#",
      },
      {
        imgPath: "./imgs/2.webp",
        jumpUrl: "#",
      },
      {
        imgPath: "./imgs/3.webp",
        jumpUrl: "#",
      },
    ];

    new Slider({
      // 轮播图容器id
      sliderId: "my-slider",
      // 轮播图列表
      imgList: imgList,
      // 是否自动播放
      autoplay: true,
      // 轮播项切换过渡动画时长
      aniTIme: 1000,
      // 自动播放轮播项切换时间间隔,单位毫秒
      intervalTime: 1000,
    }).init();
  </script>
</html>

笔者按照自顶向下的思路,讲解一下自动轮播的实现。首先定义Slider轮播构造函数,接收外部传入的参数,检查关键的参数是否设置,没有设置的话会在控制台抛出错误。

js 复制代码
/**
 * config:
 * sliderId 轮播图容器id
 * imgList 轮播图列表
 * autoplay 是否自动播放
 * aniTIme  动画时长
 * intervalTime  自动播放轮播图切换时间
 */
function Slider(config) {
  if (!config.sliderId) {
    throw new Error("轮播容器id不能为空");
  }}
  
  this.imgList = config.imgList || [];
  if (this.imgList.length === 0) {
    throw new Error("轮播列表不能为空");
  }
  
  // 轮播图片父容器
  this.sliderBoxDom=null;
  // 向前翻按钮
  this.prevBtnDom=null;
  // 向后翻按钮
  this.nextBtnDom=null;
  // 轮播分页容器
  this.sliderSpotDom=null;
  // 自动播放定时器
  this.timer = null;
  
  // 轮播外层容器
  this.sliderContainerDom = document.getElementById(config.sliderId);
  // 轮播图片外层容器每次移动的偏移量
  this.moveWidth = this.sliderContainerDom.offsetWidth;
  
  this.aniTIme = config.aniTIme || 1000;
  this.intervalTime = config.intervalTime + this.aniTIme || 2000;
  
  // 当前播放的l轮播图序号
  this.playIndex = 0;
  
  // 自动播放设置
  this.autoplay = config.autoplay;
 
}

初始化函数中做了二件事情,第一动态创建与轮播图相关的dom元素,第二判断执行自动播放逻辑。

js 复制代码
Slider.prototype = {
  init() {

    this.createSliderDom();

    if (this.autoplay) {
      this.timer = setInterval(this.toNext.bind(this, this.aniTIme), this.intervalTime);
    }
  },
}

createSliderDom方法内部的逻辑是在轮播外层容器my-slider下面创建轮播父容器slider-box及轮播项内容slider-item,以及分页器slider-spot及元素节点slider-item, 设置轮播父容器slider-box的初始偏移。

js 复制代码
  // 动态创建轮播图节点内容
  createSliderDom() {
    // 创建轮播图片容器
    this.sliderBoxDom = document.createElement("ul");
    this.sliderBoxDom.className = "slider-box";

    // 添加轮播图片
    this.sliderBoxDom.innerHTML = this.imgList
      .map((item, index) => {
        let dom = [
          `<li  class="slider-item">`,
          `<a href="${item.jumpUrl}"><img src="${item.imgPath}" alt=""></a>`,
          "</li>",
        ];
        return dom.join("");
      })
      .join("");

    // 初始偏移位置
    this.sliderBoxDom.style.transform = `translateX(0)`;

    // 创建切换轮播图的分页器按钮容器
    this.sliderSpotDom = document.createElement("ul");
    this.sliderSpotDom.className = "slider-spot";
    // 分页器
    this.sliderSpotDom.innerHTML = this.imgList
      .map((item, index) => {
        return `<li class="spot-item ${index === 0 ? "active" : ""}" data-index=${index}></li>`;
      })
      .join("");
   
    this.sliderContainerDom.appendChild(this.sliderBoxDom);
    this.sliderContainerDom.appendChild(this.sliderSpotDom);
  },

执行自动轮播的功能是依靠toNext方法完成的, 如果只有一张图片的话,无需设置轮播容器的偏移了。第一幅图偏移量为0,从第二幅图开始,按照轮播容器宽度的倍数进行偏移,当轮播到最后一张时,把轮播容器的偏移量重置0,开始新一轮的循环播放。此外toNext方法中还调用了setActiveSpot方法,setActiveSpot方法用于设置分页页码高亮,轮播到哪一项,给这一项添加高亮样式类active

js 复制代码
  // 向后翻
  toNext(aniTIme) {
    // 轮播图只有一项的话,无需设置偏移
    if (this.imgList.length === 1) return;
    this.playIndex++;

    if (this.playIndex === this.imgList.length) {
      this.playIndex = 0;
    }
     this.sliderBoxDom.style.transition = `${aniTIme / 1000}s ease-in-out`;
    this.sliderBoxDom.style.transform = `translateX(${-this.playIndex * this.moveWidth}px)`;
    this.setActiveSpot();
  },
  // 设置当前分页按钮为高亮选中状态
  setActiveSpot() {
    this.sliderSpotDom.childNodes.forEach((item, index) => {
      if (index === Math.abs(this.playIndex)) {
        item.classList.add("active");
      } else {
        item.classList.remove("active");
      }
    });
  },

你会发现实现的这个自动轮播功能,有个很大的问题,播放到最后一张图片重新开始下一轮播放时,过渡很不自然,有些突兀。而我们经常使用的一些轮播组件,最后一张和第一张的过渡很平滑,原因出在哪里,别人是怎么做到的? 带着这个疑问,我查阅了一些资料,发现里面有点名堂。

无缝轮播的实现原理

左移实现

上面的自动轮播,要想实现左移无缝轮播的效果,需要更改一下页面结构。在最后一幅图片后面,要多放置一张图片,把首张图片追加到最后一幅图的后面。

html 复制代码
    <div class="slider-container">
      <ul class="slider-box">
        <li class="slider-item"><img src="./imgs/1.jpg" alt="" /></li>
        <li class="slider-item"><img src="./imgs/2.webp" alt="" /></li>
        <li class="slider-item"><img src="./imgs/3.webp" alt="" /></li>
        <!-- 追加的首图-->
        <li class="slider-item"><img src="./imgs/1.jpg" alt="" /></li>
      </ul>
    </div>

原理我画图解释一下:假设轮播容器的宽度是375px, 只有三张图片,红框框线是轮播可视区域,其它图片都是隐藏的。 图中的数字表示第几张图片。

  1. 第一次轮播容器的水平偏移量为0,展示第一幅图
  2. 第二次轮播容器的水平偏移量为-375px,展示第二幅图
  3. 第三次轮播容器的水平偏移量为-750px,展示第三幅图
  4. 第四次是实现左移无缝轮播的关键,第四次轮播容器的水平偏移量为-1125px,展示末尾的第一幅图,这时要偷梁换柱,根据轮播动画播放时长,设置一个延时器(这个延时器的时间是小于轮播图切换的时长的),在延时器里把轮播容器水平偏移量重置为0,并且要取消轮播过渡动画。
  5. 第五次轮播容器的水平偏移量为0,展示第一幅图,开始第二轮的周而复始。

涉及的代码变动有两处,第一处是在createSliderDom方法中,创建轮播元素列表时,在末尾追加一张首图。

js 复制代码
  // 动态创建轮播图节点内容
  createSliderDom() {
    // ...
    // 创建轮播图片容器--在末尾追加首图
    let realImgList = [...this.imgList, this.imgList[0]];

    // 添加轮播图片
    this.sliderBoxDom.innerHTML = realImgList
      .map((item, index) => {
        let dom = [
          `<li  class="slider-item">`,
          `<a href="${item.jumpUrl}"><img src="${item.imgPath}" alt=""></a>`,
          "</li>",
        ];
        return dom.join("");
      })
      .join("");

     // ...
  },

第二处改动是在toNext方法中,判断播放到最后一幅图的时候,重置轮播容器水平偏移量计算变量值,并根据轮播过渡动画的时长,设置一个延时器,在延时器里,重置轮播容器水平偏移量为轮播列表中的第一幅图,并取消过渡动画,实现无缝平滑轮播。

js 复制代码
 // 向后翻
  toNext(aniTIme) {
    if (this.imgList.length === 1) return;
    this.playIndex++;

    this.sliderBoxDom.style.transition = `${aniTIme / 1000}s ease-in-out`;
    this.sliderBoxDom.style.transform = `translateX(${-this.playIndex * this.moveWidth}px)`;

    if (this.playIndex === this.imgList.length) {
      this.playIndex = 0;
      setTimeout(() => {
        this.sliderBoxDom.style.transitionProperty = "none";
        this.sliderBoxDom.style.transform = `translateX(0)`;
      }, aniTIme);
    }
    
    this.setActiveSpot();
  },

经过改造之后,现在的自动轮播效果,播放到最后一张向首张过渡时,是不是丝滑自然了很多。

右移实现

如果自动轮播的方向是向右移动的话,要实现无缝轮播,需将最后一幅图放置到列表首图前面,页面结构为:

html 复制代码
    <div class="slider-container">
      <ul class="slider-box">
         <!-- 追加的尾图-->
        <li class="slider-item"><img src="./imgs/3.jpg" alt="" /></li>
        
        <li class="slider-item"><img src="./imgs/1.jpg" alt="" /></li>
        <li class="slider-item"><img src="./imgs/2.webp" alt="" /></li>
        <li class="slider-item"><img src="./imgs/3.webp" alt="" /></li>
      </ul>
    </div>

假设轮播容器的宽度为375px,共三张图,轮播顺序为:

  1. 第一次轮播容器的水平偏移量为-375px,展示轮播列表第二幅图
  2. 第二次轮播容器的水平偏移量为 0,展示轮播列表第一幅图,第二次是实现右移无缝轮播的关键,这时要加一个延时器,在延时器里,把轮播容器的偏移量设置为-1125px,展示轮播列表最后一幅图,并取消动画过渡效果。
  3. 第三次轮播容器的水平偏移量为-1125px,展示轮播列表最后一幅图
  4. 第四次轮播容器的水平偏移量为-750px,展示轮播列表第三幅图
  5. 第五次轮播容器的水平偏移量为-375px,展示轮播列第二幅图,开始第二轮的周而复始。

引起的代码变化:第一是在createSliderDom方法中,创建轮播元素列表时,在头部追加一张尾图。此外要将轮播容器的初始偏移位置设置为-375px,让其展示第一幅轮播图。

js 复制代码
  // 动态创建轮播图节点内容
  createSliderDom() {
    // ...
    // 创建轮播图片容器
    let realImgList = [this.imgList[this.imgList.length - 1], ...this.imgList];

    // 添加轮播图片
    this.sliderBoxDom.innerHTML = realImgList
      .map((item, index) => {
        let dom = [
          `<li  class="slider-item">`,
          `<a href="${item.jumpUrl}"><img src="${item.imgPath}" alt=""></a>`,
          "</li>",
        ];
        return dom.join("");
      })
      .join("");
      
    this.playIndex=1;
    // 初始偏移位置
    this.sliderBoxDom.style.transform = `translateX(${-this.moveWidth}px)`;

    // ...
  },

第二处改动新增toPrev方法,判断播放到轮播列表第一幅的时候,重置轮播容器水平偏移量计算变量值,并根据轮播过渡动画的时长,设置一个延时器,在延时器里,重置轮播容器水平偏移量为轮播列表中的最后一幅图,并取消过渡动画,实现右移无缝平滑轮播。

js 复制代码
  // 向前翻
  toPrev(aniTIme) {
    if (this.imgList.length === 1) return;

    this.sliderBoxDom.style.transition = `${aniTIme / 1000}s ease-in-out`;
    this.sliderBoxDom.style.transform = `translateX(${-this.playIndex * this.moveWidth}px)`;
    if (this.playIndex === 0) {
      this.playIndex = this.imgList.length - 1;
      setTimeout(() => {
        this.sliderBoxDom.style.transitionProperty = "none";
        this.sliderBoxDom.style.transform = `translateX(${-this.imgList.length * this.moveWidth}px)`;
      }, aniTIme);
    } else {
      this.playIndex--;
    }
    this.setActiveSpot();
  },

右移无缝轮播效果展示:可以看到第一幅图和最后一幅图的过渡效果自然流畅。

最后

文章介绍了自动轮播的核心实现原理,至于轮播图的其它附加功能如手动分页,向前,向后翻页,以及移动端的手势滑动分页切换功能,也已实现,限于篇幅,不再赘述。亲手实现了轮播图之后,才发现无缝轮播这一块,需要特殊处理一下,才能实现平滑切换,有点名堂。果然应了那句纸上得来终觉浅,绝知此事要躬行。如果有时间有精力的话,还是建议大家把项目中常用的功能组件,自己亲自实现一遍,你一定会收获很多别人不曾知道的细节知识。本文的完整代码已经上传至码云,如果你对轮播图感兴趣的话,可以点击这里下载学习。

相关推荐
黄尚圈圈27 分钟前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水1 小时前
简洁之道 - React Hook Form
前端
正小安3 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch5 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光5 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   5 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   5 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web5 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常5 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇6 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器