本文正在参加「金石计划」
前言
最近做uniapp项目的时候需要实现拖拽排序功能,本来准备直接用 touchstart、touchmove、touchend 三剑客去实现的,但无意中发现uniapp内置了 movable-area和 movable-view组件,可快速实现该功能,索性就直接使用该组件,后续对拖拽排序做了一些扩展,希望对你有帮助。
准备
在开发之前需要先对movable-area和 movable-view组件有一定的了解。
movable-area
可拖动的范围,可以理解为画布大小,元素在该区域内进行拖动。宽高必给,不然默认为10px
movable-view
可移动的视图容器,在页面中可以拖拽滑动或双指缩放。宽高必给,不然默认为10px
该组件就是每一个拖拽的容器,必须在movable-area组件内部,否则无法拖拽。它有一些常用参数和方法,这里我们主要了解下:x
、y
、direction
、damping
、animation
以及change
方法。
- x: 即在画布中所处的left位置;
- y: 即在画布中所处的top位置;
- direction: 容器可移动的位置,可选:
all
(可任意移动)、vertical
(垂直移动)、horizontal
(水平移动)、none
(不可移动)。 - damping: 设置移动的速度;
- animation: 动画;
- change方法:移动中的回调。
实现思路
- 通过 touchstart 或 longpress 获取到当前拖拽的元素。
- 通过 movable-view的
change
方法,获取当前的拖动的距离,这里的change
返回的值是经过movable-view处理过后的,是相对touchstart
移动的值,不需要我们再额外去减去拖拽开始的位置。然后根据当前的位置去计算当前所在的moveIndex
, 然后修改对应的容器的x
和y
。 - 通过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
}
}
}
上面基本实现了拖拽的基本结构,现在来实现核心逻辑 move和 end 方法。
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;
},
实现效果
多列拖拽
上述是单列拖拽的方法,仅仅只需要控制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);
...其他代码
}
实现效果
列表滚动
如果列表是长列表,需要滚动的话,可以外面包裹一层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和每个盒子的高度自动生成 | string | auto | |
itemKey | 唯一key,必传 | string | - | - |
itemHeight | 每个拖拽盒子的高度 | string | - | 100px |
direction | 可拖拽方向,具体看movable-view | string | all/vertical/horizontal/none | all |
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插件社区了,有兴趣可以下载下来看看,希望能对你有帮助,如果对该组件有问题,可以评论区留言。