VueUse usePointerSwipe 实战指南:基于 PointerEvents 的响应式滑动手势检测
VueUse usePointerSwipe 实战指南:基于 PointerEvents 的响应式滑动手势检测
导读
usePointerSwipe 是 VueUse 中基于原生 PointerEvents 的响应式滑动手势检测工具,属于核心包 @vueuse/core 的 Sensors(传感器)类别。它统一处理鼠标、触摸屏与触控笔三类指针输入,无需区分 touchstart/touchmove/touchend 与 mousedown/mousemove/mouseup 两套事件体系,即可实现滑动开始、滑动过程、滑动结束的完整手势生命周期追踪。读完本文,你将掌握 usePointerSwipe 的全部配置项与返回值语义、其底层阈值判定与方向计算的源码原理,并能基于它快速实现「滑动删除」「滑动切换卡片」等交互。
usePointerSwipe 是什么
usePointerSwipe 提供基于 PointerEvents 的响应式滑动检测能力,核心源码位于 packages/core/usePointerSwipe/index.ts,并从 packages/core/index.ts 统一导出,可通过 import { usePointerSwipe } from '@vueuse/core' 直接使用。
相比 VueUse 中基于触摸事件的 useSwipe,usePointerSwipe 的优势在于:
- 依赖
PointerEvent,一套事件体系同时覆盖鼠标(mouse)、触摸(touch)与触控笔(pen)三种指针类型; - 通过
setPointerCapture捕获指针,指针按下后即使移出目标元素,后续的pointermove与pointerup仍会重定向回目标,滑动判定更稳定; - 默认以
passive: true注册监听器,不阻塞滚动,且自动为目标元素设置touch-action: pan-y,允许页面纵向滚动、同时拦截横向滑动,避免手势与滚动冲突。
基础用法
官方文档 packages/core/usePointerSwipe/index.md 给出的最小示例:在模板元素上挂 ref,传给组合式函数,即可拿到 isSwiping(是否正在滑动)与 direction(滑动方向):
<script setup lang="ts">
import { usePointerSwipe } from '@vueuse/core'
import { useTemplateRef } from 'vue'
const el = useTemplateRef('el')
const { isSwiping, direction } = usePointerSwipe(el)
</script>
<template>
<div ref="el">
Swipe here
</div>
</template>
target 参数类型为 MaybeRefOrGetter<HTMLElement | null | undefined>,即既可以直接传元素引用、ref 对象,也可以传返回元素的 getter 函数。内部通过 toRef(target)(来自 @vueuse/shared)统一归一化为响应式引用,并在元素挂载后自动注册事件监听。
完整 API 与配置项
UsePointerSwipeOptions 接口定义于 packages/core/usePointerSwipe/index.ts,各配置项及默认值如下:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
threshold |
number |
50 |
触发滑动判定所需的最小位移(像素),位移未达到该值时不视为滑动 |
onSwipeStart |
(e: PointerEvent) => void |
— | 指针按下(pointerdown)时回调 |
onSwipe |
(e: PointerEvent) => void |
— | 滑动过程中(pointermove 且已超过阈值)回调 |
onSwipeEnd |
(e: PointerEvent, direction: UseSwipeDirection) => void |
— | 滑动结束(pointerup / pointercancel)时回调,携带最终方向 |
pointerTypes |
PointerType<a href="https://link.gitcode.com/i/14d7ea3d8825147bda8f2181e68407c4" target="_blank">] |
['mouse', 'touch', 'pen'] |
允许监听的指针类型,PointerType = 'mouse' | 'touch' | 'pen'(见 [packages/core/types.ts) |
disableTextSelect |
boolean |
false |
是否在滑动时禁用文本选择(设置 user-select: none 系列样式) |
返回值 UsePointerSwipeReturn(见 packages/core/usePointerSwipe/index.ts):
| 返回值 | 类型 | 语义 |
|---|---|---|
isSwiping |
Readonly<ShallowRef<boolean>> |
当前是否处于滑动状态(位移已超过阈值) |
direction |
Readonly<ShallowRef<UseSwipeDirection>> |
当前滑动方向,取值为 'up' | 'down' | 'left' | 'right' | 'none' |
posStart |
DeepReadonly<Position> |
滑动起点坐标 { x, y }(clientX/clientY) |
posEnd |
DeepReadonly<Position> |
滑动终点坐标 { x, y } |
distanceX |
Readonly<ComputedRef<number>> |
水平位移距离(posStart.x - posEnd.x,右滑为负值) |
distanceY |
Readonly<ComputedRef<number>> |
垂直位移距离(posStart.y - posEnd.y,下滑为负值) |
stop |
() => void |
手动注销全部事件监听 |
其中 Position 为 { x: number; y: number }(定义于 packages/core/types.ts),UseSwipeDirection 类型则复用自 useSwipe(见 packages/core/useSwipe/index.ts)。
典型带配置示例
<script setup lang="ts">
import { usePointerSwipe } from '@vueuse/core'
import { useTemplateRef } from 'vue'
const el = useTemplateRef('el')
const { isSwiping, direction, distanceX } = usePointerSwipe(el, {
threshold: 80, // 位移 80px 才判定为滑动
pointerTypes: ['touch', 'mouse'], // 忽略触控笔输入
disableTextSelect: true, // 滑动时禁止选中文本
onSwipeStart(e) { /* 记录手势开始 */ },
onSwipe(e) { /* 实时处理位移,如驱动元素跟随 */ },
onSwipeEnd(e, direction) { /* 根据 direction 决定是否触发操作 */ },
})
</script>
源码级原理:事件链路与方向判定
usePointerSwipe 的完整实现只有约 180 行,逻辑非常紧凑,核心链路如下。
事件监听与指针捕获
函数内部通过 useEventListener(来自 packages/core/useEventListener/index.ts,注册后会在组件卸载时自动移除监听)注册三组事件:
pointerdown:校验事件类型后置isPointerDown = true,调用eventTarget.setPointerCapture(e.pointerId)捕获指针,将clientX/clientY同时写入posStart与posEnd(起点等于终点),并触发onSwipeStart;pointermove:仅当isPointerDown为真时更新posEnd;若位移首次超过阈值,将isSwiping置为true,此后每次移动触发onSwipe;pointerup/pointercancel(同一监听器接收数组形式的多事件):若正处于滑动状态则触发onSwipeEnd(e, direction.value),随后重置isPointerDown与isSwiping。
所有监听器均以 { passive: true } 注册(见 packages/core/usePointerSwipe/index.ts),保证不阻塞浏览器默认行为。
stop() 的实现即遍历内部保存的 stops 数组逐个注销监听(见 packages/core/usePointerSwipe/index.ts),可用于提前清理。
阈值与方向计算
位移与方向均基于 Vue 的 computed 计算属性实时推导(packages/core/usePointerSwipe/index.ts):
const distanceX = computed(() => posStart.x - posEnd.x)
const distanceY = computed(() => posStart.y - posEnd.y)
const isThresholdExceeded = computed(() =>
max(abs(distanceX.value), abs(distanceY.value)) >= threshold)
const direction = computed(() => {
if (!isThresholdExceeded.value)
return 'none'
if (abs(distanceX.value) > abs(distanceY.value))
return distanceX.value > 0 ? 'left' : 'right'
else
return distanceY.value > 0 ? 'up' : 'down'
})
方向判定遵循「谁位移大取谁」的原则:水平位移绝对值更大时,向左滑(distanceX > 0)判定为 'left',向右滑判定为 'right';否则按垂直位移判定 'up' / 'down'。位移未达阈值时方向恒为 'none'。
指针类型过滤
eventIsAllowed(packages/core/usePointerSwipe/index.ts)负责事件过滤:
const eventIsAllowed = (e: PointerEvent): boolean => {
const isReleasingButton = e.buttons === 0
const isPrimaryButton = e.buttons === 1
return options.pointerTypes?.includes(e.pointerType as PointerType)
?? (isReleasingButton || isPrimaryButton) ?? true
}
即:显式传入 pointerTypes 时,只处理指针类型匹配的事件;未传入时,默认接受主按钮按下(buttons === 1)与按钮释放(buttons === 0)的指针事件。
样式副作用
挂载后(tryOnMounted,来自 @vueuse/shared)会自动对目标元素设置内联样式(packages/core/usePointerSwipe/index.ts):
touch-action: pan-y:允许纵向滚动、屏蔽触摸横向滚动,保证横向滑动手势可被捕获;- 若
disableTextSelect: true,追加-webkit-user-select: none、-ms-user-select: none与user-select: none,滑动时防止误选中文本。
与 useSwipe(触摸事件方案)的取舍
如果只需要触摸屏手势、不关心鼠标与触控笔,VueUse 还提供了基于 TouchEvent 的 useSwipe。两者对比:
| 维度 | usePointerSwipe | useSwipe |
|---|---|---|
| 事件体系 | PointerEvent(鼠标/触摸/触控笔统一) |
TouchEvent(仅触摸) |
| 方向类型 | 复用 UseSwipeDirection |
定义 UseSwipeDirection 本身 |
| 监听选项 | 固定 passive: true |
支持 passive 配置(默认 true),capture: !passive 非被动模式下可调用 preventDefault 拦截横向滚动 |
| 指针捕获 | 使用 setPointerCapture |
依赖 touches.length === 1 单指约束 |
| 返回结构 | posStart/posEnd/distanceX/distanceY |
coordsStart/coordsEnd/lengthX/lengthY |
需要鼠标与触控笔统一输入、或希望跨元素稳定追踪手势时选 usePointerSwipe;需要精细控制事件 passive 行为并在 touchmove 中主动 preventDefault 时选 useSwipe。
实战:结合 distanceX 实现滑动切换
仓库中的 packages/core/usePointerSwipe/demo.vue 提供了一个可直接参考的滑动卡片示例:利用 distanceX 实时驱动元素位移与透明度,并在 onSwipeEnd 中判断滑动距离是否超过容器宽度一半,决定卡片是移出屏幕(left: 100%、opacity: 0)还是回弹复位:
<script setup lang="ts">
import { usePointerSwipe } from '@vueuse/core'
import { computed, shallowRef, useTemplateRef } from 'vue'
const target = useTemplateRef('target')
const container = useTemplateRef('container')
const containerWidth = computed(() => container.value?.offsetWidth)
const left = shallowRef('0')
const opacity = shallowRef(1)
const { distanceX, isSwiping } = usePointerSwipe(target, {
disableTextSelect: true,
onSwipe(e: PointerEvent) {
if (containerWidth.value) {
if (distanceX.value < 0) { // 向左滑
const distance = Math.abs(distanceX.value)
left.value = `${distance}px` // 元素跟随手指位移
opacity.value = 1.25 - distance / containerWidth.value // 随位移渐隐
}
else {
left.value = '0'
opacity.value = 1
}
}
},
onSwipeEnd(e: PointerEvent, direction) {
// 位移超过容器一半则滑出屏幕,否则回弹
if (distanceX.value < 0 && containerWidth.value
&& (Math.abs(distanceX.value) / containerWidth.value) >= 0.5) {
left.value = '100%'
opacity.value = 0
}
else {
left.value = '0'
opacity.value = 1
}
},
})
</script>
<template>
<div ref="container" class="relative w-full h-[80px] overflow-hidden">
<div ref="target" :class="{ 'transition-all duration-200 ease-linear': !isSwiping }"
:style="{ left, opacity }">
Swipe
</div>
</div>
</template>
要点提示:distanceX = posStart.x - posEnd.x,因此向左滑为负值、向右滑为正值,与视觉上的元素位移方向相反;滑动过程中应关闭 CSS transition(如示例中仅在 !isSwiping 时启用 transition-all),让元素跟手,滑动结束再借助 transition 平滑回弹或滑出。
测试验证与行为边界
packages/core/usePointerSwipe/index.browser.test.ts 用 Vitest + 模拟 PointerEvent 对行为做了完整验证,可作为理解 API 语义的权威参照:
- 未超阈值:位移小于
threshold时,onSwipeStart触发 1 次,onSwipe与onSwipeEnd均不触发(threshold is not exceeded用例); - 超过阈值:
onSwipeStart/onSwipe/onSwipeEnd各触发一次,onSwipeEnd携带方向'right'; - 中途回退:曾超过阈值后又回到阈值内,
onSwipe只在中途触发,结束时onSwipeEnd携带方向'none'(isSwiping仍会先置真); - 响应式:
pointerdown后isSwiping为false、direction为'none';pointermove超阈值后isSwiping变真、direction更新为'right';pointerup后isSwiping复位为false; - 类型过滤:设置
pointerTypes: ['touch']后派发鼠标指针事件不产生任何响应; - 方向矩阵:
up / down / left / right四组坐标用例均被正确识别,且onSwipeEnd收到对应方向; - stop 清理:调用
stop()后继续派发移动事件不再触发任何响应。
从这些用例可以总结出三个容易被忽略的边界行为:滑动结束后 direction 不会立即清空(保留最后一次判定结果,如 'right');手势中途回退到阈值内时结束方向为 'none';在未按下指针时派发 pointermove 会被忽略。
小结
usePointerSwipe 以极小的实现体量(单个 180 行左右的 index.ts)封装了基于 PointerEvents 的完整滑动检测能力:统一的指针类型过滤、指针捕获、阈值判定、方向计算与生命周期回调一应俱全。配合 distanceX / distanceY 响应式位移和 isSwiping 状态,即可低成本实现滑动删除、滑动分页、手势导航等常见移动端与桌面端交互;需要调整触发灵敏度时,只需修改 threshold,需要限定输入设备时,配置 pointerTypes 即可。