十九.uniapp之实现拖拽排序

3,678 阅读4分钟

本文正在参加「金石计划」

前言

最近做uniapp项目的时候需要实现拖拽排序功能,本来准备直接用 touchstarttouchmovetouchend 三剑客去实现的,但无意中发现uniapp内置了 movable-areamovable-view组件,可快速实现该功能,索性就直接使用该组件,后续对拖拽排序做了一些扩展,希望对你有帮助。

准备

在开发之前需要先对movable-areamovable-view组件有一定的了解。

movable-area

可拖动的范围,可以理解为画布大小,元素在该区域内进行拖动。宽高必给,不然默认为10px

movable-view

可移动的视图容器,在页面中可以拖拽滑动或双指缩放。宽高必给,不然默认为10px

该组件就是每一个拖拽的容器,必须在movable-area组件内部,否则无法拖拽。它有一些常用参数和方法,这里我们主要了解下:xydirectiondampinganimation以及change方法。

  • x: 即在画布中所处的left位置;
  • y: 即在画布中所处的top位置;
  • direction: 容器可移动的位置,可选:all(可任意移动)、vertical(垂直移动)、horizontal(水平移动)、none(不可移动)。
  • damping: 设置移动的速度;
  • animation: 动画;
  • change方法:移动中的回调。

实现思路

  1. 通过 touchstartlongpress 获取到当前拖拽的元素。
  2. 通过 movable-viewchange方法,获取当前的拖动的距离,这里的change返回的值是经过movable-view处理过后的,是相对touchstart移动的值,不需要我们再额外去减去拖拽开始的位置。然后根据当前的位置去计算当前所在的 moveIndex, 然后修改对应的容器的 xy
  3. 通过touchend,获取最后拖拽完的list,值重置和同步父组件list

简单实现

# tempalte
<movable-area style="width:100%;height:300px">
        <movable-view
                v-for="(item, index) in list"
                direction="all"
                :key="item.key"
                :x="0"
                :y="item.y"
                @touchstart="handleDragStart(index)"
                @change="handleMoving"
                @touchend="handleDragEnd"
        >
               {{item.title}}
        </movable-view>
</movable-area>

export default{
    data(){
       return {
         activeIndex:-1,
         moveToIndex:-1,
         list:[],
         oldIndex:-1,
         cloneList:[],
         itemHeight:50
       }
    },
    methods:{
      initList(list=[]){
        const newList = this.deepCopy(list);
        this.list = newList.map((item, index) => {
            return {
                ...item,
                y:index*this.itemHeight,
                key: Math.random() + index
            };
        });
        //拷贝一份初始list值
        this.cloneList = this.deepCopy(this.list);
      },
      

        //拖拽开始
        handleDragStart(index) {
            this.activeIndex = index;
            this.oldIndex = index;
        },

        //拖拽中
        handleMoving(e) {},

        //拖拽结束
        handleDragEnd(e) {},
      
      //简单实现深拷贝。
      deepCopy(val){
        return JSON.parse(JSON.stringify(source));
      }
    },
    watch: {
        value: {
            handler() {
                    this.initList(this.value);
            },
            immediate: true,
            deep: true
        }
    }
    
}

上面基本实现了拖拽的基本结构,现在来实现核心逻辑 moveend 方法。

move方法

触发change方法存在以下几种情况:touch 移动、touch-out-of-bounds 超出移动范围、out-of-bounds 超出移动范围后的回弹、friction 惯性

handleMoving(e){
  if (e.detail.source !== 'touch') return;
  const { x, y } = e.detail;
  const currentY = Math.floor((y + this.itemHeight / 2) / this.itemHeight);
  
  this.moveToIndex = Math.min(currentY, this.list.length - 1);

  //更新移动后的位置
  if (this.oldIndex !== this.moveToIndex && this.oldIndex !== -1 && this.moveToIndex !== -1) {
    const newList = this.deepCopy(this.cloneList);
    //交换位置
    newList.splice(this.moveToIndex, 0, ...newList.splice(this.activeIndex, 1));

    this.list.forEach((item, index) => {
       if (index !== this.activeIndex) {
         const itemIndex = newList.findIndex(val => val[this.itemKey] === item[this.itemKey]);
         item.y=itemIndex*this.itemHeight
       }
    });
    this.oldIndex = this.moveToIndex;
  }
}

