@attio/react-native-bottom-sheet-toolbox
Version:
160 lines (149 loc) • 6.75 kB
JavaScript
;
import React from "react";
import { PixelRatio } from "react-native";
import { cancelAnimation, runOnUI, useDerivedValue, withSpring } from "react-native-reanimated";
import { useBottomSheetToolboxInternalContext } from "../context";
import { DEFAULT_SHEET_SNAP_POINT_SPRING_CONFIG } from "../utils/default-sheet-snap-point-spring-config";
import { useWorkletUiCallback } from "./use-worklet-ui-callback";
const PIXEL_RATIO = PixelRatio.get();
const MIN_PERCEIVED_JUMP_DISTANCE = 10;
/**
* A priority lock for controlling the position of the bottom-sheet.
* This allows our bottom-sheet's position to be modified by multiple sources.
*/
export function useBottomSheetPositionLock(id, priority) {
const {
isReadySharedValue,
currentPositionSharedValue,
currentPositionDestinationSharedValue,
currentPositionLastVelocitySharedValue,
currentPositionLockSharedValue
} = useBottomSheetToolboxInternalContext();
const canLockSharedValue = useDerivedValue(() => {
const isReady = isReadySharedValue.get();
const currentLock = currentPositionLockSharedValue.get();
return isReady && (currentLock === null || currentLock.priority < priority);
}, [isReadySharedValue, currentPositionLockSharedValue, priority]);
const hasLockSharedValue = useDerivedValue(() => id === currentPositionLockSharedValue.get()?.id, [currentPositionLockSharedValue, id]);
const lock = useWorkletUiCallback(() => {
"worklet";
const currentLock = currentPositionLockSharedValue.get();
if (currentLock !== null) {
if (priority < currentLock.priority) {
// Lock checks should happen separately to lock requests.
// So this should never happen. However, there are super
// rare race conditions where this can happen in SharedValue
// world.
console.warn(`Unable to lock '${id}' as '${currentLock.id}' is already engaged.`);
return;
}
if (id !== currentLock.id) {
cancelAnimation(currentPositionSharedValue);
}
}
const newLock = {
id,
priority
};
currentPositionLockSharedValue.set(newLock);
}, [currentPositionLockSharedValue, currentPositionSharedValue, id, priority]);
const release = useWorkletUiCallback(() => {
"worklet";
const currentLock = currentPositionLockSharedValue.get();
if (currentLock?.id !== id) return;
currentPositionLockSharedValue.set(null);
}, [currentPositionLockSharedValue, id]);
// Release lock when we unmount
const unmountFn = () => {
runOnUI(release)();
};
const onUnmountRef = React.useRef(unmountFn);
onUnmountRef.current = unmountFn;
React.useLayoutEffect(() => () => onUnmountRef.current(), []);
const setPositionInternal = useWorkletUiCallback(v => {
"worklet";
const currentLock = currentPositionLockSharedValue.get();
if (currentLock !== null && currentLock.id !== id) {
console.warn(`Cannot setPosition with '${id}' as '${currentLock.id}' is already engaged.`);
return;
}
currentPositionSharedValue.set(v);
if (typeof v === "number") {
currentPositionDestinationSharedValue.set(v);
}
}, [currentPositionLockSharedValue, id, currentPositionSharedValue, currentPositionDestinationSharedValue.set]);
const setPositionAnimate = useWorkletUiCallback((v, animationConfig, callback) => {
"worklet";
const animConfigWithDefault = animationConfig || DEFAULT_SHEET_SNAP_POINT_SPRING_CONFIG;
const springAnim = withSpring(v, animConfigWithDefault, callback);
const oldOnStart = springAnim.onStart;
/**
* This is a fun bit of code.
*
* Sometimes the destination value we're animating to is itself being
* animated. Reanimated does a good job of handling in-flight changes to
* animation configs.
*
* However, we want changes to the destination value to supersede the
* sheet's own physical behavior (i.e. its spring config).
*
* Another way to think of this is we want the sheet to appear as if its
* easing consistently despite changes to its destination position.
*
* A practical example: if the sheet is opening simultaneously with the
* keyboard, we want the sheet's travel to compensate for the keyboard's
* height so it remains visible at all times.
*
* To achieve this, we use a rough heuristic to determine if the
* destination delta can be stepped without the user noticing. If so,
* we perform the step.
*/
springAnim.onStart = (nextAnimation, current, timestamp, previousAnimation) => {
oldOnStart(nextAnimation, current, timestamp, previousAnimation);
const tickDelta =
// biome-ignore lint/complexity/useLiteralKeys: Property 'lastTickTime' comes from an index signature, so it must be accessed with ['lastTickTime'].
Date.now() - (previousAnimation ? previousAnimation["lastTickTime"] : 0);
const goodFrameRate = 1000 / 60;
const deviatedGoodFrameRate = goodFrameRate * 1.25;
const destinationAnimating = tickDelta <= deviatedGoodFrameRate;
if (destinationAnimating) {
const destinationDelta = nextAnimation.toValue - currentPositionDestinationSharedValue.get();
const physicalDistance = Math.abs(destinationDelta * PIXEL_RATIO);
const minPerceivedJumpDistance = MIN_PERCEIVED_JUMP_DISTANCE * PIXEL_RATIO;
if (physicalDistance < minPerceivedJumpDistance) {
;
nextAnimation.current += destinationDelta;
}
}
currentPositionDestinationSharedValue.set(nextAnimation.toValue);
// biome-ignore lint/complexity/useLiteralKeys: Property 'lastTickTime' comes from an index signature, so it must be accessed with ['lastTickTime'].
nextAnimation["lastTickTime"] = Date.now();
};
setPositionInternal(springAnim);
}, [setPositionInternal, currentPositionDestinationSharedValue.get, currentPositionDestinationSharedValue.set]);
const setPosition = useWorkletUiCallback(v => {
"worklet";
setPositionAnimate(v, {
duration: 0
});
}, [setPositionAnimate]);
const setVelocity = useWorkletUiCallback(v => {
"worklet";
const currentLock = currentPositionLockSharedValue.get();
if (currentLock !== null && currentLock.id !== id) {
console.warn(`Cannot setVelocity with '${id}' as '${currentLock.id}' is already engaged.`);
return;
}
currentPositionLastVelocitySharedValue.set(v);
}, [currentPositionLockSharedValue, id, currentPositionLastVelocitySharedValue]);
return {
canLockSharedValue,
hasLockSharedValue,
lock,
release,
setPosition,
setPositionAnimate,
setVelocity
};
}
//# sourceMappingURL=use-bottom-sheet-position-lock.js.map