可拖拽、缩放、旋转组件实现细节

🌈介绍

基于 vue3.x + CompositionAPI + typescript + vite 的可拖拽、缩放、旋转的组件

  • 拖拽&区域拖拽
  • 支持缩放
  • 旋转

目标效果

在这里插入图片描述

源码地址

拖拽&区域拖拽

在这里插入图片描述

虽然叫拖拽,但却跟拖拽事件没有一点关系,主要使用mousedown、mousemove、mouseup事件来实现

代码实现

<template>
  <div
    ref="dragRef"
    :class="['es-drager']"
    :style="dragStyle"
    @mousedown="onMousedown"
  >
    <slot />
  </div>
</template>

<script setup lang='ts'>
import { computed, ref } from 'vue'
// 单位处理
const withUnit = (val: number | string = 0) => {
  return parseInt(val + '') + 'px'
}
const props = defineProps({
  boundary: { // 边界
    type: Boolean
  },
  width: {
    type: [Number, String],
    default: 100
  },
  height: {
    type: [Number, String],
    default: 100
  },
  left: {
    type: [Number, String],
    default: 0
  },
  top: {
    type: [Number, String],
    default: 0
  },
  color: {
    type: String,
    default: '#3a7afe'
  }
})
const emit = defineEmits(['move', 'resize'])

// 拖拽元素
const dragRef = ref<HTMLElement | null>(null)
// 是否按下鼠标
const isMousedown = ref(false)
// 拖拽数据
const dragData = ref({
  width: props.width,
  height: props.height,
  left: props.left,
  top: props.top
})
const dragStyle = computed(() => {
  const { width, height, left, top } = dragData.value
  return {
    width: withUnit(width),
    height: withUnit(height),
    left: withUnit(left),
    top: withUnit(top),
    '--es-drager-color': props.color
  }
})

/**
 * 鼠标按下事件
 */
function onMousedown(e: MouseEvent) {
  isMousedown.value = true
  const el = dragRef.value!
  
  // 记录按下的位置
  const downX = e.clientX
  const downY = e.clientY

  const elRect = el.getBoundingClientRect()
  // 鼠标在盒子里的位置
  const mouseX = downX - elRect.left
  const mouseY = downY - elRect.top

  const onMousemove = (e: MouseEvent) => {
    // 当前鼠标的位置减去鼠标在盒子里的位置就是要移动的距离
    let moveX = e.clientX - mouseX
    let moveY = e.clientY - mouseY
    
    dragData.value.left = moveX
    dragData.value.top = moveY
    emit && emit('move', dragData.value)
  }

  const onMouseup = (_e: MouseEvent) => {
    isMousedown.value = false
    // 移除document事件
    document.removeEventListener('mousemove', onMousemove)
    document.removeEventListener('mouseup', onMouseup)
  }
  // 位document注册鼠标移动事件
  document.addEventListener('mousemove', onMousemove)
  // 鼠标抬起事件
  document.addEventListener('mouseup', onMouseup)
}
</script>