end方法

handleDragEnd(e) {

if (this.moveToIndex !== -1 && this.activeIndex !== -1 && this.moveToIndex !== this.activeIndex) {
      this.cloneList.splice(this.moveToIndex, 0, ...this.cloneList.splice(this.activeIndex, 1));
}

//重新排序下更新后的位置。
this.initList(this.cloneList);
const endList = this.list.map(item => this.omit(item, ['y', 'key']));
this.$emit('input', endList);
this.$emit('end', endList);

this.activeIndex = -1;
this.oldIndex = -1;
this.moveToIndex = -1;
},

实现效果

5.gif

多列拖拽

上述是单列拖拽的方法,仅仅只需要控制y的值即可,但是如果是多列拖拽,还需考虑x的值,上述方法稍微小改造一下。 同时还需定义一个每行显示数量的字段column

initList(list = []) {
    const newList = this.deepCopy(list);
        this.list = newList.map((item, index) => {
            return {
                ...item,
                //getItemWidth 容器的宽度
                x: (index % this.column) * this.getItemWidth,
                //getItemHeight 容器的高度
                y:Math.floor(index / this.column) * this.getItemHeight,
                key: Math.random() + index
            };
        });
        //拷贝一份初始list值
        this.cloneList = this.deepCopy(this.list);
},



handleMoving(e){
    const currentX = Math.floor((x + this.getItemWidth / 2) / this.getItemWidth);
    const currentY = Math.floor((y + this.getItemHeight / 2) / this.getItemHeight);
    this.moveToIndex = Math.min(currentY * this.column + currentX, this.list.length - 1);
    ...其他代码
}

实现效果

5.gif

列表滚动

如果列表是长列表,需要滚动的话,可以外面包裹一层scrollView标签,然后通过移动的时候修改scrollTop的值,去实现长列表拖动。

<scroll-view scroll-y  :scroll-top="scrollTop" @scroll="handleScroll">
   ...其他代码
</scroll-view>

//js
handleScroll(e) {
   this.scrollTop = e.detail.scrollTop;
},

// move的时候添加一个 scrollIntoView 方法
scrollIntoView() {
        if (this.height === 'auto') return;
        const { height, moveToIndex, getItemHeight, scrollTop } = this;
        if ((moveToIndex + 1) * this.getItemHeight >= scrollTop + parseFloat(height)) {
                this.scrollTop = Math.min(parseFloat(this.getAreaStyle.height), scrollTop + Math.ceil(moveToIndex / 2) * this.getItemHeight);
        } else if ((moveToIndex - 1) * this.getItemHeight <= scrollTop) {
                this.scrollTop = Math.max(0, scrollTop - Math.ceil(moveToIndex / 2) * this.getItemHeight);
        }
},

使用说明

对于上面功能,已经封装成了一个组件,有兴趣可以看看。

参数说明

参数说明类型可选值默认值
value / v-model绑定值array-[]
column每行展示数量number-3
width拖拽容器的宽度string-100%
height拖拽容器的高度,若不传则默认根据column和每个盒子的高度自动生成stringauto
itemKey唯一key,必传string--
itemHeight每个拖拽盒子的高度string-100px
direction可拖拽方向,具体看movable-viewstringall/vertical/horizontal/noneall
damping阻尼系数,用于控制x或y改变时的动画和过界回弹的动画,值越大移动越快number-20

使用方法

    <basic-drag v-model="list" :column="1" itemHeight="50px" itemKey="title">
            <template #item="{element}">
                    <view class="drag-item">{{ element.title }}</view>
            </template>
    </basic-drag>

由于微信小程序的特性,所以导致 movable-view for中嵌套插槽,会导致显示更新问题,所以微信小程序不能使用插槽,其他平台是没问题的。

最后

拖拽组件源代码放在uniapp插件社区了,有兴趣可以下载下来看看,希望能对你有帮助,如果对该组件有问题,可以评论区留言。

地址:uniapp拖拽排序 - DCloud 插件市场

其他文章