Web前端之路

Vue 实现无缝轮播

2019-04-28  本文已影响6人  西山以南

很多网站都会有轮播图的需求,而简单的轮播图实现通常会在展示完最后一个子项后停止轮播,或者跳回到第一个子项重复轮播过程,这样的交互效果往往是存在断层的。接下来介绍如何实现一个无缝的轮播图,达到这样的效果:

预览地址:https://jsfiddle.net/JunreyCen/qxogapws/

核心思想其实非常简单:

  1. 当轮播到边界子项(Item 3),并继续进行横移时,把即将要展示的子项(Item 1)挪到紧挨着 Item 3 的位置,执行横移,如下图 Step 1

  2. 由于此时活跃子项的索引(index > 2)已经超出范围,在下一次横移进行前,需要把索引调整到合理范围内,并重置子项的位置,如下图 Step 2。注意,这一步需要把 transition 关闭,不然 “偷梁换柱” 的过程会被一览无遗。

“偷梁换柱” 过程

这里提供一份完整的代码实现。我稍微做了点优化,支持

原理上无非是支持多个子项的同时 “偷梁换柱” 罢了,详细的可以关注代码中的 next 函数。

<html>
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
  <head>
    <style>
      ul {
        list-style: none;
      }
      .swipe {
        position: absolute;
        left: 0;
        right: 0;
        margin: 40px auto;
        width: 90%;
        max-width: 375px;
        height: 200px;
        overflow: hidden;
      }
      .swipe-group {
        display: flex;
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100%;
      }
      .swipe-item {
        flex: 0 0 100%;
        height: 100%;
        line-height: 200px;
        text-align: center;
        font-size: 40px;
        font-weight: 600;
        color: #fff;
      }
      .swipe-item:nth-child(1) {
        background-color: aquamarine;
      }
      .swipe-item:nth-child(2) {
        background-color: chocolate;
      }
      .swipe-item:nth-child(3) {
        background-color: darksalmon;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>

    <template id="tpl">
      <div class="swipe">
        <ul class="swipe-group"
          :style="groupStyle">
          <li 
            class="swipe-item" 
            v-for="item in items"
            ref="item">
            {{item}}
          </li>
        </ul>
      </div>
    </template>

    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <script>
      new Vue({
        el: '#app',
        template: '#tpl',
        computed: {
          groupStyle() {
            return {
              'transform': `translate3d(${this.offset}px, 0, 0)`,
              'transition-duration': `${this.duration}ms`,
            };
          },
        },
        data() {
          return {
            items: [1, 2, 3],
            index: 0,           // 当前轮播项索引
            offset: 0,          // swipe组的偏移量
            duration: 0,        // 过渡动画时长
            itemWidth: 0,       // 轮播项宽度
          };
        },
        mounted() {
          if (this.$el) {
            this.itemWidth = this.$el.getBoundingClientRect().width;
          }
          this.autoplay();
        },
        methods: {
          // index 超出范围时调整
          correctIndex() {
            this.duration = 0;
            const total = this.items.length;
            if (this.index < 0) {
              this.next(total, true);
            } else if (this.index > total - 1) {
              this.next(-total, true);
            }
          },
          // 移动到达目标 index 途中的所有 swipe-item
          moveItems(indexOffset) {
            const targetIndex = this.index + indexOffset;
            if (this.index < targetIndex) {
              // 向左
              for (let i = this.index; i < targetIndex; i++) {
                this.moveItem(i + 1);
              }
            } else {
              // 向右
              for (let i = targetIndex; i < this.index; i++) {
                this.moveItem(i);
              }
            }
          },
          // 移动 swipe-item
          moveItem(index) {
            const total = this.items.length;
            const itemIndex = index % 3 < 0 ? index % 3 + 3 : index % 3;
            // 目标 index 超出范围时调整对应 swipe-item 的偏移值
            if (index > total - 1) {
              this.$refs.item[itemIndex].style.transform = `translateX(${total * this.itemWidth}px)`;
            } else if (index < 0) {
              this.$refs.item[itemIndex].style.transform = `translateX(${-total * this.itemWidth}px)`;
            } else {
              this.$refs.item[itemIndex].style.transform = 'translateX(0px)';
            }
          },
          resetItems() {
            this.$refs.item.forEach($item => {
              $item.style.transform = 'translateX(0px)';
            });
          },
          // 向左/右方向切换 indexOffset 个 swipe-item
          next(indexOffset, isCorrect) {
            isCorrect ? this.resetItems() : this.moveItems(indexOffset);
            this.index += indexOffset;
            this.offset = -this.index * this.itemWidth;
          },
          autoplay() {
            this.player = setInterval(() => {
              this.duration = 0;
              this.correctIndex();
              // 30ms延时是为了屏蔽 reset 过程中的过渡动画
              setTimeout(() => {
                this.duration = 500;
                this.next(1);
              }, 30);
            }, 1000);
          },
        },
      });
    </script>
  </body>
</html>

代码已发布在 github 上,欢迎大家提 issue 交流。

上一篇下一篇

猜你喜欢

热点阅读