react-native-screen-transitions 2.3.4 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/lib/commonjs/__configs__/presets.js +10 -5
- package/lib/commonjs/__configs__/presets.js.map +1 -1
- package/lib/commonjs/constants.js +2 -1
- package/lib/commonjs/constants.js.map +1 -1
- package/lib/commonjs/hooks/animation/use-screen-animation.js +2 -1
- package/lib/commonjs/hooks/animation/use-screen-animation.js.map +1 -1
- package/lib/commonjs/hooks/bounds/use-bound-registry.js +10 -8
- package/lib/commonjs/hooks/bounds/use-bound-registry.js.map +1 -1
- package/lib/commonjs/hooks/gestures/use-build-gestures.js +17 -10
- package/lib/commonjs/hooks/gestures/use-build-gestures.js.map +1 -1
- package/lib/commonjs/hooks/use-stable-callback-value.js +64 -0
- package/lib/commonjs/hooks/use-stable-callback-value.js.map +1 -0
- package/lib/commonjs/stores/gestures.js +2 -1
- package/lib/commonjs/stores/gestures.js.map +1 -1
- package/lib/commonjs/utils/bounds/_utils/styles.js +58 -0
- package/lib/commonjs/utils/bounds/_utils/styles.js.map +1 -0
- package/lib/commonjs/utils/gesture/reset-gesture-values.js +14 -4
- package/lib/commonjs/utils/gesture/reset-gesture-values.js.map +1 -1
- package/lib/commonjs/utils/gesture/velocity.js +2 -2
- package/lib/commonjs/utils/gesture/velocity.js.map +1 -1
- package/lib/module/__configs__/presets.js +10 -5
- package/lib/module/__configs__/presets.js.map +1 -1
- package/lib/module/constants.js +2 -1
- package/lib/module/constants.js.map +1 -1
- package/lib/module/hooks/animation/use-screen-animation.js +2 -1
- package/lib/module/hooks/animation/use-screen-animation.js.map +1 -1
- package/lib/module/hooks/bounds/use-bound-registry.js +11 -9
- package/lib/module/hooks/bounds/use-bound-registry.js.map +1 -1
- package/lib/module/hooks/gestures/use-build-gestures.js +18 -11
- package/lib/module/hooks/gestures/use-build-gestures.js.map +1 -1
- package/lib/module/hooks/use-stable-callback-value.js +60 -0
- package/lib/module/hooks/use-stable-callback-value.js.map +1 -0
- package/lib/module/stores/gestures.js +2 -1
- package/lib/module/stores/gestures.js.map +1 -1
- package/lib/module/utils/bounds/_utils/styles.js +54 -0
- package/lib/module/utils/bounds/_utils/styles.js.map +1 -0
- package/lib/module/utils/gesture/reset-gesture-values.js +14 -4
- package/lib/module/utils/gesture/reset-gesture-values.js.map +1 -1
- package/lib/module/utils/gesture/velocity.js +2 -2
- package/lib/module/utils/gesture/velocity.js.map +1 -1
- package/lib/typescript/__configs__/presets.d.ts.map +1 -1
- package/lib/typescript/constants.d.ts.map +1 -1
- package/lib/typescript/hooks/animation/use-screen-animation.d.ts.map +1 -1
- package/lib/typescript/hooks/bounds/use-bound-registry.d.ts.map +1 -1
- package/lib/typescript/hooks/gestures/use-build-gestures.d.ts.map +1 -1
- package/lib/typescript/hooks/use-stable-callback-value.d.ts +13 -0
- package/lib/typescript/hooks/use-stable-callback-value.d.ts.map +1 -0
- package/lib/typescript/stores/gestures.d.ts +2 -0
- package/lib/typescript/stores/gestures.d.ts.map +1 -1
- package/lib/typescript/types/animation.d.ts +10 -9
- package/lib/typescript/types/animation.d.ts.map +1 -1
- package/lib/typescript/types/gesture.d.ts +4 -0
- package/lib/typescript/types/gesture.d.ts.map +1 -1
- package/lib/typescript/types/utils.d.ts.map +1 -1
- package/lib/typescript/utils/bounds/_utils/styles.d.ts +8 -0
- package/lib/typescript/utils/bounds/_utils/styles.d.ts.map +1 -0
- package/lib/typescript/utils/gesture/reset-gesture-values.d.ts.map +1 -1
- package/lib/typescript/utils/gesture/velocity.d.ts +1 -1
- package/lib/typescript/utils/gesture/velocity.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__configs__/presets.ts +23 -7
- package/src/__tests__/determine-dismissal.test.ts +121 -0
- package/src/__tests__/gesture.velocity.test.ts +138 -0
- package/src/constants.ts +2 -0
- package/src/hooks/animation/use-screen-animation.tsx +1 -0
- package/src/hooks/bounds/use-bound-registry.tsx +9 -13
- package/src/hooks/gestures/use-build-gestures.tsx +21 -37
- package/src/hooks/use-stable-callback-value.tsx +68 -0
- package/src/stores/gestures.ts +5 -0
- package/src/types/animation.ts +10 -9
- package/src/types/gesture.ts +4 -0
- package/src/types/utils.ts +1 -0
- package/src/utils/bounds/_utils/styles.ts +69 -0
- package/src/utils/gesture/reset-gesture-values.ts +23 -4
- package/src/utils/gesture/velocity.ts +1 -2
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
|
|
3
|
+
mock.module("react-native", () => ({}));
|
|
4
|
+
mock.module("react-native-gesture-handler", () => ({}));
|
|
5
|
+
mock.module("react-native-reanimated", () => ({
|
|
6
|
+
clamp: (value: number, lower: number, upper: number) =>
|
|
7
|
+
Math.min(Math.max(value, lower), upper),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const { velocity } = await import("../utils/gesture/velocity");
|
|
11
|
+
|
|
12
|
+
type Directions = {
|
|
13
|
+
horizontal: boolean;
|
|
14
|
+
horizontalInverted: boolean;
|
|
15
|
+
vertical: boolean;
|
|
16
|
+
verticalInverted: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type GestureEventInit = {
|
|
20
|
+
translationX?: number;
|
|
21
|
+
translationY?: number;
|
|
22
|
+
velocityX?: number;
|
|
23
|
+
velocityY?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const createAnimations = (progress: number) =>
|
|
27
|
+
({
|
|
28
|
+
progress: { value: progress },
|
|
29
|
+
closing: { value: 0 },
|
|
30
|
+
animating: { value: 0 },
|
|
31
|
+
}) as const;
|
|
32
|
+
|
|
33
|
+
const createEvent = ({
|
|
34
|
+
translationX = 0,
|
|
35
|
+
translationY = 0,
|
|
36
|
+
velocityX = 0,
|
|
37
|
+
velocityY = 0,
|
|
38
|
+
}: GestureEventInit) =>
|
|
39
|
+
({ translationX, translationY, velocityX, velocityY }) as any;
|
|
40
|
+
|
|
41
|
+
const createDirections = (overrides: Partial<Directions> = {}) => ({
|
|
42
|
+
horizontal: false,
|
|
43
|
+
horizontalInverted: false,
|
|
44
|
+
vertical: false,
|
|
45
|
+
verticalInverted: false,
|
|
46
|
+
...overrides,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("velocity.normalize", () => {
|
|
50
|
+
it("clamps values to the configured range", () => {
|
|
51
|
+
expect(velocity.normalize(6400, 320)).toBeCloseTo(3.2, 5);
|
|
52
|
+
expect(velocity.normalize(-6400, 320)).toBeCloseTo(-3.2, 5);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("velocity.calculateProgressVelocity", () => {
|
|
57
|
+
const dimensions = { width: 320, height: 640 };
|
|
58
|
+
|
|
59
|
+
it("returns positive magnitude when progressing toward open target", () => {
|
|
60
|
+
const animations = createAnimations(0.25);
|
|
61
|
+
const event = createEvent({
|
|
62
|
+
translationX: 48,
|
|
63
|
+
translationY: 6,
|
|
64
|
+
velocityX: 800,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = velocity.calculateProgressVelocity({
|
|
68
|
+
animations: animations as any,
|
|
69
|
+
shouldDismiss: false,
|
|
70
|
+
event,
|
|
71
|
+
dimensions,
|
|
72
|
+
directions: createDirections({ horizontal: true }),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result).toBeCloseTo(2.5, 5);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("prefers the axis with greater normalized translation", () => {
|
|
79
|
+
const animations = createAnimations(0.8);
|
|
80
|
+
const event = createEvent({
|
|
81
|
+
translationX: 24,
|
|
82
|
+
translationY: -140,
|
|
83
|
+
velocityX: 120,
|
|
84
|
+
velocityY: -900,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = velocity.calculateProgressVelocity({
|
|
88
|
+
animations: animations as any,
|
|
89
|
+
shouldDismiss: true,
|
|
90
|
+
event,
|
|
91
|
+
dimensions,
|
|
92
|
+
directions: createDirections({ horizontal: true, verticalInverted: true }),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(result).toBeLessThan(0);
|
|
96
|
+
expect(Math.abs(result)).toBeCloseTo(1.406, 3);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("caps the returned magnitude using clamp", () => {
|
|
100
|
+
const animations = createAnimations(0.5);
|
|
101
|
+
const event = createEvent({
|
|
102
|
+
translationX: 10,
|
|
103
|
+
velocityX: 5000,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = velocity.calculateProgressVelocity({
|
|
107
|
+
animations: animations as any,
|
|
108
|
+
shouldDismiss: false,
|
|
109
|
+
event,
|
|
110
|
+
dimensions,
|
|
111
|
+
directions: createDirections({ horizontal: true }),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result).toBeCloseTo(3.2, 5);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("velocity.shouldPassDismissalThreshold", () => {
|
|
119
|
+
const width = 320;
|
|
120
|
+
|
|
121
|
+
it("returns true once translation alone crosses half the screen", () => {
|
|
122
|
+
expect(
|
|
123
|
+
velocity.shouldPassDismissalThreshold(170, 0, width, 0.3),
|
|
124
|
+
).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("combines translation with weighted velocity", () => {
|
|
128
|
+
expect(
|
|
129
|
+
velocity.shouldPassDismissalThreshold(40, 2500, width, 0.5),
|
|
130
|
+
).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("returns false when movement is negligible", () => {
|
|
134
|
+
expect(
|
|
135
|
+
velocity.shouldPassDismissalThreshold(0, 0, width, 0.3),
|
|
136
|
+
).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
});
|
package/src/constants.ts
CHANGED
|
@@ -33,6 +33,7 @@ export const DEFAULT_SCREEN_TRANSITION_STATE: ScreenTransitionState =
|
|
|
33
33
|
normalizedY: 0,
|
|
34
34
|
isDismissing: 0,
|
|
35
35
|
isDragging: 0,
|
|
36
|
+
direction: null,
|
|
36
37
|
},
|
|
37
38
|
bounds: {} as Record<string, BoundEntry>,
|
|
38
39
|
route: {} as RouteProp<ParamListBase>,
|
|
@@ -54,6 +55,7 @@ export const EMPTY_BOUND_HELPER_RESULT_RAW = Object.freeze({
|
|
|
54
55
|
});
|
|
55
56
|
export const ENTER_RANGE = [0, 1] as const;
|
|
56
57
|
export const EXIT_RANGE = [1, 2] as const;
|
|
58
|
+
|
|
57
59
|
export const FULLSCREEN_DIMENSIONS = (
|
|
58
60
|
dimensions: ScaledSize,
|
|
59
61
|
): MeasuredDimensions => {
|
|
@@ -45,6 +45,7 @@ const unwrap = (
|
|
|
45
45
|
normalizedY: s.gesture.normalizedY.value,
|
|
46
46
|
isDismissing: s.gesture.isDismissing.value,
|
|
47
47
|
isDragging: s.gesture.isDragging.value,
|
|
48
|
+
direction: s.gesture.direction.value,
|
|
48
49
|
},
|
|
49
50
|
bounds: Bounds.getBounds(key) || NO_BOUNDS_MAP,
|
|
50
51
|
route: s.route,
|
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
import type { SharedValue } from "react-native-reanimated/lib/typescript/commonTypes";
|
|
19
19
|
import { useKeys } from "../../providers/keys";
|
|
20
20
|
import { Bounds } from "../../stores/bounds";
|
|
21
|
-
import { flattenStyle } from "../../utils/bounds/_utils/flatten-styles";
|
|
22
21
|
import { isBoundsEqual } from "../../utils/bounds/_utils/is-bounds-equal";
|
|
22
|
+
import { prepareStyleForBounds } from "../../utils/bounds/_utils/styles";
|
|
23
23
|
import useStableCallback from "../use-stable-callback";
|
|
24
|
+
import useStableCallbackValue from "../use-stable-callback-value";
|
|
24
25
|
|
|
25
26
|
interface BoundMeasurerHookProps {
|
|
26
27
|
sharedBoundTag?: string;
|
|
@@ -48,17 +49,18 @@ export const useBoundsRegistry = ({
|
|
|
48
49
|
onPress,
|
|
49
50
|
}: BoundMeasurerHookProps) => {
|
|
50
51
|
const { previous, current, next } = useKeys();
|
|
52
|
+
const preparedStyles = useMemo(() => prepareStyleForBounds(style), [style]);
|
|
51
53
|
|
|
52
54
|
const ROOT_MEASUREMENT_SIGNAL = useContext(MeasurementUpdateContext);
|
|
53
55
|
const ROOT_SIGNAL = useSharedValue(0);
|
|
54
56
|
const IS_ROOT = !ROOT_MEASUREMENT_SIGNAL;
|
|
55
57
|
|
|
56
|
-
const emitUpdate =
|
|
58
|
+
const emitUpdate = useStableCallbackValue(() => {
|
|
57
59
|
"worklet";
|
|
58
60
|
if (IS_ROOT) ROOT_SIGNAL.value = ROOT_SIGNAL.value + 1;
|
|
59
|
-
}
|
|
61
|
+
});
|
|
60
62
|
|
|
61
|
-
const maybeMeasureAndStore =
|
|
63
|
+
const maybeMeasureAndStore = useStableCallbackValue(
|
|
62
64
|
({ onPress, skipMarkingActive }: MaybeMeasureAndStoreParams) => {
|
|
63
65
|
"worklet";
|
|
64
66
|
// Currently, there's no necessity to measure when the current route is blurred ( could potentially change in the future )
|
|
@@ -86,18 +88,17 @@ export const useBoundsRegistry = ({
|
|
|
86
88
|
|
|
87
89
|
emitUpdate();
|
|
88
90
|
|
|
89
|
-
Bounds.setBounds(key, sharedBoundTag, measured,
|
|
91
|
+
Bounds.setBounds(key, sharedBoundTag, measured, preparedStyles);
|
|
90
92
|
if (!skipMarkingActive) {
|
|
91
93
|
Bounds.setRouteActive(key, sharedBoundTag);
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
if (onPress) runOnJS(onPress)();
|
|
95
97
|
},
|
|
96
|
-
[sharedBoundTag, animatedRef, current.route.key, style, emitUpdate, next],
|
|
97
98
|
);
|
|
98
99
|
|
|
99
100
|
const hasMeasuredOnLayout = useSharedValue(false);
|
|
100
|
-
const handleInitialLayout =
|
|
101
|
+
const handleInitialLayout = useStableCallbackValue(() => {
|
|
101
102
|
"worklet";
|
|
102
103
|
|
|
103
104
|
const prevKey = previous?.route.key;
|
|
@@ -113,12 +114,7 @@ export const useBoundsRegistry = ({
|
|
|
113
114
|
// Should not measure again while in transition
|
|
114
115
|
hasMeasuredOnLayout.value = true;
|
|
115
116
|
}
|
|
116
|
-
}
|
|
117
|
-
maybeMeasureAndStore,
|
|
118
|
-
sharedBoundTag,
|
|
119
|
-
previous?.route.key,
|
|
120
|
-
hasMeasuredOnLayout,
|
|
121
|
-
]);
|
|
117
|
+
});
|
|
122
118
|
|
|
123
119
|
const captureActiveOnPress = useStableCallback(() => {
|
|
124
120
|
if (!sharedBoundTag) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
2
|
import { useWindowDimensions } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
Gesture,
|
|
@@ -26,7 +26,7 @@ import { useKeys } from "../../providers/keys";
|
|
|
26
26
|
import { Animations } from "../../stores/animations";
|
|
27
27
|
import { Gestures } from "../../stores/gestures";
|
|
28
28
|
import { NavigatorDismissState } from "../../stores/navigator-dismiss-state";
|
|
29
|
-
import { GestureOffsetState } from "../../types/gesture";
|
|
29
|
+
import { type GestureDirection, GestureOffsetState } from "../../types/gesture";
|
|
30
30
|
import { startScreenTransition } from "../../utils/animation/start-screen-transition";
|
|
31
31
|
import { applyOffsetRules } from "../../utils/gesture/check-gesture-activation";
|
|
32
32
|
import { determineDismissal } from "../../utils/gesture/determine-dismissal";
|
|
@@ -34,6 +34,7 @@ import { mapGestureToProgress } from "../../utils/gesture/map-gesture-to-progres
|
|
|
34
34
|
import { resetGestureValues } from "../../utils/gesture/reset-gesture-values";
|
|
35
35
|
import { velocity } from "../../utils/gesture/velocity";
|
|
36
36
|
import useStableCallback from "../use-stable-callback";
|
|
37
|
+
import useStableCallbackValue from "../use-stable-callback-value";
|
|
37
38
|
|
|
38
39
|
interface BuildGesturesHookProps {
|
|
39
40
|
scrollConfig: SharedValue<ScrollConfig | null>;
|
|
@@ -98,17 +99,14 @@ export const useBuildGestures = ({
|
|
|
98
99
|
NavigatorDismissState.remove(key);
|
|
99
100
|
});
|
|
100
101
|
|
|
101
|
-
const onTouchesDown =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
},
|
|
108
|
-
[initialTouch, gestureOffsetState],
|
|
109
|
-
);
|
|
102
|
+
const onTouchesDown = useStableCallbackValue((e: GestureTouchEvent) => {
|
|
103
|
+
"worklet";
|
|
104
|
+
const firstTouch = e.changedTouches[0];
|
|
105
|
+
initialTouch.value = { x: firstTouch.x, y: firstTouch.y };
|
|
106
|
+
gestureOffsetState.value = GestureOffsetState.PENDING;
|
|
107
|
+
});
|
|
110
108
|
|
|
111
|
-
const onTouchesMove =
|
|
109
|
+
const onTouchesMove = useStableCallbackValue(
|
|
112
110
|
(e: GestureTouchEvent, manager: GestureStateManagerType) => {
|
|
113
111
|
"worklet";
|
|
114
112
|
|
|
@@ -151,18 +149,24 @@ export const useBuildGestures = ({
|
|
|
151
149
|
const scrollCfg = scrollConfig.value;
|
|
152
150
|
|
|
153
151
|
let shouldActivate = false;
|
|
152
|
+
let activatedDirection: GestureDirection | null = null;
|
|
153
|
+
|
|
154
154
|
if (recognizedDirection) {
|
|
155
155
|
if (directions.vertical && isSwipingDown) {
|
|
156
156
|
shouldActivate = scrollCfg ? scrollCfg.y <= 0 : true;
|
|
157
|
+
if (shouldActivate) activatedDirection = "vertical";
|
|
157
158
|
}
|
|
158
159
|
if (directions.horizontal && isSwipingRight) {
|
|
159
160
|
shouldActivate = scrollCfg ? scrollCfg.x <= 0 : true;
|
|
161
|
+
if (shouldActivate) activatedDirection = "horizontal";
|
|
160
162
|
}
|
|
161
163
|
if (directions.verticalInverted && isSwipingUp) {
|
|
162
164
|
shouldActivate = scrollCfg ? scrollCfg.y >= maxScrollY : true;
|
|
165
|
+
if (shouldActivate) activatedDirection = "vertical-inverted";
|
|
163
166
|
}
|
|
164
167
|
if (directions.horizontalInverted && isSwipingLeft) {
|
|
165
168
|
shouldActivate = scrollCfg ? scrollCfg.x >= maxScrollX : true;
|
|
169
|
+
if (shouldActivate) activatedDirection = "horizontal-inverted";
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
|
|
@@ -176,29 +180,20 @@ export const useBuildGestures = ({
|
|
|
176
180
|
gestureOffsetState.value === GestureOffsetState.PASSED &&
|
|
177
181
|
!gestures.isDismissing?.value
|
|
178
182
|
) {
|
|
183
|
+
gestures.direction.value = activatedDirection;
|
|
179
184
|
manager.activate();
|
|
180
185
|
return;
|
|
181
186
|
}
|
|
182
187
|
},
|
|
183
|
-
[
|
|
184
|
-
initialTouch,
|
|
185
|
-
scrollConfig,
|
|
186
|
-
gestures,
|
|
187
|
-
directions,
|
|
188
|
-
gestureOffsetState,
|
|
189
|
-
dimensions,
|
|
190
|
-
gestureActivationArea,
|
|
191
|
-
gestureResponseDistance,
|
|
192
|
-
],
|
|
193
188
|
);
|
|
194
189
|
|
|
195
|
-
const onStart =
|
|
190
|
+
const onStart = useStableCallbackValue(() => {
|
|
196
191
|
"worklet";
|
|
197
192
|
gestures.isDragging.value = 1;
|
|
198
193
|
gestures.isDismissing.value = 0;
|
|
199
|
-
}
|
|
194
|
+
});
|
|
200
195
|
|
|
201
|
-
const onUpdate =
|
|
196
|
+
const onUpdate = useStableCallbackValue(
|
|
202
197
|
(event: GestureUpdateEvent<PanGestureHandlerEventPayload>) => {
|
|
203
198
|
"worklet";
|
|
204
199
|
|
|
@@ -263,10 +258,9 @@ export const useBuildGestures = ({
|
|
|
263
258
|
animations.progress.value = 1 - gestureProgress;
|
|
264
259
|
}
|
|
265
260
|
},
|
|
266
|
-
[dimensions, gestures, animations, gestureDrivesProgress, directions],
|
|
267
261
|
);
|
|
268
262
|
|
|
269
|
-
const onEnd =
|
|
263
|
+
const onEnd = useStableCallbackValue(
|
|
270
264
|
(event: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
|
|
271
265
|
"worklet";
|
|
272
266
|
|
|
@@ -307,16 +301,6 @@ export const useBuildGestures = ({
|
|
|
307
301
|
initialVelocity,
|
|
308
302
|
});
|
|
309
303
|
},
|
|
310
|
-
[
|
|
311
|
-
dimensions,
|
|
312
|
-
animations,
|
|
313
|
-
transitionSpec,
|
|
314
|
-
setNavigatorDismissal,
|
|
315
|
-
handleDismiss,
|
|
316
|
-
gestures,
|
|
317
|
-
directions,
|
|
318
|
-
gestureVelocityImpact,
|
|
319
|
-
],
|
|
320
304
|
);
|
|
321
305
|
|
|
322
306
|
return useMemo(() => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit:
|
|
3
|
+
* https://github.com/MatiPl01/react-native-sortables/blob/main/packages/react-native-sortables/src/integrations/reanimated/hooks/useStableCallbackValue.ts
|
|
4
|
+
*/
|
|
5
|
+
import { useCallback, useEffect, useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
isWorkletFunction,
|
|
8
|
+
makeMutable,
|
|
9
|
+
runOnJS,
|
|
10
|
+
} from "react-native-reanimated";
|
|
11
|
+
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: <Does not matter>
|
|
13
|
+
type AnyFunction = (...args: Array<any>) => any;
|
|
14
|
+
|
|
15
|
+
function useMutableValue<T>(initialValue: T) {
|
|
16
|
+
return useState(() => makeMutable(initialValue))[0];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// We cannot store a function as a SharedValue because reanimated will treat
|
|
20
|
+
// it as an animation and will try to execute the animation when assigned
|
|
21
|
+
// to the SharedValue. Since we want the function to be treated as a value,
|
|
22
|
+
// we wrap it in an object and store the object in the SharedValue.
|
|
23
|
+
type WrappedCallback<C extends AnyFunction> = {
|
|
24
|
+
call: C;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const wrap = <C extends AnyFunction>(callback: C): WrappedCallback<C> => {
|
|
28
|
+
if (isWorkletFunction(callback)) {
|
|
29
|
+
return { call: callback };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
call: ((...args: Parameters<C>) => {
|
|
33
|
+
"worklet";
|
|
34
|
+
runOnJS(callback)(...args);
|
|
35
|
+
}) as C,
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Creates a stable worklet callback that can be called from the UI runtime
|
|
40
|
+
* @param callback The JavaScript or worklet function to be called
|
|
41
|
+
* @returns A worklet function that can be safely called from the UI thread
|
|
42
|
+
* @default Behavior:
|
|
43
|
+
* - If passed a regular JS function, calls it on the JS thread using runOnJS
|
|
44
|
+
* - If passed a worklet function, calls it directly on the UI thread
|
|
45
|
+
* @important The returned function maintains a stable reference and properly handles
|
|
46
|
+
* thread execution based on the input callback type
|
|
47
|
+
*/
|
|
48
|
+
export default function useStableCallbackValue<C extends AnyFunction>(
|
|
49
|
+
callback?: C,
|
|
50
|
+
) {
|
|
51
|
+
const stableCallback = useMutableValue<null | WrappedCallback<C>>(null);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (callback) {
|
|
55
|
+
stableCallback.value = wrap(callback);
|
|
56
|
+
} else {
|
|
57
|
+
stableCallback.value = null;
|
|
58
|
+
}
|
|
59
|
+
}, [callback, stableCallback]);
|
|
60
|
+
|
|
61
|
+
return useCallback(
|
|
62
|
+
(...args: Parameters<C>) => {
|
|
63
|
+
"worklet";
|
|
64
|
+
stableCallback.value?.call(...args);
|
|
65
|
+
},
|
|
66
|
+
[stableCallback],
|
|
67
|
+
);
|
|
68
|
+
}
|
package/src/stores/gestures.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { makeMutable, type SharedValue } from "react-native-reanimated";
|
|
2
|
+
import type { GestureDirection } from "../types/gesture";
|
|
2
3
|
import type { ScreenKey } from "../types/navigator";
|
|
3
4
|
|
|
4
5
|
export type GestureKey =
|
|
@@ -16,6 +17,7 @@ export type GestureMap = {
|
|
|
16
17
|
normalizedY: SharedValue<number>;
|
|
17
18
|
isDismissing: SharedValue<number>;
|
|
18
19
|
isDragging: SharedValue<number>;
|
|
20
|
+
direction: SharedValue<Omit<GestureDirection, "bidirectional"> | null>;
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
const store: Record<ScreenKey, GestureMap> = {};
|
|
@@ -30,6 +32,9 @@ function ensure(routeKey: ScreenKey): GestureMap {
|
|
|
30
32
|
normalizedY: makeMutable(0),
|
|
31
33
|
isDismissing: makeMutable(0),
|
|
32
34
|
isDragging: makeMutable(0),
|
|
35
|
+
direction: makeMutable<Omit<GestureDirection, "bidirectional"> | null>(
|
|
36
|
+
null,
|
|
37
|
+
),
|
|
33
38
|
};
|
|
34
39
|
store[routeKey] = bag;
|
|
35
40
|
}
|
package/src/types/animation.ts
CHANGED
|
@@ -18,10 +18,11 @@ export type ScreenTransitionState = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export interface ScreenInterpolationProps {
|
|
21
|
-
/** Values for the screen that
|
|
21
|
+
/** Values for the screen that came before the current one in the navigation stack. */
|
|
22
22
|
previous: ScreenTransitionState | undefined;
|
|
23
|
+
/** Values for the current screen being interpolated. */
|
|
23
24
|
current: ScreenTransitionState;
|
|
24
|
-
/** Values for the screen
|
|
25
|
+
/** Values for the screen that comes after the current one in the navigation stack. */
|
|
25
26
|
next: ScreenTransitionState | undefined;
|
|
26
27
|
/** Layout measurements for the screen. */
|
|
27
28
|
layouts: {
|
|
@@ -33,19 +34,19 @@ export interface ScreenInterpolationProps {
|
|
|
33
34
|
};
|
|
34
35
|
/** The safe area insets for the screen. */
|
|
35
36
|
insets: EdgeInsets;
|
|
36
|
-
/** The
|
|
37
|
+
/** The ID of the currently active shared bound (e.g., 'a' when Transition.Pressable has sharedBoundTag='a'). */
|
|
37
38
|
activeBoundId: string;
|
|
38
|
-
/** Whether the screen is focused. */
|
|
39
|
+
/** Whether the current screen is the focused (topmost) screen in the stack. */
|
|
39
40
|
focused: boolean;
|
|
40
|
-
/**
|
|
41
|
+
/** Combined progress of current and next screen transitions, ranging from 0-2. */
|
|
41
42
|
progress: number;
|
|
42
|
-
/**
|
|
43
|
+
/** Function that provides access to bounds builders for creating shared element transitions. */
|
|
43
44
|
bounds: BoundsAccessor;
|
|
44
|
-
/** The
|
|
45
|
+
/** The screen state that is currently driving the transition (either current or next, whichever is focused). */
|
|
45
46
|
active: ScreenTransitionState;
|
|
46
|
-
/** Whether the active screen is transitioning. */
|
|
47
|
+
/** Whether the active screen is currently transitioning (either being dragged or animating). */
|
|
47
48
|
isActiveTransitioning: boolean;
|
|
48
|
-
/** Whether the active screen is
|
|
49
|
+
/** Whether the active screen is in the process of being dismissed/closed. */
|
|
49
50
|
isDismissing: boolean;
|
|
50
51
|
}
|
|
51
52
|
|
package/src/types/gesture.ts
CHANGED
|
@@ -49,4 +49,8 @@ export type GestureValues = {
|
|
|
49
49
|
* A flag indicating if the screen is in the process of dismissing.
|
|
50
50
|
*/
|
|
51
51
|
isDismissing: number;
|
|
52
|
+
/**
|
|
53
|
+
* The initial direction that activated the gesture.
|
|
54
|
+
*/
|
|
55
|
+
direction: Omit<GestureDirection, "bidirectional"> | null;
|
|
52
56
|
};
|
package/src/types/utils.ts
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
import { isSharedValue } from "react-native-reanimated";
|
|
3
|
+
import type { Any } from "../../../types/utils";
|
|
4
|
+
|
|
5
|
+
type AnyStyle = ViewStyle | TextStyle | ImageStyle;
|
|
6
|
+
type StyleValue = StyleProp<AnyStyle>;
|
|
7
|
+
type PlainStyleObject = Record<string, Any>;
|
|
8
|
+
|
|
9
|
+
function mergeStyleArrays<T extends StyleValue>(style: T): T {
|
|
10
|
+
"worklet";
|
|
11
|
+
|
|
12
|
+
// Early returns for non-objects
|
|
13
|
+
if (style === null || style === undefined || typeof style !== "object") {
|
|
14
|
+
return style;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// If not an array, return as-is
|
|
18
|
+
if (!Array.isArray(style)) {
|
|
19
|
+
return style;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Merge array of styles into single object
|
|
23
|
+
const merged: PlainStyleObject = {};
|
|
24
|
+
for (let i = 0; i < style.length; i++) {
|
|
25
|
+
const currentStyle = mergeStyleArrays(style[i] as StyleValue);
|
|
26
|
+
if (currentStyle && typeof currentStyle === "object") {
|
|
27
|
+
Object.assign(merged, currentStyle);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return merged as T;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function stripNonSerializable<T>(value: T): T | undefined {
|
|
34
|
+
if (isSharedValue(value)) return value;
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return value.map(stripNonSerializable) as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (value && typeof value === "object") {
|
|
41
|
+
const cleaned: PlainStyleObject = {};
|
|
42
|
+
for (const key in value) {
|
|
43
|
+
if (key === "current") continue;
|
|
44
|
+
|
|
45
|
+
const cleanedValue = stripNonSerializable(value[key]);
|
|
46
|
+
if (cleanedValue !== undefined) {
|
|
47
|
+
cleaned[key] = cleanedValue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return cleaned as T;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof value === "function") {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function prepareStyleForBounds(
|
|
61
|
+
style: StyleValue | undefined,
|
|
62
|
+
): PlainStyleObject {
|
|
63
|
+
if (!style) return {};
|
|
64
|
+
|
|
65
|
+
const flattened = mergeStyleArrays(style);
|
|
66
|
+
const serializable = stripNonSerializable(flattened);
|
|
67
|
+
|
|
68
|
+
return serializable || {};
|
|
69
|
+
}
|
|
@@ -31,6 +31,7 @@ export const resetGestureValues = ({
|
|
|
31
31
|
const nx =
|
|
32
32
|
gestures.normalizedX.value ||
|
|
33
33
|
event.translationX / Math.max(1, dimensions.width);
|
|
34
|
+
|
|
34
35
|
const ny =
|
|
35
36
|
gestures.normalizedY.value ||
|
|
36
37
|
event.translationY / Math.max(1, dimensions.height);
|
|
@@ -38,11 +39,29 @@ export const resetGestureValues = ({
|
|
|
38
39
|
const vxTowardZero = velocity.calculateRestoreVelocity(nx, vxNorm);
|
|
39
40
|
const vyTowardZero = velocity.calculateRestoreVelocity(ny, vyNorm);
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
let remainingAnimations = 4;
|
|
43
|
+
|
|
44
|
+
const onFinish = (finished: boolean | undefined) => {
|
|
45
|
+
"worklet";
|
|
46
|
+
if (!finished) return;
|
|
47
|
+
remainingAnimations -= 1;
|
|
48
|
+
if (remainingAnimations === 0) {
|
|
49
|
+
gestures.direction.value = null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
43
52
|
|
|
44
|
-
gestures.
|
|
45
|
-
gestures.
|
|
53
|
+
gestures.x.value = animate(0, { ...spec, velocity: vxTowardZero }, onFinish);
|
|
54
|
+
gestures.y.value = animate(0, { ...spec, velocity: vyTowardZero }, onFinish);
|
|
55
|
+
gestures.normalizedX.value = animate(
|
|
56
|
+
0,
|
|
57
|
+
{ ...spec, velocity: vxTowardZero },
|
|
58
|
+
onFinish,
|
|
59
|
+
);
|
|
60
|
+
gestures.normalizedY.value = animate(
|
|
61
|
+
0,
|
|
62
|
+
{ ...spec, velocity: vyTowardZero },
|
|
63
|
+
onFinish,
|
|
64
|
+
);
|
|
46
65
|
gestures.isDragging.value = 0;
|
|
47
66
|
gestures.isDismissing.value = Number(shouldDismiss);
|
|
48
67
|
};
|
|
@@ -41,11 +41,10 @@ const normalize = (velocityPixelsPerSecond: number, screenSize: number) => {
|
|
|
41
41
|
const calculateRestoreVelocity = (
|
|
42
42
|
currentValueNormalized: number,
|
|
43
43
|
baseVelocityNormalized: number,
|
|
44
|
-
threshold: number = NEAR_ZERO_THRESHOLD,
|
|
45
44
|
) => {
|
|
46
45
|
"worklet";
|
|
47
46
|
|
|
48
|
-
if (Math.abs(currentValueNormalized) <
|
|
47
|
+
if (Math.abs(currentValueNormalized) < NEAR_ZERO_THRESHOLD) return 0;
|
|
49
48
|
|
|
50
49
|
const directionTowardZero = Math.sign(currentValueNormalized) || 1;
|
|
51
50
|
const clampedVelocity = Math.min(Math.abs(baseVelocityNormalized), 1);
|