// gestures.js — 触控手势检测器 // // 支持 9 种手势: // 单击 · 双击 · 长按(1s) · 左滑 · 右滑 · 上滑 · 下滑 // 两指长按(1s) · 三指点击 class GestureDetector { constructor(el) { this._el = el; this._handlers = {}; this._startX = 0; this._startY = 0; this._startTime = 0; this._tapTimer = null; this._tapCount = 0; // 长按检测 this._longPressTimer = null; this._longPressTriggered = false; // 多指检测 this._maxTouches = 0; // 本次触摸中同时出现的最大手指数 this._multiTouchLPTimer = null; // 两指长按计时器 this._multiTouchHandled = false; this._bindTouch(); this._bindMouse(); this._bindKeyboard(); } on(evt, fn) { this._handlers[evt] = fn; return this; } _emit(evt) { dbg(evt); const fn = this._handlers[evt]; if (fn) fn(); } // ── 触摸事件 ── _bindTouch() { const el = this._el; el.addEventListener('touchstart', e => { e.preventDefault(); const count = e.touches.length; // 记录本次触摸出现过的最大手指数 if (count > this._maxTouches) this._maxTouches = count; if (count === 1) { // 单指:记录起点 + 启动长按计时 const t = e.touches[0]; this._startX = t.clientX; this._startY = t.clientY; this._startTime = Date.now(); this._longPressTriggered = false; this._longPressTimer = setTimeout(() => { this._longPressTriggered = true; this._longPressTimer = null; this._emit('longpress'); }, 1000); } else if (count === 2) { // 两指:取消单指长按,启动两指长按计时 this._cancelLongPress(); this._multiTouchLPTimer = setTimeout(() => { this._multiTouchHandled = true; this._multiTouchLPTimer = null; this._emit('twofinger_longpress'); }, 1000); } else if (count >= 3) { // 三指+:取消所有计时 this._cancelLongPress(); this._cancelMultiTouchLP(); } }, { passive: false }); el.addEventListener('touchmove', e => { e.preventDefault(); // 移动超 15px 取消长按 if (this._longPressTimer && e.touches.length === 1) { const t = e.touches[0]; const dx = t.clientX - this._startX; const dy = t.clientY - this._startY; if (Math.sqrt(dx * dx + dy * dy) > 15) { this._cancelLongPress(); } } // 两指移动也取消两指长按 if (this._multiTouchLPTimer) { this._cancelMultiTouchLP(); } }, { passive: false }); el.addEventListener('touchend', e => { e.preventDefault(); // 还有手指在屏幕上,等全部抬起再处理 if (e.touches.length > 0) return; // 全部手指抬起 — 根据 maxTouches 判定手势类型 const max = this._maxTouches; // 清理计时器 this._cancelLongPress(); this._cancelMultiTouchLP(); // 已在计时器中处理过(长按 / 两指长按) if (this._longPressTriggered || this._multiTouchHandled) { this._resetMultiTouch(); return; } // 三指点击 if (max >= 3) { this._resetMultiTouch(); this._emit('threefinger_tap'); return; } // 两指快速触摸(< 1s)→ 忽略 if (max === 2) { this._resetMultiTouch(); return; } // 单指:正常处理(滑动 / 点击) this._resetMultiTouch(); const t = e.changedTouches[0]; this._process(t.clientX, t.clientY); }, { passive: false }); el.addEventListener('touchcancel', () => { this._cancelLongPress(); this._cancelMultiTouchLP(); this._resetMultiTouch(); this._reset(); }, { passive: false }); } // ── 鼠标事件(桌面调试)── _bindMouse() { this._el.addEventListener('mousedown', e => { this._startX = e.clientX; this._startY = e.clientY; this._startTime = Date.now(); this._longPressTriggered = false; this._longPressTimer = setTimeout(() => { this._longPressTriggered = true; this._longPressTimer = null; this._emit('longpress'); }, 1000); }); this._el.addEventListener('mouseup', e => { this._cancelLongPress(); if (this._longPressTriggered) { this._longPressTriggered = false; return; } this._process(e.clientX, e.clientY); }); this._el.addEventListener('mousemove', e => { if (this._longPressTimer) { const dx = e.clientX - this._startX; const dy = e.clientY - this._startY; if (Math.sqrt(dx * dx + dy * dy) > 15) { this._cancelLongPress(); } } }); } // ── 键盘事件(桌面调试)── _bindKeyboard() { document.addEventListener('keydown', e => { switch (e.code) { case 'Space': e.preventDefault(); this._emit('singletap'); break; case 'Enter': e.preventDefault(); this._emit('doubletap'); break; case 'ArrowLeft': e.preventDefault(); this._emit('swipe_left'); break; case 'ArrowRight': e.preventDefault(); this._emit('swipe_right'); break; case 'ArrowUp': e.preventDefault(); this._emit('swipe_up'); break; case 'ArrowDown': e.preventDefault(); this._emit('swipe_down'); break; case 'KeyL': e.preventDefault(); this._emit('longpress'); break; case 'KeyP': e.preventDefault(); this._emit('twofinger_longpress'); break; case 'KeyQ': e.preventDefault(); this._emit('threefinger_tap'); break; } }); } // ── 手势判定(单指) ── _process(endX, endY) { const dx = endX - this._startX; const dy = endY - this._startY; const dist = Math.sqrt(dx * dx + dy * dy); const dt = Date.now() - this._startTime; // 滑动:距离 > 35px if (dist > 35) { const angle = Math.abs(Math.atan2(dy, dx) * 180 / Math.PI); let dir = null; if (angle < 45) dir = 'right'; else if (angle > 135) dir = 'left'; else if (dy < 0 && Math.abs(dy) > Math.abs(dx)) dir = 'up'; else if (dy > 0 && Math.abs(dy) > Math.abs(dx)) dir = 'down'; if (dir) { this._tapCount = 0; clearTimeout(this._tapTimer); this._emit(`swipe_${dir}`); return; } } // 点击:距离 < 20px,时间 < 500ms if (dist < 20 && dt < 500) { this._tapCount++; if (this._tapCount === 1) { this._tapTimer = setTimeout(() => { this._tapCount = 0; this._emit('singletap'); }, 250); } else if (this._tapCount >= 2) { clearTimeout(this._tapTimer); this._tapCount = 0; this._emit('doubletap'); } } } // ── 工具方法 ── _cancelLongPress() { if (this._longPressTimer) { clearTimeout(this._longPressTimer); this._longPressTimer = null; } } _cancelMultiTouchLP() { if (this._multiTouchLPTimer) { clearTimeout(this._multiTouchLPTimer); this._multiTouchLPTimer = null; } } _resetMultiTouch() { this._maxTouches = 0; this._multiTouchHandled = false; } _reset() { clearTimeout(this._tapTimer); this._tapCount = 0; this._longPressTriggered = false; this._resetMultiTouch(); } }