Uniapp中实现可拖拽元素并抽离公共方法

实现目的

日常前端开发经常会遇到悬浮按钮操作,悬浮按钮偶尔会遇到遮挡底部页面内容的情况,而使用scroll滚动控制按钮呈现会因频繁触发滚动导致性能损耗,因此自定义拖动方法,旨在用户需要时使用。

效果呈现

公共方法

首先定义事件类

javascript 复制代码
/**
 * 事件类
 */
class EventDispatcher {
  constructor() {
    this._listeners = {};
    this.ds = this.dispatchEvent;
    this.trigger = this.dispatchEvent;
    this.emit = this.dispatchEvent;
    this.off = this.removeEventListener;
    this.hs = this.hasEventListener;
    this.on = this.addEventListener;
  }
  /**
   * 添加监听事件
   * @param {string} type 事件标识
   * @param {function} listener 事件执行函数
   * @param {*} [context=this]  监听方法内的this上下文关系
   */
  addEventListener(type, listener, context) {
    if (this._listeners === undefined) this._listeners = {};

    var _listeners = this._listeners;

    if (_listeners[type] === undefined) {
      _listeners[type] = [];
    }

    if (_listeners[type].indexOf(listener) === -1) {
      if (context !== undefined) listener._ds_context = context;

      _listeners[type].push(listener);
    }
  }
  /**
   * 添加监听事件,执行一次就销毁
   * @param {string} type 事件标识
   * @param {function} listener 事件执行函数
   * @param {*} [context=this]  监听方法内的this上下文关系
   */
  once(type, listener, context) {
    listener.once = true;
    this.addEventListener(type, listener, context);
  }
  /**
   * 判断是否监听指定事件

   * @param {string } type  事件标识
   * @param {function} listener 事件执行函数
   * @returns {boolean}
   */
  hasEventListener(type, listener) {
    if (this._listeners === undefined) return false;

    var _listeners = this._listeners;

    return (
      _listeners[type] !== undefined &&
      _listeners[type].indexOf(listener) !== -1
    );
  }
  /**
   * 删除监听事件
   * @param type 事件标识
   * @param listener  时间执行函数
   */
  removeEventListener(type, listener) {
    if (this._listeners === undefined) return;

    var _listeners = this._listeners;
    var _listenerArray = _listeners[type];

    if (_listenerArray !== undefined) {
      var _index = _listenerArray.indexOf(listener);

      if (_index !== -1) {
        _listenerArray.splice(_index, 1);
      }
    }
  }
  /**
   * 派发事件
   * @param {*} event
   */
  dispatchEvent(event) {
    if (this._listeners === undefined) return;

    if (typeof event === "string") event = { type: event };

    var _listeners = this._listeners;
    var _listenerArray = _listeners[event.type];

    if (_listenerArray !== undefined) {
      event.target = this;
      var _array = _listenerArray.slice(0);
      for (var i = 0, l = _array.length; i < l; i++) {
        // console.log('--->_array[i]:',_array[i],i,_array[i].call);
        // console.log(_array[i]._ds_context,this);
        if (_array[i]._ds_context) _array[i].call(_array[i]._ds_context, event);
        else _array[i].call(this, event);

        if (_array[i].once) {
          this.removeEventListener(event.type, _array[i]);
        }
      }
    }
  }
  /**
   * 删除所有的事件监听
   */
  removeAllEventListeners() {
    this._listeners = {};
  }
}

export default EventDispatcher;

用事件类定义touch操作类

javascript 复制代码
import EventDispatcher from "./EventDispatcher"; // 引入上面定义的事件类

/**
 * 手势事件处理
 */
class TouchGestures extends EventDispatcher {
	constructor() {
		super();

		this.preV = { x: null, y: null };
		this.pinchStartLen = null;
		this.scale = 1;
		this.isDoubleTap = false;

		this.delta = null;
		this.last = null;
		this.now = null;
		this.tapTimeout = null;
		this.singleTapTimeout = null;
		this.longTapTimeout = null;
		this.swipeTimeout = null;
		this.x1 = this.x2 = this.y1 = this.y2 = null;
		this.preTapPosition = { x: null, y: null };
	}

