前言:
1.介绍:
该版本的拖拽排序由于是跨屏的,所有在开始前首先需要解决一个问题,那就是:touchmove事件的默认行为。由于我们在拖拽时,滚动条会受影响导致异常,所以我们需要阻止该事件的默认行为。此时,我们可能会想到使用e.preventDefault来阻止,但是很遗憾,uniapp在js中的e.preventDefault是无效的。那么我们只能通过修饰符的方式来阻止,但是这样会有一个问题存在,那就是我们在没有拖拽的时候页面是需要滚动的,但是加了修饰符后就会导致页面无法滑动。这个问题困扰了我很久,期间也有尝试过其他办法以及使用其他容器,例如scrollView和mobileArea,但是都无法解决该问题。最后终于被我给试出来了┭┮﹏┭┮,那就是使用WXS,在WXS中通过renturn false的方式可以进行阻止,那么我们就可以在里面随心所欲的控制什么时候阻止,什么时候不阻止了。具体的实现方式如下,感兴趣的同学可以先尝试一下。
<template>
<view style="height: 2000px;" @touchmove="handler.touchmove"><view/>
</template>
<wxs module="handler">
var touchmove = function(){
return false;
}
module.exports = {
touchmove: touchmove
}
</wxs>
2.文档:
这里贴一下一篇uniapp关于WXS的文章以及微信小程序WXS语法相关介绍的地址,建议大家先看一下:
1.谜之wxs,uni-app如何用它大幅提升性能 - DCloud问答
具体实现:
1.前置处理:
由于我们需要在WXS中处理longpress、touchmove、touchend事件,期间需要去操作数据列表,以及获取相关的dom数据。所以我们首先需要将.vue文件中的数据与WXS连接起来,此时就需要看文档了O(∩_∩)O哈哈~。如下,这块儿是在微信文档(WXS响应事件 | 微信开放文档)的最下面。
1.1 具体实现方式如下:
<view class="drag-list" :list="dataList" :baseData="baseData" :change:list="handler.listObserver" :change:baseData="handler.baseDataObserver">
</view>
<wxs module="handler">
var listObserver = function(newVal, oldVal, ownerInstance, ins) {
}
var baseDataObserver = function(newVal, oldVal, ownerInstance, ins) {
}
module.exports = {
listObserver: listObserver,
baseDataObserver: baseDataObserver,
}
</wxs>
1.2 数据处理、获取:
<script setup>
import { ref, reactive, toRefs, getCurrentInstance, onMounted, nextTick } from "vue";
const props = defineProps({
list: {
type: Array,
default: () => [],
},
columns: {
type: Number,
default: 1,
},
itemHeight: {
type: Number,
default: 0,
},
})
const { list, columns, itemHeight } = toRefs(props);
const currentInstance = getCurrentInstance();
let dataList = ref([]),
rows = ref(0), // 行数-计算获得
warpStyle = ref(""), // 容器样式-计算获得
baseData = reactive({});
const getSystemInfoAndDomInfo = () => {
// 设备信息
const {
windowWidth,
windowHeight,
platform
} = uni.getSystemInfoSync();
baseData.windowWidth = windowWidth;
baseData.windowHeight = windowHeight;
baseData.platform = platform;
// DOM信息
const query = uni.createSelectorQuery().in(currentInstance.proxy);
query.select(".drag-item").boundingClientRect();
query.select(".drag-list").boundingClientRect();
query.exec((res) => {
baseData.itemWidth = res[0].width;
baseData.itemHeight = res[0].height;
baseData.wrapLeft = res[1].left;
baseData.wrapTop = res[1].top;
})
};
const init = () => {
key = 0;
dataList.value = list.value.map((item : any, index) => {
item.data = { ...item };
item.realKey = key++;
item.sortKey = index; // 整体顺序
item.tranX = `${(item.sortKey % columns.value) * 100}%`;
item.tranY = `${Math.floor(item.sortKey / columns.value) * 100}%`;
return item;
});
rows.value = Math.ceil(list.value.length / columns.value);
baseData.rows = rows.value;
baseData.columns = columns.value;
warpStyle.value = `height: ${rows.value * itemHeight.value}rpx`
nextTick(() => {
getSystemInfoAndDomInfo();
})
};
onMounted(() => {
init();
})
</script>
1.3 在WXS中处理对应数据
var listObserver = function(newVal, oldVal, ownerInstance, ins) { // getState()返回一个object对象,当有局部变量需要存储起来后续使用的时候用这个方法 var st = ownerInstance.getState(); st.itemsInstance = ownerInstance.selectAllComponents('.drag-item'); // 所有拖拽项的ComponentDescriptor实例 st.list = newVal || []; // 拿到的处理后的数据-对应dataList st.maxEndKey = st.list.length - 1; // 该列表最大的结束位置 st.list.forEach(function(item, index) { // 将每项移动到对应的位置-设置style var itemIns = st.itemsInstance[index]; if (item && itemIns) { itemIns.setStyle({ 'transform': 'translate3d(' + item.tranX + ',' + item.tranY + ', 0)' }) if (item.fixed) itemIns.addClass("fixed"); // 固定项的class } }) } var baseDataObserver = function(newVal, oldVal, ownerInstance, ins) { var st = ownerInstance.getState(); st.baseData = newVal; st.dragging = false; // 当前是否处于拖拽状态 }
2.longpress方法:
在该方法中我们主要是将当前长按的元素移动到触摸点的中心位置并且将该初始位置记录下来
var contentHeight = 0; // 内容总高度 var longPress = function(event, ownerInstance) { var ins = event.instance; // 触发事件的ComponentDescriptor实例 var st = ownerInstance.getState(); // 触发事件所在的顶级组件/页面实例,所以这几个方法中通过ownerInstance.getState()存储的数据是共享的 var _ = st.baseData; // 这是之前存储的baseData数据 var sTouch = event.changedTouches[0]; // 当前触摸信息 if (!sTouch) return; contentHeight = Math.ceil(st.list.length / _.columns) * _.itemHeight; st.cur = ins.getDataset().index; // 当前操作元素索引 var item = st.list[st.cur]; // 当前操作元素 if (item && item.fixed) return; // 该项是固定项的话就直接return 不做后续操作 if (st.dragging) return; // 判断当前是否正在进行拖拽 - 防止多指 st.dragging = true; // 通过添加遮罩层的方式,使多指失效 var mark = ownerInstance.selectComponent(".mark"); mark.setStyle({ display: "block" }) // 计算X,Y轴初始位移 将该元素中心点移动到触摸位置 st.tranX = _.columns === 1 ? 0 : sTouch.pageX - (_.itemWidth / 2 + _.wrapLeft); st.tranY = sTouch.pageY - (_.itemHeight / 2 + _.wrapTop); st.sId = sTouch.identifier; // 当前触摸点标识 // 设置当前拖拽项偏移量 ins.setStyle({ 'transform': 'translate3d(' + st.tranX + 'px, ' + st.tranY + 'px, 0)' }) // 当前拖拽项不使用过渡动画 st.itemsInstance.forEach(function(item, index) { item.removeClass("tran").removeClass("cur"); item.addClass(index === st.cur ? "cur" : "tran"); }) // 这里是调用vue文件中的震动方法 加上体验会好一点 ownerInstance.callMethod("vibrate"); }
3.touchmove方法(重点):
接下来就是该功能的重点了-拖拽,我们在这里首选需要根据拖动来实时移动被拖拽项,并且计算该项的endKey(结束位置的sortKey)以及计算其他项的sortKey
var isOutRange = function(x1, y1, x2, y2, x3, y3) { return x1 < 0 || x1 >= y1 || x2 < 0 || x2 >= y2 || x3 < 0 || x3 >= y3 }; var sortCore = function(sKey, eKey, st) { var _ = st.baseData; var excludeFix = function(cKey, type) { if (st.list[cKey].fixed) { type ? --cKey : ++cKey; return excludeFix(cKey, type); } return cKey; } if (eKey >= st.maxEndKey) { eKey = st.maxEndKey; } var endRealKey = -1; st.list.forEach(function(item) { if (item.sortKey === eKey) endRealKey = item.realKey; }); return st.list.map(function(item) { if (item.fixed) return item; var cKey = item.sortKey; var rKey = item.realKey; if (sKey < eKey) { // 正序拖动 if (cKey > sKey && cKey <= eKey) { --rKey; cKey = excludeFix(--cKey, true); } else if (cKey === sKey) { rKey = endRealKey; cKey = eKey; } } else if (sKey > eKey) { // 倒序拖动 if (cKey >= eKey && cKey < sKey) { ++rKey cKey = excludeFix(++cKey, false); } else if (cKey === sKey) { rKey = endRealKey; cKey = eKey; } } if (item.sortKey !== cKey) { item.tranX = (cKey % _.columns) * 100 + "%"; item.tranY = Math.floor(cKey / _.columns) * 100 + "%"; item.sortKey = cKey; item.realKey = rKey; } return item; }); } var touchMove = function(event, ownerInstance) { var ins = event.instance; var st = ownerInstance.getState(); var _ = st.baseData; var mTouch = event.changedTouches[0]; if (!st.dragging) return; // 如果不是同一个触发点则返回 if (st.sId !== mTouch.identifier) return; // 计算X,Y轴位移, 单列时候X轴初始不做位移 var tranX = _.columns === 1 ? 0 : mTouch.pageX - (_.itemWidth / 2 + _.wrapLeft); var tranY = mTouch.pageY - (_.itemHeight / 2 + _.wrapTop); // 到顶到底自动滑动 if (mTouch.clientY > _.windowHeight - _.itemHeight - (_.itemHeight / 4)) { if (mTouch.pageY > contentHeight + _.itemHeight) return // 当前触摸点pageY + item高度 - 屏幕高度 ownerInstance.callMethod("handlePageSroll", { scrollTop: mTouch.pageY + _.itemHeight - _.windowHeight }); } else if (mTouch.clientY < _.itemHeight) { // 当前触摸点pageY - item高度 - 顶部固定区域高度 ownerInstance.callMethod("handlePageSroll", { scrollTop: mTouch.pageY - _.itemHeight }); } // 设置当前激活元素偏移量 ins.setStyle({ 'transform': 'translate3d(' + tranX + 'px, ' + tranY + 'px, 0)' }) // 获取目标sortKey、计算结束时的sortKey值 var startKey = st.list[st.cur].sortKey; var curX = Math.round(tranX / _.itemWidth); var curY = Math.round(tranY / _.itemHeight); var endKey = curX + _.columns * curY; // 目标项是固定项则返回 var item = st.list[endKey]; if (item && item.fixed) { return false; } // X轴或Y轴超出范围则返回 if (isOutRange(curX, _.columns, curY, _.rows, endKey, st.list.length)) { return false; } // 防止拖拽过程中发生乱序问题以及消除重复震动 if (startKey === endKey || startKey === st.preStartKey) { return false; } st.preStartKey = startKey; // 排序 这里的排序其实只是通过更改sortKey重新计算了元素的偏移值 实际在节点中的顺序是没有改变的 var list = sortCore(startKey, endKey, st); st.itemsInstance.forEach(function(itemIns, index) { var item = list[index]; if (index !== st.cur) { itemIns.setStyle({ 'transform': 'translate3d(' + item.tranX + ',' + item.tranY + ', 0)' }); } }); st._list = list; ownerInstance.callMethod("vibrate"); return false; }
4.touchend方法:
最后就是我们的touchend方法,在这里主要是去做一些数据的重置处理,以及将数据进行真实排序反馈到vue文件中,以便于去做后续的处理,否则这个排序是毫无意义的。
var realSort = function(list) { var listData = []; list.forEach(function(item) { listData[item.sortKey] = item.data; }) return listData; } var touchEnd = function(event, ownerInstance) { var ins = event.instance; var st = ownerInstance.getState(); var _ = st.baseData; if (!st.dragging) return; if (st._list) { var realListData = realSort(st._list || []); // 排序后的数据 // 这里同上也是调用vue文件中的sortEnd方法 ownerInstance.callMethod("sortEnd", { realListData: realListData }); } // 这里在拖拽结束后 就将遮罩层移除 var mark = ownerInstance.selectComponent(".mark"); mark.setStyle({ display: "none" }) ins.addClass("tran"); ins.setStyle({ 'transform': 'translate3d(' + st.list[st.cur].tranX + ',' + st.list[st.cur].tranY + ', 0)' }); st.dragging = false; st.preStartKey = -1; st.cur = -1; st.tranX = 0; st.tranY = 0; delete st._list }
4.vue文件中剩下的方法介绍:
4.1 vibrate方法:
// 这里主要是判断是不是模拟器 不然在模拟器上抖着难受 let vibrate = () => { if (baseData.platform !== "devtools") { vibrate = function () { uni.vibrateShort(); } } else { vibrate = function () { } } vibrate(); } // 在vue3中需要通过defineExpose方法,将该方法暴露出去,否则WXS中无法调用 defineExpose({ vibrate, })
4.2 handlePageScroll方法:
由于android在直接使用下面ios方法来滚动时,会出现滑动较慢的情况,就感觉一卡一卡的,所以做了一下处理,这里同样也需要使用到defineExpose
let isScroll = ref(false), timer = null; const androidPageScroll = (data : any) => { if (isScroll.value) return isScroll.value = true; timer = setTimeout(() => { uni.pageScrollTo({ scrollTop: data.scrollTop, duration: 0 }); clearTimeout(timer) timer = null isScroll.value = false; }, 10) } const iosPageScroll = (data : any) => { uni.pageScrollTo({ scrollTop: data.scrollTop, duration: 300 }) } let handlePageSroll = (data : any) => { if (baseData.platform === "android") { handlePageSroll = androidPageScroll; } else { handlePageSroll = iosPageScroll; } handlePageSroll(data); }
4.3 sortEnd方法:
这个就是拖拽结束后获取真实排序数据的方法
const sortEnd = (data : any) => { console.log("sortEnd", data.realListData) }
5.页面样式:
<style scoped lang="scss"> .drag-list { width: 750rpx; position: relative; overflow: hidden; .drag-item { position: absolute; z-index: 1; top: 0; left: 0; view { width: 100%; height: 100%; } &.tran { transition: transform 0.3s !important; } &.cur { z-index: 3; } &.fixed { z-index: 0 !important; } } .mark { display: none; position: fixed; width: 100vh; height: 100vh; z-index: 2; background-color: transparent; } } </style>
结尾:
完整代码我放在gitee上了,感兴趣的小伙伴可以拉下来看看https://gitee.com/zhang-yang/drag-uni-app