目录
效果展示
实现长按拖动排序功能
该代码实现了一个基于触摸事件的长按拖动排序功能,适用于移动端应用或小程序开发。通过触摸手势,用户可以拖动列表项重新排序,交互体验流畅。触摸事件捕获与处理 通过@touchstart、@touchmove和@touchend三个触摸事件监听用户操作。在touchstart时记录初始位置和当前拖拽项,touchmove阶段计算偏移量并实时更新位置,touchend时重置拖拽状态。positions对象保存每个元素的排序顺序和垂直位置坐标。draggingId标记当前拖拽的元素ID,isDragging是否正在拖拽中。
拖拽排序的实现主要依赖以下几个关键点:
-
触摸/鼠标事件监听:
-
touchstart(触摸开始)
-
touchmove(触摸移动)
-
touchend(触摸结束)
-
-
位置计算与交换逻辑:
-
记录初始触摸位置(startY)
-
计算拖拽偏移量(offsetY)
-
检测是否需要交换位置(基于 itemHeight和阈值)
-
-
动画与过渡效果:
-
使用 transform: translateY() 实现平滑移动
-
动态调整 zIndex确保拖拽元素在最上层
-
拖拽时禁用 transition,结束时启用
-
拖动过程中的位置交换逻辑
checkSwapPosition方法处理拖动过程中的位置交换判断。当拖动项目超过相邻项目一半高度时触发交换:
checkSwapPosition(id, currentPos) {
const threshold = this.itemHeight / 2
const currentIndex = this.sortedList.findIndex(item => item.id === id)
// 检查上方和下方元素是否满足交换条件
// ...
}
swapItems方法实际执行交换操作,更新两个项目的排序顺序并重新计算所有项目位置:
swapItems(id1, id2) {
const temp = this.positions[id1].order
this.positions[id1].order = this.positions[id2].order
this.positions[id2].order = temp
this.updatePositions()
}
性能优化与边界处理
代码通过touchmove.prevent阻止默认滚动行为,避免与页面滚动冲突。拖动结束时在onTouchEnd中重置所有状态:
onTouchEnd() {
if (!this.isDragging) return
this.isDragging = false
this.offsetY = 0
this.draggingId = null
}
初始化时使用this.$set确保positions的响应性,避免直接赋值导致的数据更新问题。
完整代码
<template>
<view class="list">
<view class="title">长按拖动模块排序</view>
<view
class="list-item"
v-for="item in sortedList"
:key="item.id"
:style="{
transform: draggingId === item.id ? `translateY(${offsetY}px)` : '',
transition: isDragging ? 'none' : 'transform 0.2s ease',
zIndex: draggingId === item.id ? 99 : 1,
backgroundColor: draggingId === item.id ? '#f5f5f5' : '#fff'
}"
@touchstart="onTouchStart($event, item.id)"
@touchmove.prevent="onTouchMove($event, item.id)"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<image class="item-image" src="../../static/images/sorting.png"></image>
<text>{{ item.name }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
list: [
{ id: 1, name: '1号' },
{ id: 2, name: '2号' },
{ id: 3, name: '3号' },
{ id: 4, name: '4号' },
{ id: 5, name: '5号' },
],
positions: {},
startY: 0,
offsetY: 0,
draggingId: null,
isDragging: false,
itemHeight: 55
}
},
computed: {
sortedList() {
return [...this.list].sort((a, b) => (this.positions[a.id]?.order || 0) - (this.positions[b.id]?.order || 0))
}
},
mounted() {
this.initPositions()
},
methods: {
initPositions() {
this.list.forEach((item, index) => {
this.$set(this.positions, item.id, {
order: index,
y: index * this.itemHeight
})
})
},
onTouchStart(event, id) {
this.isDragging = true
this.draggingId = id
this.startY = event.touches[0].clientY
},
onTouchMove(event, id) {
if (!this.isDragging || this.draggingId !== id) return
const touchY = event.touches[0].clientY
this.offsetY = touchY - this.startY
// 计算当前拖拽位置
const currentPos = this.positions[id].y + this.offsetY
// 检查是否需要交换
this.checkSwapPosition(id, currentPos)
},
checkSwapPosition(id, currentPos) {
const threshold = this.itemHeight / 2
const currentIndex = this.sortedList.findIndex(item => item.id === id)
const currentItem = this.positions[id]
// 检查上方元素
if (currentIndex > 0) {
const prevItem = this.positions[this.sortedList[currentIndex - 1].id]
if (currentPos < prevItem.y + threshold) {
this.swapItems(id, this.sortedList[currentIndex - 1].id)
return
}
}
// 检查下方元素
if (currentIndex < this.sortedList.length - 1) {
const nextItem = this.positions[this.sortedList[currentIndex + 1].id]
if (currentPos > nextItem.y - threshold) {
this.swapItems(id, this.sortedList[currentIndex + 1].id)
return
}
}
},
swapItems(id1, id2) {
// 交换order值
const temp = this.positions[id1].order
this.positions[id1].order = this.positions[id2].order
this.positions[id2].order = temp
// 更新位置信息
this.updatePositions()
// 重置偏移量
this.offsetY = 0
this.startY += (this.positions[id1].y - this.positions[id2].y)
},
updatePositions() {
this.sortedList.forEach((item, index) => {
this.positions[item.id].y = index * this.itemHeight
})
},
onTouchEnd() {
if (!this.isDragging) return
this.isDragging = false
this.offsetY = 0
this.draggingId = null
}
}
}
</script>
<style lang="scss" scoped>
.list {
padding-top: 24rpx;
}
.title{
margin-left: 24rpx;
margin-bottom: 24rpx;
font-size: 28rpx;
font-weight: 400;
letter-spacing: 0px;
color: rgba(138, 138, 138, 1);
}
.list-item{
display: flex;
align-items: center;
gap: 24rpx;
border-top: 0.5rpx solid rgba(230, 230, 230, 1);
border-bottom: 0.5rpx solid rgba(230, 230, 230, 1);
padding: 31rpx 24rpx;
.item-image{
width: 32rpx;
height: 32rpx;
}
.item-text{
font-size: 28px;
font-weight: 400;
letter-spacing: 0px;
color: rgba(48, 63, 82, 1);
}
}
.list-item:last-child{
// border-bottom: 2rpx solid rgba(230, 230, 230, 1);
}
</style>