react-native-zoom-toolkit
Version:
Smoothly zoom any image, video or component you want!
335 lines (331 loc) • 12 kB
JavaScript
import React, { useContext } from 'react';
import Animated, { Easing, cancelAnimation, runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue, withDecay, withTiming } from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { clamp } from '../../commons/utils/clamp';
import { pinchTransform } from '../../commons/utils/pinchTransform';
import { useVector } from '../../commons/hooks/useVector';
import { snapPoint } from '../../commons/utils/snapPoint';
import { crop } from '../../commons/utils/crop';
import { usePinchCommons } from '../../commons/hooks/usePinchCommons';
import { getSwipeDirection } from '../../commons/utils/getSwipeDirection';
import { GalleryContext } from './context';
import { ScaleMode, SwipeDirection } from '../../commons/types';
const minScale = 1;
const config = {
duration: 300,
easing: Easing.linear
};
/*
* Pinchable views are really heavy components, therefore in order to maximize performance
* only a single pinchable view is shared among all the list items, items listen to this
* component updates and only update themselves if they are the current item.
*/
const Reflection = ({
length,
maxScale,
itemSize,
vertical,
tapOnEdgeToItem,
allowPinchPanning,
pinchCenteringMode,
onTap,
onPanStart,
onPanEnd,
onPinchStart: onUserPinchStart,
onPinchEnd: onUserPinchEnd,
onSwipe: onUserSwipe,
onVerticalPull
}) => {
const {
activeIndex,
fetchIndex,
scroll,
scrollOffset,
isScrolling,
rootSize,
rootChildSize,
translate,
scale
} = useContext(GalleryContext);
const offset = useVector(0, 0);
const origin = useVector(0, 0);
const delta = useVector(0, 0);
const scaleOffset = useSharedValue(1);
const detectorTranslate = useVector(0, 0);
const detectorScale = useSharedValue(1);
const time = useSharedValue(0);
const position = useVector(0, 0);
const isPullingVertical = useSharedValue(false);
const pullReleased = useSharedValue(false);
const boundsFn = scaleValue => {
'worklet';
const {
width: cWidth,
height: cHeight
} = rootChildSize;
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;
};
const snapToScrollPosition = e => {
'worklet';
const index = activeIndex.value;
const prev = itemSize.value * clamp(index - 1, 0, length - 1);
const current = itemSize.value * index;
const next = itemSize.value * clamp(index + 1, 0, length - 1);
const velocity = vertical ? e.velocityY : e.velocityX;
const toScroll = snapPoint(scroll.value, velocity, [prev, current, next]);
if (toScroll !== current) fetchIndex.value = index + (toScroll === next ? 1 : -1);
scroll.value = withTiming(toScroll, config, () => {
activeIndex.value = fetchIndex.value;
isScrolling.value = false;
toScroll !== current && reset(0, 0, minScale, false);
});
};
const onSwipe = direction => {
'worklet';
let toIndex = activeIndex.value;
if (direction === SwipeDirection.UP && vertical) toIndex += 1;
if (direction === SwipeDirection.DOWN && vertical) toIndex -= 1;
if (direction === SwipeDirection.LEFT && !vertical) toIndex += 1;
if (direction === SwipeDirection.RIGHT && !vertical) toIndex -= 1;
toIndex = clamp(toIndex, 0, length - 1);
if (toIndex === activeIndex.value) return;
fetchIndex.value = toIndex;
scroll.value = withTiming(toIndex * itemSize.value, config, () => {
activeIndex.value = toIndex;
isScrolling.value = false;
reset(0, 0, minScale, false);
});
};
useAnimatedReaction(() => ({
translate: translate.y.value,
scale: scale.value,
isPulling: isPullingVertical.value,
released: pullReleased.value
}), val => {
const shouldPull = !vertical && val.scale === 1 && val.isPulling;
shouldPull && (onVerticalPull === null || onVerticalPull === void 0 ? void 0 : onVerticalPull(val.translate, val.released));
}, [translate, scale, isPullingVertical, pullReleased]);
useAnimatedReaction(() => ({
width: rootSize.width.value,
height: rootSize.height.value
}), () => reset(0, 0, minScale, false), [rootSize]);
const {
gesturesEnabled,
onPinchStart,
onPinchUpdate,
onPinchEnd
} = usePinchCommons({
container: rootSize,
detectorTranslate,
detectorScale,
translate,
offset,
origin,
scale,
scaleOffset,
minScale,
maxScale,
delta,
allowPinchPanning,
scaleMode: ScaleMode.BOUNCE,
pinchCenteringMode,
boundFn: boundsFn,
userCallbacks: {
onPinchStart: onUserPinchStart,
onPinchEnd: onUserPinchEnd
}
});
const pinch = Gesture.Pinch().onStart(onPinchStart).onUpdate(onPinchUpdate).onEnd(onPinchEnd);
const pan = Gesture.Pan().maxPointers(1).enabled(gesturesEnabled).onStart(e => {
onPanStart && runOnJS(onPanStart)(e);
cancelAnimation(translate.x);
cancelAnimation(translate.y);
cancelAnimation(detectorTranslate.x);
cancelAnimation(detectorTranslate.y);
const isVerticalPan = Math.abs(e.velocityY) > Math.abs(e.velocityX);
isPullingVertical.value = isVerticalPan && scale.value === 1 && !vertical;
isScrolling.value = true;
time.value = performance.now();
position.x.value = e.absoluteX;
position.y.value = e.absoluteY;
scrollOffset.value = scroll.value;
offset.x.value = translate.x.value;
offset.y.value = translate.y.value;
}).onUpdate(e => {
if (isPullingVertical.value) {
translate.y.value = e.translationY;
return;
}
const toX = offset.x.value + e.translationX;
const toY = offset.y.value + e.translationY;
const {
x: boundX,
y: boundY
} = boundsFn(scale.value);
const exceedX = Math.max(0, Math.abs(toX) - boundX);
const exceedY = Math.max(0, Math.abs(toY) - boundY);
const scrollX = -1 * Math.sign(toX) * exceedX;
const scrollY = -1 * Math.sign(toY) * exceedY;
scroll.value = clamp(scrollOffset.value + (vertical ? scrollY : scrollX), 0, (length - 1) * itemSize.value);
translate.x.value = clamp(toX, -1 * boundX, boundX);
translate.y.value = clamp(toY, -1 * boundY, boundY);
detectorTranslate.x.value = clamp(toX, -1 * boundX, boundX);
detectorTranslate.y.value = clamp(toY, -1 * boundY, boundY);
}).onEnd(e => {
const bounds = boundsFn(scale.value);
const direction = getSwipeDirection(e, {
boundaries: bounds,
time: time.value,
position: {
x: position.x.value,
y: position.y.value
},
translate: {
x: isPullingVertical.value ? 100 : translate.x.value,
y: isPullingVertical.value ? 0 : translate.y.value
}
});
direction !== undefined && onSwipe(direction);
direction !== undefined && onUserSwipe && runOnJS(onUserSwipe)(direction);
if (isPullingVertical.value) {
pullReleased.value = true;
translate.y.value = withTiming(0, undefined, finished => {
isPullingVertical.value = !finished;
pullReleased.value = !finished;
});
return;
}
const isSwipingH = direction === SwipeDirection.LEFT || direction === SwipeDirection.RIGHT;
const isSwipingV = direction === SwipeDirection.UP || direction === SwipeDirection.DOWN;
const snapV = vertical && (direction === undefined || isSwipingH);
const snapH = !vertical && (direction === undefined || isSwipingV);
onPanEnd && runOnJS(onPanEnd)(e);
(snapV || snapH) && snapToScrollPosition(e);
const configX = {
velocity: e.velocityX,
clamp: [-bounds.x, bounds.x]
};
const configY = {
velocity: e.velocityY,
clamp: [-bounds.y, bounds.y]
};
translate.x.value = withDecay(configX);
translate.y.value = withDecay(configY);
detectorTranslate.x.value = withDecay(configX);
detectorTranslate.y.value = withDecay(configY);
});
const tap = Gesture.Tap().enabled(gesturesEnabled).numberOfTaps(1).maxDuration(250).onEnd(e => {
const gallerySize = {
width: rootSize.width.value,
height: rootSize.height.value
};
const {
crop: result
} = crop({
scale: scale.value,
context: {
flipHorizontal: false,
flipVertical: false,
rotationAngle: 0
},
canvas: gallerySize,
cropSize: gallerySize,
resolution: gallerySize,
position: {
x: translate.x.value,
y: translate.y.value
}
});
const tapEdge = 44 / scale.value;
const leftEdge = result.originX + tapEdge;
const rightEdge = result.originX + result.width - tapEdge;
let toIndex = activeIndex.value;
const canGoToItem = tapOnEdgeToItem && !vertical;
if (e.x <= leftEdge && canGoToItem) toIndex -= 1;
if (e.x >= rightEdge && canGoToItem) toIndex += 1;
if (toIndex === activeIndex.value && onTap) {
runOnJS(onTap)(e, activeIndex.value);
return;
}
toIndex = clamp(toIndex, 0, length - 1);
scroll.value = toIndex * itemSize.value;
activeIndex.value = toIndex;
fetchIndex.value = toIndex;
reset(0, 0, minScale, false);
});
const doubleTap = Gesture.Tap().enabled(gesturesEnabled).numberOfTaps(2).maxDuration(250).onEnd(e => {
const originX = e.x - rootSize.width.value / 2;
const originY = e.y - rootSize.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);
});
const detectorStyle = useAnimatedStyle(() => ({
width: Math.max(rootSize.width.value, rootChildSize.width.value),
height: Math.max(rootSize.height.value, rootChildSize.height.value),
position: 'absolute',
zIndex: Number.MAX_SAFE_INTEGER,
transform: [{
translateX: detectorTranslate.x.value
}, {
translateY: detectorTranslate.y.value
}, {
scale: detectorScale.value
}]
}));
const composed = Gesture.Race(pan, pinch, Gesture.Exclusive(doubleTap, tap));
return /*#__PURE__*/React.createElement(GestureDetector, {
gesture: composed
}, /*#__PURE__*/React.createElement(Animated.View, {
style: detectorStyle
}));
};
export default /*#__PURE__*/React.memo(Reflection, (prev, next) => {
return prev.onTap === next.onTap && prev.onPanStart === next.onPanStart && prev.onPanEnd === next.onPanEnd && prev.onPinchStart === next.onPinchStart && prev.onPinchEnd === next.onPinchEnd && prev.onSwipe === next.onSwipe && prev.length === next.length && prev.vertical === next.vertical && prev.tapOnEdgeToItem === next.tapOnEdgeToItem && prev.allowPinchPanning === next.allowPinchPanning && prev.pinchCenteringMode === next.pinchCenteringMode && prev.onVerticalPull === next.onVerticalPull;
});
//# sourceMappingURL=Reflection.js.map