UNPKG

@attio/react-native-bottom-sheet-toolbox

Version:
227 lines (186 loc) 8.14 kB
import React from "react" import {PixelRatio} from "react-native" import { cancelAnimation, runOnUI, type SpringAnimation, useDerivedValue, type WithSpringConfig, 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: string, priority: number) { 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: number) => { "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: number, animationConfig?: WithSpringConfig, callback?: (finished?: boolean) => void ) => { "worklet" const animConfigWithDefault = animationConfig || DEFAULT_SHEET_SNAP_POINT_SPRING_CONFIG const springAnim = withSpring( v, animConfigWithDefault, callback ) as unknown as SpringAnimation 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 as number) - currentPositionDestinationSharedValue.get() const physicalDistance = Math.abs(destinationDelta * PIXEL_RATIO) const minPerceivedJumpDistance = MIN_PERCEIVED_JUMP_DISTANCE * PIXEL_RATIO if (physicalDistance < minPerceivedJumpDistance) { ;(nextAnimation.current as number) += destinationDelta } } currentPositionDestinationSharedValue.set(nextAnimation.toValue as number) // 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 as unknown as number) }, [ setPositionInternal, currentPositionDestinationSharedValue.get, currentPositionDestinationSharedValue.set, ] ) const setPosition = useWorkletUiCallback( (v: number) => { "worklet" setPositionAnimate(v, {duration: 0}) }, [setPositionAnimate] ) const setVelocity = useWorkletUiCallback( (v: number) => { "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, } }