输入 · 11
拖拽判定
Drag arbitration · drag-arbitration手指按下那一刻还不知道用户想干什么:点一下?横拖?还是只是想滚动页面?先观察几像素,再决定。
几乎所有可拖的画廊都要同时处理点击打开、横向拖动和纵向页面滚动。判定做不好,就会出现“想滚页面却拖动了轮播”“轻点被当成拖动打不开”“拖动一开始突然跳一下”。三个小规则——移动阈值、轴向偏置、点击容差——解决这三类问题。
公式
moved = |p − down| if moved < threshold → pending (nothing moves yet) else lock once: |dx| > bias·|dy| → axis x (let the page keep vertical scroll) |dy| > bias·|dx| → axis y otherwise → free report movement from the LOCK point, not the press point on release: maxMoved < tapSlop → tap
参数
| 本页默认 | 成熟实现 | |
|---|---|---|
| threshold | 8 px | momentum-slider axisThreshold: 8太小会把手抖当成拖动;太大则拖动起步迟钝。 |
| bias | 1.2 | momentum-slider horizontalBias: 1.2大于 1 才有“犹豫区”:斜着拖不会误判。 |
| tapSlop | 6 px | momentum-slider clickThreshold: 5 |
| touch-action | pan-y | 横向轮播设 touch-action: pan-y,把纵向滚动留给浏览器 |
怎样做好
- 从锁定点开始计算位移,而不是从按下点。否则越过阈值的那一帧,元素会一下子跳过 8 px。use-gesture 把触发时的阈值记在
_step里,再从位移中减掉。@use-gesture core/src/engines/Engine.ts:274–289 - 只锁一次。确定方向后整个手势保持不变,不要每帧重新判断,否则拖到一半会在横竖之间来回切换。
- 纵向手势要真的放行:用
touch-action: pan-y或不调用preventDefault,让页面照常滚动。锁定为横向后才接管。 - 点击判定看“整个手势里走过的最大距离”,不是松手时的位置——拖出去再拖回原点不算点击。
- 鼠标、触摸、
pointercancel都要正常收尾:取消时不要触发点击,也不要给出甩动速度。
在我们的案例里
42 个案例有拖拽,24 个单独做了点击容差,只有 5 个显式锁定了轴向。momentum-slider 是完整范例:8 px 阈值、1.2 偏置、5 px 点击容差,外加 touch-action: pan-y。
出现在 42 个案例
参考实现 · basics.js
/**
* Decide what a pointer gesture is: a tap, a horizontal drag, or a vertical one.
* Nothing is committed until the pointer has moved `threshold` px; then the dominant
* axis wins if it beats the other by `bias`. Movement is reported from the lock point,
* so the element does not jump by the threshold.
*/
export function createGestureArbiter({ threshold = 8, bias = 1.2, axis = 'auto', tapSlop = 6 } = {}) {
let start = null, lock = null, origin = null, maxDist = 0;
return {
down(x, y) { start = { x, y }; lock = null; origin = null; maxDist = 0; },
move(x, y) {
if (!start) return { state: 'idle' };
const dx = x - start.x, dy = y - start.y;
maxDist = Math.max(maxDist, Math.hypot(dx, dy));
if (!lock) {
if (Math.hypot(dx, dy) < threshold) return { state: 'pending', dx, dy };
const ax = Math.abs(dx), ay = Math.abs(dy);
lock = axis !== 'auto' ? axis : ax > ay * bias ? 'x' : ay > ax * bias ? 'y' : 'free';
origin = { x, y };
}
return { state: 'dragging', axis: lock,
dx: lock === 'y' ? 0 : x - origin.x, dy: lock === 'x' ? 0 : y - origin.y };
},
up() {
const result = { tap: !!start && maxDist < tapSlop, axis: lock };
start = null; lock = null;
return result;
},
};
}