最近的業務有涉及到需要將列表做成無限循環滾動,即第一個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