在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 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; /*webkit浏览器*/ -ms-user-select:none; /*IE10*/ -khtml-user-select:none; /*早期浏览器*/ user-select:none; } } </style>
以上是相关内容,请大家多多关注。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/127703.html
摘要:全家桶仿腾讯体育一年一度的总决赛,相信球迷用的最多的就是腾讯体育这款,刚好上手,当练手就把这个仿下来。这样刚进去的时候页面加载时间明显减短。可以包含任意异步操作。 Vue2.0全家桶仿腾讯体育APP 一年一度的NBA总决赛,相信球迷用的最多的就是腾讯体育这款APP,刚好上手Vue,当练手就把这个APP仿下来。 showImg(https://segmentfault.com/img/r...
摘要:最近接触移动端开发。自己写一个类似微博的图片预览器来学习一下移动端手势的实现和的属性的使用。在发生时将坐标位置向减得到位移量。双手势单手势双手势单手势触发移动事件图片放大功能的实现我采用了的属性进行缩放,并且设置来设置缩放中心位置。 最近接触vue.js移动端开发。自己写一个类似微博的图片预览器来学习一下移动端手势的实现和css3的属性的使用。 目标分析 首先分析图片预览器的功能: 1...
移动端拖拽悬浮按钮如何用vue实现,下面看看具体内容: 功能介绍: 开发中,要考虑用户体验,悬浮按钮不仅要显示在侧边,更是可以允许随意拖拽换位。 需求描述: 1、按钮悬浮显示在页面侧边; 2、当手指长按左键按钮,即可允许拖拽改变位置; 3、按钮移动结束,手指松开,计算距离左右两侧距离并自动移动至侧边显示; 4、移动至侧边后,按钮根据具体左右两次位置判断改变现实样式; 整体思路:...
摘要:先看本次文章先实现内容拖拽和滑动动画后续文章一步一步增加功能比如滚动条下拉加载等功能说点湿的其实代码量挺大的近行还有另一个类似的库他的代码量和差不多因为原理都是一样的阅读他们的代码发现里面很多逻辑其实都是在做手势判断比如拖拽和划还有部分元 showImg(https://segmentfault.com/img/remote/1460000018779771?w=914&h=129);...
摘要:注意点在鼠标操作拖放期间,有一些事件可能触发多次,比如和。可拖拽元素,建议使用,设定可拖拽元素的鼠标游标,提升交互。在中使用拖拽中使用可以直接绑定到组件上。 什么是 Drag and Drop (拖放)? 简单来说,HTML5 提供了 Drag and Drop API,允许用户用鼠标选中一个可拖动元素,移动鼠标拖放到一个可放置到元素的过程。 我相信每个人都或多或少接触过拖放,比如浏览...
阅读 498·2023-03-27 18:33
阅读 706·2023-03-26 17:27
阅读 606·2023-03-26 17:14
阅读 575·2023-03-17 21:13
阅读 498·2023-03-17 08:28
阅读 1752·2023-02-27 22:32
阅读 1259·2023-02-27 22:27
阅读 2065·2023-01-20 08:28