UNPKG

react-native-zoom-toolkit

Version:

Most complete set of pinch to zoom utilites for React Native

357 lines (354 loc) 11.5 kB
import React, { useImperativeHandle } from 'react'; import { StyleSheet, View } from 'react-native'; import Animated, { clamp, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming } from 'react-native-reanimated'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { crop } from '../../commons/utils/crop'; import { useSizeVector } from '../../commons/hooks/useSizeVector'; import { getCropRotatedSize } from '../../commons/utils/getCropRotatedSize'; import { usePanCommons } from '../../commons/hooks/usePanCommons'; import { usePinchCommons } from '../../commons/hooks/usePinchCommons'; import { getMaxScale } from '../../commons/utils/getMaxScale'; import { useVector } from '../../commons/hooks/useVector'; import withCropValidation from '../../commons/hoc/withCropValidation'; const TAU = Math.PI * 2; const RAD2DEG = 180 / Math.PI; const CropZoom = props => { const { reference, children, cropSize, resolution, minScale = 1, maxScale: userMaxScale, scaleMode = 'bounce', panMode = 'free', allowPinchPanning = true, onUpdate, onGestureEnd, OverlayComponent, onPanStart: onUserPanStart, onPanEnd: onUserPanEnd, onPinchStart: onUserPinchStart, onPinchEnd: onUserPinchEnd, onTap } = props; const initialSize = getCropRotatedSize({ crop: cropSize, resolution: resolution, angle: 0 }); const translate = useVector(0, 0); const offset = useVector(0, 0); const scale = useSharedValue(minScale); const scaleOffset = useSharedValue(minScale); const rotation = useSharedValue(0); const rotate = useVector(0, 0); const rootSize = useSizeVector(0, 0); const childSize = useSizeVector(initialSize.width, initialSize.height); const gestureSize = useSizeVector(initialSize.width, initialSize.height); const sizeAngle = useSharedValue(0); const maxScale = useDerivedValue(() => { const scaleValue = getMaxScale({ width: childSize.width.value, height: childSize.height.value }, resolution); return userMaxScale ?? scaleValue; }, [childSize, userMaxScale, resolution]); useDerivedValue(() => { const size = getCropRotatedSize({ crop: cropSize, resolution, angle: sizeAngle.value }); let finalSize = 0; const max = Math.max(rootSize.width.value, rootSize.height.value); if (childSize.width.value > childSize.height.value) { const sizeOffset = initialSize.width - cropSize.width; finalSize = max + sizeOffset; } else { const sizeOffset = initialSize.height - cropSize.height; finalSize = max + sizeOffset; } gestureSize.width.value = finalSize; gestureSize.height.value = finalSize; childSize.width.value = withTiming(size.width); childSize.height.value = withTiming(size.height); }, [rootSize, cropSize, resolution, childSize, sizeAngle]); useDerivedValue(() => { onUpdate === null || onUpdate === void 0 || onUpdate({ containerSize: { width: rootSize.width.value, height: rootSize.height.value }, childSize: { width: childSize.width.value, height: childSize.height.value }, maxScale: maxScale.value, translateX: translate.x.value, translateY: translate.y.value, scale: scale.value, rotate: rotation.value, rotateX: rotate.x.value, rotateY: rotate.y.value }); }, [rootSize, childSize, maxScale, translate, scale, rotation, rotate]); const boundsFn = optionalScale => { 'worklet'; const scaleVal = optionalScale ?? scale.value; let size = { width: childSize.width.value, height: childSize.height.value }; const isInInverseAspectRatio = rotation.value % Math.PI !== 0; if (isInInverseAspectRatio) { size = { width: size.height, height: size.width }; } const boundX = Math.max(0, size.width * scaleVal - cropSize.width) / 2; const boundY = Math.max(0, size.height * scaleVal - cropSize.height) / 2; return { x: boundX, y: boundY }; }; function measureRootContainer(e) { rootSize.width.value = e.nativeEvent.layout.width; rootSize.height.value = e.nativeEvent.layout.height; } const { gesturesEnabled, onTouchesDown, onTouchesMove, onTouchesUp, onPinchStart, onPinchUpdate, onPinchEnd } = usePinchCommons({ container: gestureSize, translate, offset, scale, scaleOffset, minScale, maxScale, allowPinchPanning, scaleMode, pinchMode: 'free', boundFn: boundsFn, userCallbacks: { onGestureEnd: onGestureEnd, onPinchStart: onUserPinchStart, onPinchEnd: onUserPinchEnd } }); const { onPanStart, onPanChange, onPanEnd } = usePanCommons({ container: gestureSize, translate, offset, panMode, boundFn: boundsFn, userCallbacks: { onGestureEnd: onGestureEnd, onPanStart: onUserPanStart, onPanEnd: onUserPanEnd } }); const pinch = Gesture.Pinch().withTestId('pinch').manualActivation(true).onTouchesDown(onTouchesDown).onTouchesMove(onTouchesMove).onTouchesUp(onTouchesUp).onStart(onPinchStart).onUpdate(onPinchUpdate).onEnd(onPinchEnd); const pan = Gesture.Pan().withTestId('pan').enabled(gesturesEnabled).maxPointers(1).onStart(onPanStart).onChange(onPanChange).onEnd(onPanEnd); const tap = Gesture.Tap().withTestId('tap').enabled(gesturesEnabled).maxDuration(250).numberOfTaps(1).runOnJS(true).onEnd(e => onTap === null || onTap === void 0 ? void 0 : onTap(e)); const detectorStyle = useAnimatedStyle(() => { return { width: gestureSize.width.value, height: gestureSize.height.value, position: 'absolute', transform: [{ translateX: translate.x.value }, { translateY: translate.y.value }, { scale: scale.value }] }; }, [gestureSize, translate, scale]); const childStyle = useAnimatedStyle(() => { return { width: childSize.width.value, height: childSize.height.value, transform: [{ translateX: translate.x.value }, { translateY: translate.y.value }, { scale: scale.value }, { rotate: `${rotation.value}rad` }, { rotateX: `${rotate.x.value}rad` }, { rotateY: `${rotate.y.value}rad` }] }; }, [childSize, translate, scale, rotation, rotate]); // Reference handling section const resetTo = (st, animate = true) => { translate.x.value = animate ? withTiming(st.translateX) : st.translateX; translate.y.value = animate ? withTiming(st.translateY) : st.translateY; scale.value = animate ? withTiming(st.scale) : st.scale; scaleOffset.value = st.scale; rotate.x.value = animate ? withTiming(st.rotateX) : st.rotateX; rotate.y.value = animate ? withTiming(st.rotateY) : st.rotateY; rotation.value = animate ? withTiming(st.rotate, undefined, () => { canRotate.value = true; rotation.value = rotation.value % TAU; }) : st.rotate % TAU; }; const canRotate = useSharedValue(true); const handleRotate = (animate = true, clockwise = true, cb) => { if (!canRotate.value) return; if (animate) canRotate.value = false; // Determine the direction multiplier based on clockwise or counterclockwise rotation const direction = clockwise ? 1 : -1; const toAngle = rotation.value + direction * (Math.PI / 2); sizeAngle.value = toAngle; cb === null || cb === void 0 || cb(toAngle % TAU); resetTo({ translateX: 0, translateY: 0, scale: minScale, rotate: toAngle, rotateX: rotate.x.value, rotateY: rotate.y.value }, animate); }; const flipHorizontal = (animate = true, cb) => { const toAngle = rotate.y.value !== Math.PI ? Math.PI : 0; cb === null || cb === void 0 || cb(toAngle * RAD2DEG); rotate.y.value = animate ? withTiming(toAngle) : toAngle; }; const flipVertical = (animate = true, cb) => { const toAngle = rotate.x.value !== Math.PI ? Math.PI : 0; cb === null || cb === void 0 || cb(toAngle * RAD2DEG); rotate.x.value = animate ? withTiming(toAngle) : toAngle; }; const handleCrop = fixedWidth => { const context = { rotationAngle: rotation.value * RAD2DEG, flipHorizontal: rotate.y.value === Math.PI, flipVertical: rotate.x.value === Math.PI }; const result = crop({ scale: scale.value, cropSize: cropSize, resolution: resolution, itemSize: { width: childSize.width.value, height: childSize.height.value }, translation: { x: translate.x.value, y: translate.y.value }, isRotated: context.rotationAngle % 180 !== 0, fixedWidth }); return { crop: result.crop, resize: result.resize, context }; }; const getState = () => { return { containerSize: { width: rootSize.width.value, height: rootSize.height.value }, childSize: { width: childSize.width.value, height: childSize.height.value }, maxScale: maxScale.value, translateX: translate.x.value, translateY: translate.y.value, scale: scale.value, rotate: rotation.value, rotateX: rotate.x.value, rotateY: rotate.y.value }; }; const setTransformState = (state, animate = true) => { const toScale = clamp(state.scale, minScale, maxScale.value); const { x: boundX, y: boundY } = boundsFn(toScale); const translateX = clamp(state.translateX, -1 * boundX, boundX); const translateY = clamp(state.translateY, -1 * boundY, boundY); const DEG90 = Math.PI / 2; const toRotate = Math.floor(state.rotate % (Math.PI * 2) / DEG90) * DEG90; const rotateX = Math.sign(state.rotateX - DEG90) === 1 ? Math.PI : 0; const rotateY = Math.sign(state.rotateY - DEG90) === 1 ? Math.PI : 0; resetTo({ translateX, translateY, scale: toScale, rotate: toRotate, rotateX, rotateY }, animate); }; useImperativeHandle(reference, () => ({ getState: getState, setTransformState: setTransformState, rotate: handleRotate, flipHorizontal: flipHorizontal, flipVertical: flipVertical, reset: animate => resetTo({ translateX: 0, translateY: 0, scale: minScale, rotate: 0, rotateX: 0, rotateY: 0 }, animate), crop: handleCrop })); const rootStyle = { minWidth: cropSize.width, minHeight: cropSize.height }; return /*#__PURE__*/React.createElement(View, { style: [styles.root, rootStyle, styles.center], onLayout: measureRootContainer }, /*#__PURE__*/React.createElement(Animated.View, { style: childStyle }, children), /*#__PURE__*/React.createElement(View, { style: styles.absolute, pointerEvents: 'none' }, OverlayComponent === null || OverlayComponent === void 0 ? void 0 : OverlayComponent()), /*#__PURE__*/React.createElement(GestureDetector, { gesture: Gesture.Race(pinch, pan, tap) }, /*#__PURE__*/React.createElement(Animated.View, { style: detectorStyle }))); }; const styles = StyleSheet.create({ root: { flex: 1 }, absolute: { position: 'absolute' }, center: { justifyContent: 'center', alignItems: 'center' } }); export default withCropValidation(CropZoom); //# sourceMappingURL=CropZoom.js.map