css3 拖拽-一个端庄的小程序拖拽排序组件实现

2023-08-27 0 1,593 百度已收录

前言#

最近,po主在写小程序的过程中,遇到了一个拖拽排序的需求。 网上一搜,被拒绝了,于是他自己实现了。

灵感#

首先,因为没有现成的小程序案例可供我参考,所以有点困惑,所以可以找一个h5的拖拽实现的参考。 所以我在看了jquery插件网络上的几个拖拽排序实现之后基本就确定了想法。 可能使用transform来进行转换。 是的,灵感和这些东西都是借来的~~

确定需求#要能拖拽,毕竟是拖拽排序,拖拽一定是第一位的。 能够排序,必须先有拖拽,然后才会有这一天~~偏离轨道了,拖完之后还得排序,不然和movable-view有什么区别。 您可以自定义列数以实现不同的解释。 这里,你要考虑列表排序、相册排序等不同情况对所需列数的不同要求。 没有bug,呃呃呃,我尽力了。实现思路#

首先,可以拖拽的元素至少必须是相同大小的,至于不规则大小,或者大小之间的倍数关系,则不在本实现的范围之内。

然后我们找到对应需求的解决方案:

拖放实现#

使用movable-view来实现拖拽,这种方法简单快捷,但是因为我们的灵感是使用transform进行变换,而这里的movable-view本身也是用transform实现的,所以会产生冲突,所以放弃它。 使用自定义手势,例如 touchstart、touchmove、touchend。 是的,又是这三个家伙,虽然我们在做下拉刷新的时候用了movable-view,抛弃了这三个兄弟。 但是金子总会发光的,今天是大家三兄弟大显身手的时候了(振翔警告)。 废话就多了,咱们言归正传,使用自定义手势可以让我们轻松掌控每一个细节。 美中不足的是他们的老公没有提供自动停止冒泡的功能。 当然,catch 并绑定事件不支持动态切换,因此非常棘手,但这不是问题,只是有点技巧而已。

排序实现#

排序基于拖放。 touchstart、touchmove、touchend三兄弟可以在接收到触摸信息后动态估计当前元素的排序位置,然后根据当前激活元素的排序位置动态改变字段中其他元素的位置。 大概意思是十个兄弟排成一排,老大上去,走到老二的位置css3 拖拽,老三向前看css3 拖拽,老二向后看。 当然这是乱序的,也有倒序的,比如第十个孩子移动到老大的位置,那么老大到第九就必须依次向后移动一个位置。

自定义列数#

自定义列数并不困难。 小程序组件暴露一个列属性,然后将估计过程中的固定列数改为该参数。

实施分析#

首先touchstart、touchmove、touchend三兄弟

长按#

这里为了体验一下,touchstart改为长按触发。 首先,我们需要设置一个状态触摸来指示我们正在拖放。 然后我们得到pageX,pageY。 注意,我们得到的是pageX,pageY而不是clientX,clientY,因为我们的拖动组件有底部可能有边距或者其他元素。 这时候如果获取clientX和clientY就会出错。 这里,将当前pageX和pageY设置为初始触摸点startX和startY。

然后需要估计初始化的活动单元的偏转位置tranX和tranY。 这里为了优化体验,在列数为1时初始化tranX,不进行位移,将tranY移动到当前活动元素的中间位置。 当有多列时,set tranX 和 tranY 全部位移到当前活动元素的中间。

最后,设置当前活动元素的索引 cur 和偏移量 tranX、tranY。 然后振动wx.vibrateShort()来体验其中的美妙。

/**
 * 长按触发移动排序
 */
longPress(e) {
 this.setData({
 touch: true
 });
 this.startX = e.changedTouches[0].pageX
 this.startY = e.changedTouches[0].pageY
 let index = e.currentTarget.dataset.index;
 if(this.data.columns === 1) { // 单列时候X轴初始不做位移
 this.tranX = 0;
 } else { // 多列的时候计算X轴初始位移, 使 item 水平中心移动到点击处
 this.tranX = this.startX - this.item.width / 2 - this.itemWrap.left;
 }
 // 计算Y轴初始位移, 使 item 垂直中心移动到点击处
 this.tranY = this.startY - this.item.height / 2 - this.itemWrap.top;
 this.setData({
 cur: index,
 tranX: this.tranX,
 tranY: this.tranY,
 });
 wx.vibrateShort();
}

触摸移动#

touchmove每次都是故事的主角,这一次也不例外。 您可以通过查看完整的代码来判断。 首先,您需要确定是否正在拖放。 如果没有,您需要返回。

css3 拖拽-一个端庄的小程序拖拽排序组件实现

然后判断是否超过一屏。 这是什么意思呢,因为我们的拖拽元素可能会超出很多甚至超出整个屏幕,需要通过滑动的方式来处理。 但是我们这里使用了catch:touchmove事件所以它会阻止页面滑动。 所以我们需要当元素超过一屏的时候,这里有两种情况。 一种是当我们将元素拖放到页面顶部时,页面手动向上滚动一个元素高度的距离,另一种是当我们将元素拖放到页面底部时,页面手动向上滚动向下滚动一个元素高度的距离。

