UNPKG

@likashefqet/react-native-image-zoom

Version:

A performant zoomable image written in Reanimated v2+ 🚀

343 lines (339 loc) • 14.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useGestures = void 0; var _react = require("react"); var _reactNativeGestureHandler = require("react-native-gesture-handler"); var _reactNativeReanimated = require("react-native-reanimated"); var _clamp = require("../utils/clamp"); var _limits = require("../utils/limits"); var _types = require("../types"); var _useAnimationEnd = require("./useAnimationEnd"); var _useInteractionId = require("./useInteractionId"); var _usePanGestureCount = require("./usePanGestureCount"); var _sum = require("../utils/sum"); const withTimingConfig = { easing: _reactNativeReanimated.Easing.inOut(_reactNativeReanimated.Easing.quad) }; const useGestures = ({ width, height, center, minScale = 1, maxScale = 5, scale: scaleValue, doubleTapScale = 3, maxPanPointers = 2, isPanEnabled = true, isPinchEnabled = true, isSingleTapEnabled = false, isDoubleTapEnabled = false, onInteractionStart, onInteractionEnd, onPinchStart, onPinchEnd, onPanStart, onPanEnd, onSingleTap = () => {}, onDoubleTap = () => {}, onProgrammaticZoom = () => {}, onResetAnimationEnd }) => { const isInteracting = (0, _react.useRef)(false); const isPinching = (0, _react.useRef)(false); const { isPanning, startPan, endPan } = (0, _usePanGestureCount.usePanGestureCount)(); const savedScale = (0, _reactNativeReanimated.useSharedValue)(1); const internalScaleValue = (0, _reactNativeReanimated.useSharedValue)(1); const scale = scaleValue ?? internalScaleValue; const initialFocal = { x: (0, _reactNativeReanimated.useSharedValue)(0), y: (0, _reactNativeReanimated.useSharedValue)(0) }; const savedFocal = { x: (0, _reactNativeReanimated.useSharedValue)(0), y: (0, _reactNativeReanimated.useSharedValue)(0) }; const focal = { x: (0, _reactNativeReanimated.useSharedValue)(0), y: (0, _reactNativeReanimated.useSharedValue)(0) }; const savedTranslate = { x: (0, _reactNativeReanimated.useSharedValue)(0), y: (0, _reactNativeReanimated.useSharedValue)(0) }; const translate = { x: (0, _reactNativeReanimated.useSharedValue)(0), y: (0, _reactNativeReanimated.useSharedValue)(0) }; const { getInteractionId, updateInteractionId } = (0, _useInteractionId.useInteractionId)(); const { onAnimationEnd } = (0, _useAnimationEnd.useAnimationEnd)(onResetAnimationEnd); const reset = (0, _react.useCallback)(() => { 'worklet'; const interactionId = getInteractionId(); savedScale.value = 1; const lastScaleValue = scale.value; scale.value = (0, _reactNativeReanimated.withTiming)(1, withTimingConfig, (...args) => onAnimationEnd(interactionId, _types.ANIMATION_VALUE.SCALE, lastScaleValue, ...args)); initialFocal.x.value = 0; initialFocal.y.value = 0; savedFocal.x.value = 0; savedFocal.y.value = 0; const lastFocalXValue = focal.x.value; focal.x.value = (0, _reactNativeReanimated.withTiming)(0, withTimingConfig, (...args) => onAnimationEnd(interactionId, _types.ANIMATION_VALUE.FOCAL_X, lastFocalXValue, ...args)); const lastFocalYValue = focal.y.value; focal.y.value = (0, _reactNativeReanimated.withTiming)(0, withTimingConfig, (...args) => onAnimationEnd(interactionId, _types.ANIMATION_VALUE.FOCAL_Y, lastFocalYValue, ...args)); savedTranslate.x.value = 0; savedTranslate.y.value = 0; const lastTranslateXValue = translate.x.value; translate.x.value = (0, _reactNativeReanimated.withTiming)(0, withTimingConfig, (...args) => onAnimationEnd(interactionId, _types.ANIMATION_VALUE.TRANSLATE_X, lastTranslateXValue, ...args)); const lastTranslateYValue = translate.y.value; translate.y.value = (0, _reactNativeReanimated.withTiming)(0, withTimingConfig, (...args) => onAnimationEnd(interactionId, _types.ANIMATION_VALUE.TRANSLATE_Y, lastTranslateYValue, ...args)); }, [savedScale, scale, initialFocal.x, initialFocal.y, savedFocal.x, savedFocal.y, focal.x, focal.y, savedTranslate.x, savedTranslate.y, translate.x, translate.y, getInteractionId, onAnimationEnd]); const moveIntoView = () => { 'worklet'; if (scale.value > 1) { const rightLimit = _limits.limits.right(width, scale); const leftLimit = -rightLimit; const bottomLimit = _limits.limits.bottom(height, scale); const topLimit = -bottomLimit; const totalTranslateX = (0, _sum.sum)(translate.x, focal.x); const totalTranslateY = (0, _sum.sum)(translate.y, focal.y); if (totalTranslateX > rightLimit) { translate.x.value = (0, _reactNativeReanimated.withTiming)(rightLimit, withTimingConfig); focal.x.value = (0, _reactNativeReanimated.withTiming)(0, withTimingConfig); } else if (totalTranslateX < leftLimit) { translate.x.value = (0, _reactNativeReanimated.withTiming)(leftLimit, withTimingConfig); focal.x.value = (0, _reactNativeReanimated.withTiming)(0, withTimingConfig); } if (totalTranslateY > bottomLimit) { translate.y.value = (0, _reactNativeReanimated.withTiming)(bottomLimit, withTimingConfig); focal.y.value = (0, _reactNativeReanimated.withTiming)(0, withTimingConfig); } else if (totalTranslateY < topLimit) { translate.y.value = (0, _reactNativeReanimated.withTiming)(topLimit, withTimingConfig); focal.y.value = (0, _reactNativeReanimated.withTiming)(0, withTimingConfig); } } else { reset(); } }; const zoom = event => { 'worklet'; if (event.scale > 1) { (0, _reactNativeReanimated.runOnJS)(onProgrammaticZoom)(_types.ZOOM_TYPE.ZOOM_IN); scale.value = (0, _reactNativeReanimated.withTiming)(event.scale, withTimingConfig); focal.x.value = (0, _reactNativeReanimated.withTiming)((center.x - event.x) * (event.scale - 1), withTimingConfig); focal.y.value = (0, _reactNativeReanimated.withTiming)((center.y - event.y) * (event.scale - 1), withTimingConfig); } else { (0, _reactNativeReanimated.runOnJS)(onProgrammaticZoom)(_types.ZOOM_TYPE.ZOOM_OUT); reset(); } }; const onInteractionStarted = () => { if (!isInteracting.current) { isInteracting.current = true; onInteractionStart === null || onInteractionStart === void 0 || onInteractionStart(); updateInteractionId(); } }; const onInteractionEnded = () => { if (isInteracting.current && !isPinching.current && !isPanning()) { if (isDoubleTapEnabled) { moveIntoView(); } else { reset(); } isInteracting.current = false; onInteractionEnd === null || onInteractionEnd === void 0 || onInteractionEnd(); } }; const onPinchStarted = event => { onInteractionStarted(); isPinching.current = true; onPinchStart === null || onPinchStart === void 0 || onPinchStart(event); }; const onPinchEnded = (...args) => { isPinching.current = false; onPinchEnd === null || onPinchEnd === void 0 || onPinchEnd(...args); onInteractionEnded(); }; const onPanStarted = event => { onInteractionStarted(); startPan(); onPanStart === null || onPanStart === void 0 || onPanStart(event); }; const onPanEnded = (...args) => { endPan(); onPanEnd === null || onPanEnd === void 0 || onPanEnd(...args); onInteractionEnded(); }; const panWhilePinchingGesture = _reactNativeGestureHandler.Gesture.Pan().enabled(isPanEnabled).averageTouches(true).enableTrackpadTwoFingerGesture(true).minPointers(2).maxPointers(maxPanPointers).onStart(event => { (0, _reactNativeReanimated.runOnJS)(onPanStarted)(event); savedTranslate.x.value = translate.x.value; savedTranslate.y.value = translate.y.value; }).onUpdate(event => { translate.x.value = savedTranslate.x.value + event.translationX; translate.y.value = savedTranslate.y.value + event.translationY; }).onEnd((event, success) => { const rightLimit = _limits.limits.right(width, scale); const leftLimit = -rightLimit; const bottomLimit = _limits.limits.bottom(height, scale); const topLimit = -bottomLimit; if (scale.value > 1 && isDoubleTapEnabled) { translate.x.value = (0, _reactNativeReanimated.withDecay)({ velocity: event.velocityX, velocityFactor: 0.6, rubberBandEffect: true, rubberBandFactor: 0.9, clamp: [leftLimit - focal.x.value, rightLimit - focal.x.value] }, () => { if (event.velocityX >= event.velocityY) { (0, _reactNativeReanimated.runOnJS)(onPanEnded)(event, success); } }); translate.y.value = (0, _reactNativeReanimated.withDecay)({ velocity: event.velocityY, velocityFactor: 0.6, rubberBandEffect: true, rubberBandFactor: 0.9, clamp: [topLimit - focal.y.value, bottomLimit - focal.y.value] }, () => { if (event.velocityY > event.velocityX) { (0, _reactNativeReanimated.runOnJS)(onPanEnded)(event, success); } }); } else { (0, _reactNativeReanimated.runOnJS)(onPanEnded)(event, success); } }); const panOnlyGesture = _reactNativeGestureHandler.Gesture.Pan().enabled(isPanEnabled).averageTouches(true).enableTrackpadTwoFingerGesture(true).minPointers(1).maxPointers(1).onTouchesDown((_, manager) => { if (scale.value <= 1) { manager.fail(); } }).onStart(event => { (0, _reactNativeReanimated.runOnJS)(onPanStarted)(event); savedTranslate.x.value = translate.x.value; savedTranslate.y.value = translate.y.value; }).onUpdate(event => { translate.x.value = savedTranslate.x.value + event.translationX; translate.y.value = savedTranslate.y.value + event.translationY; }).onEnd((event, success) => { const rightLimit = _limits.limits.right(width, scale); const leftLimit = -rightLimit; const bottomLimit = _limits.limits.bottom(height, scale); const topLimit = -bottomLimit; if (scale.value > 1 && isDoubleTapEnabled) { translate.x.value = (0, _reactNativeReanimated.withDecay)({ velocity: event.velocityX, velocityFactor: 0.6, rubberBandEffect: true, rubberBandFactor: 0.9, clamp: [leftLimit - focal.x.value, rightLimit - focal.x.value] }, () => { if (event.velocityX >= event.velocityY) { (0, _reactNativeReanimated.runOnJS)(onPanEnded)(event, success); } }); translate.y.value = (0, _reactNativeReanimated.withDecay)({ velocity: event.velocityY, velocityFactor: 0.6, rubberBandEffect: true, rubberBandFactor: 0.9, clamp: [topLimit - focal.y.value, bottomLimit - focal.y.value] }, () => { if (event.velocityY > event.velocityX) { (0, _reactNativeReanimated.runOnJS)(onPanEnded)(event, success); } }); } else { (0, _reactNativeReanimated.runOnJS)(onPanEnded)(event, success); } }); const pinchGesture = _reactNativeGestureHandler.Gesture.Pinch().enabled(isPinchEnabled).onStart(event => { (0, _reactNativeReanimated.runOnJS)(onPinchStarted)(event); savedScale.value = scale.value; savedFocal.x.value = focal.x.value; savedFocal.y.value = focal.y.value; initialFocal.x.value = event.focalX; initialFocal.y.value = event.focalY; }).onUpdate(event => { scale.value = (0, _clamp.clamp)(savedScale.value * event.scale, minScale, maxScale); focal.x.value = savedFocal.x.value + (center.x - initialFocal.x.value) * (scale.value - savedScale.value); focal.y.value = savedFocal.y.value + (center.y - initialFocal.y.value) * (scale.value - savedScale.value); }).onEnd((...args) => { (0, _reactNativeReanimated.runOnJS)(onPinchEnded)(...args); }); const doubleTapGesture = _reactNativeGestureHandler.Gesture.Tap().enabled(isDoubleTapEnabled).numberOfTaps(2).maxDuration(250).onStart(event => { if (scale.value === 1) { (0, _reactNativeReanimated.runOnJS)(onDoubleTap)(_types.ZOOM_TYPE.ZOOM_IN); scale.value = (0, _reactNativeReanimated.withTiming)(doubleTapScale, withTimingConfig); focal.x.value = (0, _reactNativeReanimated.withTiming)((center.x - event.x) * (doubleTapScale - 1), withTimingConfig); focal.y.value = (0, _reactNativeReanimated.withTiming)((center.y - event.y) * (doubleTapScale - 1), withTimingConfig); } else { (0, _reactNativeReanimated.runOnJS)(onDoubleTap)(_types.ZOOM_TYPE.ZOOM_OUT); reset(); } }); const singleTapGesture = _reactNativeGestureHandler.Gesture.Tap().enabled(isSingleTapEnabled).numberOfTaps(1).maxDistance(24).onStart(event => { (0, _reactNativeReanimated.runOnJS)(onSingleTap)(event); }); const animatedStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({ transform: [{ translateX: translate.x.value }, { translateY: translate.y.value }, { translateX: focal.x.value }, { translateY: focal.y.value }, { scale: scale.value }] }), [translate.x, translate.y, focal.x, focal.y, scale]); const getInfo = () => { const totalTranslateX = translate.x.value + focal.x.value; const totalTranslateY = translate.y.value + focal.y.value; return { container: { width, height, center }, scaledSize: { width: width * scale.value, height: height * scale.value }, visibleArea: { x: Math.abs(totalTranslateX - width * (scale.value - 1) / 2), y: Math.abs(totalTranslateY - height * (scale.value - 1) / 2), width, height }, transformations: { translateX: totalTranslateX, translateY: totalTranslateY, scale: scale.value } }; }; const pinchPanGestures = _reactNativeGestureHandler.Gesture.Simultaneous(pinchGesture, panWhilePinchingGesture); const tapGestures = _reactNativeGestureHandler.Gesture.Exclusive(doubleTapGesture, singleTapGesture); const gestures = isDoubleTapEnabled || isSingleTapEnabled ? _reactNativeGestureHandler.Gesture.Race(pinchPanGestures, panOnlyGesture, tapGestures) : pinchPanGestures; return { gestures, animatedStyle, zoom, reset, getInfo }; }; exports.useGestures = useGestures; //# sourceMappingURL=useGestures.js.map