Skip to content

轮播图

思路

  • 页面初始化:动态创建img元素、设置容器宽度、动态创建指示器。
  • 交互: 核心函数moveTo、next、prev
  • 连通交互:指示器点击、左右切换、自动轮播
  • 感受核心思想,先把核心功能实现,剩余的细节再修正。 期间会遇到一个又一个的问题,类似于boss,打败一个得到一个奖励。
  • 当遇到某个函数知道某个状态(动画结束),另一个函数却又依赖这个状态时,那么就可以使用回调模式。

实现

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>无缝轮播图</title>
    <link rel="stylesheet" href="./css/index.css" />
  </head>
  <body>
    <div class="carousel-container">
      <div class="carousel-list"></div>
      <div class="indicator"></div>
      <div class="arrow">
        <div class="arrow-left"></div>
        <div class="arrow-right"></div>
      </div>
    </div>
    <script src="./js/animate.js"></script>
    <script src="./js/index.js"></script>
  </body>
</html>
css
.carousel-container {
  width: 500px;
  height: 300px;
  overflow: hidden;
  /* outline: 2px solid lightblue; */
  margin: 0 auto;
  position: relative;
}

.carousel-list {
  width: 500%;
  height: 100%;
}

.carousel-item {
  width: 500px;
  height: 100%;
  object-fit: cover;
  float: left;
}

.indicator {
  position: absolute;
  width: 100%;
  left: 0;
  bottom: 0;
  height: 30px;
  text-align: center;
}
.indicator-item {
  display: inline-block;
  width: 20px;
  height: 2px;
  background: #fff;
  cursor: pointer;
  margin: 0 5px;
}
.indicator-item.active {
  background: #ffa44f;
}

.arrow-left,
.arrow-right {
  position: absolute;
  width: 50px;
  height: 100%;
  top: 0;
  background: rgba(0, 0, 0, 0.5);
  cursor: pointer;
  opacity: 0;
  color: #fff;
}
.arrow-left:hover,
.arrow-right:hover {
  opacity: 1;
}

.arrow-left {
  left: 0;
}

.arrow-left::before {
  content: '<';
}

.arrow-right {
  right: 0;
}
.arrow-right::before {
  content: '>';
}