然后我们设置重新估计的tranX和tranY,并获取当前元素的排序键key作为初始originKey,然后使用calculateMoving方法通过当前的tranX和tranY估计endKey。

最后我们调用this.insert(originKey, endKey)方法对链表进行排序

touchMove(e) {
 if (!this.data.touch) return;
 let tranX = e.touches[0].pageX - this.startX + this.tranX,
 tranY = e.touches[0].pageY - this.startY + this.tranY;
 let overOnePage = this.data.overOnePage;
 // 判断是否超过一屏幕, 超过则需要判断当前位置动态滚动page的位置
 if(overOnePage) {
 if(e.touches[0].clientY > this.windowHeight - this.item.height) {
 wx.pageScrollTo({
 scrollTop: e.touches[0].pageY + this.item.height - this.windowHeight,
 duration: 300
 });
 } else if(e.touches[0].clientY < this.item.height) {
 wx.pageScrollTo({
 scrollTop: e.touches[0].pageY - this.item.height,
 duration: 300
 });
 }
 }
 this.setData({tranX: tranX, tranY: tranY});
 let originKey = e.currentTarget.dataset.key;
 let endKey = this.calculateMoving(tranX, tranY);
 // 防止拖拽过程中发生乱序问题
 if (originKey == endKey || this.originKey == originKey) return;
 this.originKey = originKey;
 this.insert(originKey, endKey);
}

计算移动方法#

通过上面的介绍,我们已经基本完成了拖拽排序的主要功能,但是还有两个关键功能没有解决。 其中之一是calculateMoving方法,该方法根据当前偏移量tranX和tranY估计目标key。

具体估算规则:

根据列表的粗细和列数估计当前拖放元素的行数 根据 tranX 和当前元素 i 的长度计算 y 轴上的倾斜数 根据 tranX 和当前元素 i 的长度计算 y 轴上的倾斜数以tranY和当前元素j的高度确定i和j的最大值和最小值根据公式计算目标key endKey = i + columns * j 确定目标key的最大值并返回目标key

/**
 * 根据当前的手指偏移量计算目标key
 */
calculateMoving(tranX, tranY) {
 let rows = Math.ceil(this.data.list.length / this.data.columns) - 1,
 i = Math.round(tranX / this.item.width),
 j = Math.round(tranY / this.item.height);
 i = i > (this.data.columns - 1) ? (this.data.columns - 1) : i;
 i = i < 0 ? 0 : i;
 j = j  rows ? rows : j;
 let endKey = i + this.data.columns * j;
 endKey = endKey >= this.data.list.length ? this.data.list.length - 1 : endKey;
 return endKey
}

插入方法#

css3 拖拽-一个端庄的小程序拖拽排序组件实现

拖动排序中另一个没有解决的主要功能是insert方法。 该方法根据originKey(起始键)和endKey(目标键)对链表进行重新排序。

具体排序规则:

首先确定起点和终点的大小,并进行不同的逻辑处理。 循环list列表进行逻辑处理。 如果origin小于end,则origin到end之间(不包括origin,包括end)所有元素的key减1,并将origin的key值替换为end。 如果origin大于end,则end到origin之间(不包括origin,包括end)所有元素的key加1,并将origin的key值设置为end调用getPosition方法进行渲染

/**
 * 根据起始key和目标key去重新计算每一项的新的key
 */
