@attio/react-native-bottom-sheet-toolbox
Version:
149 lines (122 loc) • 5.2 kB
text/typescript
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)
}