react-use 的 usePinchZoom 传感器 Hook 实战:用 Pointer Events 检测双指捏合缩放

react-use 的 usePinchZoom 传感器 Hook 实战:用 Pointer Events 检测双指捏合缩放 react-use 的 usePinchZoom 传感器 Hook 实战用 Pointer Events 检测双指捏合缩放【免费下载链接】react-useReact Hooks — 项目地址: https://gitcode.com/gh_mirrors/re/react-useusePinchZoom是 react-use当前仓库版本 17.6.1见 package.json提供的一个传感器Sensor类 Hook它监听目标元素上的指针触摸事件计算两根手指之间的水平距离变化从而告知调用方用户当前正在放大ZOOMING_IN还是缩小ZOOMING_OUT。读完本文你将掌握该 Hook 的完整 API、源码级实现原理并能够直接写出基于它实现的可运行图片缩放组件。一、usePinchZoom 是什么react-use 将 Hooks 分为多个类别其中传感器类 Hook 的定义在 docs/Sensors.md 中给出它们监听某种接口的变化并强制组件携带最新状态重新渲染。usePinchZoom正是这一类中的一员它被列在 README.md 的 Sensors 分组下其定位是追踪 pointer 事件检测捏合缩放是放大还是缩小。与基于传统touchstart/touchmove的写法不同该 Hook 采用标准Pointer Eventspointerdown、pointermove、pointerup等实现因此对鼠标、触摸屏、触控笔等统一抽象。它在仓库中通过 src/index.ts 以export { default as usePinchZoom } from ./usePinchZoom对外导出首次引入记录在 CHANGELOG.md。二、安装与导入从react-use包中导入即可import { usePinchZoom } from react-use;仓库内部完整实现位于 src/usePinchZoom.ts类型声明与默认导出均来自该文件。如果需要在代码中引用方向枚举ZoomState从仓库源码看它定义在 src/usePinchZoom.ts 中且并未从 src/index.ts 入口导出入口仅导出 Hook 本身仓库内的演示 stories/usePinchZoom.story.tsx 是直接import { ZoomState } from ../src/usePinchZoom引用的你可以参照这一方式。三、API 签名与返回值从 src/usePinchZoom.ts 的源码可以归纳出完整的 API 形态项目说明函数签名usePinchZoom(ref: RefObjectHTMLElement)参数ref绑定手势监听的目标 DOM 元素的 ref必须是HTMLElement如div、img的容器返回值{ zoomingState, pinchState }zoomingState缩放方向值为ZoomState.ZOOMING_IN、ZoomState.ZOOMING_OUT或初始的nullpinchState当前两根指针之间的水平距离像素无手势时为0其中ZoomState枚举定义为src/usePinchZoom.tsexport enum ZoomState { ZOOMING_IN ZOOMING_IN, ZOOMING_OUT ZOOMING_OUT, } export type ZoomStateType ZoomState.ZOOMING_IN | ZoomState.ZOOMING_OUT;返回值在无手势时由源码第 104-106 行保证为稳定结构{ zoomingState: null, pinchState: 0 }一旦检测到方向变化zoomingState即为枚举值pinchState为对应pointermove事件时刻的两指水平间距Math.abs(clientX1 - clientX2)单位为像素。四、基础用法文档示例完整版原文档 docs/usePinchZoom.md 给出了最核心的用法——把缩放方向变化映射为图片的zoom样式。下面是对其完整继承并补充注释、修正依赖数组的版本import { useEffect, useRef, useState } from react; import { usePinchZoom } from react-use; const Demo () { const [scale, setState] useState(1); const scaleRef useRef(); const { zoomingState, pinchState } usePinchZoom(scaleRef); useEffect(() { if (zoomingState ZOOMING_IN) { // 手指距离增大 → 放大 setState((s) s 0.1); } else if (zoomingState ZOOMING_OUT) { // 手指距离减小 → 缩小 setState((s) s - 0.1); } }, [zoomingState, pinchState]); return ( div ref{scaleRef} img srchttps://www.olympus-imaging.co.in/content/000107506.jpg style{{ zoom: scale }} altscale img / /div ); }; export default Demo;注意字符串值原文档示例中写的是ZOOM_IN/ZOOM_OUT而仓库源码实际产生的方向值是ZOOMING_IN/ZOOMING_OUT见 src/usePinchZoom.ts。直接复制原文档字符串将永远匹配不到分支建议按上面代码使用枚举对应的字符串值或像仓库演示那样导入ZoomState枚举进行比较。这是使用该 Hook 时最容易踩的坑。上面的useEffect依赖同时包含zoomingState与pinchState与仓库演示 stories/usePinchZoom.story.tsx 保持一致用函数式更新setState((s) s 0.1)可避免连续缩放时基于过期 state 计算的问题。五、工作原理源码级解析理解实现有助于你判断它在真实项目中的行为边界。核心逻辑全部在 src/usePinchZoom.ts 中关键结构如下。5.1 缓存结构与手势状态export type CacheRef { prevDiff: number; evCache: ArrayPointerEvent; };Hook 内部用useMemo维护一个cacheRefsrc/usePinchZoom.ts初始值为{ evCache: [], prevDiff: -1 }仅当ref.current引用变化时才重建evCache当前仍处于按下状态的指针事件缓存用于跟踪两根活动指针prevDiff上一次pointermove时两指的水平间距初始为-1表示尚无有效基准。5.2 三个核心事件处理函数pointerdown_handlersrc/usePinchZoom.tspointerdown时将事件对象推入evCache表示这根手指已按下。pointermove_handlersrc/usePinchZoom.ts这是判定缩放方向的核心先在缓存中找到与该pointerId匹配的记录并更新为最新事件当evCache.length 2恰好两根手指按下时计算当前水平距离curDiff Math.abs(clientX1 - clientX2)只有当prevDiff 0即已有上一次基准时才做比较curDiff prevDiff置为ZOOMING_INcurDiff prevDiff置为ZOOMING_OUT无论是否触发状态更新都会把curDiff写入prevDiff作为下一次比较的基准。pointerup_handlersrc/usePinchZoom.ts指针抬起或cancel/out/leave时调用remove_event把该指针从缓存中移除当剩余指针不足两根时把prevDiff重置为-1等待下一次完整手势重新建立基准。5.3 事件绑定Hook 通过useEffectsrc/usePinchZoom.ts在ref.current上一次性挂载ref.current.onpointerdown pointerdown_handler; ref.current.onpointermove pointermove_handler; ref.current.onpointerup pointerup_handler; ref.current.onpointercancel pointerup_handler; ref.current.onpointerout pointerup_handler; ref.current.onpointerleave pointerup_handler;它直接赋值onpointer*属性而非addEventListener依赖数组为[ref?.current]因此 ref 变化时会重新绑定组件卸载时由 React 一并清理 DOM 属性上的处理函数。5.4 数据流小结pointerdown缓存指针→ pointermove两指时算水平距离并对比 prevDiff → 距离增大 → setZoomingState([ZOOMING_IN, curDiff]) → 距离减小 → setZoomingState([ZOOMING_OUT, curDiff]) → pointerup/cancel/out/leave移除指针指针不足两根则重置基准六、实现细节与使用注意事项结合源码使用该 Hook 时有以下几点值得注意均为从源码结构可确认或可推断的事实只计算水平方向距离实现中使用的是Math.abs(evCache[0].clientX - evCache[1].clientX)src/usePinchZoom.ts即一维水平距离而非两指间的真实欧氏距离。垂直方向的捏合、斜向捏合的距离变化不会被完整度量。需要全方向距离的场景例如用Math.hypot(dx, dy)计算对角线长度需要自行扩展。只有恰好两根指针才判定evCache.length 2是进入判定分支的硬条件。若用户捏合过程中第三根手指落下缓存长度变为 3判定暂时失效直到某根手指抬起回到 2 根才恢复。这不是缺陷而是双指手势语义下的合理取舍。首次 move 不产生状态由于prevDiff初始为-1双指按下后的第一次pointermove只建立基准、不触发zoomingState更新从第二次 move 起才会比较并返回方向。pinchState在无手势时固定为0src/usePinchZoom.ts。不干预浏览器默认手势从源码看Hook 没有设置touch-action: none也未调用preventDefault()。在触屏浏览器中双指捏合可能同时触发页面自身的缩放/滚动行为两者叠加时体验可能受影响。如果你的目标场景需要独占手势可在目标元素 CSS 上自行加上touch-action: none。状态与样式解耦Hook 只报告方向与距离不替你执行任何缩放变换。实际缩放多少本例每步 0.1、用什么样式zoom或transform: scale()完全由调用方决定这保证了 Hook 的通用性。七、进阶实战带边界约束的图片缩放组件把上面的思路整合成一个更完整的组件加入缩放范围限制避免无限放大或缩到看不见import { useEffect, useRef, useState } from react; import { usePinchZoom } from react-use; const MIN_SCALE 0.5; const MAX_SCALE 3; const STEP 0.1; const PinchZoomImage ({ src }) { const [scale, setScale] useState(1); const ref useRef(null); const { zoomingState } usePinchZoom(ref); useEffect(() { setScale((prev) { if (zoomingState ZOOMING_IN) { return Math.min(prev STEP, MAX_SCALE); } if (zoomingState ZOOMING_OUT) { return Math.max(prev - STEP, MIN_SCALE); } return prev; }); }, [zoomingState]); return ( div ref{ref} style{{ touchAction: none, overflow: hidden }} img src{src} altpinch zoom target style{{ zoom: scale, transition: zoom 80ms linear, }} / /div ); }; export default PinchZoomImage;要点说明touchAction: none让浏览器把手势事件完全交给元素对应上文注意事项 4用Math.min/Math.max把缩放值钳制在[0.5, 3]防止越界加入轻微过渡动画可让缩放过程更平滑但注意zoom属性在现代浏览器中的过渡支持以 Chromium 系为主跨浏览器时可改用transform: scale(scale)并设置transformOrigin: 0 0。八、本地运行与演示仓库提供了该 Hook 的 Storybook 演示位于 stories/usePinchZoom.story.tsx其中Demo组件展示了与本文第四节相同的缩放逻辑并挂载在Sensors/usePinchZoom分组下包含Docs渲染 docs/usePinchZoom.md 原文和Default可交互演示两个 story。在仓库根目录执行以下命令即可本地启动yarn install yarn storybookStorybook 默认运行在http://localhost:6008端口由 package.json 中的storybook: start-storybook -p 6008脚本指定进入后导航到 Sensors → usePinchZoom 的 Default 页面即可在支持触摸的屏幕上体验双指缩放或使用浏览器开发者工具的触摸模拟进行调试。九、小结usePinchZoom是一个轻量、聚焦的传感器 Hook输入一个元素 ref输出捏合方向 水平间距把最繁琐的 pointer 事件缓存、距离比较与基准重置逻辑封装在约 100 行的实现内src/usePinchZoom.ts。它在图片查看器、地图缩放、幻灯片手势等双指捏合交互场景中即插即用同时也要记住它的两个天然边界——只度量水平距离、只处理恰好两根指针复杂手势需求需要在此基础上自行扩展。【免费下载链接】react-useReact Hooks — 项目地址: https://gitcode.com/gh_mirrors/re/react-use创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考