UNPKG

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

Version:
149 lines (122 loc) 5.2 kB
import * as React from "react" import {Dimensions} from "react-native" import {Gesture} from "react-native-gesture-handler" import { runOnJS, type SharedValue, useAnimatedReaction, useSharedValue, } from "react-native-reanimated" import {useBottomSheetToolboxInternalContext} from "../context" import {BottomSheetPositionBuiltInLockPriority, friction} from "../utils" import {useBottomSheetPositionLock} from "./use-bottom-sheet-position-lock" const LOCK_ID_NAMESPACE = "useBottomSheetDragGesture" // Gesture velocity is calculated between gesture updates. // If the user does not move their finger, the gesture doesn't see update // and so the velocity is miss-leading (i.e. not 0) // We detect this by checking the time between the last gesture update. const SUPPRESS_VELOCITY_TIME_MS = 150 export interface UseBottomSheetDragGestureArgs { id: string disabled?: boolean clamped?: boolean frozenSharedValue?: SharedValue<boolean> } export function useBottomSheetDragGesture({ id, disabled = false, clamped = false, frozenSharedValue, }: UseBottomSheetDragGestureArgs) { const { currentPositionSharedValue, currentPositionLastVelocitySharedValue, wrapperHeightSharedValue, normalizedSnapPointsSharedValue, } = useBottomSheetToolboxInternalContext() const {lock, release, setPosition, setVelocity, canLockSharedValue, hasLockSharedValue} = useBottomSheetPositionLock( `${LOCK_ID_NAMESPACE}:${id}`, BottomSheetPositionBuiltInLockPriority.GESTURE ) const lastLockRef = React.useRef<string>(id) React.useEffect(() => { if (id !== lastLockRef.current) throw new Error("Lock id can't be changed.") }, [id]) const startPositionSharedValue = useSharedValue(0) const lastUpdateTime = useSharedValue(0) const [enabled, setEnabled] = React.useState(false) useAnimatedReaction( () => canLockSharedValue.get() || hasLockSharedValue.get(), (lockable, lastLockable) => { if (lockable === lastLockable) return runOnJS(setEnabled)(lockable) }, [canLockSharedValue, hasLockSharedValue.get] ) const [windowHeight, setWindowHeight] = React.useState(Dimensions.get("window").height) React.useEffect(() => { const handle = Dimensions.addEventListener("change", ({window}) => { setWindowHeight(window.height) }) return () => handle.remove() }, []) const frozenOriginOffsetYSharedValue = useSharedValue(0) return Gesture.Pan() .onBegin(() => { if (!canLockSharedValue.get()) return startPositionSharedValue.set(currentPositionSharedValue.get()) frozenOriginOffsetYSharedValue.set(0) }) .onUpdate((evt) => { const normalizedSnapPoints = normalizedSnapPointsSharedValue.get() const wrapperHeight = wrapperHeightSharedValue.get() const lockable = canLockSharedValue.get() || hasLockSharedValue.get() if (!lockable || !normalizedSnapPoints || !wrapperHeight) return // We want to lock onUpdate, rather than onBegin, so nested // touches do no unnecessarily lock the position. if (!hasLockSharedValue.get()) { lock() } if (frozenSharedValue?.get()) { frozenOriginOffsetYSharedValue.set(evt.translationY) return } setVelocity(evt.velocityY) const rawPosition = startPositionSharedValue.get() - evt.translationY + frozenOriginOffsetYSharedValue.get() const maxSnapPoint = Math.max(...normalizedSnapPoints) const overshoot = Math.max(0, rawPosition - maxSnapPoint) if (clamped) { const clampedPosition = Math.max(Math.min(rawPosition, maxSnapPoint), 0) setPosition(clampedPosition) } else if (overshoot > 0) { const topInset = Math.max(1, windowHeight - wrapperHeight) const frictionRatio = friction(overshoot / topInset) const frictionPosition = maxSnapPoint + topInset * frictionRatio const finalPosition = Math.min(frictionPosition, wrapperHeight + topInset) setPosition(finalPosition) } else { setPosition(rawPosition) } lastUpdateTime.set(Date.now()) }) .onFinalize((evt) => { if (!hasLockSharedValue.get()) return if (frozenSharedValue?.get()) { setVelocity(0) } else { const currentTime = Date.now() const velocity = -evt.velocityY const timeSinceLastUpdate = currentTime - lastUpdateTime.get() currentPositionLastVelocitySharedValue.set( timeSinceLastUpdate > SUPPRESS_VELOCITY_TIME_MS ? 0 : velocity ) setVelocity(velocity) } release() }) .enabled(enabled && !disabled) }