原创内容,引用请注明原处
<template>
<div class="template-swiper-move">
<ul
ref="swiper"
class="swiper-list"
:style="{
transform: `translateX(${moveX}px)`,
transitionDuration: `${transDuration}ms`,
}"
@mousedown="handlerDragStart"
@mouseup="handlerMouseUp"
@touchstart="handlerTouchStart"
@touchmove="handlerTouchMove"
@touchend="handlerTouchEnd"
>
<slot></slot>
</ul>
</div>
</template>
模板使用的transform来滑动,注意:使用 transform 和 position 滑动不一样,获取到的dom数据会有不同,例如offsetLeft 在 transform下是无法获得的,同理position下可以获取到offsetLeft
1.事件处理
- pc端使用mousedown 和 mouseup
- 移动端使用touch的整个流程
2.使用变量
props: {
dataList: {
type: Array,
default: () => [],
},
},
data() {
return {
cellList: [], // 记录子集
moveX: 0, // 移动量
widthGap: 0, // 实际可滚动距离(左边界为0, widthGap为右边界)
oldMoveX: 0, // 上一次滑动的位置
oldx: 0, // 当前摁下的鼠标位置
transDuration: 0, // 设置动画时长
cellLeft: [], // 记录所有cell 的offsetLeft 值
}
},
3. 功能代码
handlerDragStart(event) {
const swiper = this.$refs.swiper
const scrollWidth = swiper.scrollWidth
this.widthGap = scrollWidth - this.$refs.swiper.clientWidth// 可滚动范围
this.oldMoveX = this.moveX // oldMoveX 是上一次滑动留下的距离
this.oldx = event.layerX
window.addEventListener('mousemove', this.handlerDragMove)
},
鼠标摁下,求得滚动范围,以及上一次滚动距离,当前鼠标摁下的距离,并且像window 添加鼠标滑动事件
handlerDragMove(event) {
const slideGap = event.layerX - this.oldx
this.moveX = this.oldMoveX + slideGap
},
鼠标滑动记录,当前滑动的距离,并且给偏移量moveX 赋值,
- 用鼠标滑动停止的距离layerX 减去 鼠标摁下时的即时位置, 获取鼠标实际滑动的距离,
- 实际滑动距离,加上上次移动的距离,等于最终滑动的偏移量
handlerMouseUp() {
window.removeEventListener('mousemove', this.handlerDragMove)
this.slideEnd()
},
// 滑动后结束处理
slideEnd() {
new Promise((resolve, reject) => {
this.transDuration = 500
resolve()
}).then(() => {
this.checkCenterCell()
setTimeout(() => {
this.transDuration = 0
}, this.transDuration)
})
},
// 查看当前,最近的cell
checkCenterCell() {
// moveX代表了显示容器的左侧实时位置, area 代表右侧
const area = this.$refs.swiper.clientWidth - this.moveX
const cell = this.cellList[0].elm.clientWidth
// 比较显示容器里
const left = this.cellLeft.reduce((prev, next) => {
// 显示容器左侧到prev右侧,
const a = prev + cell - Math.abs(this.moveX)
// 显示容器右侧到next左侧
const b = area - next
// b<0 a<0 排除掉离当前滑动距离太远的元素,否则会出现负大于正的情况
if (b < 0) {
return prev
} else if (a < 0) {
return next
} else if (Math.abs(a) > Math.abs(b)) { // 判断可用区域大小
return prev
} else {
return next
}
})
let index = 0
this.cellLeft.forEach((v, i) => {
if (v === left) {
index = i
}
})
const space = this.$refs.swiper.clientWidth - this.cellList[index].elm.clientWidth) / 2
// 传递修改标志位
this.$emit('changeActive', index)
/*
1.第一种可能,划出左右最大范围
2.第二种可能,在正常区域内滑动
*/
// 当移动,划出最大显示范围时,回弹至正常区域
if (this.moveX > 0) {
this.moveX = space - this.cellList[0].elm.offsetLeft
} else if (this.moveX < -this.widthGap) {
this.moveX =
space - this.cellList[this.cellList.length - 1].elm.offsetLeft
}
// 遍历当前子元素集,谁的距离,离当前滑动距离最近,居中回弹,获取其下标
this.moveX = space - this.cellList[index].elm.offsetLeft
},
- slideEnd()方法是添加一个动画效果, 滑动时不需要执行时间,所以duration设置为0 但当元素回弹时,需要动画效果,这是通过异步设置一个执行时间。
- checkCenterCell() 寻找一个最近的需要回弹的元素,通过reduce循环,判断出当前有效距离最大的元素,确定元素的下标,进行回弹
- checkCenterCell() 中的space常量,是保证子元素在页面居中,所以需要左右边距一致,这是就需要去计算他的宽度
4.css
<style lang="scss" scoped>
.template-swiper-move {
overflow: hidden;
.swiper-list {
transition-property: transform;
transition-timing-function: ease;
display: flex;
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */
-khtml-user-select: none; /* Konqueror */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none;
}
}
</style>
页面是通过slot接受子元素,子元素样式由各页面自定
5.移动端
移动端与pc代码主流程一致,touchmove对应 mousemove, 移动端获取event的距离有所不同,需要event.targetTouches[0]
6.完整js代码
<script>
export default {
props: {
isCircle: {
type: Boolean,
default: false,
},
dataList: {
type: Array,
default: () => [],
},
active: {
type: Number,
default: 0,
},
},
data() {
return {
cellList: [], // 记录子集
moveX: 0, // 移动量
widthGap: 0, // 实际可滚动距离(左边界为0, widthGap为右边界)
oldMoveX: 0, // 上一次滑动的位置
oldx: 0, // 当前摁下的鼠标位置
transDuration: 0, // 设置动画时长
cellLeft: [], // 记录所有cell 的offsetLeft 值
}
},
watch: {
dataList: {
handler(val) {
if (val.length) {
this.$nextTick(() => {
this.handelrReady()
})
}
},
immediate: true,
},
},
mounted() {
window.addEventListener('resize', this.checkCenterCell)
// 隐藏状态下获取数据都是0, 当尺寸变化时,重新获取一次
window.addEventListener('resize', this.handelrReady)
},
beforeDestroy() {
window.removeEventListener('resize', this.checkCenterCell)
window.removeEventListener('resize', this.handelrReady)
},
methods: {
handelrReady() {
// 获取slot传递过来的li元素
this.cellList = this.$slots.default
// this.cellLeft = this.$slots.default.map(
// (v) => v.elm.offsetLeft - v.elm.clientWidth / 2
// )
this.cellLeft = this.$slots.default.map((v) => v.elm.offsetLeft)
this.checkCenterCell()
},
// 拖拽
handlerDragStart(event) {
const swiper = this.$refs.swiper
const scrollWidth = swiper.scrollWidth
this.widthGap = scrollWidth - this.$refs.swiper.clientWidth // 可滚动范围
this.oldMoveX = this.moveX // oldMoveX 是上一次滑动留下的距离
this.oldx = event.layerX
window.addEventListener('mousemove', this.handlerDragMove)
},
handlerDragMove(event) {
const slideGap = event.layerX - this.oldx
this.moveX = this.oldMoveX + slideGap
},
handlerMouseUp() {
window.removeEventListener('mousemove', this.handlerDragMove)
this.slideEnd()
},
handlerTouchStart(event) {
const swiper = this.$refs.swiper
const scrollWidth = swiper.scrollWidth
this.widthGap = scrollWidth - this.$refs.swiper.clientWidth // 可滚动范围
this.oldMoveX = this.moveX // offsetleft 是上一次滑动留下的距离
this.oldx = event.targetTouches[0].screenX // 当前点击的位置
},
handlerTouchMove(event) {
const slideGap = event.targetTouches[0].screenX - this.oldx
this.moveX = this.oldMoveX + slideGap
},
handlerTouchEnd() {
const obj = {}
for (const attr in this.$refs.swiper) {
obj[attr] = this.$refs.swiper[attr]
}
this.slideEnd()
},
// 滑动后结束处理
slideEnd() {
new Promise((resolve, reject) => {
this.transDuration = 500
resolve()
}).then(() => {
this.checkCenterCell()
setTimeout(() => {
this.transDuration = 0
}, this.transDuration)
})
},
// 查看当前,最近的cell
checkCenterCell() {
const area = this.$refs.swiper.clientWidth - this.moveX // moveX代表了显示容器的左侧实时位置, area 代表右侧
const cell = this.cellList[0].elm.clientWidth
// 比较显示容器里
const left = this.cellLeft.reduce((prev, next) => {
const a = prev + cell - Math.abs(this.moveX) // 显示容器左侧到prev右侧,
const b = area - next
if (b < 0) { // b<0 a<0 排除掉离当前滑动距离太远的元素,否则会出现负大于正的情况
return prev
} else if (a < 0) {
return next
} else if (Math.abs(a) > Math.abs(b)) { // 判断可用区域
return prev
} else {
return next
}
})
let index = 0
this.cellLeft.forEach((v, i) => {
if (v === left) {
index = i
}
})
const space =
(this.$refs.swiper.clientWidth - this.cellList[index].elm.clientWidth) / 2
// 传递修改标志位
this.$emit('changeActive', index)
/*
1.第一种可能,划出左右最大范围
2.第二种可能,在正常区域内滑动
*/
// 当移动,划出最大显示范围时,回弹至正常区域
if (this.moveX > 0) {
this.moveX = space - this.cellList[0].elm.offsetLeft
} else if (this.moveX < -this.widthGap) {
this.moveX =
space - this.cellList[this.cellList.length - 1].elm.offsetLeft
}
// 遍历当前子元素集,谁的距离,离当前滑动距离最近,居中回弹,获取其下标
this.moveX = space - this.cellList[index].elm.offsetLeft
},
},
}
</script>