美文网首页Web前端之路
Vue 实现无缝轮播

Vue 实现无缝轮播

作者: 西山以南 | 来源:发表于2019-04-28 15:45 被阅读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 交流。

    相关文章

      网友评论

        本文标题:Vue 实现无缝轮播

        本文链接:https://www.haomeiwen.com/subject/zosfnqtx.html