@attio/react-native-bottom-sheet-toolbox
Version:
227 lines (186 loc) • 8.14 kB
text/typescript
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,
}
}