	touchstart(evt) {
		if (!evt.touches) return;
		this.now = Date.now();
		this.x1 = evt.touches[0].x ?? evt.touches[0].pageX;
		this.y1 = evt.touches[0].y ?? evt.touches[0].pageY;
		this.delta = this.now - (this.last || this.now);
		// console.log("touchStart", evt.touches, this.x1, this.y1);
		this.ds({ type: "touchStart", evt });
		if (this.preTapPosition.x !== null) {
			this.isDoubleTap =
				this.delta > 0 && this.delta <= 250 && Math.abs(this.preTapPosition.x - this.x1) < 30 && Math.abs(this.preTapPosition.y - this.y1) < 30;
		}
		this.preTapPosition.x = this.x1;
		this.preTapPosition.y = this.y1;
		this.last = this.now;
		let preV = this.preV,
			len = evt.touches.length;
		if (len > 1) {
			this._cancelLongTap();
			this._cancelSingleTap();
			let otx = evt.touches[1].x ?? evt.touches[1].pageX;
			let oty = evt.touches[1].y ?? evt.touches[1].pageY;
			let v = { x: otx - this.x1, y: oty - this.y1 };
			preV.x = v.x;
			preV.y = v.y;
			this.pinchStartLen = getLen(preV);
			// console.log('multipointStart');
			this.ds({ type: "multipointStart", evt });
		}
		this.longTapTimeout = setTimeout(
			function() {
				// console.log('longTap');
				this.ds({ type: "longTap", evt });
			}.bind(this),
			750
		);
	}

	touchmove(evt) {
		if (!evt.touches) return;
		let preV = this.preV,
			len = evt.touches.length,
			currentX = evt.touches[0].pageX ?? evt.touches[0].pageX,
			currentY = evt.touches[0].pageY ?? evt.touches[0].pageY;
		this.isDoubleTap = false;
		if (len > 1) {
			let otx = evt.touches[1].pageX ?? evt.touches[1].pageX;
			let oty = evt.touches[1].pageY ?? evt.touches[1].pageY;
			let v = { x: otx - currentX, y: oty - currentY };

			if (preV.x !== null) {
				if (this.pinchStartLen > 0) {
					evt.scale = getLen(v) / this.pinchStartLen;
					// console.log('pinch', evt.scale);
					this.ds({ type: "pinch", evt, scale: evt.scale });
				}

				evt.angle = getRotateAngle(v, preV);
				// console.log('rotate', evt.angle);
				this.ds({ type: "rotate", evt, angle: evt.angle });
			}
			preV.x = v.x;
			preV.y = v.y;
		} else {
			if (this.x2 !== null) {
				evt.deltaX = currentX - this.x2;
				evt.deltaY = currentY - this.y2;
			} else {
				evt.deltaX = 0;
				evt.deltaY = 0;
			}
			// console.log("pressMove", currentY,this.y2,evt.deltaY);
			this.ds({ type: "pressMove", evt });
		}

		// console.log('touchMove');
		this.ds({ type: "touchMove", evt });

		this._cancelLongTap();
		this.x2 = currentX;
		this.y2 = currentY;
		if (len > 1) {
			// evt.preventDefault();
		}
	}

	touchend(evt) {
		if (!evt.touches) return;
		this._cancelLongTap();
		let self = this;
		if (evt.touches.length < 2) {
			// console.log('multipointEnd');
			this.ds({ type: "multipointEnd", evt });
		}
		// console.log('touchEnd');
		this.ds({ type: "touchEnd", evt });
		//swipe
		
		if ((this.x2 && Math.abs(this.x1 - this.x2) > 30) || (this.y2 && Math.abs(this.y1 - this.y2) > 30)) {
			evt.direction = this._swipeDirection(this.x1, this.x2, this.y1, this.y2);
			this.swipeTimeout = setTimeout(function() {
				// console.log('swipe');
				self.ds({ type: "swipe", evt, direction: evt.direction });
			}, 0);
		} else {
			this.tapTimeout = setTimeout(function() {
				// console.info("tap");
				self.ds({ type: "tap", evt });
				// trigger double tap immediately
				if (self.isDoubleTap) {
					// console.log('doubleTap');
					self.ds({ type: "doubleTap", evt });
					clearTimeout(self.singleTapTimeout);
					self.isDoubleTap = false;
				}
			}, 0);

			if (!self.isDoubleTap) {
				self.singleTapTimeout = setTimeout(function() {
					// console.log('singleTap');
					self.ds({ type: "singleTap", evt });
				}, 250);
			}
		}

		this.preV.x = 0;
		this.preV.y = 0;
		this.scale = 1;
		this.pinchStartLen = null;
		this.x1 = this.x2 = this.y1 = this.y2 = null;
	}

