Skip to content

图片拖拽排序

📖首先必须要对一下 API 有所了解(如果已经了解了可以请略过)

draggable

拖拽事件

转入正题,效果图长咋样的呢?

图片拖拽排序

原理

  1. 把图片设置为可拖动元素。
  2. 添加拖拽事件,监听拖拽行为。
  3. 拖拽事件中计算触摸点的位置,计算当前触摸点属于哪张图片上(第几行第几列),在相对把位置修改即可(修改渲染数组)。

基本原理就是这样,剩余的就是体验问题,比如添加个动画等。

可以先不往下看,根据原理尝试一遍,看看是否能完成,或许比往下看收获更多呢。

实现

布局

这里是使用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
}

解析

  1. 获取父容器的相关值,计算鼠标相对父容器的横轴坐标。
  2. 判断拖拽是否超出父容器,超出 return
  3. 根据相对父容器的横轴坐标,计算出当前是第几行第几列,因为图片的大小、padding 我们都是知道的嘛,然后求出在数组中拖拽元素的索引,和目标位置的索引。
  4. 然后对比是否位置改变了,如果改变了就操作数组即可。

注意:需要限制 currentIndex 最大值,否则会超出数组的长度。或者采用‘相对元素定位方法’,即移动后的位置相对于谁的前面,如[a, b, c, d],b 移到 c 位置,相对就是在 d 前面,若没有就代表放到最后。

最后,大功告成😉😉😉!!!

完整代码

Released under the MIT License.