封装一个磁吸自由拖拽 功能

0 阅读2分钟

需要一个简单的按钮,可以在屏幕上自由拖动,并且在拖动时能够检测到边界并进行“碰撞”反弹效果。下面是一个使用 Vue 3 实现这个功能的示例。

实现步骤

  1. 创建一个 Vue 3 项目。
  2. 创建一个可拖动的按钮组件。
  3. 实现边界碰撞检测和反弹效果。 可以使用 CSS 过渡来为吸附动作添加动画效果。我们只需要在按钮的样式中添加过渡属性,并在停止拖拽时触发过渡。
<template>
  <div ref="button" class="draggable-button" @mousedown="startDrag" @touchstart="startDrag" :style="{ top: `${position.y}px`, left: `${position.x}px`, transition: isSnapping ? 'left 0.3s, top 0.3s' : 'none' }">
    <slot />
  </div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { px2rem } from '@/hooks';

const position = ref({ x: 0, y: 0 });
const dragging = ref(false);
const isSnapping = ref(false);
const button = ref<HTMLElement | null>(null);
let offsetX = 0;
let offsetY = 0;

const startDrag = (event: MouseEvent | TouchEvent) => {
  dragging.value = true;
  isSnapping.value = false;
  const clientX = (event as MouseEvent).clientX || (event as TouchEvent).touches[0].clientX;
  const clientY = (event as MouseEvent).clientY || (event as TouchEvent).touches[0].clientY;
  offsetX = clientX - position.value.x;
  offsetY = clientY - position.value.y;
  document.addEventListener('mousemove', onDrag);
  document.addEventListener('mouseup', stopDrag);
  document.addEventListener('touchmove', onDrag);
  document.addEventListener('touchend', stopDrag);
};

const onDrag = (event: MouseEvent | TouchEvent) => {
  if (!dragging.value) return;
  const clientX = (event as MouseEvent).clientX || (event as TouchEvent).touches[0].clientX;
  const clientY = (event as MouseEvent).clientY || (event as TouchEvent).touches[0].clientY;
  let newX = clientX - offsetX;
  let newY = clientY - offsetY;
  const { innerWidth, innerHeight } = window;
  const buttonWidth = button.value?.offsetWidth || 0;
  const buttonHeight = button.value?.offsetHeight || 0;

  // 边界碰撞检测和反弹效果
  if (newX < 0) newX = 0;
  if (newX + buttonWidth > innerWidth) newX = innerWidth - buttonWidth;
  if (newY < 0) newY = 0;
  if (newY + buttonHeight > innerHeight) newY = innerHeight - buttonHeight;

  position.value = { x: newX, y: newY };
};

const stopDrag = () => {
  dragging.value = false;
  document.removeEventListener('mousemove', onDrag);
  document.removeEventListener('mouseup', stopDrag);
  document.removeEventListener('touchmove', onDrag);
  document.removeEventListener('touchend', stopDrag);

  // 吸附到最近的视口边缘
  const { innerWidth } = window;
  const buttonWidth = button.value?.offsetWidth || 0;
  const middleX = innerWidth / 2;
  if (position.value.x + buttonWidth / 2 < middleX) {
    // 吸附到左边
    position.value.x = 0;
  } else {
    // 吸附到右边
    position.value.x = innerWidth - buttonWidth;
  }

  // 触发过渡
  isSnapping.value = true;

  // 清除过渡效果(为了下一次拖拽时不受干扰)
  setTimeout(() => {
    isSnapping.value = false;
  }, 500); // 过渡时间与 CSS 中的过渡时间一致
};

onMounted(() => {
  button.value = document.querySelector('.draggable-button');
  // 设置初始位置为右下角
  const { innerWidth, innerHeight } = window;
  const buttonWidth = button.value?.offsetWidth || 0;
  const buttonHeight = button.value?.offsetHeight || 0;
  const bottom = px2rem('50').replace(new RegExp('rem', 'g'), '');
  position.value = {
    x: innerWidth - Number(buttonWidth),
    y: innerHeight - (Number(buttonHeight) + Number(bottom) * 60)
  };
});

onBeforeUnmount(() => {
  stopDrag();
});
</script>

<style scoped lang="less">
.draggable-button {
  position: absolute;
  background-color: transparent;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: grab;
  user-select: none;
  border-radius: 5px;
  z-index: 999;
  animation-delay: 2s;
}
</style>

使用

   <FloatingBubble v-if="showClaimWidget">
        <div class="widgetReward">
          <div class="widgetClaim" @click="handleClaim">
            <!-- <img src="@/assets/images/rewardWindow/Arrow.png" alt="" srcset="" />  -->
          </div>
        </div>
      </FloatingBubble>