.arrow-left::before,
.arrow-right::before {
  position: absolute;
  left: 0;
  width: 100%;
  text-align: center;
  top: 50%;
  height: 30px;
  line-height: 30px;
  margin-top: -15px;
}
js
function createAnimation(options) {
  var from = options.from; // 起始值
  var to = options.to; // 结束值
  var totalMS = options.totalMS || 1000; // 变化总时间
  var duration = options.duration || 15; // 动画间隔时间
  var times = Math.floor(totalMS / duration); // 变化的次数
  var dis = (to - from) / times; // 每一次变化改变的值
  var curTimes = 0; // 当前变化的次数
  var timerId = setInterval(function () {
    from += dis;
    curTimes++; // 当前变化增加一次
    if (curTimes >= times) {
      // 变化的次数达到了
      from = to; // 变化完成了
      clearInterval(timerId); // 不再变化了
      options.onmove && options.onmove(from);
      options.onend && options.onend();
      return;
    }
    // 无数的可能性
    options.onmove && options.onmove(from);
  }, duration);
}
js
(function () {
  function $(selector) {
    return document.querySelector(selector);
  }

  // 初始化
  var curIndex = 0; // 当前显示的是第几张图片
  var doms = {
    container: $('.carousel-container'),
    carouselList: $('.carousel-list'),
    indicator: $('.indicator'),
    arrowLeft: $('.arrow-left'),
    arrowRight: $('.arrow-right'),
  };
  var containerWidth = doms.container.clientWidth; // 容器可见区域的宽度
  var urls = [
    './img/Wallpaper1.jpg',
    './img/Wallpaper2.jpg',
    './img/Wallpaper3.jpg',
    './img/Wallpaper4.jpg',
    './img/Wallpaper5.jpg',
  ]; //记录了要显示的所有轮播图的图片路径

  function init() {
    function _createImg(url) {
      var img = document.createElement('img');
      img.src = url;
      img.className = 'carousel-item';
      doms.carouselList.appendChild(img);
    }

    for (var i = 0; i < urls.length; i++) {
      _createImg(urls[i]);
      // 创建指示器
      var div = document.createElement('div');
      div.className = 'indicator-item';
      doms.indicator.appendChild(div);
    }

    // 多加一张额外的图片
    _createImg(urls[0]);
    // 设置容器宽度
    doms.carouselList.style.width = doms.carouselList.children.length + '00%';
    // 设置指示器的激活状态
    setIndicatorStatus();
  }

  /**
   * 根据curIndex设置指示器的状态
   */
  function setIndicatorStatus() {
    // 1. 将目前激活的指示器取消激活
    var active = $('.indicator-item.active');
    if (active) {
      active.className = 'indicator-item';
    }
    // 2. 激活当前的指示器
    var index = curIndex;
    if (index > urls.length - 1) {
      index = 0;
    }
    doms.indicator.children[index].className = 'indicator-item active';
  }

  init();

  // 交互
  var totalMS = 300;
  var isPlaying = false; // 是否有正在进行的切换动画
  /**
   * 将轮播图从当前的位置,切换到newIndex位置
   * @param {*} newIndex 新的位置的图片索引
   */
  function moveTo(newIndex, onend) {
    if (isPlaying || newIndex === curIndex) {
      return; // 有动画进行 或 切换目标和当前一致,不做任何事情
    }
    isPlaying = true;
    var from = parseFloat(doms.carouselList.style.marginLeft) || 0;
    var to = -newIndex * containerWidth;

    createAnimation({
      from: from,
      to: to,
      totalMS: totalMS,
      onmove: function (n) {
        doms.carouselList.style.marginLeft = n + 'px';
      },
      onend: function () {
        isPlaying = false;
        onend && onend();
      },
    });

    curIndex = newIndex; // 更改当前显示的图片索引
    setIndicatorStatus();
  }

  // 处理指示器的点击事件
  for (var i = 0; i < doms.indicator.children.length; i++) {
    (function (i) {
      doms.indicator.children[i].onclick = function () {
        moveTo(i);
      };
    })(i);
  }

  function next() {
    var newIndex = curIndex + 1;
    var onend;
    if (newIndex === doms.carouselList.children.length - 1) {
      // 切换到最后一张图片了
      // 等动画完成后,要回到第一张图片
      onend = function () {
        doms.carouselList.style.marginLeft = 0;
        curIndex = 0;
      };
    }
    moveTo(newIndex, onend);
  }

  function prev() {
    var newIndex = curIndex - 1;
    if (newIndex < 0) {
      var maxIndex = doms.carouselList.children.length - 1;
      doms.carouselList.style.marginLeft = -maxIndex * containerWidth + 'px';
      newIndex = maxIndex - 1;
    }
    moveTo(newIndex);
  }

  doms.arrowLeft.onclick = prev;
  doms.arrowRight.onclick = next;

  var duration = 2000; // 自动切换的间隔
  var timerId;
  function autoStart() {
    if (timerId) {
      // 已经有自动切换在进行了
      return;
    }
    timerId = setInterval(next, duration);
  }

  function autoStop() {
    clearInterval(timerId);
    timerId = null;
  }

  autoStart(); // 最开始自动切换
  doms.container.onmouseenter = autoStop;
  doms.container.onmouseleave = autoStart;
})();
js
/* 工具 */
function $(selector) {
    return document.querySelector(selector);
}


/* 初始化 */
const pageData = {
    imgWidth: 500,//图片宽度
    totalMS: 300,//动画总时长
    isPlaying: false,//动画是否正在进行
    timerId: null,//计时器id
    timerDuration: 2000,//计时器间歇
}