insert(origin, end) {
 let list;
 if (origin  {
 if (item.key > origin && item.key  end) {
 list = this.data.list.map((item) => {
 if (item.key >= end && item.key < origin) {
 item.key = item.key + 1;
 } else if (item.key == origin) {
 item.key = end;
 }
 return item
 });
 this.getPosition(list);
 }
}

获取位置方法#

在上面的insert方法中,我们最终调用了getPosition方法,该方法用于估计每个元素的tranX和tranY并进行渲染。 初始化渲染时也需要调用该函数。 因此,增加了一个振动变量,用于不同的加工判断。

该函数执行逻辑:

首先循环传入的数据data,根据下面的公式估计每个元素的tranX和tranY(this.item.width、this.item.height分别是元素的宽度和高度,this.data .columns 为列号,item.key 为当前元素的排序键值) item.tranX = this.item.width * (item.key % this.data.columns); item.tranY = Math.floor(item.key / this.data .columns) * this.item.height; 设置处理后的列表数据列表,判断是否需要进行摇动、触发风暴逻辑。 此确定用于区分初始化调用和插入方法。 初始化的时候不需要先设置itemTransition,设置为true就可以让item发生变化,然后用动画效果来摇动,wx.vibrateShort(),嗯~,这是个好东西。 最后复制一份listData然后触发change事件抛出排序后的数据

最后注意,这个函数并没有改变列表中真正的排序,而是根据key进行伪排序,因为如果改变列表中每一项的顺序,dom结构就会改变,这样就无法实现我们想要的丝滑效果 但是最后 this.triggerEvent('change', {listData: listData}) 是真正排序后的数据,并且 key, tranX, tranY 的原始数据信息已经被去掉了(这里每一项数据都有key,tranX,tranY是因为在初始化的时候就已经处理过了,所以使用的时候不需要考虑)

/**
 * 根据排序后 list 数据进行位移计算
 */
getPosition(data, vibrate = true) {
 let list = data.map((item, index) => {
 item.tranX = this.item.width * (item.key % this.data.columns);
 item.tranY = Math.floor(item.key / this.data.columns) * this.item.height;
 return item
 });
 this.setData({
 list: list
 });
 if(!vibrate) return;
 this.setData({
 itemTransition: true
 })
 wx.vibrateShort();
 let listData= [];
 list.forEach((item) => {
 listData[item.key] = item.data
 });
 this.triggerEvent('change', {listData: listData});
}

css3 拖拽-一个端庄的小程序拖拽排序组件实现

触摸结束#

写了这么久,三兄弟就剩下最后一个了。 这位dei哥看起来不努力啊,就两行代码?

是的,就两行...一行确定拖放是否正在进行,另一行清除缓存数据

touchEnd() {
 if (!this.data.touch) return;
 this.clearData();
}

清除数据方法#

由于重复使用,我选择将这个逻辑用一层包裹起来。

/**
 * 清除参数
 */
clearData() {
 this.originKey = -1;
 this.setData({
 touch: false,
 cur: -1,
 tranX: 0,
 tranY: 0
 });
}

初始化方法#

介绍完三兄弟和他们的表兄弟,故事就剩下我们的init方法了。

init方法执行逻辑:

首先是对传入的listData进行处理并添加key、tranX、tranY等信息,然后将处理后的list和itemTransition设置为false(这样初始化时就看不到动画)获取windowHeight来获取的宽度和高度属性每个item并设置为this.item,然后使用初始化执行this.getPosition(this.data.list, false)来设置动态估计的父元素高度itemWrapHeight,因为这里使用了绝对定位和transform,父元素无法获取到Height,因此是自动估计的,并在父元素item-wrap的节点信息末尾获取形参并估计是否超过一屏,并设置overOnePage的值

init() {
 // 遍历数据源增加扩展项, 以用作排序使用
 let list = this.data.listData.map((item, index) => {
 let data = {
 key: index,
 tranX: 0,
 tranY: 0,
 data: item
 }
 return data
 });
 this.setData({
 list: list,
 itemTransition: false
 });
 this.windowHeight = wx.getSystemInfoSync().windowHeight;
 // 获取每一项的宽高等属性
 this.createSelectorQuery().select(".item").boundingClientRect((res) => {
 let rows = Math.ceil(this.data.list.length / this.data.columns);
 this.item = res;
 this.getPosition(this.data.list, false);
 let itemWrapHeight = rows * res.height;
 this.setData({
 itemWrapHeight: itemWrapHeight
 });
 this.createSelectorQuery().select(".item-wrap").boundingClientRect((res) => {
 this.itemWrap = res;
 let overOnePage = itemWrapHeight + res.top > this.windowHeight;
 this.setData({
 overOnePage: overOnePage
 });
 }).exec();
 }).exec();
}

wxml#

以下是整个组件的wxml。 具体渲染部分使用具体节点,传入每一项的数据。 具体节点用于具体显示的效果以及组件本身的代码前馈。 如果有性能问题或者你觉得麻烦的话,可以直接编译这个组件下的样式代码。

 
 
 
 
 
 
 
 
 滑动此区域滚动页面
 

wxss#

这里我直接把scss代码拉下来了,这样可以看得更清楚,完整代码最后会给出地址

@import "../../assets/css/variables";
.item-wrap {
 position: relative;
 .item {
 position: absolute;
 width: 100%;
 z-index: 1;
 &.itemTransition {
 transition: transform 0.3s;
 }
 &.cur {
 z-index: 2;
 background: $mainColorActive;
 transition: initial;
 }
 }
}
.indicator {
 position: fixed;
 z-index: 99999;
 right: 0rpx;
 top: 50%;
 margin-top: -250rpx;
 padding: 20rpx;
 & > view {
 width: 36rpx;
 height: 500rpx;
 background: #ffffff;
 border-radius: 30rpx;
 box-shadow: 0 0 10rpx -4rpx rgba(0, 0, 0, 0.5);
 color: $mainColor;
 padding-top: 90rpx;
 box-sizing: border-box;
 font-size: 24rpx;
 text-align: center;
 opacity: 0.8;
 }
}

写在最后#

另外,这个组件受限于陌陌自身的API和部分功能,超过一屏时会出现滑动困难。 这里我做了判断,添加了一个指示器,当超过一屏时辅助滑动。 风格略有改变(因为感觉有点丑……)

其他一切似乎都很好......

还有一点,这个组件基本上没有用到太多与小程序相关的功能,所以基于这个考虑,用h5来实现应该是可以的。 如果有h5的需求,应该也能满足...

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 css3 css3 拖拽-一个端庄的小程序拖拽排序组件实现 https://www.wkzy.net/game/166926.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务