vue拖拽指令——模拟苹果触控球停靠

1,049 阅读2分钟

序言

vue元素拖拽指令,结束时模拟苹果手机触控球达到自动停靠效果。

代码流程附带详细注释,文章不再赘述。

把代码重新详细注释了一次,并精简了文章(掘金主题效果很炫)。

一、效果图:

二、拖拽指令

import Vue from 'vue';
// 拖拽结束自动靠边停方法
import autoStop from './methods';

// 元素拖拽指令
Vue.directive('dragElement', {
  // inserted插入指令,确保元素已经渲染完成
  inserted: function (el) {
    // 保存初始状态的变量
    let startX;
    let startY;
    const {
      // 存在定位的父级元素及其宽高(即元素可移动限制区域,先假设为parentNode,不是相对父级定位可以再调整)
      parentNode: {
        clientWidth: pW,
        clientHeight: pH
      },
      // 元素计算宽高(如果停靠不想完全靠边,可给元素添加对应padding)
      offsetWidth: eW,
      offsetHeight: eH
    } = el;

    // 触控开始事件
    el.addEventListener('touchstart', (event) => {
      // 1、如果元素上存在自定义的私有属性__VueDragTimer__,则清空该定时器
      // 2、__VueDragTimer__:自定义定时器属性,首尾双下划线挂载于元素上,方便在解绑指令时(unbind)清除定时器,并可删除属性
      if (el.__VueDragTimer__) clearInterval(el.__VueDragTimer__);
      // 通过touch属性计算获取初始位置
      const {pageX, pageY} = event.touches[0];
      const {
        // 元素距离限制区域上左距离,此变量需要实时获取
        offsetTop: eT,
        offsetLeft: eL
      } = el;
      startX = parseInt(pageX - eL);
      startY = parseInt(pageY - eT);
    });

    // 拖动事件
    el.addEventListener('touchmove', (event) => {
      // 添加阻止默认事件,防止影响到元素点击事件
      event.preventDefault();
      // 获取touch事件,计算并实时改变元素位置(拖拽效果)
      const {pageX, pageY} = event.touches[0];
      let movePageX = parseInt(pageX - startX);
      let movePageY = parseInt(pageY - startY);
      // 超出限制区域,禁止越界
      if (movePageX <= 0) {
        movePageX = 0;
      } else if (movePageX >= pW - eW) {
        movePageX = pW - eW;
      }
      if (movePageY <= 0) {
        movePageY = 0;
      } else if (movePageY >= pH - eH) {
        movePageY = pH - eH;
      }
      el.style.left = movePageX + 'px';
      el.style.top = movePageY + 'px';
    });

    // 拖拽结束,自动停靠
    el.addEventListener('touchend', () => {
      autoStop(el);
    });
  },

  // 解绑元素时,清除定时器并删除挂载在元素上的自定义定时器属性
  unbind: function (el) {
    if (el.__VueDragTimer__) clearInterval(el.__VueDragTimer__);
    delete el.__VueDragTimer__;
  }
});

三、自动停靠方法

// 元素移动方法
const autoMove = function (el, changeAttr, endValue) {
  // 定义 起点、运动中当前位置 变量
  let currentValue;
  const {
    // 元素距离限制区域上左距离
    offsetTop: eT,
    offsetLeft: eL
  } = el;
  // 改变属性为 'left' 时,起点赋值为:元素与限制区左边的距离
  if (changeAttr === 'left') currentValue = eL;
  // 改变属性为 'top' 时,起点赋值为:元素与限制区上边的距离
  if (changeAttr === 'top') currentValue = eT;
  // 移动步距设置为 终点与起点 距离的 1 / 33,值的正负表示移动方向
  const step = (endValue - currentValue) / 33;
  // 定时器存在则清除处理
  if (el.__VueDragTimer__) clearInterval(el.__VueDragTimer__);
  // 设置自动停靠定时器
  el.__VueDragTimer__ = setInterval(() => {
    // 若当前位置与终点差值 已经 小于步距,则直接定位到终点位置(js计算精度导致此判断不可少),否则缩小步距的距离
    if (Math.abs(endValue - currentValue) < Math.abs(step)) {
      el.style[changeAttr] = endValue + 'px';
      clearInterval(el.__VueDragTimer__);
      delete el.__VueDragTimer__;
    } else {
      currentValue += step;
      el.style[changeAttr] = currentValue + 'px';
    }
  }, 5);
};

/**
 * 自动停靠方法:
 * 停靠原则
 * 1、默认左右停靠开关永久开启
 * 2、上下停靠开关在距离上下<=50像素时开启
 * 3、最终移动方向:从停靠开关开启的方向中,取需要移动距离最小的方向
 */
const autoStop = function (el) {
  const {
    // 存在定位的父级元素及其宽高(即元素可移动限制区域,先假设为parentNode,不是相对父级定位可以再调整)
    parentNode: {
      clientWidth: pW,
      clientHeight: pH
    },
    // 元素距离限制区域上左距离
    offsetTop: eT,
    offsetLeft: eL,
    // 元素计算宽高(如果停靠不想完全靠边,可给元素添加对应padding)
    offsetWidth: eW,
    offsetHeight: eH
  } = el;
  /**
   * 停靠配置
   * changeAttr: 当前配置方案改变的元素属性 'top'| 'left'
   * endValue: 停靠动画结束时,需要改变的属性的值(终点)
   * distance:当前配置,距离目标结束位置的距离
   * toggle: 配置开启开关
   */
  const stopConfigs = [
    { // 上移贴边配置
      changeAttr: 'top',
      endValue: 0,
      distance: eT,
      toggle: eT <= 50
    }, { // 下移贴边配置
      changeAttr: 'top',
      endValue: pH - eH,
      distance: pH - eT - eH,
      toggle: pH - eT - eH <= 50
    }, { // 左移贴边配置
      changeAttr: 'left',
      endValue: 0,
      distance: eL,
      toggle: true
    }, { // 右移贴边配置
      changeAttr: 'left',
      endValue: pW - eW,
      distance: pW - eL - eW,
      toggle: true
    }
  ];
  // 先重配置中选出开关为true的,然后按distance排序后选出其值最小的配置,并获取对应的changeAttr、endValue
  const {changeAttr, endValue} = stopConfigs.filter(o => o.toggle).sort((a, b) => a.distance - b.distance)[0];
  autoMove(el, changeAttr, endValue);
};

export default autoStop;