const doms = {
    container: $('.carousel-container'),
    carouselList: $('.carousel-list'),
    indicator: $('.indicator'),
    arrowLeft: $('.arrow-left'),
    arrowRight: $('.arrow-right'),
}

var urls = [
    './img/Wallpaper1.jpg',
    './img/Wallpaper2.jpg',
    './img/Wallpaper3.jpg',
    './img/Wallpaper4.jpg',
    './img/Wallpaper5.jpg',
];

let currIndex = 0;//当前索引

// 初始函数
function init() {
    function _createImg(url) {
        const img = document.createElement('img');
        img.src = url;
        img.className = 'carousel-item';
        doms.carouselList.appendChild(img);
    }

    function _createIndicators() {
        const div = document.createElement('div');
        div.className = 'indicator-item';
        div.style.margin = "0 4px";
        doms.indicator.appendChild(div);
    }


    urls.forEach(url => _createImg(url));//创建图片
    _createImg(urls[0]);//克隆第一张图片
    doms.carouselList.style.width = `${doms.carouselList.children.length * 100}%`;//设置容器宽度
    urls.forEach(url => _createIndicators());//创建指示器
    setIndicatorStatus();//默认激活第一个指示器
}

init();

/* 交互 */
// 设置指示器激活
function setIndicatorStatus() {
    const active = $(".indicator-item.active");
    if (active) {
        active.className = "indicator-item";
    }

    // 边界处理
    let index = currIndex;
    if (index > urls.length - 1) {
        index = 0;
    }
    doms.indicator.children[index].classList.add('active');
}

// 移动到目标
function moveTo(index, cb) {
    if (pageData.isPlaying) {
        return;
    }

    pageData.isPlaying = true;
    const from = parseFloat(doms.carouselList.style.marginLeft) || 0;
    const to = -index * pageData.imgWidth;

    createAnimation({
        from: from,
        to: to,
        totalMS: pageData.totalMS,
        onmove: function (n) {
            doms.carouselList.style.marginLeft = n + 'px';
        },
        onend: function () {
            pageData.isPlaying = false;
            cb && cb();//动画结束后执行的回调和外面的逻辑关联起来
        }
    })

    currIndex = index;
    setIndicatorStatus();
}

// 下一张
function next() {
    if (pageData.isPlaying) {
        return;
    }

    currIndex++;
    let endCb = null;//动画结束后的回调

    // 边界处理
    if (currIndex == doms.carouselList.children.length - 1) {
        endCb = function () {
            doms.carouselList.style.marginLeft = 0;
            currIndex = 0;
        };
    }

    moveTo(currIndex, endCb);
}

// 上一张
function prev() {
    if (pageData.isPlaying) {
        return;
    }

    currIndex--;

    // 边界处理
    if (currIndex < 0) {
        const maxIdx = doms.carouselList.children.length - 1;
        doms.carouselList.style.marginLeft = - maxIdx * pageData.imgWidth + 'px';
        currIndex = maxIdx - 1;
    }

    moveTo(currIndex);
}

// 自动轮播
function autoPlay() {
    if (pageData.timerId) {
        return;
    }
    pageData.timerId = setInterval(next, pageData.timerDuration);
}

// 停止轮播
function stopPlay() {
    clearInterval(pageData.timerId);
    pageData.timerId = null;
}

// 绑定事件-指示器
for (let i = 0; i < doms.indicator.children.length; i++) {
    doms.indicator.children[i].onclick = function () {
        moveTo(i);
    }
}

// 绑定事件-开始/停止轮播
autoPlay();
doms.container.onmouseenter = stopPlay;
doms.container.onmouseleave = autoPlay;

// 绑定事件-左右箭头
doms.arrowLeft.onclick = prev;
doms.arrowRight.onclick = next;

MIT License