	touchcancel(evt) {
		clearTimeout(this.singleTapTimeout);
		clearTimeout(this.tapTimeout);
		clearTimeout(this.longTapTimeout);
		clearTimeout(this.swipeTimeout);
		// console.log('touchCancel');
		this.ds({ type: "touchCancel", evt });
	}

	_cancelLongTap() {
		clearTimeout(this.longTapTimeout);
	}

	_cancelSingleTap() {
		clearTimeout(this.singleTapTimeout);
	}

	_swipeDirection(x1, x2, y1, y2) {
		return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? "left" : "right") : y1 - y2 > 0 ? "up" : "down";
	}

	destroy() {
		if (this.singleTapTimeout) clearTimeout(this.singleTapTimeout);
		if (this.tapTimeout) clearTimeout(this.tapTimeout);
		if (this.longTapTimeout) clearTimeout(this.longTapTimeout);
		if (this.swipeTimeout) clearTimeout(this.swipeTimeout);

		this.element.removeEventListener("touchstart", this.start);
		this.element.removeEventListener("touchmove", this.move);
		this.element.removeEventListener("touchend", this.end);
		this.element.removeEventListener("touchcancel", this.cancel);

		this.preV = this.pinchStartLen = this.scale = this.isDoubleTap = this.delta = this.last = this.now = this.tapTimeout = this.singleTapTimeout = this.longTapTimeout = this.swipeTimeout = this.x1 = this.x2 = this.y1 = this.y2 = this.preTapPosition = this.rotate = this.touchStart = this.multipointStart = this.multipointEnd = this.pinch = this.swipe = this.tap = this.doubleTap = this.longTap = this.singleTap = this.pressMove = this.touchMove = this.touchEnd = this.touchCancel = null;

		return null;
	}
}

function getLen(v) {
	return Math.sqrt(v.x * v.x + v.y * v.y);
}

function dot(v1, v2) {
	return v1.x * v2.x + v1.y * v2.y;
}

function getAngle(v1, v2) {
	let mr = getLen(v1) * getLen(v2);
	if (mr === 0) return 0;
	let r = dot(v1, v2) / mr;
	if (r > 1) r = 1;
	return Math.acos(r);
}

function cross(v1, v2) {
	return v1.x * v2.y - v2.x * v1.y;
}

function getRotateAngle(v1, v2) {
	let angle = getAngle(v1, v2);
	if (cross(v1, v2) > 0) {
		angle *= -1;
	}

	return (angle * 180) / Math.PI;
}

export default TouchGestures;

业务代码中使用

html 复制代码
<template>
     <!-- 使用transform做偏移量呈现 -->
     <view class="default-class" :style="{transform:`translateY(${OffsetY}px)`}" @click="xxx" @touchstart="touchstart" @touchmove.stop.prevent="touchmove" @touchend="touchend" @touchcancel="touchcancel">批量继承</view>
</template>

<script>
import TouchGestures from "@/common/TouchGestures";

const touchGestures = new TouchGestures();

export default {
     data() {
        return {
            OffsetY: 0, // 偏移量
        }
     },
     methods:{
        // touch操作
        touchstart(event) {
            touchGestures.touchstart(event);
        },
        touchmove(event) {
            touchGestures.touchmove(event);
        },
        touchend(event) {
            touchGestures.touchend(event);
        },
        touchcancel(event) {
            touchGestures.touchcancel(event);
        },
     },
     onReady() {
        touchGestures.on("pressMove", ({ evt: { deltaY } }) => {
            //evt.deltaX和evt.deltaY代表在屏幕上移动的距离,evt.target可以用来判断点击的对象
            // console.log("deltaY :>> ", deltaY);
            this.OffsetY += deltaY;
        });
    },
}
</script>
<style lang="less" scoped>
    .default-class{
        position: fixed;
        right: 40rpx;
        bottom: 160rpx;
        font-size: 28rpx;
        border-radius: 50%;
        width: 80rpx;
        height: 80rpx;
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 10rpx;
        text-align: center;
    }
</style>
javascript 复制代码
相关推荐
一颗花生米。2 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
勿语&3 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
正小安6 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光7 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   7 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发