实现目的
日常前端开发经常会遇到悬浮按钮操作,悬浮按钮偶尔会遇到遮挡底部页面内容的情况,而使用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