|
| 1 | +import { type SharedValue, useDerivedValue } from 'react-native-reanimated'; |
| 2 | +import type { BottomSheetProps } from '../components/bottomSheet'; |
| 3 | +import { INITIAL_LAYOUT_VALUE } from '../constants'; |
| 4 | +import type { DetentsState, LayoutState } from '../types'; |
| 5 | +import { normalizeSnapPoint } from '../utilities'; |
| 6 | + |
| 7 | +/** |
| 8 | + * A custom hook that computes and returns the animated detent positions for a bottom sheet component. |
| 9 | + * |
| 10 | + * This hook normalizes the provided snap points (detents), optionally adds a dynamic detent based on content size, |
| 11 | + * and calculates key positions such as the highest detent and the closed position. It supports both static and dynamic |
| 12 | + * sizing, and adapts to modal and detached sheet modes. |
| 13 | + * |
| 14 | + * @param detents - The snap points for the bottom sheet, which can be an array or an object with a `value` property. |
| 15 | + * @param layoutState - A shared animated value containing the current layout state (container, handle, and content heights). |
| 16 | + * @param enableDynamicSizing - Whether dynamic sizing based on content height is enabled. |
| 17 | + * @param maxDynamicContentSize - The maximum allowed content size for dynamic sizing. |
| 18 | + * @param detached - Whether the bottom sheet is in detached mode. |
| 19 | + * @param $modal - Whether the bottom sheet is presented as a modal. |
| 20 | + * @param bottomInset - The bottom inset to apply when the sheet is modal or detached (default is 0). |
| 21 | + */ |
| 22 | +export const useAnimatedDetents = ( |
| 23 | + detents: BottomSheetProps['snapPoints'], |
| 24 | + layoutState: SharedValue<LayoutState>, |
| 25 | + enableDynamicSizing: BottomSheetProps['enableDynamicSizing'], |
| 26 | + maxDynamicContentSize: BottomSheetProps['maxDynamicContentSize'], |
| 27 | + detached: BottomSheetProps['detached'], |
| 28 | + $modal: BottomSheetProps['$modal'], |
| 29 | + bottomInset: BottomSheetProps['bottomInset'] = 0 |
| 30 | +) => { |
| 31 | + const state = useDerivedValue<DetentsState>(() => { |
| 32 | + const { containerHeight, handleHeight, contentHeight } = layoutState.get(); |
| 33 | + |
| 34 | + // early exit, if container layout is not ready |
| 35 | + if (containerHeight === INITIAL_LAYOUT_VALUE) { |
| 36 | + return {}; |
| 37 | + } |
| 38 | + |
| 39 | + // extract detents from provided props |
| 40 | + const _detents = detents |
| 41 | + ? 'value' in detents |
| 42 | + ? detents.value |
| 43 | + : detents |
| 44 | + : []; |
| 45 | + |
| 46 | + // normalized all provided detents, converting percentage |
| 47 | + // values into absolute values. |
| 48 | + let _normalizedDetents = _detents.map(snapPoint => |
| 49 | + normalizeSnapPoint(snapPoint, containerHeight) |
| 50 | + ) as number[]; |
| 51 | + |
| 52 | + let highestDetentPosition = |
| 53 | + _normalizedDetents[_normalizedDetents.length - 1]; |
| 54 | + let closedDetentPosition = containerHeight; |
| 55 | + if ($modal || detached) { |
| 56 | + closedDetentPosition = containerHeight + bottomInset; |
| 57 | + } |
| 58 | + |
| 59 | + if (!enableDynamicSizing) { |
| 60 | + return { |
| 61 | + detents: _normalizedDetents, |
| 62 | + highestDetentPosition, |
| 63 | + closedDetentPosition, |
| 64 | + }; |
| 65 | + } |
| 66 | + |
| 67 | + // early exit, if dynamic sizing is enabled and |
| 68 | + // content height is not calculated yet. |
| 69 | + if (contentHeight === INITIAL_LAYOUT_VALUE) { |
| 70 | + return {}; |
| 71 | + } |
| 72 | + |
| 73 | + // early exit, if handle height is not calculated yet. |
| 74 | + if (handleHeight === INITIAL_LAYOUT_VALUE) { |
| 75 | + return {}; |
| 76 | + } |
| 77 | + |
| 78 | + // calculate a new detents based on content height. |
| 79 | + const dynamicSnapPoint = |
| 80 | + containerHeight - |
| 81 | + Math.min( |
| 82 | + contentHeight + handleHeight, |
| 83 | + maxDynamicContentSize !== undefined |
| 84 | + ? maxDynamicContentSize |
| 85 | + : containerHeight |
| 86 | + ); |
| 87 | + |
| 88 | + // push dynamic detent into the normalized detents, |
| 89 | + // only if it does not exists in the provided list already. |
| 90 | + if (!_normalizedDetents.includes(dynamicSnapPoint)) { |
| 91 | + _normalizedDetents.push(dynamicSnapPoint); |
| 92 | + } |
| 93 | + |
| 94 | + // sort all detents. |
| 95 | + _normalizedDetents = _normalizedDetents.sort((a, b) => b - a); |
| 96 | + |
| 97 | + highestDetentPosition = _normalizedDetents[0]; |
| 98 | + |
| 99 | + // locate the dynamic detent index. |
| 100 | + const dynamicDetentIndex = _normalizedDetents.indexOf(dynamicSnapPoint); |
| 101 | + |
| 102 | + return { |
| 103 | + detents: _normalizedDetents, |
| 104 | + dynamicDetentIndex, |
| 105 | + highestDetentPosition, |
| 106 | + closedDetentPosition, |
| 107 | + }; |
| 108 | + }, [ |
| 109 | + detents, |
| 110 | + layoutState, |
| 111 | + enableDynamicSizing, |
| 112 | + maxDynamicContentSize, |
| 113 | + detached, |
| 114 | + $modal, |
| 115 | + bottomInset, |
| 116 | + ]); |
| 117 | + return state; |
| 118 | +}; |
0 commit comments