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 复制代码
相关推荐
AlienZHOU12 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc16 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师17 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen18 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒18 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow18 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81818 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007818 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明18 小时前
第 6 章 组件进阶
前端·vue.js
江华森18 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端