今天遇到一个大屏需求:
1️⃣初始进入页面停留5秒,然后开始滚动
2️⃣最后一条数据出现在最后一行时候暂停5秒,然后返回1️⃣
依次循环,发现vue-seamless-scroll的方法 ScrollEnd是监测最后一条数据消失在第一行才回调,不能满足我的需求,于是在node_modules里面找到他的文件重写
主要优化点
// 上
if (Math.abs(this.yPos) >= this.realBoxHeight - 520)
this.realBoxHeight是你所有数据加起来的行高, 520是可视区域的行高, yPos是向上滚动的距离,是个负值,滚动了一行,假设行高40,那就是 -40
// 到达底部时暂停5秒
this._stopMove()
setTimeout(() => {
this.yPos = 0
// 重置位置后也暂停5秒再开始滚动
setTimeout(() => {
this._startMove()
}, 5000)
}, 5000)
我修改完成的文件
<template>
<div ref="wrap">
<div
:style="leftSwitch"
v-if="navigation"
:class="leftSwitchClass"
@click="leftSwitchClick"
>
<slot name="left-switch"></slot>
</div>
<div
:style="rightSwitch"
v-if="navigation"
:class="rightSwitchClass"
@click="rightSwitchClick"
>
<slot name="right-switch"></slot>
</div>
<div
ref="realBox"
:style="pos"
@mouseenter="enter"
@mouseleave="leave"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
>
<div ref="slotList" :style="float">
<slot></slot>
</div>
<div v-html="copyHtml" :style="float"></div>
</div>
</div>
</template>
<script>
require('comutils/animationFrame')()
const arrayEqual = require('comutils/arrayEqual')
const copyObj = require('comutils/copyObj')
export default {
name: 'vue-seamless-scroll',
data() {
return {
xPos: 0,
yPos: 0,
delay: 0,
copyHtml: '',
height: 0,
width: 0, // 外容器宽度
realBoxWidth: 0 // 内容实际宽度
}
},
props: {
data: {
type: Array,
default: () => {
return []
}
},
classOption: {
type: Object,
default: () => {
return {}
}
}
},
computed: {
leftSwitchState() {
return this.xPos < 0
},
rightSwitchState() {
return Math.abs(this.xPos) < this.realBoxWidth - this.width
},
leftSwitchClass() {
return this.leftSwitchState ? '' : this.options.switchDisabledClass
},
rightSwitchClass() {
return this.rightSwitchState ? '' : this.options.switchDisabledClass
},
leftSwitch() {
return {
position: 'absolute',
margin: `${this.height / 2}px 0 0 -${this.options.switchOffset}px`,
transform: 'translate(-100%,-50%)'
}
},
rightSwitch() {
return {
position: 'absolute',
margin: `${this.height / 2}px 0 0 ${this.width +
this.options.switchOffset}px`,
transform: 'translateY(-50%)'
}
},
float() {
return this.isHorizontal
? { float: 'left', overflow: 'hidden' }
: { overflow: 'hidden' }
},
pos() {
return {
transform: `translate(${this.xPos}px,${this.yPos}px)`,
transition: `all ${this.ease} ${this.delay}ms`,
overflow: 'hidden'
}
},
defaultOption() {
return {
step: 0.25, //步长
limitMoveNum: 14, //启动无缝滚动最小数据数
hoverStop: true, //是否启用鼠标hover控制
direction: 1, // 0 往下 1 往上 2向左 3向右
openTouch: true, //开启移动端touch
singleHeight: 0, //单条数据高度有值hoverStop关闭
singleWidth: 0, //单条数据宽度有值hoverStop关闭
waitTime: 1000, //单步停止等待时间
switchOffset: 30,
autoPlay: true,
navigation: false,
switchSingleStep: 134,
switchDelay: 400,
switchDisabledClass: 'disabled',
isSingleRemUnit: false // singleWidth/singleHeight 是否开启rem度量
}
},
options() {
return copyObj({}, this.defaultOption, this.classOption)
},
navigation() {
return this.options.navigation
},
autoPlay() {
if (this.navigation) return false
return this.options.autoPlay
},
scrollSwitch() {
return this.data.length >= this.options.limitMoveNum
},
hoverStopSwitch() {
return this.options.hoverStop && this.autoPlay && this.scrollSwitch
},
canTouchScroll() {
return this.options.openTouch
},
isHorizontal() {
return this.options.direction > 1
},
baseFontSize() {
return this.options.isSingleRemUnit
? parseInt(
window.getComputedStyle(document.documentElement, null).fontSize
)
: 1
},
realSingleStopWidth() {
return this.options.singleWidth * this.baseFontSize
},
realSingleStopHeight() {
return this.options.singleHeight * this.baseFontSize
},
step() {
let singleStep
let step = this.options.step
if (this.isHorizontal) {
singleStep = this.realSingleStopWidth
} else {
singleStep = this.realSingleStopHeight
}
if (singleStep > 0 && singleStep % step > 0) {
console.error(
'如果设置了单步滚动,step需是单步大小的约数,否则无法保证单步滚动结束的位置是否准确。~~~~~'
)
}
return step
}
},
methods: {
reset() {
this._cancle()
this._initMove()
},
leftSwitchClick() {
if (!this.leftSwitchState) return
// 小于单步距离
if (Math.abs(this.xPos) < this.options.switchSingleStep) {
this.xPos = 0
return
}
this.xPos += this.options.switchSingleStep
},
rightSwitchClick() {
if (!this.rightSwitchState) return
// 小于单步距离
if (
this.realBoxWidth - this.width + this.xPos <
this.options.switchSingleStep
) {
this.xPos = this.width - this.realBoxWidth
return
}
this.xPos -= this.options.switchSingleStep
},
_cancle() {
cancelAnimationFrame(this.reqFrame || '')
},
touchStart(e) {
if (!this.canTouchScroll) return
let timer
const touch = e.targetTouches[0] //touches数组对象获得屏幕上所有的touch,取第一个touch
const { waitTime, singleHeight, singleWidth } = this.options
this.startPos = {
x: touch.pageX,
y: touch.pageY
} //取第一个touch的坐标值
this.startPosY = this.yPos //记录touchStart时候的posY
this.startPosX = this.xPos //记录touchStart时候的posX
if (!!singleHeight && !!singleWidth) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
this._cancle()
}, waitTime + 20)
} else {
this._cancle()
}
},
touchMove(e) {
//当屏幕有多个touch或者页面被缩放过,就不执行move操作
if (
!this.canTouchScroll ||
e.targetTouches.length > 1 ||
(e.scale && e.scale !== 1)
)
return
const touch = e.targetTouches[0]
const { direction } = this.options
this.endPos = {
x: touch.pageX - this.startPos.x,
y: touch.pageY - this.startPos.y
}
event.preventDefault() //阻止触摸事件的默认行为,即阻止滚屏
const dir = Math.abs(this.endPos.x) < Math.abs(this.endPos.y) ? 1 : 0 //dir,1表示纵向滑动,0为横向滑动
if (dir === 1 && direction < 2) {
// 表示纵向滑动 && 运动方向为上下
this.yPos = this.startPosY + this.endPos.y
} else if (dir === 0 && direction > 1) {
// 为横向滑动 && 运动方向为左右
this.xPos = this.startPosX + this.endPos.x
}
},
touchEnd() {
if (!this.canTouchScroll) return
let timer
const direction = this.options.direction
this.delay = 50
if (direction === 1) {
if (this.yPos > 0) this.yPos = 0
} else if (direction === 0) {
let h = (this.realBoxHeight / 2) * -1
if (this.yPos < h) this.yPos = h
} else if (direction === 2) {
if (this.xPos > 0) this.xPos = 0
} else if (direction === 3) {
let w = this.realBoxWidth * -1
if (this.xPos < w) this.xPos = w
}
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
this.delay = 0
this._move()
}, this.delay)
},
enter() {
if (this.hoverStopSwitch) this._stopMove()
},
leave() {
if (this.hoverStopSwitch) this._startMove()
},
_move() {
// 鼠标移入时拦截_move()
if (this.isHover) return
this._cancle() //进入move立即先清除动画 防止频繁touchMove导致多动画同时进行
this.reqFrame = requestAnimationFrame(
function() {
const h = this.realBoxHeight / 2 //实际高度
const w = this.realBoxWidth / 2 //宽度
let { direction, waitTime } = this.options
let { step } = this
if (direction === 1) {
// 上
if (Math.abs(this.yPos) >= this.realBoxHeight - 520) {
this.$emit('ScrollEnd')
// 到达底部时暂停5秒
this._stopMove()
setTimeout(() => {
this.yPos = 0
// 重置位置后也暂停5秒再开始滚动
setTimeout(() => {
this._startMove()
}, 5000)
}, 5000)
return
}
this.yPos -= step
} else if (direction === 0) {
// 下
if (this.yPos >= 0) {
this.$emit('ScrollEnd')
// 到达顶部时暂停5秒
this._stopMove()
setTimeout(() => {
this.yPos = h * -1
// 重置位置后也暂停5秒再开始滚动
setTimeout(() => {
this._startMove()
}, 5000)
}, 5000)
return
}
this.yPos += step
} else if (direction === 2) {
// 左
if (Math.abs(this.xPos) >= w) {
this.$emit('ScrollEnd')
// 到达左边界时暂停5秒
this._stopMove()
setTimeout(() => {
this.xPos = 0
// 重置位置后也暂停5秒再开始滚动
setTimeout(() => {
this._startMove()
}, 5000)
}, 5000)
return
}
this.xPos -= step
} else if (direction === 3) {
// 右
if (this.xPos >= 0) {
this.$emit('ScrollEnd')
// 到达右边界时暂停5秒
this._stopMove()
setTimeout(() => {
this.xPos = w * -1
// 重置位置后也暂停5秒再开始滚动
setTimeout(() => {
this._startMove()
}, 5000)
}, 5000)
return
}
this.xPos += step
}
// 移除单步滚动逻辑,改为连续滚动
this._move()
}.bind(this)
)
},
_initMove() {
this.$nextTick(() => {
const { switchDelay } = this.options
const { autoPlay, isHorizontal } = this
this._dataWarm(this.data)
this.copyHtml = '' //清空copy
if (isHorizontal) {
this.height = this.$refs.wrap.offsetHeight
this.width = this.$refs.wrap.offsetWidth
let slotListWidth = this.$refs.slotList.offsetWidth
this.$refs.realBox.style.width = slotListWidth + 'px'
this.realBoxWidth = slotListWidth
}
if (autoPlay) {
this.ease = 'ease-in'
this.delay = 0
} else {
this.ease = 'linear'
this.delay = switchDelay
return
}
// 是否可以滚动判断
if (this.scrollSwitch) {
let timer
if (timer) clearTimeout(timer)
this.realBoxHeight = this.$refs.realBox.offsetHeight
// 初始化时暂停5秒后开始滚动
setTimeout(() => {
this._move()
}, 5000)
} else {
this._cancle()
this.yPos = this.xPos = 0
}
})
},
_dataWarm(data) {
if (data.length > 100) {
console.warn(
`数据达到了${data.length}条有点多哦~,可能会造成部分老旧浏览器卡顿。`
)
}
},
_startMove() {
this.isHover = false //开启_move
this._move()
},
_stopMove() {
this.isHover = true //关闭_move
this._cancle()
}
},
watch: {
data(newData, oldData) {
this._dataWarm(newData)
// 每次数据更新都重置滚动
this.yPos = 0
this.xPos = 0
this._stopMove()
// 5秒后开始新的滚动
setTimeout(() => {
if (this.data.length >= this.options.limitMoveNum) {
this._initMove()
this._startMove()
}
}, 5000)
},
autoPlay(bol) {
if (bol) {
this.reset()
} else {
this._stopMove()
}
}
},
beforeCreate() {
this.reqFrame = null // move动画的animationFrame定时器
this.isHover = false // mouseenter mouseleave 控制this._move()的开关
this.ease = 'ease-in'
},
beforeDestroy() {
this._cancle()
}
}
</script>
源文件
<template>
<div ref="wrap">
<div
:style="leftSwitch"
v-if="navigation"
:class="leftSwitchClass"
@click="leftSwitchClick"
>
<slot name="left-switch"></slot>
</div>
<div
:style="rightSwitch"
v-if="navigation"
:class="rightSwitchClass"
@click="rightSwitchClick"
>
<slot name="right-switch"></slot>
</div>
<div
ref="realBox"
:style="pos"
@mouseenter="enter"
@mouseleave="leave"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
>
<div ref="slotList" :style="float">
<slot></slot>
</div>
<div v-html="copyHtml" :style="float"></div>
</div>
</div>
</template>
<script>
require('comutils/animationFrame')()
const arrayEqual = require('comutils/arrayEqual')
const copyObj = require('comutils/copyObj')
export default {
name: 'vue-seamless-scroll',
data () {
return {
xPos: 0,
yPos: 0,
delay: 0,
copyHtml: '',
height: 0,
width: 0, // 外容器宽度
realBoxWidth: 0, // 内容实际宽度
}
},
props: {
data: {
type: Array,
default: () => {
return []
}
},
classOption: {
type: Object,
default: () => {
return {}
}
}
},
computed: {
leftSwitchState () {
return this.xPos < 0
},
rightSwitchState () {
return Math.abs(this.xPos) < (this.realBoxWidth - this.width)
},
leftSwitchClass () {
return this.leftSwitchState ? '' : this.options.switchDisabledClass
},
rightSwitchClass () {
return this.rightSwitchState ? '' : this.options.switchDisabledClass
},
leftSwitch () {
return {
position: 'absolute',
margin: `${this.height / 2}px 0 0 -${this.options.switchOffset}px`,
transform: 'translate(-100%,-50%)'
}
},
rightSwitch () {
return {
position: 'absolute',
margin: `${this.height / 2}px 0 0 ${this.width + this.options.switchOffset}px`,
transform: 'translateY(-50%)'
}
},
float () {
return this.isHorizontal ? { float: 'left', overflow: 'hidden' } : { overflow: 'hidden' }
},
pos () {
return {
transform: `translate(${this.xPos}px,${this.yPos}px)`,
transition: `all ${this.ease} ${this.delay}ms`,
overflow: 'hidden'
}
},
defaultOption () {
return {
step: 1, //步长
limitMoveNum: 5, //启动无缝滚动最小数据数
hoverStop: true, //是否启用鼠标hover控制
direction: 1, // 0 往下 1 往上 2向左 3向右
openTouch: true, //开启移动端touch
singleHeight: 0, //单条数据高度有值hoverStop关闭
singleWidth: 0, //单条数据宽度有值hoverStop关闭
waitTime: 1000, //单步停止等待时间
switchOffset: 30,
autoPlay: true,
navigation: false,
switchSingleStep: 134,
switchDelay: 400,
switchDisabledClass: 'disabled',
isSingleRemUnit: false // singleWidth/singleHeight 是否开启rem度量
}
},
options () {
return copyObj({}, this.defaultOption, this.classOption)
},
navigation () {
return this.options.navigation
},
autoPlay () {
if (this.navigation) return false
return this.options.autoPlay
},
scrollSwitch () {
return this.data.length >= this.options.limitMoveNum
},
hoverStopSwitch () {
return this.options.hoverStop && this.autoPlay && this.scrollSwitch
},
canTouchScroll () {
return this.options.openTouch
},
isHorizontal () {
return this.options.direction > 1
},
baseFontSize () {
return this.options.isSingleRemUnit ? parseInt(window.getComputedStyle(document.documentElement, null).fontSize) : 1
},
realSingleStopWidth () {
return this.options.singleWidth * this.baseFontSize
},
realSingleStopHeight () {
return this.options.singleHeight * this.baseFontSize
},
step () {
let singleStep
let step = this.options.step
if (this.isHorizontal) {
singleStep = this.realSingleStopWidth
} else {
singleStep = this.realSingleStopHeight
}
if (singleStep > 0 && singleStep % step > 0) {
console.error('如果设置了单步滚动,step需是单步大小的约数,否则无法保证单步滚动结束的位置是否准确。~~~~~')
}
return step
}
},
methods: {
reset () {
this._cancle()
this._initMove()
},
leftSwitchClick () {
if (!this.leftSwitchState) return
// 小于单步距离
if (Math.abs(this.xPos) < this.options.switchSingleStep) {
this.xPos = 0
return
}
this.xPos += this.options.switchSingleStep
},
rightSwitchClick () {
if (!this.rightSwitchState) return
// 小于单步距离
if ((this.realBoxWidth - this.width + this.xPos) < this.options.switchSingleStep) {
this.xPos = this.width - this.realBoxWidth
return
}
this.xPos -= this.options.switchSingleStep
},
_cancle () {
cancelAnimationFrame(this.reqFrame || '')
},
touchStart (e) {
if (!this.canTouchScroll) return
let timer
const touch = e.targetTouches[0] //touches数组对象获得屏幕上所有的touch,取第一个touch
const { waitTime, singleHeight, singleWidth } = this.options
this.startPos = {
x: touch.pageX,
y: touch.pageY
} //取第一个touch的坐标值
this.startPosY = this.yPos //记录touchStart时候的posY
this.startPosX = this.xPos //记录touchStart时候的posX
if (!!singleHeight && !!singleWidth) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
this._cancle()
}, waitTime + 20)
} else {
this._cancle()
}
},
touchMove (e) {
//当屏幕有多个touch或者页面被缩放过,就不执行move操作
if (!this.canTouchScroll || e.targetTouches.length > 1 || e.scale && e.scale !== 1) return
const touch = e.targetTouches[0]
const { direction } = this.options
this.endPos = {
x: touch.pageX - this.startPos.x,
y: touch.pageY - this.startPos.y
}
event.preventDefault(); //阻止触摸事件的默认行为,即阻止滚屏
const dir = Math.abs(this.endPos.x) < Math.abs(this.endPos.y) ? 1 : 0 //dir,1表示纵向滑动,0为横向滑动
if (dir === 1 && direction < 2) { // 表示纵向滑动 && 运动方向为上下
this.yPos = this.startPosY + this.endPos.y
} else if (dir === 0 && direction > 1) { // 为横向滑动 && 运动方向为左右
this.xPos = this.startPosX + this.endPos.x
}
},
touchEnd () {
if (!this.canTouchScroll) return
let timer
const direction = this.options.direction
this.delay = 50
if (direction === 1) {
if (this.yPos > 0) this.yPos = 0
} else if (direction === 0) {
let h = this.realBoxHeight / 2 * -1
if (this.yPos < h) this.yPos = h
} else if (direction === 2) {
if (this.xPos > 0) this.xPos = 0
} else if (direction === 3) {
let w = this.realBoxWidth * -1
if (this.xPos < w) this.xPos = w
}
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
this.delay = 0
this._move()
}, this.delay)
},
enter () {
if (this.hoverStopSwitch) this._stopMove()
},
leave () {
if (this.hoverStopSwitch) this._startMove()
},
_move () {
// 鼠标移入时拦截_move()
if (this.isHover) return
this._cancle() //进入move立即先清除动画 防止频繁touchMove导致多动画同时进行
this.reqFrame = requestAnimationFrame(
function () {
const h = this.realBoxHeight / 2 //实际高度
const w = this.realBoxWidth / 2 //宽度
let { direction, waitTime } = this.options
let { step } = this
if (direction === 1) { // 上
if (Math.abs(this.yPos) >= h) {
this.$emit('ScrollEnd')
this.yPos = 0
}
this.yPos -= step
} else if (direction === 0) { // 下
if (this.yPos >= 0) {
this.$emit('ScrollEnd')
this.yPos = h * -1
}
this.yPos += step
} else if (direction === 2) { // 左
if (Math.abs(this.xPos) >= w) {
this.$emit('ScrollEnd')
this.xPos = 0
}
this.xPos -= step
} else if (direction === 3) { // 右
if (this.xPos >= 0) {
this.$emit('ScrollEnd')
this.xPos = w * -1
}
this.xPos += step
}
if (this.singleWaitTime) clearTimeout(this.singleWaitTime)
if (!!this.realSingleStopHeight) { //是否启动了单行暂停配置
if (Math.abs(this.yPos) % this.realSingleStopHeight < step) { // 符合条件暂停waitTime
this.singleWaitTime = setTimeout(() => {
this._move()
}, waitTime)
} else {
this._move()
}
} else if (!!this.realSingleStopWidth) {
if (Math.abs(this.xPos) % this.realSingleStopWidth < step) { // 符合条件暂停waitTime
this.singleWaitTime = setTimeout(() => {
this._move()
}, waitTime)
} else {
this._move()
}
} else {
this._move()
}
}.bind(this)
)
},
_initMove () {
this.$nextTick(() => {
const { switchDelay } = this.options
const { autoPlay, isHorizontal } = this
this._dataWarm(this.data)
this.copyHtml = '' //清空copy
if (isHorizontal) {
this.height = this.$refs.wrap.offsetHeight
this.width = this.$refs.wrap.offsetWidth
let slotListWidth = this.$refs.slotList.offsetWidth
// 水平滚动设置warp width
if (autoPlay) {
// 修正offsetWidth四舍五入
slotListWidth = slotListWidth * 2 + 1
}
this.$refs.realBox.style.width = slotListWidth + 'px'
this.realBoxWidth = slotListWidth
}
if (autoPlay) {
this.ease = 'ease-in'
this.delay = 0
} else {
this.ease = 'linear'
this.delay = switchDelay
return
}
// 是否可以滚动判断
if (this.scrollSwitch) {
let timer
if (timer) clearTimeout(timer)
this.copyHtml = this.$refs.slotList.innerHTML
setTimeout(() => {
this.realBoxHeight = this.$refs.realBox.offsetHeight
this._move()
}, 0);
} else {
this._cancle()
this.yPos = this.xPos = 0
}
})
},
_dataWarm (data) {
if (data.length > 100) {
console.warn(`数据达到了${data.length}条有点多哦~,可能会造成部分老旧浏览器卡顿。`);
}
},
_startMove () {
this.isHover = false //开启_move
this._move()
},
_stopMove () {
this.isHover = true //关闭_move
// 防止频频hover进出单步滚动,导致定时器乱掉
if (this.singleWaitTime) clearTimeout(this.singleWaitTime)
this._cancle()
},
},
mounted () {
this._initMove()
},
watch: {
data (newData, oldData) {
this._dataWarm(newData)
//监听data是否有变更
if (!arrayEqual(newData, oldData)) {
this.reset()
}
},
autoPlay (bol) {
if (bol) {
this.reset()
} else {
this._stopMove()
}
}
},
beforeCreate () {
this.reqFrame = null // move动画的animationFrame定时器
this.singleWaitTime = null // single 单步滚动的定时器
this.isHover = false // mouseenter mouseleave 控制this._move()的开关
this.ease = 'ease-in'
},
beforeDestroy () {
this._cancle()
clearTimeout(this.singleWaitTime)
}
}
</script>