使用js实现列表无限循环滚动

  最近的业务有涉及到需要将列表做成无限循环滚动,即第一个element滚出边界之后需要自动跳到队尾,参与下一轮滚动,达到无限滚动的效果。

  

  最终实现效果如上图所示,下面讲一下思路。

// js
<div class="scroll-container">
  <div
    v-for="index in 8"
    :key="index"
    class="scroll-item"
    :style="styleFormatter(index - 1)"
    ref="scrollItem"
  >
    {{ index }}
  </div>
</div>

// css
.scroll-container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: row;
  position: relative;
  overflow: hidden;
}

.scroll-item {
  display: flex;
  flex-shrink: 0;
  width: 200px;
  height: 100%;
  position: absolute;
  transition-property: left;
  transition-timing-function: linear;
}

  初始化时,会将scroll-item的定位改为绝对定位,相对元素是scroll-container,然后根据每个元素的宽度给每个scroll-item赋上left值,这样就可以让每个元素都无缝衔接上了(比如说每个元素的宽度都是200px,那么第一个元素left就是0,第二个元素left就是200px,以此类推,当然也可以设置为不等宽以及加上margin,思路是一样的)。scroll-item包含在scroll-container容器内,首先scroll-container需要将overflow设置成hidden,将超出范围的元素进行隐藏。至于滚动效果,本demo使用的是transition来控制元素滚动动画。在已知每个元素的宽度(假设为width)的情况下,可以将left从初始位置滚动到-width,则元素会完全隐藏掉,再通过修改css中transition-duration属性来控制动画执行时间即可。

  以本demo为例,每个scroll-item的宽度都为200px,滚动速度是30px/s,那么第一个元素的transition-duration就是(200 / 30) = 6.67s了,以此类推。具体实现如下:

// 根据索引计算当前元素所在位置
styleFormatter (index) {
  return {
    backgroundColor: this.colorList[index],
    left: `${index * this.width}px`
  }
}

// 控制滚动开始
scroll () {
  const elementList = this.$refs.scrollItem
  elementList.forEach(ele => {
    ele.style.transitionDuration = `${(ele.offsetLeft + this.width) / this.speed}s`
    ele.style.left = `-${this.width}px`
  })
}

  styleFormatter()是根据每个元素的索引以及宽度来初始化每个元素的left,在本demo中还加上了background-color方便识别。重点是scroll()里面,这里面用到了我们小学二年级学到的公式,路程 = 速度 x 时间,已知该元素需要行走的路程是自身的位置offsetLeft加上自身的宽度width,以及速度speed(自定义),易得该元素需要行走的时间time,也就是对应这里面的动画执行时间transition-duration。这样就可以保证每个scroll-item的运行速度是一致的,以及每一个元素是能够衔接起来的。

  这样就能够实现一轮动画了,下面开始思考如何实现无限滚动。当第一个元素完成动画(已经完全隐藏了)的时候,将其插入到剩下元素中的队尾,重新开始执行动画,是不是就可以实现无限滚动了。我们知道元素是有个事件叫transitionend,可以监听当前的动画是否已经执行完毕。那么事情就变得简单了,试试看:

  

// 初始化listener,监听滚动动画
// 当元素完全滚动至显示范围外时,则将该元素插入队尾
initListener () {
  const elementList = this.$refs.scrollItem

  elementList.forEach(ele => {
    // 当动画结束后会触发transitionend事件
    ele.addEventListener('transitionend', () => {
      // 计算队尾位置
      const lastestElementPosition = this.lastestElementPosition()
      ele.style.transitionDuration = `0s`
      ele.style.left = `${lastestElementPosition + this.width}px`

      // 渲染完毕之后开始滚动
      this.$nextTick(() => {
        ele.style.transitionDuration = `${(ele.offsetLeft + 200) / this.speed}s`
        ele.style.left = `-${this.width}px`
      })
    })
  })
}

