feat(viewer): 关掉库自带触摸切图,改由松手判定 —— 未放大也能拖动、不再一碰就切
采用 Claude 提出的方案(第二步),替换此前所有"阈值接管"尝试。
为什么之前的尝试都不行:
· 第 1 版在 pointerup 后 view() 弹回 —— 拖动中途库早已按 1px 判据翻页,事后补救无效。
· 第 2 版从外面改写 viewer.action —— 库的捏合/双击/过渡全靠这个状态机,外部改写导
致双指捏合失效、拖动带过渡延迟、切图错乱(老师真机实测四条全中)。
· 第 3 版在 document 捕获阶段 stopPropagation 拦 pointermove —— 库用
HAS_POINTER_EVENT ? 'pointermove' : 'touchmove' 选通道,真机可能走 touchmove,
拦截整段失效;且监听器注册在库之后,同一节点同一阶段抢不到前面。真机实测无效。
本次做法(读源码 + 真机特征共同指向的最小改动):
· new Viewer(...) 加 slideOnTouch: false —— 单指动作永远是 'move',库自带的平移就是
跟手拖动;不再产生 'switch' 动作 ⇒ 1px 判据与 others.js 的 'switched' 闩锁都不存在。
· 松手时自己判定:够远(> max(56px, 视口宽×0.18))或够快(> 0.5px/ms 且 > 30px),
且横向占优(|dx| > |dy|*1.5)⇒ viewer.next(false)/prev(false);否则 viewer.reset()
带动画回弹。放大态或第二指(捏合)出现时一律放权给库。
· 全程不写库的内部状态,也不拦事件 ⇒ 不依赖 pointer/touch 通道,捏合/双击/过渡不受影响。
本机实测(390x844 手机模拟 + CDP 真触摸)全绿:
【未放大可拖动】按下 left=20 → 中途 left=-20 → 松手回弹 left=20 ⇒ 跟手 ✓ 回弹 ✓
横划 20px / 竖划 80px → 不切 ✓ 横划 120px → 切 ✓ 拖到 30px 中途 → 不翻页 ✓
双指捏合 w 351→1615 ✓ 双击放大 w 1615→2751 ✓
注:放大态下本方案不接管切图(一律放权给库做平移),与上一版"放大后 120px 也切"不同。
本机 9 套 jsdom 测试全绿。
This commit is contained in:
1 parent
f535c8c6fb
commit
a3f5c82287
3 files changed
+39
-43
No files matched your search
+2
-2
@@ -165,7 +165,7 @@ RUN php -r "file_exists('.env') || copy('.env.example', '.env');" \
|
||||
RUN printf '%s\n' \
|
||||
'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./public/js/context-js/context-js.js' \
|
||||
'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./resources/js/context-js.js' \
|
||||
'dad60266ca53a1af4b52567dac2d0dbb ./resources/views/user/images.blade.php' \
|
||||
'b88fe8302a60b4ca5cdaaf69d7ac01b8 ./resources/views/user/images.blade.php' \
|
||||
| md5sum -c - \
|
||||
&& grep -q "assetVersion('js/context-js/context-js.js')" ./resources/views/user/images.blade.php \
|
||||
&& grep -q 'isIOSWebKit' ./public/js/context-js/context-js.js \
|
||||
@@ -208,7 +208,7 @@ RUN APP_SRC_MD5=$(find app config routes -type f -print0 2>/dev/null | sort -z |
|
||||
"fork_sha=${FORK_SHA}" \
|
||||
"lsky_commit=${LSKY_COMMIT}" \
|
||||
"context_js_md5=c0e513ec8e93fd81b34b3c6de5cf5eb8" \
|
||||
"images_blade_md5=dad60266ca53a1af4b52567dac2d0dbb" \
|
||||
"images_blade_md5=b88fe8302a60b4ca5cdaaf69d7ac01b8" \
|
||||
"app_src_md5=${APP_SRC_MD5}" \
|
||||
"app_js_md5=${APP_JS_MD5}" \
|
||||
"app_css_md5=${APP_CSS_MD5}" \
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
# 三个文件各自的 md5(与 Dockerfile「自证 2」里的期望值一致):
|
||||
# c0e513ec8e93fd81b34b3c6de5cf5eb8 src/resources/js/context-js.js
|
||||
# c0e513ec8e93fd81b34b3c6de5cf5eb8 src/public/js/context-js/context-js.js
|
||||
# dad60266ca53a1af4b52567dac2d0dbb src/resources/views/user/images.blade.php
|
||||
# b88fe8302a60b4ca5cdaaf69d7ac01b8 src/resources/views/user/images.blade.php
|
||||
#
|
||||
# 上游原文件 md5(改动前,供比对):
|
||||
# c8e57f6232848ca8277341ddf1a3a7a6 resources/js/context-js.js
|
||||
|
||||
@@ -535,6 +535,8 @@ class="relative inline-flex h-4 w-7 shrink-0 items-center rounded-full bg-brand
|
||||
};
|
||||
const viewer = new Viewer(document.getElementById('images-grid'), {
|
||||
url: 'data-original',
|
||||
slideOnTouch: false, // 关掉库自带的触摸切图:单指动作永远是 move ⇒ 库自己的平移就是跟手拖动
|
||||
|
||||
// 到头不再绕回(老师明确不要循环:第一张向右滑会绕到最后一张,且那一下会让
|
||||
// 控件重建画布/缩略图条 ⇒ iOS 整个界面左偏、安卓缩略图全空,见后续修复记录)
|
||||
loop: false,
|
||||
@@ -642,52 +644,46 @@ class="relative inline-flex h-4 w-7 shrink-0 items-center rounded-full bg-brand
|
||||
document.addEventListener('touchcancel', () => { navDown = false; }, true);
|
||||
})();
|
||||
|
||||
/* 触摸端切图阈值接管(最小侵入版):只拦事件,绝不写库的内部状态。
|
||||
* 为什么必须这样 —— 血的教训:库的捏合、双击、移动过渡全靠它自己的
|
||||
* this.action 状态机(handlers.js:367-375 在 pointerdown 时一次性定值,
|
||||
* 且移动时若 action 为 'move' 会摘掉过渡类)。从外面改写这个值 ⇒
|
||||
* 双指捏合被覆盖失效、拖动带上 CSS 过渡延迟、切图行为也乱 —— 全崩。
|
||||
* 所以这里只做一件事:单指触摸、且位移还没到阈值时,在捕获阶段
|
||||
* stopPropagation,让库收不到这次移动(它就不会切图);一旦划够阈值就
|
||||
* 立刻放行,之后全部交给库原样处理(跟手切图本来就是它自带的)。
|
||||
* 多指(捏合)一根手指都不干预;库的内部状态自始至终不碰。 */
|
||||
/* 触摸端切图判定(接管 slideOnTouch 后的配套逻辑):
|
||||
* 库里 slideOnTouch:false ⇒ 单指动作永远是 'move',库自己的平移就是跟手拖动,
|
||||
* 不再出现 'switch' 动作 ⇒ 1px 判据与"闩锁"都不存在。切图还是回弹由松手时决定。
|
||||
* 为什么不拦事件:库用 HAS_POINTER_EVENT ? 'pointermove' : 'touchmove' 选通道,
|
||||
* 拦 pointermove 在真机上可能整段失效(踩过)。这里只读 viewer.action 判定是否放权,
|
||||
* 一个字都不写库的内部状态 —— 捏合/双击/过渡因此完全不受影响。 */
|
||||
(function () {
|
||||
const SWIPE_NOT_ZOOMED = 60; // 未放大:划够这么多才切图
|
||||
const SWIPE_ZOOMED = 120; // 放大后:更钝,避免和拖动打架
|
||||
const DIRECTION_RATIO = 1.2; // |dx| 要明显大于 |dy| 才算横划
|
||||
let active = 0; // 当前按下的手指数
|
||||
let downX = 0;
|
||||
let downY = 0;
|
||||
let zoomed = false;
|
||||
let qualified = true; // 默认放行(非画布/多指都走放行)
|
||||
const FAR_RATIO = 0.18; // 够远:视口宽的这个比例
|
||||
const FAR_MIN = 56; // 或至少这么多 px
|
||||
const FAST_SPEED = 0.5; // 够快:px/ms
|
||||
let g = null;
|
||||
const inCanvas = (el) => el instanceof Element && !!el.closest('.viewer-canvas');
|
||||
const isZoomed = () => {
|
||||
const im = document.querySelector('.viewer-canvas img');
|
||||
const c = document.querySelector('.viewer-canvas');
|
||||
if (!im || !c) return false;
|
||||
return im.getBoundingClientRect().width > c.getBoundingClientRect().width + 2;
|
||||
const isFit = () => {
|
||||
const d = viewer.imageData;
|
||||
const v = viewer.viewerData;
|
||||
if (!d || !v) return false;
|
||||
return d.x >= 0 && d.y >= 0 && d.width <= v.width && d.height <= v.height;
|
||||
};
|
||||
document.addEventListener('pointerdown', (e) => {
|
||||
active += 1;
|
||||
if (active > 1 || !inCanvas(e.target)) { qualified = true; return; }
|
||||
downX = e.clientX;
|
||||
downY = e.clientY;
|
||||
zoomed = isZoomed();
|
||||
qualified = false;
|
||||
if (!e.isPrimary) { if (g) g.multi = true; return; } // 第二指 ⇒ 捏合,放权给库
|
||||
g = (e.pointerType !== 'mouse' && inCanvas(e.target))
|
||||
? { x: e.clientX, y: e.clientY, t: e.timeStamp, fit: isFit(), multi: false }
|
||||
: null;
|
||||
}, true);
|
||||
document.addEventListener('pointermove', (e) => {
|
||||
if (active > 1 || qualified) return; // 捏合中 / 已放行:不干预
|
||||
const dx = e.clientX - downX;
|
||||
const dy = e.clientY - downY;
|
||||
const need = zoomed ? SWIPE_ZOOMED : SWIPE_NOT_ZOOMED;
|
||||
if (Math.abs(dx) >= need && Math.abs(dx) > Math.abs(dy) * DIRECTION_RATIO) {
|
||||
qualified = true; // 划够了:从这一下起放行
|
||||
return;
|
||||
document.addEventListener('pointerup', (e) => {
|
||||
if (!g || !e.isPrimary) return;
|
||||
const s = g;
|
||||
g = null;
|
||||
if (!s.fit || s.multi) return; // 放大态 / 捏合:一律交给库
|
||||
const dx = e.clientX - s.x;
|
||||
const dy = e.clientY - s.y;
|
||||
const dt = Math.max(1, e.timeStamp - s.t);
|
||||
const far = Math.abs(dx) > Math.max(FAR_MIN, window.innerWidth * FAR_RATIO);
|
||||
const fast = Math.abs(dx) / dt > FAST_SPEED && Math.abs(dx) > 30;
|
||||
if ((far || fast) && Math.abs(dx) > Math.abs(dy) * 1.5) {
|
||||
if (dx < 0) viewer.next(false); else viewer.prev(false);
|
||||
} else if (dx !== 0 || dy !== 0) {
|
||||
viewer.reset(); // 带动画的回弹
|
||||
}
|
||||
e.stopPropagation(); // 没划够:库收不到 ⇒ 不会切图
|
||||
}, true);
|
||||
document.addEventListener('pointerup', () => { active = Math.max(0, active - 1); }, true);
|
||||
document.addEventListener('pointercancel', () => { active = Math.max(0, active - 1); }, true);
|
||||
}, false);
|
||||
})();
|
||||
|
||||
/* 双击放大:库里的「模拟双击」(handlers.js:424-451)只看两次抬手的间隔(硬编码 500ms),
|
||||
|
||||
Reference in new issue
Block a user