Skip to content

Commit 5de7bd3

Browse files
committed
refactor: optimise snap points/detents state
1 parent e26426a commit 5de7bd3

File tree

10 files changed

+325
-250
lines changed

10 files changed

+325
-250
lines changed

src/components/bottomSheet/BottomSheet.tsx

Lines changed: 138 additions & 83 deletions
Large diffs are not rendered by default.

src/components/bottomSheet/BottomSheetContent.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function BottomSheetContentComponent({
4848
enableContentPanningGesture,
4949
animatedPosition,
5050
animatedLayoutState,
51-
animatedHighestSnapPoint,
51+
animatedDetentsState,
5252
animatedSheetHeight,
5353
animatedKeyboardState,
5454
isInTemporaryPosition,
@@ -140,8 +140,10 @@ function BottomSheetContentComponent({
140140
return 0;
141141
}
142142

143+
const { highestDetentPosition } = animatedDetentsState.get();
144+
143145
const highestSnapPoint = Math.max(
144-
animatedHighestSnapPoint.get(),
146+
highestDetentPosition ?? 0,
145147
animatedPosition.get()
146148
);
147149
/**
@@ -172,7 +174,7 @@ function BottomSheetContentComponent({
172174
overDragResistanceFactor,
173175
animatedPosition,
174176
animatedLayoutState,
175-
animatedHighestSnapPoint,
177+
animatedDetentsState,
176178
animatedKeyboardState,
177179
]);
178180
//#endregion

src/constants.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,18 @@ const ANIMATION_EASING: Animated.EasingFunction = Easing.out(Easing.exp);
7676
const ANIMATION_DURATION = 250;
7777

7878
const ANIMATION_CONFIGS = Platform.select<WithTimingConfig | WithSpringConfig>({
79-
ios: {
79+
android: {
80+
duration: ANIMATION_DURATION,
81+
easing: ANIMATION_EASING,
82+
},
83+
default: {
8084
damping: 500,
8185
stiffness: 1000,
8286
mass: 3,
8387
overshootClamping: true,
8488
restDisplacementThreshold: 10,
8589
restSpeedThreshold: 10,
8690
},
87-
default: {
88-
duration: ANIMATION_DURATION,
89-
easing: ANIMATION_EASING,
90-
},
9191
});
9292

9393
const SCROLLABLE_DECELERATION_RATE_MAPPER = {

src/contexts/internal.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
} from '../constants';
1515
import type {
1616
AnimationState,
17+
DetentsState,
1718
KeyboardState,
1819
LayoutState,
1920
Scrollable,
@@ -35,6 +36,7 @@ export interface BottomSheetInternalContextType
3536
>
3637
> {
3738
// animated states
39+
animatedDetentsState: SharedValue<DetentsState>;
3840
animatedAnimationState: SharedValue<AnimationState>;
3941
animatedSheetState: SharedValue<SHEET_STATE>;
4042
animatedKeyboardState: SharedValue<KeyboardState>;
@@ -47,12 +49,9 @@ export interface BottomSheetInternalContextType
4749
animatedScrollableStatus: SharedValue<SCROLLABLE_STATUS>;
4850

4951
// animated values
50-
animatedSnapPoints: SharedValue<number[]>;
5152
animatedPosition: SharedValue<number>;
5253
animatedIndex: SharedValue<number>;
5354
animatedSheetHeight: SharedValue<number>;
54-
animatedHighestSnapPoint: SharedValue<number>;
55-
animatedClosedPosition: SharedValue<number>;
5655
isInTemporaryPosition: SharedValue<boolean>;
5756

5857
// methods

src/hooks/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export { useAnimatedLayout } from './useAnimatedLayout';
2020
export { useAnimatedKeyboard } from './useAnimatedKeyboard';
2121
export { useStableCallback } from './useStableCallback';
2222
export { usePropsValidator } from './usePropsValidator';
23-
export { useAnimatedSnapPoints } from './useAnimatedSnapPoints';
23+
export { useAnimatedDetents } from './useAnimatedDetents';
2424
export { useReactiveSharedValue } from './useReactiveSharedValue';
2525
export {
2626
useBoundingClientRect,

src/hooks/useAnimatedDetents.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
};

src/hooks/useAnimatedSnapPoints.ts

Lines changed: 0 additions & 129 deletions
This file was deleted.

0 commit comments

Comments
 (0)