// 计算最后一个元素的位置
lastestElementPosition () {
  const elementList = this.$refs.scrollItem
  return Math.max(...elementList.map(ele => {
    // 注意,这里不能直接读取ele.style.left,这样只能读到终点位置的偏移量,而我们需要的是当前元素的实际位置,需要通过getComputedStyle来获取
    const styles = getComputedStyle(ele, null)
    return parseFloat(styles.left)
  }))
}

  通过addEventListener来监听transitionend事件,当动画执行完毕之后,把当前元素插入到队尾。这时候需要去找到执行列表的最后一个元素,通过每个元素的left值来判断,left值最大则代表该元素是滚动列表中的最后一个元素。注意,这里需要先将transition-duration设置为0,不然元素从当前位置移动至队尾会有一个动画过程。

  将元素移动至队尾之后,剩下的时候就好处理了,处理方式跟scroll()方法一致。

  至此,将列表无限循环滚动就实现出来了,完整代码在下面,有需自取。

<template>
  <div class="main">
    <div class="scroll-container">
      <div
        v-for="index in 8"
        :key="index"
        class="scroll-item"
        :style="styleFormatter(index - 1)"
        ref="scrollItem"
      >
        {{ index }}
      </div>
    </div>
    <div class="controller">
      <button class="btn" @click="toggleClick">{{ isScrolling ? '暂停滚动' : '开始滚动' }}</button>
    </div>
  </div>
</template>

<script>
export default {
  data () {
    return {
      colorList: [
        'gray',
        'antiquewhite',
        'aquamarine',
        'cadetblue',
        'darkgoldenrod',
        'mediumaquamarine',
        'violet',
        'royalblue'
      ],

      // 每个元素的宽度
      width: 200,

      // 滚动速度
      speed: 100,

      isScrolling: false
    }
  },
  mounted () {
    this.initListener()
  },
  methods: {
    toggleClick () {
      if (this.isScrolling) {
        this.isScrolling = false
        this.pause()
      } else {
        this.isScrolling = true
        this.scroll()
      }
    },

    // 根据索引计算当前元素所在位置
    styleFormatter (index) {
      return {
        backgroundColor: this.colorList[index],
        left: `${index * this.width}px`
      }
    },

    // 控制滚动开始
    scroll () {
      const elementList = this.$refs.scrollItem
      elementList.forEach(ele => {
        ele.style.transitionDuration = `${(ele.offsetLeft + this.width) / this.speed}s`
        ele.style.left = `-${this.width}px`
      })
    },

    // 暂停滚动
    pause () {
      const elementList = this.$refs.scrollItem
      elementList.forEach(ele => {
        const styles = getComputedStyle(ele, null)
        console.log(styles.left)
        ele.style.transitionDuration = '0s'
        ele.style.left = `${styles.left}`
      })
    },

    // 初始化listener,监听滚动动画
    // 当元素完全滚动至显示范围外时,则将该元素插入队尾
    initListener () {
      const elementList = this.$refs.scrollItem

      elementList.forEach(ele => {
        // 当动画结束后会触发transitionend事件
        ele.addEventListener('transitionend', () => {
          // 计算队尾位置
          const lastestElementPosition = this.lastestElementPosition()
          ele.style.transitionDuration = `0s`
          ele.style.left = `${lastestElementPosition + this.width}px`

          // 渲染完毕之后开始滚动
          this.$nextTick(() => {
            ele.style.transitionDuration = `${(ele.offsetLeft + 200) / this.speed}s`
            ele.style.left = `-${this.width}px`
          })
        })
      })
    },

    // 计算最后一个元素的位置
    lastestElementPosition () {
      const elementList = this.$refs.scrollItem
      return Math.max(...elementList.map(ele => {
        // 注意,这里不能直接读取ele.style.left,这样只能读到终点位置的偏移量,而我们需要的是当前元素的实际位置,需要通过getComputedStyle来获取
        const styles = getComputedStyle(ele, null)
        return parseFloat(styles.left)
      }))
    }
  }
}
</script>

<style scoped>
  .scroll-container {
    width: 100%;
    height: calc(100% - 50px);
    display: flex;
    flex-direction: row;
    position: relative;
    overflow: hidden;
  }

  .scroll-item {
    display: flex;
    flex-shrink: 0;
    width: 200px;
    height: 100%;
    position: absolute;
    transition-property: left;
    transition-timing-function: linear;
  }

  .btn {
    margin-top: 20px;
    height: 30px;
    line-height: 30px;
  }
</style>

  demo地址:https://github.com/16hmchen/infiniteScroll

 

posted @ 2020-11-05 23:27  卑微小陈的随笔  阅读(10424)  评论(0编辑  收藏  举报