图片拖拽排序
📖首先必须要对一下 API 有所了解(如果已经了解了可以请略过)
转入正题,效果图长咋样的呢?
原理
- 把图片设置为可拖动元素。
- 添加拖拽事件,监听拖拽行为。
- 拖拽事件中计算触摸点的位置,计算当前触摸点属于哪张图片上(第几行第几列),在相对把位置修改即可(修改渲染数组)。
基本原理就是这样,剩余的就是体验问题,比如添加个动画等。
可以先不往下看,根据原理尝试一遍,看看是否能完成,或许比往下看收获更多呢。
实现
布局
这里是使用vue 3编写,其他框架版本原理一样。
布局应该难不了各位,不一定按我布局方式来写,或许有更好的方法。
这里我是根据传入的列数,获取父容器宽度,动态划分每个图片模块的宽高;还监听了屏幕的尺寸改变
resize
,不过记得卸载时要移除监听器。
vue
<template>
<div
class="drag-wrap"
ref="dragWrapRef"
>
<ul class="drag-list" :style="{ height: `${drapWrapHeight}px` }">
<li
class="item"
v-for="(item, index) in dragList"
:key="item.id"
:style="{
width: `${imgSize}px`,
height: `${imgSize}px`,
left: `${imgLeft(index)}px`,
top: `${imgTop(index)}px`,
}"
>
<img :src="item.imgUrl" alt="" />
</li>
</ul>
</div>
</template>
<script>
import {
onMounted,
reactive,
ref,
toRefs,
toRef,
computed,
onUnmounted,
} from 'vue'
export default {
props: {
column: {
type: Number,
default: 4,
},
dataSource: {
type: Array,
default: [
{
id: 1,
name: 'one',
imgUrl:
'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4008842364,2792264191&fm=26&gp=0.jpg',
},
{
id: 2,
name: 'two',
imgUrl:
'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1961155531,4083413222&fm=26&gp=0.jpg',
},
{
id: 3,
name: 'three',
imgUrl:
'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1127316522,293083506&fm=26&gp=0.jpg',
},
{
id: 4,
name: 'four',
imgUrl:
'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3423787266,1419039532&fm=26&gp=0.jpg',
},
{
id: 5,
name: 'five',
imgUrl:
'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3771115016,2161480362&fm=26&gp=0.jpg',
},
],
},
},
setup(props) {
const { column } = toRefs(props)
const state = reactive({
dragList: props.dataSource
})
const IMAGE_PADDING = 10
const dragWrapRef = ref(null) // 父容器对象
const drapWrapHeight = ref(0) // 父容器高度
const imgSize = ref(0) // 图片宽高
// 重新计算尺寸
const handleResize = () => {
const wrapW = dragWrapRef.value.clientWidth // 获取宽度
imgSize.value =
(wrapW - (column.value - 1) * IMAGE_PADDING) / column.value // 计算图片的宽高
const row = Math.ceil(state.dragList.length / column.value) // 计算出多少行
drapWrapHeight.value = row * imgSize.value + (row - 1) * IMAGE_PADDING // 计算容器的高度
}
// 设置屏幕监听
window.addEventListener('resize', handleResize)
// 计算图片left值
const imgLeft = computed(() => {
return (index) => {
return (index % column.value) * (imgSize.value + IMAGE_PADDING)
}
})
// 计算图片top值
const imgTop = computed(() => {
return (index) => {
const row = Math.floor(index / column.value)
return row * imgSize.value + row * IMAGE_PADDING
}
})
onMounted(() => {
handleResize()
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
return {
dragList: toRef(state, 'dragList'),
dragWrapRef,
imgSize,
drapWrapHeight,
imgLeft,
imgTop,
}
},
}
</script>
<style lang="scss" scoped>
.drag-wrap {
margin: 10px;
.drag-list {
position: relative;
width: 100%;
.item {
overflow: hidden;
position: absolute;
display: flex;
align-items: center;
border-radius: 10px;
box-shadow: 0 0 10px #ccc;
background-color: #eee;
transition: all 200ms ease-in-out;
img {
display: block;
width: 100%;
}
&.dragging {
transform: scale(1.04);
opacity: 0.7;
}
}
}
}
</style>>
拖拽
❗❗❗ 以下部分可能使用伪代码,请勿直接复制。
现在进行原理二,把元素改为可拖拽元素,并添加拖拽事件
vue
<li
...
@dragstart="(e) => handleDragStart(e, item)"
draggable="true"
>
<img :src="item.imgUrl" alt="" draggable="false" />
</li>
javascript
// 拖动开始
const handleDragStart = (e, item) => {
// 添加拖拽开始,把元素放大修改透明度的视绝效果 css类
e.target && e.target.classList.add('dragging')
// 记录拖拽元素的DOM
dragItemRef = e.target
// 记录拖拽元素的数据
dragItemData = toRaw(item)
}
解析
这里我之所以把 img
改为不可拖拽,是因为 img
元素默认是可以拖拽的,你们可以尝试去掉,就看到会出现什么问题了。
li
标签改为可拖拽,并添加拖拽开始事件。主要是记录数据、添加视觉css。
然后还需要添加一个拖拽结束事件,为了节省内存,设置到父容器上,当然也可以改为为每个拖拽元素设置,但没必要。(dragover
待会讲)
主要是清除数据,移除拖拽css。
vue
<div
class="drag-wrap"
ref="dragWrapRef"
@dragend="handleDragEnd"
@dragover="handleDragOver"
>
...
</div>
javascript
// 拖动结束
const handleDragEnd = () => {
// 隐藏拖动时的css
if (dragItemRef) {
dragItemRef.classList.remove('dragging')
}
// 清除数据
dragItemRef = null
dragItemData = null
}
计算
javascript
// 拖动到可释放目标上触发
const handleDragOver = (e) => {
// 禁止默认事件
e.preventDefault()
// 更新渲染列表,重要!
updateDragList(e.clientX, e.clientY)
}
dragover
当元素或者选择的文本被拖拽到一个有效的放置目标上时,触发 dragover
事件(每几百毫秒触发一次)。
通过它的 event
对象上获取当前鼠标的横轴坐标。然后进行原理三的计算。
javascript
/**
* @description: 更新列表
* @param {number} x 横坐标
* @param {number} y 纵坐标
* @return {void}
*/
const updateDragList = (x, y) => {
if (!dragWrapRef.value || !dragItemRef || !dragItemData) return
const dropRect = dragWrapRef.value.getBoundingClientRect()
if (!dropRect) return
// 求出鼠标相对父容器的位置
const offsetX = x - dropRect.left
const offsetY = y - dropRect.top
// 表示超出容器
if (
offsetX < 0 ||
offsetX > dropRect.width ||
offsetY < 0 ||
offsetY > dropRect.height
)
return
// 计算出移动到第几行第几列
const col = Math.floor(offsetX / imgSize.value) // 列
const row = Math.floor(offsetY / imgSize.value) // 行
let currentIndex = row * column.value + col // 目标位置后一个index
currentIndex =
currentIndex > state.dragList.length - 1 ?
state.dragList.length - 1 :
currentIndex
const fromIndex = state.dragList.indexOf(dragItemData) // 拖拽元素的index
const newList = insertBefore(
toRaw(state.dragList),
dragItemData,
fromIndex,
currentIndex
) // 重新排序后的数组
state.dragList = newList
}
javascript
/**
* @description: 从from插入到to位置,返回改变后的数组
* @param {Array} list 数组
* @param {Object} fromData 拖拽对象数据
* @param {number} from 移动前的index
* @param {number} to 目标index
* @return {Array}
*/
const insertBefore = (list, fromData, from, to) => {
if (from === to) return list
const newList = [...list]
newList.splice(from, 1)
newList.splice(to, 0, fromData)
return newList
}
解析:
- 获取父容器的相关值,计算鼠标相对父容器的横轴坐标。
- 判断拖拽是否超出父容器,超出
return
。 - 根据相对父容器的横轴坐标,计算出当前是第几行第几列,因为图片的大小、
padding
我们都是知道的嘛,然后求出在数组中拖拽元素的索引,和目标位置的索引。 - 然后对比是否位置改变了,如果改变了就操作数组即可。
注意:需要限制 currentIndex
最大值,否则会超出数组的长度。或者采用‘相对元素定位方法’,即移动后的位置相对于谁的前面,如[a, b, c, d]
,b 移到 c 位置,相对就是在 d 前面,若没有就代表放到最后。
最后,大功告成😉😉😉!!!