react-native-zoom-toolkit
Version:
Most complete set of pinch to zoom utilites for React Native
185 lines (153 loc) • 5.65 kB
text/typescript
import {
cancelAnimation,
withTiming,
useSharedValue,
withDecay,
runOnJS,
type SharedValue,
} from 'react-native-reanimated';
import type {
GestureUpdateEvent,
PanGestureChangeEventPayload,
PanGestureHandlerEventPayload,
} from 'react-native-gesture-handler';
import { clamp } from '../utils/clamp';
import { useVector } from './useVector';
import { friction } from '../utils/friction';
import { getSwipeDirection } from '../utils/getSwipeDirection';
import type {
PanMode,
BoundsFuction,
Vector,
SizeVector,
PanGestureEventCallback,
PanGestureEvent,
SwipeDirection,
} from '../types';
type PanCommmonOptions = {
container: SizeVector<SharedValue<number>>;
translate: Vector<SharedValue<number>>;
offset: Vector<SharedValue<number>>;
panMode: PanMode;
decay?: boolean;
boundFn: BoundsFuction;
userCallbacks: Partial<{
onGestureEnd: () => void;
onPanStart: PanGestureEventCallback;
onPanEnd: PanGestureEventCallback;
onSwipe: (direction: SwipeDirection) => void;
onOverPanning: (x: number, y: number) => void;
}>;
};
type PanGestureUpdadeEvent = GestureUpdateEvent<
PanGestureHandlerEventPayload & PanGestureChangeEventPayload
>;
export const usePanCommons = (options: PanCommmonOptions) => {
const {
container,
translate,
offset,
panMode,
decay,
boundFn,
userCallbacks,
} = options;
const { onSwipe, onGestureEnd, onOverPanning } = userCallbacks;
const time = useSharedValue<number>(0);
const position = useVector(0, 0);
const gestureEnd = useSharedValue<number>(0); // Gimmick value to trigger onGestureEnd callback
const isWithinBoundX = useSharedValue<boolean>(true);
const isWithinBoundY = useSharedValue<boolean>(true);
const onPanStart = (e: PanGestureEvent) => {
'worklet';
userCallbacks.onPanStart && runOnJS(userCallbacks.onPanStart)(e);
cancelAnimation(translate.x);
cancelAnimation(translate.y);
offset.x.value = translate.x.value;
offset.y.value = translate.y.value;
time.value = performance.now();
position.x.value = e.absoluteX;
position.y.value = e.absoluteY;
};
const onPanChange = (e: PanGestureUpdadeEvent) => {
'worklet';
const toX = e.translationX + offset.x.value;
const toY = e.translationY + offset.y.value;
const { x: boundX, y: boundY } = boundFn();
const exceedX = Math.max(0, Math.abs(toX) - boundX);
const exceedY = Math.max(0, Math.abs(toY) - boundY);
isWithinBoundX.value = exceedX === 0;
isWithinBoundY.value = exceedY === 0;
if ((exceedX > 0 || exceedY > 0) && onOverPanning) {
const ex = Math.sign(toX) * exceedX;
const ey = Math.sign(toY) * exceedY;
onOverPanning(ex, ey);
}
// Simplify both free and clamp pan modes in one condition due to their similarity
if (panMode !== 'friction') {
const isFree = panMode === 'free';
translate.x.value = isFree ? toX : clamp(toX, -1 * boundX, boundX);
translate.y.value = isFree ? toY : clamp(toY, -1 * boundY, boundY);
return;
}
const overScrollFraction =
Math.max(container.width.value, container.height.value) * 1.5;
if (isWithinBoundX.value) {
translate.x.value = toX;
} else {
const fraction = Math.abs(Math.abs(toX) - boundX) / overScrollFraction;
const frictionX = friction(clamp(fraction, 0, 1));
translate.x.value += e.changeX * frictionX;
}
if (isWithinBoundY.value) {
translate.y.value = toY;
} else {
const fraction = Math.abs(Math.abs(toY) - boundY) / overScrollFraction;
const frictionY = friction(clamp(fraction, 0, 1));
translate.y.value += e.changeY * frictionY;
}
};
const onPanEnd = (e: PanGestureEvent) => {
'worklet';
if (panMode === 'clamp' && onSwipe) {
const boundaries = boundFn();
const direction = getSwipeDirection(e, {
boundaries,
time: time.value,
position: { x: position.x.value, y: position.y.value },
translate: { x: translate.x.value, y: translate.y.value },
});
if (direction !== undefined) {
runOnJS(onSwipe)(direction);
return;
}
}
userCallbacks.onPanEnd && runOnJS(userCallbacks.onPanEnd)(e);
const { x: boundX, y: boundY } = boundFn();
const clampX: [number, number] = [-1 * boundX, boundX];
const clampY: [number, number] = [-1 * boundY, boundY];
const toX = clamp(translate.x.value, -1 * boundX, boundX);
const toY = clamp(translate.y.value, -1 * boundY, boundY);
const decayX = decay && isWithinBoundX.value;
const decayY = decay && isWithinBoundY.value;
const decayConfigX = { velocity: e.velocityX, clamp: clampX };
const decayConfigY = { velocity: e.velocityY, clamp: clampY };
translate.x.value = decayX ? withDecay(decayConfigX) : withTiming(toX);
translate.y.value = decayY ? withDecay(decayConfigY) : withTiming(toY);
const restX = Math.abs(Math.abs(translate.x.value) - boundX);
const restY = Math.abs(Math.abs(translate.y.value) - boundY);
gestureEnd.value = restX > restY ? translate.x.value : translate.y.value;
if (decayX || decayY) {
const config = restX > restY ? decayConfigX : decayConfigY;
gestureEnd.value = withDecay(config, (finished) => {
finished && onGestureEnd && runOnJS(onGestureEnd)();
});
} else {
const toValue = restX > restY ? toX : toY;
gestureEnd.value = withTiming(toValue, undefined, (finished) => {
finished && onGestureEnd && runOnJS(onGestureEnd)();
});
}
};
return { onPanStart, onPanChange, onPanEnd };
};