目录
一、拖拽效果
二、拖拽组件drag-item
wxml
<wxs module="dragToSort" src="./index.wxs"></wxs>
<view class="drag-and-drop-item" linked="{{linked}}" change:linked="{{dragToSort.linked}}" poslist="{{posList}}" change:poslist="{{dragToSort.posListChanged}}" data-idx="{{idx}}" data-drag-direction="{{dragY ? 'dragY' : (dragX ? 'dragX' : '')}}">
<slot name="body"></slot>
<slot name="btn" bind:longpress="{{dragToSort.longPress}}" bind:touchmove="{{dragToSort.touchMove}}" bind:touchend="{{dragToSort.touchEnd}}" data-idx="{{idx}}" data-drag-direction="{{dragY ? 'dragY' : (dragX ? 'dragX' : '')}}" data-pos-list="{{posList}}" data-window="{{window}}"></slot>
</view>
wxs
var arrayCompare = function (property) {
return function (a, b) {
var value1 = a[property]
var value2 = b[property]
return value1 - value2
}
}
var getBounds = function (idx, posList, dragDirection) { // 获取边界值
var posList = posList.concat([]).sort(arrayCompare('currentIndex'))
var bounds = []
var sum = 0
var pre = 0
var center = 0
var next = 0
if (dragDirection == 'dragY') { // y轴可拖拽
posList.forEach(function (item, index) {
if (index < idx || index > idx) { // pre
bounds.push(sum + (item.size.height / 2))
sum += item.size.height
} else if (index == idx) {
pre = sum
bounds.push(sum + (item.size.height / 2))
sum += item.size.height
center = bounds[bounds.length - 1]
next = sum
}
})
} else if (dragDirection == 'dragX') { // x轴可拖拽
posList.forEach(function (item, index) {
if (index < idx || index > idx) { // pre
bounds.push(sum + (item.size.width / 2))
sum += item.size.width
} else if (index == idx) {
pre = sum
bounds.push(sum + (item.size.width / 2))
sum += item.size.width
center = bounds[bounds.length - 1]
next = sum
}
})
}
bounds = bounds.filter(function (item) {
return item != center
}).map(function (item) {
if (item > center) {
return item - next
} else if (item < center) {
return item - pre
}
})
return { bounds, pre, center, next }
}
var initBounds = function (e, ownerInstance) {
var state = ownerInstance.getState()
var window = e.currentTarget.dataset.window
var idx = e.currentTarget.dataset.idx
var dragDirection = e.currentTarget.dataset.dragDirection
var currentIdx = state.posList[idx].currentIndex
var clientX = parseInt(e.touches[0].clientX)
var clientY = parseInt(e.touches[0].clientY)
// 拖拽的是第几个,用于高亮显示等
ownerInstance.addClass('dragging')
ownerInstance.callMethod('log', {event: 'initBounds', ownerInstance: ownerInstance, e: e})
var wxsStyle = {}
if (dragDirection == 'dragY') {
wxsStyle = {'-webkit-transform': 'translateY(' + state.posList[idx].translate.y + 'px)', 'transform': 'translateY(' + state.posList[idx].translate.y + 'px)', 'zIndex': '99'}
} else if (dragDirection == 'dragX') {
wxsStyle = {'-webkit-transform': 'translateX(' + state.posList[idx].translate.x + 'px)', 'transform': 'translateX(' + state.posList[idx].translate.x + 'px)', 'zIndex': '99'}
}
ownerInstance.setStyle(wxsStyle)
// 记录安全区域
state.window = window
// 拖拽起始点坐标(clientX, clientY)
state.lastPoint = {x: clientX, y: clientY}
state.dragStartOffset = { x: clientX, y: clientY }
state.offsetRect = {top: 20, right: state.window.width - 20, bottom: state.window.height - 20, left: 20}
// 拖拽的元素,记录下此时的数据,在拖拽过程中使用,此处要深拷贝
state.dragEle = JSON.parse(JSON.stringify(state.posList[idx]))
// 基于拖拽的元素当前的排序,获取边界信息,并获取到前一个/后一个的边界值(即超过该边界值时,排序会发生变化)
var boundResult = getBounds(currentIdx, state.posList, dragDirection)
state.bounds = boundResult.bounds
state.boundaryPre = state.bounds[currentIdx - 1]
state.boundaryNext = state.bounds[currentIdx]
// 初始化拖拽元素最终的偏移量
state.dragEndDistance = 0
// 初始化拖拽元素所在的排序位置(实时变化)为undefined
state._currentIdx = undefined
// 记录当前排序,用于拖拽结束后对比排序,决定是否触发change事件
var _posList = JSON.parse(JSON.stringify(state.posList))
state._sortArr = _posList.sort(arrayCompare('currentIndex')).map(function (item) {
return item.originalIndex
})
ownerInstance.callMethod('getScrollerRect')
}
module.exports = {
posListChanged: function (newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
if (!oldVal && newVal) {
state.transitionStyle = ownerInstance.getComputedStyle(['-webkit-transition', 'transition'])
}
if (newVal && !state.dragging && (state.idx || state.idx === 0) && state.dragElement) {
ownerInstance.removeClass('dragging')
ownerInstance.setStyle({'-webkit-transition': state.transitionStyle ? (state.transitionStyle['-webkit-transition'] + ', transform 0.3s') : 'transform 0.3s', 'transition': state.transitionStyle ? (state.transitionStyle['transition'] + ', transform 0.3s') : 'transform 0.3s', 'transform': 'translateY(' + newVal[state.idx].translate.y + 'px)', transform: 'translateY(' + newVal[state.idx].translate.y + 'px)', 'z-index': '1'})
}
},
linked: function (newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
if (newVal) {
state.dragElement = ownerInstance.selectComponent('.drag-and-drop-item')
var rect = ownerInstance.getBoundingClientRect()
var dataset = state.dragElement.getDataset()
ownerInstance.callMethod('updatePosList', {rect, dataset})
state.idx = dataset.idx
}
},
longPress: function (e, ownerInstance) {
var state = ownerInstance.getState()
state.dragElement = ownerInstance.selectComponent('.drag-and-drop-item')
state.posList = e.currentTarget.dataset.posList
state.dragging = true
if (state.posList && state.posList.length > 0) {
initBounds(e, ownerInstance)
}
},
touchMove: function (e, ownerInstance) {
var state = ownerInstance.getState()
if (!state.dragStartOffset) { // 起始点坐标不存在则认为没有进行拖拽操作,在拖拽结束的时候需要重置为undefined
return
}
var dragDirection = e.currentTarget.dataset.dragDirection
var clientX = parseInt(e.touches[0].clientX)
var clientY = parseInt(e.touches[0].clientY)
// 拖拽元素相对于起始点的偏移坐标
state.point = {x: clientX, y: clientY}
var distanceOffset = { x: clientX - state.dragStartOffset.x, y: clientY - state.dragStartOffset.y }
// 初始化拖拽元素即将达到的排序
var willIndex = undefined
// 初始化纵向/横向的拖拽距离int
var distance = 0
// 拖拽过程中,拖拽元素所在的排序位置(实时变化)
state._currentIdx = state._currentIdx == undefined ? state.dragEle.currentIndex : state._currentIdx
// 拖拽元素的初始排序(排序列表初始化时的排序)
var originalIndex = state.dragEle.originalIndex
// 判断拖拽方向
// var direction = ''
// var outOffArea = ''
// if (dragDirection == 'dragY') {
// direction = state.point.y - state.lastPoint.y > 0 ? 'y' : '-y'
// outOffArea = (state.point.y - state.posList[originalIndex].size.height / 2 < state.offsetRect.top) || (state.point.y + state.posList[originalIndex].size.height / 2 > state.offsetRect.bottom)
// } else if (dragDirection == 'dragX') {
// direction = state.point.x - state.lastPoint.x > 0 ? 'x' : '-x'
// outOffArea = (state.point.x - state.posList[originalIndex].size.width / 2 < state.offsetRect.left) || (state.point.x + state.posList[originalIndex].size.width / 2 > state.offsetRect.right)
// }
// if (outOffArea) {
// }
// // 实时更新拖拽元素的位置
if (dragDirection == 'dragY') {
state.posList[originalIndex].translate.y = state.dragEle.translate.y + distanceOffset.y
ownerInstance.setStyle({'-webkit-transition': 'none', 'transition': 'none', '-webkit-transform': 'translateY(' + state.posList[originalIndex].translate.y + 'px)', 'transform': 'translateY(' + state.posList[originalIndex].translate.y + 'px)', 'z-index': '99'})
distance = distanceOffset.y
} else if (dragDirection == 'dragX') {
state.posList[originalIndex].translate.x = state.dragEle.translate.x + distanceOffset.x
ownerInstance.setStyle({'-webkit-transition': 'none', 'transition': 'none', '-webkit-transform': 'translateX(' + state.posList[originalIndex].translate.x + 'px)', 'transform': 'translateX(' + state.posList[originalIndex].translate.x + 'px)', 'z-index': '99'})
distance = distanceOffset.x
}
if (!state.currentIndexChanging) { // 当拖拽元素在设置时不执行,避免太过频繁的更新数据导致卡顿
// 前一个的边界值存在 且 距离超过该边界时,认为排序向前移动
if (state.boundaryPre !== undefined && distance < state.boundaryPre) {
// 更新排序,并获取拖拽元素所取代元素的初始排序(排序列表初始化时的排序)
willIndex = state._currentIdx - 1
var preOriginalIndex = state.posList.filter(function (item) {
return item.currentIndex === willIndex
})[0].originalIndex
// 更新currentIndex字段,并更改拖拽元素所取代元素的偏移
if (dragDirection == 'dragY') {
state.posList[preOriginalIndex].translate.y = state.posList[preOriginalIndex].translate.y + state.dragEle.size.height
} else if (dragDirection == 'dragX') {
state.posList[preOriginalIndex].translate.x = state.posList[preOriginalIndex].translate.x + state.dragEle.size.width
}
state.posList[preOriginalIndex].currentIndex = parseInt(state._currentIdx)
state.posList[originalIndex].currentIndex = parseInt(willIndex)
ownerInstance.callMethod('updatePos', {pos: state.posList[preOriginalIndex], updateIndex: preOriginalIndex, oldPos: state._currentIdx, newPos: willIndex})
// 更新操作数据
state._currentIdx = state._currentIdx - 1
if (dragDirection == 'dragY') {
state.dragEndDistance -= state.posList[preOriginalIndex].size.height
} else if (dragDirection == 'dragX') {
state.dragEndDistance -= state.posList[preOriginalIndex].size.width
}
// 当排序有变化时,锁定,直到界面更新渲染完毕后取消锁定
state.currentIndexChanging = true
} else if (state.boundaryNext !== undefined && distance > state.boundaryNext) {
// 更新排序,并获取拖拽元素所取代元素的初始排序(排序列表初始化时的排序)
willIndex = state._currentIdx + 1
var nextOriginalIndex = state.posList.filter(function (item) {
return item.currentIndex === willIndex
})[0].originalIndex
// 更新currentIndex字段,并更改拖拽元素所取代元素的偏移
if (dragDirection == 'dragY') {
state.posList[nextOriginalIndex].translate.y = state.posList[nextOriginalIndex].translate.y - state.dragEle.size.height
} else if (dragDirection == 'dragX') {
state.posList[nextOriginalIndex].translate.x = state.posList[nextOriginalIndex].translate.x - state.dragEle.size.width
}
state.posList[nextOriginalIndex].currentIndex = parseInt(state._currentIdx)
state.posList[originalIndex].currentIndex = parseInt(willIndex)
ownerInstance.callMethod('updatePos', {pos: state.posList[nextOriginalIndex], updateIndex: nextOriginalIndex, oldPos: state._currentIdx, newPos: willIndex})
// 更新操作数据
state._currentIdx = state._currentIdx + 1
if (dragDirection == 'dragY') {
state.dragEndDistance += state.posList[nextOriginalIndex].size.height
} else if (dragDirection == 'dragX') {
state.dragEndDistance += state.posList[nextOriginalIndex].size.width
}
// 当排序有变化时,锁定,直到界面更新渲染完毕后取消锁定
state.currentIndexChanging = true
}
// 如果排序有变化,则更新前一个/后一个边界值
if (state.currentIndexChanging) {
state.boundaryPre = state.bounds[state._currentIdx - 1]
state.boundaryNext = state.bounds[state._currentIdx]
}
}
state.lastPoint = {x: clientX, y: clientY}
state.currentIndexChanging = false
if (state.dragging) {
return false
}
},
touchEnd: function (e, ownerInstance) {
var state = ownerInstance.getState()
if (!state.dragStartOffset) {
return
}
var dragDirection = e.currentTarget.dataset.dragDirection
// 清除拖拽起始点坐标,仅在长按后可获得起始点坐标
state.dragStartOffset = undefined
state.dragging = false
if (state.dragEle) {
var originalIndex = state.dragEle.originalIndex
ownerInstance.removeClass('dragging')
// ownerInstance.removeClass('item-dragging')
// 根据最终拖拽元素的偏移,更新拖拽元素的位置
if (dragDirection == 'dragY') {
state.posList[originalIndex].translate.y = state.dragEle.translate.y + state.dragEndDistance
ownerInstance.setStyle({'-webkit-transition': state.transitionStyle ? (state.transitionStyle['-webkit-transition'] + ', transform 0.3s') : 'transform 0.3s', 'transition': state.transitionStyle ? (state.transitionStyle['transition'] + ', transform 0.3s') : 'transform 0.3s', '-webkit-transform': 'translateY(' + state.posList[originalIndex].translate.y + 'px)', 'transform': 'translateY(' + state.posList[originalIndex].translate.y + 'px)', 'z-index': '1'})
} else if (dragDirection == 'dragX') {
state.posList[originalIndex].translate.x = state.dragEle.translate.x + state.dragEndDistance
ownerInstance.setStyle({'-webkit-transition': state.transitionStyle ? (state.transitionStyle['-webkit-transition'] + ', transform 0.3s') : 'transform 0.3s', 'transition': state.transitionStyle ? (state.transitionStyle['transition'] + ', transform 0.3s') : 'transform 0.3s', '-webkit-transform': 'translateX(' + state.posList[originalIndex].translate.x + 'px)', 'transform': 'translateX(' + state.posList[originalIndex].translate.x + 'px)', 'z-index': '1'})
}
ownerInstance.callMethod('updatePos', {pos: state.posList[originalIndex]})
}
var _posList = JSON.parse(JSON.stringify(state.posList))
var sortArr = _posList.sort(arrayCompare('currentIndex')).map(function (item) {
return item.originalIndex
})
if (state._sortArr && JSON.stringify(state._sortArr) != JSON.stringify(sortArr)) { // 排序改变,触发change事件,否则不触发
ownerInstance.callMethod('parentChange', { sort: sortArr })
}
state._sortArr = undefined
}
}
js
Component({
relations: {
'../drag-to-sort/index': {
type: 'parent', // 关联的目标节点应为父节点
linked: function(target) {
const {dragY, dragX} = target.data
const systemInfo = wx.getSystemInfoSync()
this.parent = target
this.setData({ dragY, dragX, window: {width: systemInfo.windowWidth, height: systemInfo.windowHeight}, linked: true })
this.updatePosList = data => {
const {rect, dataset} = data
target.posList = target.posList || []
target.posList.push({originalIndex: dataset.idx, currentIndex: dataset.idx, size: {width: rect.width, height: rect.height}, translate: {x: 0, y: 0}})
target.posListLen = target.posListLen || 0
target.posListLen += 1
if (target.posListLen == target.data.length) {
target.posList = target.posList.sort(this.arrayCompare('currentIndex'))
target.allChildLinked(target.posList)
}
}
}
}
},
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
properties: {
idx: {
type: Number,
value: 0
}
},
data: {
},
methods: {
getScrollerRect() {
return
this.parent.getScrollerRect()
},
updatePos({pos, updateIndex, oldPos, newPos}) {
console.log('updatePos', pos, updateIndex, oldPos, newPos)
let posList = this.parent.posList.map(item => {
if (item.originalIndex == pos.originalIndex) {
return pos
} else {
return item
}
})
this.parent.posList = posList
if (updateIndex !== undefined) {
this.parent.noticeChildInited(updateIndex)
} else {
this.parent.noticeChildInited()
}
if (oldPos != newPos) {
this.parentPosChange({oldPos, newPos})
}
},
updatePosDate(posList) {
this.setData({posList})
},
arrayCompare (property) {
return function (a, b) {
var value1 = a[property]
var value2 = b[property]
return value1 - value2
}
},
log(data) {
console.log(data)
},
parentChange(data) {
this.parent.change(data)
},
parentPosChange(data) {
this.parent.posChange(data)
}
}
})
wxss
.drag-and-drop-item{
word-break: break-all;
position: relative;
z-index: 1;
display: inherit;
justify-content: inherit;
align-items: inherit;
flex-direction: inherit;
flex-wrap: inherit;
flex: 1;
}
:host{
position: relative;
}
json
{
"component": true
}
三、拖拽组件drag-to-sort
wxml
<view class="drag-and-drop-list{{dragY ? ' drag-y' : ''}}{{dragX ? ' drag-x' : ''}}" data-drag-direction="{{dragY ? 'dragY' : (dragX ? 'dragX' : '')}}">
<slot></slot>
</view>
js
Component({
relations: {
'../drag-item/index': {
type: 'child', // 关联的目标节点应为子节点
linked: function(target) {
this.child = this.child || []
this.child.push(target)
}
}
},
properties: {
dragY: {
type: Boolean,
value: false
},
dragX: {
type: Boolean,
value: false
},
length: {
type: Number,
value: 0
}
},
data: {
allLoaded: false
},
methods: {
getScrollerRect() {
const _query = this.createSelectorQuery()
const query = wx.createSelectorQuery()
_query.select('.drag-and-drop-list').boundingClientRect().exec(res => {
console.log('_query_res', res)
})
query.select('#scroller').fields({
size: true,
scrollOffset: true,
properties: ['scrollX', 'scrollY'],
context: true,
}).exec(res => {
console.log('query_res', res)
})
},
noticeChildInited(index) { // 如果index不为undefined,着仅更新index指定的子元素,否则认为更新全部元素
if (this.child && this.posList) {
this.child.forEach(child => {
if (index === undefined || (index !== undefined && child.data.idx == index)) {
child.updatePosDate(this.posList)
}
})
}
},
log(data) {
console.log(data)
},
allChildLinked(posList) {
const pages = getCurrentPages()
const lastPage = pages[pages.length - 1]
setTimeout(() => {
const query = wx.createSelectorQuery()
query.select('#scroller').scrollOffset().exec((res) => {
console.log('rect', res)
})
}, 3000)
// console.log('rect', rect)
let _posList = []
let height = 0
let width = 0
posList.forEach(item => {
height += item.size.height
width += item.size.width
})
let spaceTop = 0
let spaceLeft = 0
_posList = posList.map(item => {
item.space = {top: spaceTop, right: width - spaceLeft - item.size.width, bottom: height - spaceTop - item.size.height, left: spaceLeft}
if (this.data.dragY) {
item.space.left = null
item.space.right = null
} else if (this.data.dragX) {
item.space.top = null
item.space.bottom = null
}
spaceTop += item.size.height
spaceLeft += item.size.width
return item
})
this.posList = _posList
console.log('posList', this.posList)
this.noticeChildInited()
},
change(data) {
this.triggerEvent('change', data)
},
posChange(data) {
// console.log('posChange', data)
this.triggerEvent('poschange', data)
}
}
})
json
{
"component": true
}
wxss
.wrapper{
position: relative;
}
.drag-and-drop-list{
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
position: relative;
}
.drag-and-drop-list.drag-y{
flex-direction: column;
}
.drag-and-drop-list.drag-x{
flex-direction: row;
}
.drag-and-drop-item{
word-break: break-all;
position: relative;
z-index: 1;
}
四、使用组件
wxml
<!--index.wxml-->
<view class="container">
<scroll-view id="scroller" scroll-y>
<drag-to-sort length="{{list.length}}" drag-y bind:change="sortChange" bind:poschange="posChange">
<drag-item wx:for="{{list}}" wx:key="*this" class="item" idx="{{index}}">
<view slot="body" class="body">{{item}}</view>
<view slot="btn" class="btn">
<view class="btn-sign"></view>
</view>
</drag-item>
</drag-to-sort>
</scroll-view>
</view>
js
Page({
data: {
list: ['测试11111', '测试22222', '测试33333', '测试444444', '测试555555', '测试66666', '测试77777', '测试88888888', '测试9999999999999', '101010101010', '20202020202002020','3030303030303030', '404040404040404040', '5050505050505050', '6060606060606060606', '70707070707070707070'],
},
sortChange(e) {
console.log('sortChange', e)
},
posChange(e) {
// console.log('posChange', e)
// wx.vibrateShort({
// type: 'light'
// })
}
})
json
{
"usingComponents": {
"drag-to-sort": "/components/drag-to-sort/index",
"drag-item": "/components/drag-item/index"
}
}
wxss
/**index.wxss**/
#scroller{
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
.item{
min-height: 80rpx;
font-size: 32rpx;
line-height: 56rpx;
display: flex;
justify-content: space-between;
align-items: center;
word-break: break-all;
position: relative;
padding: 0 0 0 20px;
transition: all 0.3s;
}
.item.dragging{
background: pink;
color: yellow
}
.body{
flex: 1;
}
.btn{
padding: 10px 20px 10px 20px;
}
.btn .btn-sign, .btn:before, .btn:after{
display: block;
content: " ";
margin: 4px 0;
width: 40rpx;
height: 2px;
background: #999999;
border-radius: 2px;
}
原作者链接:小程序实现列表拖拽排序 | 微信开放社区