vue如何实现移动端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:rel="nofollow noopener noreferrer" 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:rel="nofollow noopener noreferrer" 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; /*webkit浏览器*/
   -ms-user-select:none; /*IE10*/
   -khtml-user-select:none; /*早期浏览器*/
   user-select:none;
  }
 }
</style>

以上就是vue如何实现移动端touch拖拽排序。生活是一棵大树,我就是一片小树叶,我摇啊摇,我真快乐。更多关于vue如何实现移动端touch拖拽排序请关注haodaima.com其它相关文章!

标签: 实现 拖拽