diff --git a/README.md b/README.md index 284f94e121..d1de6ae6c0 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ - [`useMeasure`](./docs/useMeasure.md) and [`useSize`](./docs/useSize.md) — tracks an HTML element's dimensions. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemeasure--demo) - [`createBreakpoint`](./docs/createBreakpoint.md) — tracks `innerWidth` - [`useScrollbarWidth`](./docs/useScrollbarWidth.md) — detects browser's native scrollbars width. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usescrollbarwidth--demo) + - [`usePinchZoom`](./docs/usePinchZoom.md) — tracks pointer events to detect pinch zoom in and out status. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usePinchZoom--demo)

- [**UI**](./docs/UI.md) diff --git a/docs/usePinchZoom.md b/docs/usePinchZoom.md new file mode 100644 index 0000000000..212283d39e --- /dev/null +++ b/docs/usePinchZoom.md @@ -0,0 +1,36 @@ +# `usePinchZoon` + +React sensor hook that tracks the changes in pointer touch events and detects value of pinch difference and tell if user is zooming in or out. + +## Usage + +```jsx +import { usePinchZoon } from "react-use"; + +const Demo = () => { + const [scale, setState] = useState(1); + const scaleRef = useRef(); + const { zoomingState, pinchState } = usePinchZoom(scaleRef); + + useEffect(() => { + if (zoomingState === "ZOOM_IN") { + // perform zoom in scaling + setState(scale + 0.1) + } else if (zoomingState === "ZOOM_OUT") { + // perform zoom out in scaling + setState(scale - 0.1) + } + }, [zoomingState]); + + return ( +
+ +
+ ); +}; +``` diff --git a/src/index.ts b/src/index.ts index 4c8901a562..62b69356b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,6 +109,7 @@ export { useMultiStateValidator } from './useMultiStateValidator'; export { default as useWindowScroll } from './useWindowScroll'; export { default as useWindowSize } from './useWindowSize'; export { default as useMeasure } from './useMeasure'; +export { default as usePinchZoom } from './usePinchZoom'; export { useRendersCount } from './useRendersCount'; export { useFirstMountState } from './useFirstMountState'; export { default as useSet } from './useSet'; diff --git a/src/usePinchZoom.ts b/src/usePinchZoom.ts new file mode 100644 index 0000000000..330c2052d5 --- /dev/null +++ b/src/usePinchZoom.ts @@ -0,0 +1,109 @@ +import { RefObject, useEffect, useMemo, useState } from 'react'; + +export type CacheRef = { + prevDiff: number; + evCache: Array; +}; + +export enum ZoomState { + 'ZOOMING_IN' = 'ZOOMING_IN', + 'ZOOMING_OUT' = 'ZOOMING_OUT', +} + +export type ZoomStateType = ZoomState.ZOOMING_IN | ZoomState.ZOOMING_OUT; + +const usePinchZoom = (ref: RefObject) => { + const cacheRef = useMemo( + () => ({ + evCache: [], + prevDiff: -1, + }), + [ref.current] + ); + + const [zoomingState, setZoomingState] = useState<[ZoomStateType, number]>(); + + const pointermove_handler = (ev: PointerEvent) => { + // This function implements a 2-pointer horizontal pinch/zoom gesture. + // + // If the distance between the two pointers has increased (zoom in), + // the target element's background is changed to 'pink' and if the + // distance is decreasing (zoom out), the color is changed to 'lightblue'. + // + // This function sets the target element's border to 'dashed' to visually + // indicate the pointer's target received a move event. + // Find this event in the cache and update its record with this event + for (let i = 0; i < cacheRef.evCache.length; i++) { + if (ev.pointerId == cacheRef.evCache[i].pointerId) { + cacheRef.evCache[i] = ev; + break; + } + } + + // If two pointers are down, check for pinch gestures + if (cacheRef.evCache.length == 2) { + // console.log(prevDiff) + // Calculate the distance between the two pointers + const curDiff = Math.abs(cacheRef.evCache[0].clientX - cacheRef.evCache[1].clientX); + + if (cacheRef.prevDiff > 0) { + if (curDiff > cacheRef.prevDiff) { + // The distance between the two pointers has increased + setZoomingState([ZoomState.ZOOMING_IN, curDiff]); + } + if (curDiff < cacheRef.prevDiff) { + // The distance between the two pointers has decreased + setZoomingState([ZoomState.ZOOMING_OUT, curDiff]); + } + } + + // Cache the distance for the next move event + cacheRef.prevDiff = curDiff; + } + }; + + const pointerdown_handler = (ev: PointerEvent) => { + // The pointerdown event signals the start of a touch interaction. + // This event is cached to support 2-finger gestures + cacheRef.evCache.push(ev); + // console.log('pointerDown', ev); + }; + + const pointerup_handler = (ev: PointerEvent) => { + // Remove this pointer from the cache and reset the target's + // background and border + remove_event(ev); + + // If the number of pointers down is less than two then reset diff tracker + if (cacheRef.evCache.length < 2) { + cacheRef.prevDiff = -1; + } + }; + + const remove_event = (ev: PointerEvent) => { + // Remove this event from the target's cache + for (let i = 0; i < cacheRef.evCache.length; i++) { + if (cacheRef.evCache[i].pointerId == ev.pointerId) { + cacheRef.evCache.splice(i, 1); + break; + } + } + }; + + useEffect(() => { + if (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; + } + }, [ref?.current]); + + return zoomingState + ? { zoomingState: zoomingState[0], pinchState: zoomingState[1] } + : { zoomingState: null, pinchState: 0 }; +}; + +export default usePinchZoom; diff --git a/stories/usePinchZoom.story.tsx b/stories/usePinchZoom.story.tsx new file mode 100644 index 0000000000..2394b96306 --- /dev/null +++ b/stories/usePinchZoom.story.tsx @@ -0,0 +1,37 @@ +import { storiesOf } from '@storybook/react'; +import { useEffect, useRef, useState } from 'react'; +import { usePinchZoom } from '../src'; +import { ZoomState } from '../src/usePinchZoom'; +import ShowDocs from './util/ShowDocs'; + +const Demo = () => { + const [scale, setState] = useState(1); + const scaleRef = useRef(); + const { zoomingState, pinchState } = usePinchZoom(scaleRef); + + useEffect(() => { + if (zoomingState === ZoomState.ZOOMING_IN) { + // perform zoom in scaling + setState(scale + 0.1); + } else if (zoomingState === ZoomState.ZOOMING_OUT) { + // perform zoom out in scaling + setState(scale - 0.1); + } + }, [zoomingState, pinchState]); + + return ( +
+ scale img +
+ ); +}; + +storiesOf('Sensors/usePinchZoom', module) + .add('Docs', () => ) + .add('Default', () => );