@likashefqet/react-native-image-zoom
Version:
A performant zoomable image written in Reanimated v2+ 🚀
343 lines (339 loc) • 14.3 kB
JavaScript
;
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