使用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