1、需求背景
产品需要在购物车加一个左右滑动更多的功能,由于是PC端,大致扫描了下使用的UI库,貌似没有单独提供此类组件,反正有时间,就自己造一个轮子试试
2、先看效果
大致有一个橡皮筋的效果,可能没那么细致,凑合着用吧
3、思路分析
由于添加功能,所以最好不动以前代码,那么自然就想到单独封一个带插槽的组件,由上图效果我们可以大致得出几点
- 可分为三部,left、content、right,自然需要提供三个插槽
- 滑动方向,这个就比较简单了,记录一个鼠标按下初始位置,监听移动事件,同时计算移动位置与初始值之差,就可以得出移动方向
- 动画效果,我们可以使用css过渡实现,包括回弹效果
4、html结构确定
<template>
<div class="swiper-cell-box"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseout="onMouseUp"
@mouseup="onMouseUp"
@touchstart="onMouseDown"
@touchmove="onMouseMove"
@touchend="onMouseUp">
<div class="wrapper" :style="getTransformStyle" >
<div class="left" ref="left" >
<slot name="left" ></slot>
</div>
<div class="content">
<slot name="content" ></slot>
</div>
<div class="right" ref="right" >
<slot name="right" ></slot>
</div>
</div>
</div>
</template>
我们第一个div块,用于我们事件监听(可以看到兼容了移动端),内部再用一个div,用于实现左右平移,最后内部就是三个插槽了
5、实现之滑动方向确定
这个,就不在累赘,简单代码如下
onMouseDown(e) {
// 记录初始位置、及添加一个正在移动的标识
this.startPageX = e.pageX || e.changedTouches[0].pageX
this.isDragging = true
},
onMouseMove(e) {
if(!this.isDragging) return
const pageX = e.pageX || e.changedTouches[0].pageX
let offset = pageX - this.startPageX
// 确定方向
this.direction = offset < 0 ? 'left' : 'right'
},
onMouseUp() {
if(!this.isDragging) return
// 有开,就有关
this.isDragging = false
this.startPageX = 0
},
6、实现之跟手移动
简单说就是,我边移动,它跟着走,我移动10px,它也得乖乖移动10px,那么问题自然变成了我计算移动的距离,再动态修改css位移量
这一块主要是 移动时的事情
onMouseMove(e) {
if(!this.isDragging) return
const pageX = e.pageX || e.changedTouches[0].pageX
let offset = pageX - this.startPageX
this.direction = offset < 0 ? 'left' : 'right'
// 要动态赋值
this.translateX = offset
},
上述代码,当我们往左滑时,offset 值为负,div就会往左滑动,基本能实现跟手滑,不过一般这种滑动都会有一个最大响应距离,人话就是,不可能dom一直跟随你滑动吧!
所以,左右滑动的最大距离就是左右隐藏两块dom的宽度,这个好解决
data() {
return {
sidesWidth: {
left: 0,
right: 0
}
}
}
mounted() {
// 获取一下,方便做边界值对比
this.$nextTick(() => {
Object.keys(this.sidesWidth).forEach(key => {
this.sidesWidth[key] = this.getWidth(this.$refs[key])
})
})
},
上面代码,我们能实现基本跟手动,同时也引入了边界的限定,但是还有几个小细节,比如:应该是轻轻滑动一下(或者超过明确的响应距离),就应该处于展开或者收起状态,这个就需要在滑动结束时,判断当前滑动的距离,以确定最终的状态
7、实现之滑动最终优化
问题一:最终状态的确定
我们可以通过滑动距离以及当前的状态(展开、收起),来确定最终是展开还是收起
问题二:滑动最大距离的限定
可以在移动时,拿滑动距离与边界值比较,不管是向左还是向右,均不能超过边界值,可以想象成坐标轴(-10,10),最大不能超过10,最小能小于-10
这个逻辑可以用这个公式得出translateX = Math.max(-10, Math.min(10, 当前计算出要位移的距离)),慢慢体会
onMouseMove(e) {
if(!this.isDragging) return
const pageX = e.pageX || e.changedTouches[0].pageX
let offset = pageX - this.startPageX
this.direction = offset < 0 ? 'left' : 'right'
// 将要位移的距离 这里直接加,就不用考虑正负问题
// 假设右边处于收起状态 this.currentX = 50 + 0
// 假设右边处于展开状态(假设滑动方向往右,则offset 为正) this.currentX = 50 + -80 = -30
this.currentX = offset + this.startTranslateX
// 边界限定
const min = Math.min(0, -this.sidesWidth.right)
const max = Math.max(0, this.sidesWidth.left)
this.currentX = Math.max(min, Math.min(max, this.currentX))
this.translateX = this.currentX
},
onMouseUp() {
if(!this.isDragging) return
const offset = Math.abs(Math.abs(this.translateX) - Math.abs(this.startTranslateX))
// 设置展开、收起状态
// 当为收起状态,偏移量大于30,则为展开状态
// 当已经为展开状态时 偏移量小于30,应仍为展开状态
let isExpanded = (!this.startTranslateX && offset > this.offsetValue) || (this.startTranslateX && offset < this.offsetValue)
? true
: false
this.translateX = isExpanded
? Math.sign(this.currentX) * this.sidesWidth[this.direction === 'right' ? 'left' : 'right']
: 0
this.isDragging = false
this.startPageX = 0
},
8、技术总结
左右跟手滑动核心思路是通过计算滑动的距离,动态设置css位移量,这个过程看似简单,但也有几个小细节,比如边界值的限定、弹性效果其实是设置的响应距离、元素移动的距离不是一味的使用偏移量(当处于收起状态时,移动距离 = 偏移量,当处于某一侧展开时,移动距离 = 初始位移距离 + 偏移量)
最后也可以再扩展一些api,比如:手动打开、关闭、以及结束后的回调
完整代码如下
<template>
<div class="swiper-cell-box"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseout="onMouseUp"
@mouseup="onMouseUp"
@touchstart="onMouseDown"
@touchmove="onMouseMove"
@touchend="onMouseUp">
<div class="wrapper" :style="getTransformStyle" >
<div class="left" ref="left" >
<slot name="left" ></slot>
</div>
<div class="content">
<slot name="content" ></slot>
</div>
<div class="right" ref="right" >
<slot name="right" ></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'RetailSiwperCell',
props: {
/** 偏移量 **/
offsetValue: {
type: [Number,String],
default: 30
}
},
data() {
return {
isDragging: false,
startPageX: 0,
translateX: 0,
sidesWidth: {
left: 0,
right: 0
},
direction: '',
startTranslateX: 0,
currentX: 0
}
},
mounted() {
this.$nextTick(() => {
Object.keys(this.sidesWidth).forEach(key => {
this.sidesWidth[key] = this.getWidth(this.$refs[key])
})
})
},
computed: {
getTransformStyle() {
return {
transform: `translate3d(${this.translateX}px, 0px, 0px)`,
'transition-duration': `${this.isDragging ? '0s' : '0.6s'}`
}
}
},
methods: {
onMouseDown(e) {
this.startPageX = e.pageX || e.changedTouches[0].pageX
this.isDragging = true
this.startTranslateX = this.translateX
},
onMouseMove(e) {
if(!this.isDragging) return
const pageX = e.pageX || e.changedTouches[0].pageX
let offset = pageX - this.startPageX
this.direction = offset < 0 ? 'left' : 'right'
// 将要位移的距离
this.currentX = offset + this.startTranslateX
// 边界限定
const min = Math.min(0, -this.sidesWidth.right)
const max = Math.max(0, this.sidesWidth.left)
this.currentX = Math.max(min, Math.min(max, this.currentX))
this.translateX = this.currentX
},
onMouseUp() {
if(!this.isDragging) return
const offset = Math.abs(Math.abs(this.translateX) - Math.abs(this.startTranslateX))
// 设置展开、收起状态
// 当为收起状态,偏移量大于30,则为展开状态
// 当已经为展开状态时 偏移量小于30,应仍为展开状态
let isExpanded = (!this.startTranslateX && offset > this.offsetValue) || (this.startTranslateX && offset < this.offsetValue)
? true
: false
this.translateX = isExpanded
? Math.sign(this.currentX) * this.sidesWidth[this.direction === 'right' ? 'left' : 'right']
: 0
this.isDragging = false
this.startPageX = 0
},
getWidth(el) {
return el.getBoundingClientRect().width || 0
},
/**
* @description: 关闭
* @return {*}
*/
close() {
this.translateX = 0
},
/**
* @description: 打开
* @param {*} position left | right
* @return {*}
*/
open(position = '') {
if(!position) return
const width = this.sidesWidth[position]
this.translateX = position === 'right' ? -width : width
}
}
}
</script>
<style>
.swiper-cell-box{
overflow: hidden;
border: 1px solid #eee;
}
.wrapper{
position: relative;
user-select: none;
}
.left{
position: absolute;
left: 0;
top: 0;
height: 100%;
transform: translateX(-100%);
}
.right{
position: absolute;
right: 0;
top: 0;
height: 100%;
transform: translateX(100%);
}
</style>