react-native-zoom-toolkit
Version:
Smoothly zoom any image, video or component you want!
264 lines (262 loc) • 8.7 kB
JavaScript
import React, { useImperativeHandle } from 'react';
import { StyleSheet } from 'react-native';
import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue, withTiming } from 'react-native-reanimated';
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
import { clamp } from '../../commons/utils/clamp';
import { useVector } from '../../commons/hooks/useVector';
import { getMaxScale } from '../../commons/utils/getMaxScale';
import { useSizeVector } from '../../commons/hooks/useSizeVector';
import { usePanCommons } from '../../commons/hooks/usePanCommons';
import { pinchTransform } from '../../commons/utils/pinchTransform';
import { usePinchCommons } from '../../commons/hooks/usePinchCommons';
import { PanMode, PinchCenteringMode, ScaleMode } from '../../commons/types';
import withResumableValidation from '../../commons/hoc/withResumableValidation';
import { getPinchPanningStatus } from '../../commons/utils/getPinchPanningStatus';
const ResumableZoom = props => {
const ref = props.reference;
const {
children,
extendGestures = false,
decay = true,
tapsEnabled = true,
panEnabled = true,
pinchEnabled = true,
minScale = 1,
maxScale: userMaxScale = 6,
panMode = PanMode.CLAMP,
scaleMode = ScaleMode.BOUNCE,
pinchCenteringMode = PinchCenteringMode.CLAMP,
allowPinchPanning: pinchPanning,
onTap,
onGestureActive,
onGestureEnd,
onSwipe,
onPinchStart: onUserPinchStart,
onPinchEnd: onUserPinchEnd,
onPanStart: onUserPanStart,
onPanEnd: onUserPanEnd,
onOverPanning
} = props;
const allowPinchPanning = pinchPanning ?? getPinchPanningStatus();
const translate = useVector(0, 0);
const offset = useVector(0, 0);
const scale = useSharedValue(minScale);
const scaleOffset = useSharedValue(minScale);
const origin = useVector(0, 0);
const delta = useVector(0, 0);
const rootSize = useSizeVector(0, 0);
const childSize = useSizeVector(0, 0);
const extendedSize = useSizeVector(0, 0);
const detectorTranslate = useVector(0, 0);
const detectorScale = useSharedValue(minScale);
const maxScale = useDerivedValue(() => {
if (typeof userMaxScale === 'object') {
return getMaxScale({
width: childSize.width.value,
height: childSize.height.value
}, userMaxScale);
}
return userMaxScale;
}, [userMaxScale, childSize]);
useDerivedValue(() => {
extendedSize.width.value = extendGestures ? Math.max(rootSize.width.value, childSize.width.value) : childSize.width.value;
extendedSize.height.value = extendGestures ? Math.max(rootSize.height.value, childSize.height.value) : childSize.height.value;
}, [extendGestures, rootSize, childSize]);
const boundsFn = scaleValue => {
'worklet';
const {
width: cWidth,
height: cHeight
} = childSize;
const {
width: rWidth,
height: rHeight
} = rootSize;
const boundX = Math.max(0, cWidth.value * scaleValue - rWidth.value) / 2;
const boundY = Math.max(0, cHeight.value * scaleValue - rHeight.value) / 2;
return {
x: boundX,
y: boundY
};
};
const reset = (toX, toY, toScale, animate = true) => {
'worklet';
detectorTranslate.x.value = translate.x.value;
detectorTranslate.y.value = translate.y.value;
detectorScale.value = scale.value;
translate.x.value = animate ? withTiming(toX) : toX;
translate.y.value = animate ? withTiming(toY) : toY;
scale.value = animate ? withTiming(toScale) : toScale;
detectorTranslate.x.value = animate ? withTiming(toX) : toX;
detectorTranslate.y.value = animate ? withTiming(toY) : toY;
detectorScale.value = animate ? withTiming(toScale) : toScale;
};
useDerivedValue(() => {
onGestureActive === null || onGestureActive === void 0 || onGestureActive({
width: childSize.width.value,
height: childSize.height.value,
translateX: translate.x.value,
translateY: translate.y.value,
scale: scale.value
});
}, [translate, childSize, scale]);
const {
gesturesEnabled,
onPinchStart,
onPinchUpdate,
onPinchEnd
} = usePinchCommons({
container: extendGestures ? extendedSize : childSize,
detectorTranslate,
detectorScale,
translate,
offset,
origin,
scale,
scaleOffset,
minScale,
maxScale,
delta,
allowPinchPanning,
scaleMode,
pinchCenteringMode,
boundFn: boundsFn,
userCallbacks: {
onGestureEnd,
onPinchStart: onUserPinchStart,
onPinchEnd: onUserPinchEnd
}
});
const {
onPanStart,
onPanChange,
onPanEnd
} = usePanCommons({
container: extendGestures ? extendedSize : childSize,
detectorTranslate,
translate,
offset,
scale,
panMode,
boundFn: boundsFn,
decay,
userCallbacks: {
onSwipe,
onGestureEnd,
onPanStart: onUserPanStart,
onPanEnd: onUserPanEnd,
onOverPanning
}
});
const pinch = Gesture.Pinch().enabled(pinchEnabled).onStart(onPinchStart).onUpdate(onPinchUpdate).onEnd(onPinchEnd);
const pan = Gesture.Pan().enabled(panEnabled && gesturesEnabled).maxPointers(1).onStart(onPanStart).onChange(onPanChange).onEnd(onPanEnd);
const tap = Gesture.Tap().enabled(tapsEnabled && gesturesEnabled).maxDuration(250).numberOfTaps(1).runOnJS(true).onEnd(e => onTap === null || onTap === void 0 ? void 0 : onTap(e));
const doubleTap = Gesture.Tap().enabled(tapsEnabled && gesturesEnabled).maxDuration(250).numberOfTaps(2).onEnd(e => {
const originX = e.x - extendedSize.width.value / 2;
const originY = e.y - extendedSize.height.value / 2;
const toScale = scale.value >= maxScale.value * 0.8 ? minScale : maxScale.value;
const {
x,
y
} = pinchTransform({
toScale: toScale,
fromScale: scale.value,
origin: {
x: originX,
y: originY
},
delta: {
x: 0,
y: 0
},
offset: {
x: translate.x.value,
y: translate.y.value
}
});
const {
x: boundX,
y: boundY
} = boundsFn(toScale);
const toX = clamp(x, -1 * boundX, boundX);
const toY = clamp(y, -1 * boundY, boundY);
reset(toX, toY, toScale, true);
});
const measureRoot = e => {
rootSize.width.value = e.nativeEvent.layout.width;
rootSize.height.value = e.nativeEvent.layout.height;
};
const measureChild = e => {
childSize.width.value = e.nativeEvent.layout.width;
childSize.height.value = e.nativeEvent.layout.height;
};
const childStyle = useAnimatedStyle(() => ({
transform: [{
translateX: translate.x.value
}, {
translateY: translate.y.value
}, {
scale: scale.value
}]
}), [translate, scale]);
const detectorStyle = useAnimatedStyle(() => {
return {
width: extendedSize.width.value,
height: extendedSize.height.value,
position: 'absolute',
transform: [{
translateX: detectorTranslate.x.value
}, {
translateY: detectorTranslate.y.value
}, {
scale: detectorScale.value
}]
};
}, [childSize, rootSize, detectorTranslate, detectorScale]);
const requestState = () => {
return {
width: childSize.width.value,
height: childSize.height.value,
translateX: translate.x.value,
translateY: translate.y.value,
scale: scale.value
};
};
const assignState = (state, animate = true) => {
const toScale = clamp(state.scale, minScale, maxScale.value);
const {
x: boundX,
y: boundY
} = boundsFn(toScale);
const toX = clamp(state.translateX, -1 * boundX, boundX);
const toY = clamp(state.translateY, -1 * boundY, boundY);
reset(toX, toY, toScale, animate);
};
useImperativeHandle(ref, () => ({
reset: animate => reset(0, 0, minScale, animate),
requestState: requestState,
assignState: assignState
}));
const composedTap = Gesture.Exclusive(doubleTap, tap);
const composedGesture = Gesture.Race(pinch, pan, composedTap);
return /*#__PURE__*/React.createElement(GestureHandlerRootView, {
style: styles.root,
onLayout: measureRoot
}, /*#__PURE__*/React.createElement(Animated.View, {
style: childStyle,
onLayout: measureChild
}, children), /*#__PURE__*/React.createElement(GestureDetector, {
gesture: composedGesture
}, /*#__PURE__*/React.createElement(Animated.View, {
style: detectorStyle
})));
};
const styles = StyleSheet.create({
root: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
});
export default withResumableValidation(ResumableZoom);
//# sourceMappingURL=ResumableZoom.js.map