react-native-screen-transitions 2.3.3 → 2.4.0
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 +34 -15
- package/lib/commonjs/__configs__/presets.js +10 -5
- package/lib/commonjs/__configs__/presets.js.map +1 -1
- package/lib/commonjs/components/controllers/screen-lifecycle.js +5 -8
- package/lib/commonjs/components/controllers/screen-lifecycle.js.map +1 -1
- package/lib/commonjs/components/create-transition-aware-component.js +2 -2
- package/lib/commonjs/components/create-transition-aware-component.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 +18 -14
- 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/module/__configs__/presets.js +10 -5
- package/lib/module/__configs__/presets.js.map +1 -1
- package/lib/module/components/controllers/screen-lifecycle.js +5 -8
- package/lib/module/components/controllers/screen-lifecycle.js.map +1 -1
- package/lib/module/components/create-transition-aware-component.js +2 -2
- package/lib/module/components/create-transition-aware-component.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 +19 -15
- 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/typescript/__configs__/presets.d.ts.map +1 -1
- package/lib/typescript/components/controllers/screen-lifecycle.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 +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/utils/bounds/_utils/styles.d.ts +7 -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/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/components/controllers/screen-lifecycle.tsx +5 -7
- package/src/components/create-transition-aware-component.tsx +2 -2
- package/src/constants.ts +2 -0
- package/src/hooks/animation/use-screen-animation.tsx +1 -0
- package/src/hooks/bounds/use-bound-registry.tsx +23 -23
- 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/utils/bounds/_utils/styles.ts +68 -0
- package/src/utils/gesture/reset-gesture-values.ts +22 -4
|
@@ -173,7 +173,6 @@ export const ElasticCard = (
|
|
|
173
173
|
/**
|
|
174
174
|
* Applies to both screens ( previous and incoming)
|
|
175
175
|
*/
|
|
176
|
-
|
|
177
176
|
const scale = interpolate(progress, [0, 1, 2], [0, 1, 0.8]);
|
|
178
177
|
|
|
179
178
|
// applies to current screen
|
|
@@ -367,16 +366,33 @@ export const SharedAppleMusic = (
|
|
|
367
366
|
|
|
368
367
|
const normX = active.gesture.normalizedX;
|
|
369
368
|
const normY = active.gesture.normalizedY;
|
|
369
|
+
const initialDirection = active.gesture.direction;
|
|
370
370
|
|
|
371
371
|
/**
|
|
372
372
|
* ===============================
|
|
373
373
|
* Animations for both bounds
|
|
374
374
|
* ===============================
|
|
375
375
|
*/
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
const
|
|
376
|
+
const xResistance = initialDirection === "horizontal" ? 0.7 : 0.4;
|
|
377
|
+
const yResistance = initialDirection === "vertical" ? 0.7 : 0.4;
|
|
378
|
+
|
|
379
|
+
const xScaleOuput = initialDirection === "horizontal" ? [1, 0.5] : [1, 1];
|
|
380
|
+
const yScaleOuput = initialDirection === "vertical" ? [1, 0.5] : [1, 1];
|
|
381
|
+
|
|
382
|
+
const dragX = interpolate(
|
|
383
|
+
normX,
|
|
384
|
+
[-1, 0, 1],
|
|
385
|
+
[-screen.width * xResistance, 0, screen.width * xResistance],
|
|
386
|
+
"clamp",
|
|
387
|
+
);
|
|
388
|
+
const dragY = interpolate(
|
|
389
|
+
normY,
|
|
390
|
+
[-1, 0, 1],
|
|
391
|
+
[-screen.height * yResistance, 0, screen.height * yResistance],
|
|
392
|
+
"clamp",
|
|
393
|
+
);
|
|
394
|
+
const dragXScale = interpolate(normX, [0, 1], xScaleOuput);
|
|
395
|
+
const dragYScale = interpolate(normY, [0, 1], yScaleOuput);
|
|
380
396
|
|
|
381
397
|
const boundValues = bounds({
|
|
382
398
|
method: focused ? "content" : "transform",
|
|
@@ -387,8 +403,8 @@ export const SharedAppleMusic = (
|
|
|
387
403
|
|
|
388
404
|
const opacity = interpolate(
|
|
389
405
|
progress,
|
|
390
|
-
[0, 0.
|
|
391
|
-
[0, 1, 1,
|
|
406
|
+
[0, 0.25, 1.25, 2],
|
|
407
|
+
[0, 1, 1, 0],
|
|
392
408
|
"clamp",
|
|
393
409
|
);
|
|
394
410
|
|
|
@@ -0,0 +1,121 @@
|
|
|
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 { determineDismissal } = await import(
|
|
11
|
+
"../utils/gesture/determine-dismissal"
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
describe("determineDismissal", () => {
|
|
15
|
+
const dimensions = { width: 320, height: 640 };
|
|
16
|
+
|
|
17
|
+
it("dismisses when horizontal translation exceeds the threshold", () => {
|
|
18
|
+
const { shouldDismiss } = determineDismissal({
|
|
19
|
+
event: {
|
|
20
|
+
translationX: 170,
|
|
21
|
+
translationY: 0,
|
|
22
|
+
velocityX: 0,
|
|
23
|
+
velocityY: 0,
|
|
24
|
+
},
|
|
25
|
+
directions: {
|
|
26
|
+
vertical: false,
|
|
27
|
+
verticalInverted: false,
|
|
28
|
+
horizontal: true,
|
|
29
|
+
horizontalInverted: false,
|
|
30
|
+
},
|
|
31
|
+
dimensions,
|
|
32
|
+
gestureVelocityImpact: 0.3,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(shouldDismiss).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("ignores movement in disallowed directions", () => {
|
|
39
|
+
const { shouldDismiss } = determineDismissal({
|
|
40
|
+
event: {
|
|
41
|
+
translationX: 200,
|
|
42
|
+
translationY: 0,
|
|
43
|
+
velocityX: 0,
|
|
44
|
+
velocityY: 0,
|
|
45
|
+
},
|
|
46
|
+
directions: {
|
|
47
|
+
vertical: true,
|
|
48
|
+
verticalInverted: false,
|
|
49
|
+
horizontal: false,
|
|
50
|
+
horizontalInverted: false,
|
|
51
|
+
},
|
|
52
|
+
dimensions,
|
|
53
|
+
gestureVelocityImpact: 0.3,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(shouldDismiss).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("dismisses vertical gestures when velocity pushes the projection past the threshold", () => {
|
|
60
|
+
const { shouldDismiss } = determineDismissal({
|
|
61
|
+
event: {
|
|
62
|
+
translationX: 0,
|
|
63
|
+
translationY: 40,
|
|
64
|
+
velocityX: 0,
|
|
65
|
+
velocityY: 1800,
|
|
66
|
+
},
|
|
67
|
+
directions: {
|
|
68
|
+
vertical: true,
|
|
69
|
+
verticalInverted: false,
|
|
70
|
+
horizontal: false,
|
|
71
|
+
horizontalInverted: false,
|
|
72
|
+
},
|
|
73
|
+
dimensions,
|
|
74
|
+
gestureVelocityImpact: 0.3,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(shouldDismiss).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("respects inverted horizontal directions", () => {
|
|
81
|
+
const { shouldDismiss } = determineDismissal({
|
|
82
|
+
event: {
|
|
83
|
+
translationX: -160,
|
|
84
|
+
translationY: 0,
|
|
85
|
+
velocityX: -700,
|
|
86
|
+
velocityY: 0,
|
|
87
|
+
},
|
|
88
|
+
directions: {
|
|
89
|
+
vertical: false,
|
|
90
|
+
verticalInverted: false,
|
|
91
|
+
horizontal: false,
|
|
92
|
+
horizontalInverted: true,
|
|
93
|
+
},
|
|
94
|
+
dimensions,
|
|
95
|
+
gestureVelocityImpact: 0.25,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(shouldDismiss).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns false when movement never exceeds the composite threshold", () => {
|
|
102
|
+
const { shouldDismiss } = determineDismissal({
|
|
103
|
+
event: {
|
|
104
|
+
translationX: 30,
|
|
105
|
+
translationY: 0,
|
|
106
|
+
velocityX: 100,
|
|
107
|
+
velocityY: 0,
|
|
108
|
+
},
|
|
109
|
+
directions: {
|
|
110
|
+
vertical: false,
|
|
111
|
+
verticalInverted: false,
|
|
112
|
+
horizontal: true,
|
|
113
|
+
horizontalInverted: false,
|
|
114
|
+
},
|
|
115
|
+
dimensions,
|
|
116
|
+
gestureVelocityImpact: 0.2,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(shouldDismiss).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -22,14 +22,12 @@ export const ScreenLifecycleController = ({
|
|
|
22
22
|
const key = current.navigation.getParent()?.getState().key;
|
|
23
23
|
const requestedDismissOnNavigator = NavigatorDismissState.get(key);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
25
|
+
const isEnabled = current.options.enableTransitions;
|
|
26
|
+
const isRequestedDismissOnNavigator = requestedDismissOnNavigator;
|
|
27
|
+
const isFirstScreen = current.navigation.getState().index === 0;
|
|
30
28
|
|
|
31
|
-
//
|
|
32
|
-
if (
|
|
29
|
+
// If transitions are disabled, or the dismissal was on the local root, or this is the first screen of the stack, reset the stores
|
|
30
|
+
if (!isEnabled || isRequestedDismissOnNavigator || isFirstScreen) {
|
|
33
31
|
resetStoresForScreen(current);
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
@@ -62,7 +62,7 @@ export function createTransitionAwareComponent<P extends object>(
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
const {
|
|
65
|
-
|
|
65
|
+
handleInitialLayout,
|
|
66
66
|
captureActiveOnPress,
|
|
67
67
|
MeasurementSyncProvider,
|
|
68
68
|
} = useBoundsRegistry({
|
|
@@ -79,7 +79,7 @@ export function createTransitionAwareComponent<P extends object>(
|
|
|
79
79
|
ref={animatedRef}
|
|
80
80
|
style={[style, associatedStyles]}
|
|
81
81
|
onPress={captureActiveOnPress}
|
|
82
|
-
onLayout={runOnUI(
|
|
82
|
+
onLayout={runOnUI(handleInitialLayout)}
|
|
83
83
|
collapsable={!sharedBoundTag}
|
|
84
84
|
>
|
|
85
85
|
{children}
|
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,18 +18,23 @@ 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;
|
|
27
28
|
animatedRef: AnimatedRef<View>;
|
|
28
|
-
|
|
29
29
|
style: StyleProps;
|
|
30
30
|
onPress?: ((...args: unknown[]) => void) | undefined;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
interface MaybeMeasureAndStoreParams {
|
|
34
|
+
onPress?: ((...args: unknown[]) => void) | undefined;
|
|
35
|
+
skipMarkingActive?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
interface MeasurementUpdateContextType {
|
|
34
39
|
updateSignal: SharedValue<number>;
|
|
35
40
|
}
|
|
@@ -43,28 +48,23 @@ export const useBoundsRegistry = ({
|
|
|
43
48
|
style,
|
|
44
49
|
onPress,
|
|
45
50
|
}: BoundMeasurerHookProps) => {
|
|
46
|
-
const { previous, current } = useKeys();
|
|
51
|
+
const { previous, current, next } = useKeys();
|
|
52
|
+
const preparedStyles = useMemo(() => prepareStyleForBounds(style), [style]);
|
|
47
53
|
|
|
48
54
|
const ROOT_MEASUREMENT_SIGNAL = useContext(MeasurementUpdateContext);
|
|
49
55
|
const ROOT_SIGNAL = useSharedValue(0);
|
|
50
56
|
const IS_ROOT = !ROOT_MEASUREMENT_SIGNAL;
|
|
51
|
-
const hasMeasured = useSharedValue(false);
|
|
52
57
|
|
|
53
|
-
const emitUpdate =
|
|
58
|
+
const emitUpdate = useStableCallbackValue(() => {
|
|
54
59
|
"worklet";
|
|
55
60
|
if (IS_ROOT) ROOT_SIGNAL.value = ROOT_SIGNAL.value + 1;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const maybeMeasureAndStore =
|
|
59
|
-
({
|
|
60
|
-
onPress,
|
|
61
|
-
skipMarkingActive,
|
|
62
|
-
}: {
|
|
63
|
-
onPress?: () => void;
|
|
64
|
-
skipMarkingActive?: boolean;
|
|
65
|
-
}) => {
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const maybeMeasureAndStore = useStableCallbackValue(
|
|
64
|
+
({ onPress, skipMarkingActive }: MaybeMeasureAndStoreParams) => {
|
|
66
65
|
"worklet";
|
|
67
|
-
|
|
66
|
+
// Currently, there's no necessity to measure when the current route is blurred ( could potentially change in the future )
|
|
67
|
+
if (!sharedBoundTag || next) return;
|
|
68
68
|
|
|
69
69
|
const measured = measure(animatedRef);
|
|
70
70
|
|
|
@@ -88,21 +88,21 @@ export const useBoundsRegistry = ({
|
|
|
88
88
|
|
|
89
89
|
emitUpdate();
|
|
90
90
|
|
|
91
|
-
Bounds.setBounds(key, sharedBoundTag, measured,
|
|
91
|
+
Bounds.setBounds(key, sharedBoundTag, measured, preparedStyles);
|
|
92
92
|
if (!skipMarkingActive) {
|
|
93
93
|
Bounds.setRouteActive(key, sharedBoundTag);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (onPress) runOnJS(onPress)();
|
|
97
97
|
},
|
|
98
|
-
[sharedBoundTag, animatedRef, current.route.key, style, emitUpdate],
|
|
99
98
|
);
|
|
100
99
|
|
|
101
|
-
const
|
|
100
|
+
const hasMeasuredOnLayout = useSharedValue(false);
|
|
101
|
+
const handleInitialLayout = useStableCallbackValue(() => {
|
|
102
102
|
"worklet";
|
|
103
103
|
|
|
104
104
|
const prevKey = previous?.route.key;
|
|
105
|
-
if (!sharedBoundTag ||
|
|
105
|
+
if (!sharedBoundTag || hasMeasuredOnLayout.value || !prevKey) {
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -112,9 +112,9 @@ export const useBoundsRegistry = ({
|
|
|
112
112
|
// Should skip mark active if we are in a transition
|
|
113
113
|
maybeMeasureAndStore({ skipMarkingActive: true });
|
|
114
114
|
// Should not measure again while in transition
|
|
115
|
-
|
|
115
|
+
hasMeasuredOnLayout.value = true;
|
|
116
116
|
}
|
|
117
|
-
}
|
|
117
|
+
});
|
|
118
118
|
|
|
119
119
|
const captureActiveOnPress = useStableCallback(() => {
|
|
120
120
|
if (!sharedBoundTag) {
|
|
@@ -152,7 +152,7 @@ export const useBoundsRegistry = ({
|
|
|
152
152
|
);
|
|
153
153
|
|
|
154
154
|
return {
|
|
155
|
-
|
|
155
|
+
handleInitialLayout,
|
|
156
156
|
captureActiveOnPress,
|
|
157
157
|
MeasurementSyncProvider,
|
|
158
158
|
};
|
|
@@ -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
|
+
}
|