vue实现移动端touch拖拽排序
目录
- 功能介绍:
- 大致需求:
- 整体思路:
- 简单效果展示:
- 具体实现:
- 一、display:flex+v-for布局:
- 二、touch事件绑定:
- 三、卡片移动:
- 四、获取手指所在位置:
- 五、操作数组(删除或插入元素):
- 六、手指离开屏幕:
- 七、备注:
- 八、完整代码:
本文实例为大家分享了vue实现移动端touch拖拽排序的具体代码,供大家参考,具体内容如下
功能介绍:
在移动端开发中,希望实现类似支付宝应用管理页面的可拖拽排序交互。
大致需求:
1、卡片按照一定顺序排序,超出横向范围换行显示;
2、手指长按卡片,可进行拖拽控制,卡片追随手指移动;
3、卡片移动到相应位置,该位置上的卡片向后或向前更换位置,当前位置空出;
4、松开手指,卡片可回到原位置或新位置进行展示;
整体思路:
1、卡片实行flex弹性布局,通过数组的遍历可自动显示在相应位置;
2、手指长按可使用定时器来判断,若手指松开,则关闭定时器,等待下次操作再启用;
3、跟随手指移动的卡片可使用absolute定位控制,同时根据手指位置判断当前所在位置;
4、位置发生改变时,控制数组添加或删除相应元素,从而实现换位效果;
简单效果展示:
具体实现:
一、display:flex+v-for布局:
使用弹性布局实现
<!-- 外层ul控制卡片范围 -->
<ul>
<li class="libox" v-for="(item, ind) in list" :key="ind">
<div>
<!-- div显示数组内容 -->
{{item.name}}
</div>
</li>
</ul>
data() {
return {
list: [
{ name: '1' }, // 卡片内容
{ name: '2' },
{ name: '3' }
]
}
},
ul {
width: 100%;
height: 100%;
display: flex; // 弹性布局
flex-wrap: wrap;
overflow: hidden; // 超出部分隐藏,目的阻止横向滚动
.libox {
width: 25%; // 这里以4列为例
height: 70px;
>div {
background-color:#eee;
width: calc(100% - 10px);
height: 36px;
border-radius: 18px;
}
}
}
二、touch事件绑定:
应用到touchstart,touchmove,touchend事件,使用定时器实现长按效果:
<div
@touchstart="touchstart($event, item)"
@touchmove="touchMove($event, item)"
@touchend="touchEnd($event, item)"
>
{{item.name}}
</div>
data() {
return {
timeOutEvent: 0
};
},
methods: {
// 手指触摸事件
touchstart(ev, item) {
// 定时器控制长按时间,超过500毫秒开始进行拖拽
this.timeOutEvent = setTimeout(() => {
this.longClick = 1;
}, 500);
},
// 手指在屏幕上移动
touchMove(ev) {
// 未达到500毫秒就移动则不触发长按,清空定时器
clearTimeout(this.timeOutEvent);
},
// 手指离开屏幕
touchEnd() {
clearTimeout(this.timeOutEvent);
}
}
三、卡片移动:
在ul中增加一个独立的不在循环中的li标签,改为absolute定位,通过动态修改li标签top、left属性实现跟随手指移动效果。
<ul>
<li v-show="selectItem.name" class="selectBox" ref="selectBox">
{{selectItem.name}}
</li>
</ul>
ul {
position: relative;
// 此li标签的样式与循环li标签内的div样式保持一致
// 背景色加深,代表被手指选中
.selectBox {
position: absolute;
width: calc(25% - 10px);
height: 36px;
border-radius: 18px;
background-color:#6981c8;
color:white;
}
}
当卡片被选中,将卡片内容赋值给全局变量,判断卡片显示隐藏(v-show判断,隐藏但占位),实现选中元素位置空出效果:
手指位置通过touchmove获取:
<div
@touchstart="touchstart($event, item)"
@touchmove="touchMove($event, item)"
@touchend="touchEnd($event, item)"
@click="listClickHandler(item)"
v-show="item.name !== selectItem.name"
>
{{item.name}}
</div>
touchstart(ev, item) {
this.timeOutEvent = setTimeout(() => {
this.longClick = 1;
this.selectItem = item; // 将卡片内容赋值给全局变量
const selectDom = ev.target; // li元素
// 元素初始位置
this.oldNodePos = {
x: selectDom.offsetLeft,
y: selectDom.offsetTop
};
// 鼠标原始位置
this.oldMousePos = {
x: ev.touches[0].pageX,
y: ev.touches[0].pageY
};
const lefts = this.oldMousePos.x - this.oldNodePos.x; // x轴偏移量
const tops = this.oldMousePos.y - this.oldNodePos.y; // y轴偏移量
const { pageX, pageY } = ev.touches[0]; // 手指位置
this.$refs.selectBox.style.left = `${pageX - lefts}px`;
this.$refs.selectBox.style.top = `${pageY - tops}px`;
}, 500);
},
touchMove(ev) {
clearTimeout(this.timeOutEvent);
// this.longClick === 1判断是否长按
if (this.longClick === 1) {
const selectDom = ev.target.parentNode; // li元素
const lefts = this.oldMousePos.x - this.oldNodePos.x; // x轴偏移量
const tops = this.oldMousePos.y - this.oldNodePos.y; // y轴偏移量
const { pageX, pageY } = ev.touches[0]; // 手指位置
this.$refs.selectBox.style.left = `${pageX - lefts}px`;
this.$refs.selectBox.style.top = `${pageY - tops}px`;
}
}
四、获取手指所在位置:
cardIndex(selDom, moveleft, movetop) {
const liWid = selDom.clientWidth; // li宽度
const liHei = selDom.clientHeight; // li高度
const newWidNum = Math.ceil((moveleft / liWid)); // 手指所在列
const newHeiNum = Math.ceil((movetop / liHei)); // 手指所在行
const newPosNum = (newHeiNum - 1) * 4 + newWidNum; // 手指所在位置
// 判断是否是新位置并且没有超出列表数量范围
if (this.oldIndex !== newPosNum &&
newPosNum <= this.list.length) {
// 将新的位置赋值给全局变量oldIndex
this.oldIndex = newPosNum;
}
}
五、操作数组(删除或插入元素):
监听oldIndex的值,若发生改变则执行操作数组函数
watch: {
oldIndex(newVal) {
const oldIndex = this.list.indexOf(this.selectItem);
this.list.splice(oldIndex, 1);
this.list.splice(newVal - 1, 0, this.selectItem);
}
},
六、手指离开屏幕:
手指离开屏幕,清空选中的元素selectItem,跟随手指移动的卡片(li.selectBox)自动隐藏,在循环中隐藏的卡片(li)则会显示,实现换位效果。
touchEnd() {
clearTimeout(this.timeOutEvent);
this.selectItem = {};
}
七、备注:
上面的代码是基于div容器内只有文字没有其他dom元素实现,后发现若div中存在dom元素例如svg,则【$event】选中的值会变成其子元素,且拖拽排序出现问题,希望知道原因的小伙伴可以评论或私信告诉我一下,非常感谢。
粗暴的解决方式:
div容器增加after蒙版,可设置为透明色:
div
position: relative;
&::after {
content: '';
width: 100%;
height: 100%;
background: rgba(255, 177, 177, 0.3); // 背景色
position: absolute;
top: 0;
left: 0;
}
}
八、完整代码:
<template>
<div>
<ul>
<li
class="libox"
v-for="(item, index) in list"
:key="index"
:id="'card' + (index + 1)"
>
<div
@touchstart="touchstart($event, item)"
@touchmove="touchMove($event, item)"
@touchend="touchEnd($event, item)"
v-show="item.name !== selectItem.name"
>
{{item.name}}
<svg class="icon svg-icon" aria-hidden="true">
<use :xlink:href="item.icon" rel="external nofollow" ></use>
</svg>
</div>
</li>
<li v-show="selectItem.name" class="selectBox" ref="selectBox">
{{selectItem.name}}
<svg class="icon svg-icon" aria-hidden="true">
<use :xlink:href="selectItem.icon" rel="external nofollow" ></use>
</svg>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
// 列表数据
list: [
{ name: '1', selected: true, icon: '#icon-mianxingbenzivg' },
{ name: '2', selected: true, icon: '#icon-mianxingchizi' },
{ name: '3', selected: true, icon: '#icon-mianxingdiannao' },
{ name: '4', selected: true, icon: '#icon-mianxingdayinji' },
{ name: '5', selected: true, icon: '#icon-mianxingdingshuqi' },
{ name: '6', selected: true, icon: '#icon-mianxingheiban' },
{ name: '7', selected: true, icon: '#icon-mianxinggangbi' },
{ name: '8', selected: true, icon: '#icon-mianxingboshimao' },
{ name: '9', selected: true, icon: '#icon-mianxingjisuanqi' },
{ name: '10', selected: true, icon: '#icon-mianxinghuaxue' },
{ name: '11', selected: true, icon: '#icon-mianxingqianbi' },
{ name: '12', selected: true, icon: '#icon-mianxingshubao' },
{ name: '13', selected: true, icon: '#icon-mianxingshuicaibi' },
{ name: '14', selected: true, icon: '#icon-mianxingtushu' },
],
// 选中元素内容
selectItem: {},
timeOutEvent: 0,
oldNodePos: {
x: 0,
y: 0,
},
oldMousePos: {
x: 0,
y: 0
},
oldIndex: 0,
// 长按标识
longClick: 0
};
},
watch: {
oldIndex(newVal) {
const oldIndex = this.list.findIndex(r=> r.name === this.selectItem.name);
this.list.splice(oldIndex, 1);
this.list.splice(newVal, 0, this.selectItem);
}
},
methods: {
touchstart(ev, item) {
this.longClick = 0;
const that = this;
const selectDom = ev.currentTarget; // div元素
this.timeOutEvent = setTimeout(() => {
that.longClick = 1;
that.selectItem = item;
// 元素初始位置
that.oldNodePos = {
x: selectDom.offsetLeft,
y: selectDom.offsetTop
};
// 鼠标原始位置
that.oldMousePos = {
x: ev.touches[0].pageX,
y: ev.touches[0].pageY
};
const lefts = that.oldMousePos.x - that.oldNodePos.x; // x轴偏移量
const tops = that.oldMousePos.y - that.oldNodePos.y; // y轴偏移量
const { pageX, pageY } = ev.touches[0]; // 手指位置
that.$refs.selectBox.style.left = `${pageX - lefts}px`;
that.$refs.selectBox.style.top = `${pageY - tops}px`;
}, 500);
},
touchMove(ev) {
clearTimeout(this.timeOutEvent);
const selectDom = ev.currentTarget.parentNode; // li元素
if (this.longClick === 1) {
const lefts = this.oldMousePos.x - this.oldNodePos.x; // x轴偏移量
const tops = this.oldMousePos.y - this.oldNodePos.y; // y轴偏移量
const { pageX, pageY } = ev.touches[0]; // 手指位置
this.$refs.selectBox.style.left = `${pageX - lefts}px`;
this.$refs.selectBox.style.top = `${pageY - tops}px`;
this.cardIndex(selectDom, pageX, pageY);
}
},
touchEnd() {
clearTimeout(this.timeOutEvent);
this.selectItem = {};
},
cardIndex(selDom, moveleft, movetop) {
const liWid = selDom.clientWidth;
const liHei = selDom.clientHeight;
const newWidthNum = Math.ceil((moveleft / liWid)); // 哪一列
const newHeightNum = Math.ceil((movetop / liHei)); // 哪一行
const newPositionNum = (newHeightNum - 1) * 4 + newWidthNum;
if (this.oldIndex !== newPositionNum - 1) {
if (newPositionNum <= this.list.length) {
this.oldIndex = newPositionNum - 1;
} else {
this.oldIndex = this.list.length - 1;
}
}
}
}
}
</script>
<style lang="scss" scoped>
@mixin myFlexCenter{
display: flex;
justify-content: center;
align-items: center;
}
ul {
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
position: relative;
overflow: hidden;
.libox {
width: 25%;
height: 100px;
border-right: 1px dashed #cccccc;
border-bottom: 1px dashed #cccccc;
box-sizing: border-box;
@include myFlexCenter;
>div {
width: calc(100% - 10px);
height: 75px;
border-radius: 18px;
@include myFlexCenter;
position: relative;
&::after {
content: '';
width: 100%;
height: 100%;
background: rgba(255, 177, 177, 0.3);
position: absolute;
top: 0;
left: 0;
}
>svg {
width: 75px;
height: 75px;
}
}
}
.selectBox{
position: absolute;
width: calc(25% - 10px);
height: 75px;
border-radius: 18px;
>svg {
width: 75px;
height: 75px;
}
background-color: rgba(0, 0, 0, 0.1);
color:white;
@include myFlexCenter;
-moz-user-select:none;
-webkit-user-select:none;
-ms-user-select:none;
-khtml-user-select:none;
user-select:none;
}
}
</style>
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程网。
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341