<style lang='scss'>
.es-drager {
  position: absolute;
  z-index: 1000;
  border: 1px solid var(--es-drager-color, #3a7afe);
}
</style>

可以看到,核心逻辑主要在 onMousedown 事件处理函数中

步骤分析:

  1. 为拖拽元素注册mousedown事件
  2. 鼠标按下记录当前鼠标的位置,鼠标在元素中的位置
  3. 给document注册mousemove、mouseup事件
  4. mousemove事件中计算当前元素的位置
  5. mouseup中移除document事件

添加边界(区域拖拽)

区域拖拽主要是为了限制元素只能在最近定位父级元素中移动

function onMousedown(e: MouseEvent) {
  isMousedown.value = true
  const el = dragRef.value!
  const downX = e.clientX
  const downY = e.clientY
  const elRect = el.getBoundingClientRect()

  // 鼠标在盒子里的位置
  const mouseX = downX - elRect.left
  const mouseY = downY - elRect.top

  // 提前计算最小最大边界值
  let minX = 0, maxX = 0, minY = 0, maxY = 0
  if (props.boundary) {
    const parentEl = el.parentElement || document.body
    const parentElRect = parentEl!.getBoundingClientRect()
    // 最小x
    minX = parentElRect.left
    // 最大x
    maxX = parentElRect.left + parentElRect.width - elRect.width
    // 最小y
    minY = parentElRect.top
    // 最大y
    maxY = parentElRect.top + parentElRect.height - elRect.height
  }

  const onMousemove = (e: MouseEvent) => {
    let moveX = e.clientX - mouseX
    let moveY = e.clientY - mouseY

    if (props.boundary) {
      // 判断x最小最大边界
      // moveX = moveX < minX ? minX : moveX > maxX ? maxX : moveX
      moveX = moveX < minX ? minX : moveX
      moveX = moveX > maxX ? maxX : moveX
      
      // 判断y最小最大边界
      // moveY = moveY < minY ? minY : moveY > maxY ? maxY : moveY
      moveY = moveY < minY ? minY : moveY
      moveY = moveY > maxY ? maxY : moveY
    }
    
    dragData.value.left = moveX
    dragData.value.top = moveY
    emit && emit('move', dragData.value)
  }

  const onMouseup = (_e: MouseEvent) => {
    isMousedown.value = false
    document.removeEventListener('mousemove', onMousemove)
    document.removeEventListener('mouseup', onMouseup)
  }
  document.addEventListener('mousemove', onMousemove)
  document.addEventListener('mouseup', onMouseup)
}

边界值说明:

  1. 最小x,最近定位父级标签的left
  2. 最大x,最近定位父级标签的left + 父级的width - 元素的width
  3. 最小y,最近定位父级标签的top
  4. 最大y,最近定位父级标签的top + 父级的height - 元素的height

缩放

在这里插入图片描述

可以看到元素周围有8个小圆点,可以从不同的方向放大或缩小元素

显示小圆点,计算位置

<template>
  <div
    ref="dragRef"
    :class="['es-drager']"
    :style="dragStyle"
    @mousedown="onMousedown"
  >
    <slot />

    <div v-show="selected">
      <div
        v-for="item in dotList"
        :key="item.side"
        class="es-drager-dot"
        :data-side="item.side"
        :style="getDotStyle(item)"
        @mousedown="onDotMousedown(item, $event)"
      >
      </div>
    </div>
  </div>
</template>
<script setup lang='ts'>
import { computed, ref } from 'vue'

type IDotSide = 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
type IDot = {
  side: IDotSide,
  cursor?: string
}
const dotList: IDot[] = [
  { side: 'top', cursor: 'n-resize' },
  { side: 'bottom', cursor: 'n-resize' },
  { side: 'left', cursor: 'e-resize' },
  { side: 'right', cursor: 'e-resize' },
  { side: 'top-left', cursor: 'se-resize' },
  { side: 'top-right', cursor: 'sw-resize' },
  { side: 'bottom-left', cursor: 'sw-resize' },
  { side: 'bottom-right', cursor: 'se-resize' }
]
const selected = ref(true)

const emit = defineEmits(['move', 'resize'])

// 计算圆点位置
function getDotStyle(item: IDot) {
  const [side, position] = item.side.split('-')
  const style = { [side]: '0%', cursor: item.cursor }
  if (!position) {
    const side2 = ['top', 'bottom'].includes(side) ? 'left' : 'top'
    style[side2] = '50%'
  } else {
    style[position] = '0%'
  }

  return style
}

function onDotMousedown(dotInfo: IDot, e: MouseEvent) {
  e.stopPropagation()
  e.preventDefault()
  // ...
}
</script>
<style lang='scss'>
.es-drager-dot {
  position: absolute;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background-color: var(--es-drager-color, #3a7afe);
  transform: translate(-50%, -50%);
  cursor: se-resize;
  &[data-side*="right"] {
    transform: translate(50%, -50%);
  }
  &[data-side*="bottom"] {
    transform: translate(-50%, 50%);
  }
  &[data-side="bottom-right"] {
    transform: translate(50%, 50%);
  }
}
</style>
  • selected 选中拖拽元素时显示缩放小圆点
  • 主要关注点还是在小圆点的拖拽上,onDotMousedown 事件处理函数
function onDotMousedown(dotInfo: IDot, e: MouseEvent) {
  e.stopPropagation()
  e.preventDefault()
  // 获取鼠标按下的坐标
  const downX = e.clientX
  const downY = e.clientY
  const el = dragRef.value!
  const elRect = el.getBoundingClientRect()
  
  const onMousemove = (e: MouseEvent) => {
    // 移动的x距离
    const disX = e.clientX - downX
    // 移动的y距离
    const disY = e.clientY - downY

    const [side, position] = dotInfo.side.split('-')

    // 是否是上方缩放圆点
    const hasT = side === 'top'
    // 是否是左方缩放圆点
    const hasL = [side, position].includes('left')
    
    let width = elRect.width + (hasL ? -disX : disX)
    let height = elRect.height + (hasT ? -disY : disY)
    
    // 如果是左侧缩放圆点,修改left位置
    let left = elRect.left + (hasL ? disX : 0)

    // 如果是上方缩放圆点,修改top位置
    let top = elRect.top + (hasT ? disY : 0)

    if (!position) { // 如果是四个正方位
      if (['top', 'bottom'].includes(side)) {
        // 上下就不改变宽度
        width = elRect.width
      } else {
        // 左右就不改变高度
        height = elRect.height
      }
    }

    // 处理逆向缩放
    if (width < 0) {
      width = -width
      left -= width
    }
    if (height < 0) {
      height = -height
      top -= height
    }

    dragData.value = { left, top, width, height }
    emit('resize', dragData.value)
  }

  const onMouseup = (_e: MouseEvent) => {
    document.removeEventListener('mousemove', onMousemove)
    document.removeEventListener('mouseup', onMouseup)
  }
  document.addEventListener('mousemove', onMousemove)
  document.addEventListener('mouseup', onMouseup)
}

解析:

  1. 鼠标按下记录按下的坐标
  2. 移动时分别计算x和y移动的距离
  3. 右方和下方缩放比较好实现,直接改变宽高就行,原来的宽度+移动的距离
  4. 需要注意的是上方和左方的缩放,因为这两个方向,不仅要修改宽高,还需要修改位置
  5. let width = elRect.width + (hasL ? -disX : disX) 如果是左侧缩放圆点,现在的宽度减去移动的x距离。同时 let left = elRect.left + (hasL ? disX : 0) left需要加上移动的x,可能这样写更好理解一点
  let width = elRect.width, left = elRect.left
  if (hasL) {
    width -= disX
    left += disX
  } else {
    width += disX
  }
  1. 如果是四个正方向,上下无需修改宽度,左右无需修改高度

旋转

在这里插入图片描述

在计算角度的时候会使用到Math的atan2方法,返回从原点 (0,0) 到 (x,y) 点的线段与 x 轴正方向之间的平面角度 (弧度值),也就是 Math.atan2(y,x)

关于旋转按钮的样式就不贴代码了,直接从mousdown事件开始吧

function onRotateMousedown(e: MouseEvent) {
  e.stopPropagation()
  e.preventDefault()
  const el = dragRef.value!
  const elRect = el.getBoundingClientRect()
  // 旋转中心位置
  const centerX = elRect.left + elRect.width / 2
  const centerY = elRect.top + elRect.height / 2

  function onMousemove(e: MouseEvent) {
    const diffX = centerX - e.clientX
    const diffY = centerY - e.clientY
    // Math.atan2(y,x) 返回从原点 (0,0) 到 (x,y) 点的线段与 x 轴正方向之间的平面角度 (弧度值)
    const radians = Math.atan2(diffY, diffX)

    // 计算角度
    dragData.value.angle = radians * 180 / Math.PI - 90 // 角度
    emit('rotate', angle.value)
  }

  const onMouseup = (_e: MouseEvent) => {
    document.removeEventListener('mousemove', onMousemove)
    document.removeEventListener('mouseup', onMouseup)
  }
  document.addEventListener('mousemove', onMousemove)
  document.addEventListener('mouseup', onMouseup)
}
  1. 首先要计算出旋转中心位置
  2. 鼠标移动计算移动的点和中心点的距离
  3. 使用Math.atan2计算弧度,注意 y 是第一个参数
  4. 使用弧度计算出角度,后面减去了90是因为当前旋转按钮在正上方,而默认计算的是从正左方开始的

最后

细心的伙伴可能发现了一个问题,就是旋转后再缩放会很奇怪,而且旋转后鼠标经过缩放圆点上时的样式也不相称。

下次再来解决这个问题吧!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity中实现UI的拖拽缩放旋转的功能,可以通过以下步骤实现: 1. 拖拽功能: 首先,需要为UI元素添加一个EventTrigger组件。在EventTrigger组件中,添加一个PointerDown、PointerUp、Drag三个事件。然后,在代码中实现相应的事件处理。例如: ```csharp using UnityEngine; using UnityEngine.EventSystems; public class UIDrag : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler { private RectTransform rectTransform; private Vector2 offset; private void Start() { rectTransform = GetComponent<RectTransform>(); } public void OnPointerDown(PointerEventData eventData) { offset = rectTransform.anchoredPosition - eventData.position; } public void OnPointerUp(PointerEventData eventData) { } public void OnDrag(PointerEventData eventData) { rectTransform.anchoredPosition = eventData.position + offset; } } ``` 这里使用了RectTransform组件,通过计算鼠标点击位置与UI元素位置的偏移量,实现了UI元素的拖拽。 2. 缩放功能: 为UI元素添加一个EventTrigger组件,添加一个PointerDown、PointerUp、Drag三个事件。然后,在代码中实现相应的事件处理。例如: ```csharp using UnityEngine; using UnityEngine.EventSystems; public class UIScale : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler { private RectTransform rectTransform; private Vector2 offset; private Vector2 originalSize; private void Start() { rectTransform = GetComponent<RectTransform>(); originalSize = rectTransform.sizeDelta; } public void OnPointerDown(PointerEventData eventData) { offset = eventData.position - rectTransform.position; } public void OnPointerUp(PointerEventData eventData) { } public void OnDrag(PointerEventData eventData) { Vector2 currentPos = eventData.position; float distance = Vector2.Distance(currentPos, rectTransform.position); float scale = distance / offset.magnitude; rectTransform.sizeDelta = originalSize * scale; } } ``` 这里使用了RectTransform组件,通过计算鼠标点击位置与UI元素中心点的距离,实现了UI元素的缩放。 3. 旋转功能: 为UI元素添加一个EventTrigger组件,添加一个PointerDown、PointerUp、Drag三个事件。然后,在代码中实现相应的事件处理。例如: ```csharp using UnityEngine; using UnityEngine.EventSystems; public class UIRotate : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler { private RectTransform rectTransform; private Vector2 offset; private float originalRotation; private void Start() { rectTransform = GetComponent<RectTransform>(); originalRotation = rectTransform.localEulerAngles.z; } public void OnPointerDown(PointerEventData eventData) { offset = eventData.position - rectTransform.position; } public void OnPointerUp(PointerEventData eventData) { } public void OnDrag(PointerEventData eventData) { Vector2 currentPos = eventData.position; Vector2 dir = currentPos - rectTransform.position; float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; rectTransform.localEulerAngles = new Vector3(0, 0, angle - offset.magnitude); } } ``` 这里使用了RectTransform组件,通过计算鼠标点击位置与UI元素中心点的方向,计算出旋转角度,实现了UI元素的旋转。 以上就是实现UI元素拖拽缩放旋转的步骤,可以根据自己的需求进行相应的修改和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值