react-native-screen-transitions 2.2.0 → 2.2.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.
Files changed (72) hide show
  1. package/lib/commonjs/components/bound-capture.js +7 -2
  2. package/lib/commonjs/components/bound-capture.js.map +1 -1
  3. package/lib/commonjs/components/controllers/screen-lifecycle.js +3 -9
  4. package/lib/commonjs/components/controllers/screen-lifecycle.js.map +1 -1
  5. package/lib/commonjs/components/create-transition-aware-component.js +2 -1
  6. package/lib/commonjs/components/create-transition-aware-component.js.map +1 -1
  7. package/lib/commonjs/components/root-transition-aware.js +3 -1
  8. package/lib/commonjs/components/root-transition-aware.js.map +1 -1
  9. package/lib/commonjs/hooks/animation/use-associated-style.js +41 -6
  10. package/lib/commonjs/hooks/animation/use-associated-style.js.map +1 -1
  11. package/lib/commonjs/hooks/bounds/use-bound-registry.js +8 -6
  12. package/lib/commonjs/hooks/bounds/use-bound-registry.js.map +1 -1
  13. package/lib/commonjs/providers/transition-styles.js +5 -1
  14. package/lib/commonjs/providers/transition-styles.js.map +1 -1
  15. package/lib/commonjs/stores/bounds/_utils.js +118 -0
  16. package/lib/commonjs/stores/bounds/_utils.js.map +1 -0
  17. package/lib/commonjs/stores/bounds/index.js +116 -0
  18. package/lib/commonjs/stores/bounds/index.js.map +1 -0
  19. package/lib/commonjs/stores/utils/reset-stores-for-screen.js +1 -4
  20. package/lib/commonjs/stores/utils/reset-stores-for-screen.js.map +1 -1
  21. package/lib/module/components/bound-capture.js +7 -2
  22. package/lib/module/components/bound-capture.js.map +1 -1
  23. package/lib/module/components/controllers/screen-lifecycle.js +3 -9
  24. package/lib/module/components/controllers/screen-lifecycle.js.map +1 -1
  25. package/lib/module/components/create-transition-aware-component.js +2 -1
  26. package/lib/module/components/create-transition-aware-component.js.map +1 -1
  27. package/lib/module/components/root-transition-aware.js +3 -1
  28. package/lib/module/components/root-transition-aware.js.map +1 -1
  29. package/lib/module/hooks/animation/use-associated-style.js +42 -7
  30. package/lib/module/hooks/animation/use-associated-style.js.map +1 -1
  31. package/lib/module/hooks/bounds/use-bound-registry.js +8 -6
  32. package/lib/module/hooks/bounds/use-bound-registry.js.map +1 -1
  33. package/lib/module/providers/transition-styles.js +5 -1
  34. package/lib/module/providers/transition-styles.js.map +1 -1
  35. package/lib/module/stores/bounds/_utils.js +113 -0
  36. package/lib/module/stores/bounds/_utils.js.map +1 -0
  37. package/lib/module/stores/bounds/index.js +112 -0
  38. package/lib/module/stores/bounds/index.js.map +1 -0
  39. package/lib/module/stores/utils/reset-stores-for-screen.js +1 -4
  40. package/lib/module/stores/utils/reset-stores-for-screen.js.map +1 -1
  41. package/lib/typescript/components/bound-capture.d.ts.map +1 -1
  42. package/lib/typescript/components/create-transition-aware-component.d.ts.map +1 -1
  43. package/lib/typescript/hooks/animation/use-associated-style.d.ts +4 -4
  44. package/lib/typescript/hooks/animation/use-associated-style.d.ts.map +1 -1
  45. package/lib/typescript/hooks/bounds/use-bound-registry.d.ts.map +1 -1
  46. package/lib/typescript/providers/transition-styles.d.ts +4 -1
  47. package/lib/typescript/providers/transition-styles.d.ts.map +1 -1
  48. package/lib/typescript/stores/bounds/_utils.d.ts +24 -0
  49. package/lib/typescript/stores/bounds/_utils.d.ts.map +1 -0
  50. package/lib/typescript/stores/{bounds.d.ts → bounds/index.d.ts} +3 -13
  51. package/lib/typescript/stores/bounds/index.d.ts.map +1 -0
  52. package/lib/typescript/stores/utils/reset-stores-for-screen.d.ts +1 -3
  53. package/lib/typescript/stores/utils/reset-stores-for-screen.d.ts.map +1 -1
  54. package/package.json +3 -3
  55. package/src/__tests__/bounds.store.test.ts +185 -0
  56. package/src/components/bound-capture.tsx +5 -2
  57. package/src/components/controllers/screen-lifecycle.tsx +3 -3
  58. package/src/components/create-transition-aware-component.tsx +1 -0
  59. package/src/components/root-transition-aware.tsx +1 -1
  60. package/src/hooks/animation/use-associated-style.tsx +42 -7
  61. package/src/hooks/bounds/use-bound-registry.tsx +9 -12
  62. package/src/providers/transition-styles.tsx +8 -2
  63. package/src/stores/bounds/_utils.ts +161 -0
  64. package/src/stores/bounds/index.ts +125 -0
  65. package/src/stores/utils/reset-stores-for-screen.ts +1 -7
  66. package/LICENSE +0 -21
  67. package/lib/commonjs/stores/bounds.js +0 -205
  68. package/lib/commonjs/stores/bounds.js.map +0 -1
  69. package/lib/module/stores/bounds.js +0 -201
  70. package/lib/module/stores/bounds.js.map +0 -1
  71. package/lib/typescript/stores/bounds.d.ts.map +0 -1
  72. package/src/stores/bounds.ts +0 -227
@@ -1,32 +1,22 @@
1
1
  import { type MeasuredDimensions, type StyleProps } from "react-native-reanimated";
2
- import type { ScreenTransitionState } from "../types/animation";
3
- import type { ScreenKey } from "../types/navigator";
2
+ import type { ScreenTransitionState } from "../../types/animation";
3
+ import type { ScreenKey } from "../../types/navigator";
4
4
  declare function setBounds(screenId: string, boundId: string, bounds?: MeasuredDimensions | null, styles?: StyleProps): void;
5
5
  declare function getBounds(screenId: string): Record<string, {
6
6
  bounds: MeasuredDimensions;
7
7
  styles: StyleProps;
8
8
  }>;
9
- declare function setActiveBoundId(boundId: string): void;
10
- declare function getActiveBoundId(): string | null;
11
9
  declare function setRouteActive(routeKey: string, boundId: string): void;
12
10
  declare function getRouteActive(routeKey: string): string;
13
- declare function setTransitionHint(fromKey: string, toKey: string, boundId: string): void;
14
- declare function getTransitionHint(fromKey: string, toKey: string): string | null;
15
11
  declare function clear(routeKey: ScreenKey): void;
16
- declare function clearActive(): void;
17
12
  declare function getActiveBound(current: ScreenTransitionState, next: ScreenTransitionState | undefined, previous: ScreenTransitionState | undefined): string;
18
13
  export declare const Bounds: {
19
14
  setBounds: typeof setBounds;
20
15
  getBounds: typeof getBounds;
21
- setActiveBoundId: typeof setActiveBoundId;
22
- getActiveBoundId: typeof getActiveBoundId;
23
16
  setRouteActive: typeof setRouteActive;
24
17
  getRouteActive: typeof getRouteActive;
25
- setTransitionHint: typeof setTransitionHint;
26
- getTransitionHint: typeof getTransitionHint;
27
18
  clear: typeof clear;
28
- clearActive: typeof clearActive;
29
19
  getActiveBound: typeof getActiveBound;
30
20
  };
31
21
  export {};
32
- //# sourceMappingURL=bounds.d.ts.map
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/stores/bounds/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,kBAAkB,EAEvB,KAAK,UAAU,EACf,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAavD,iBAAS,SAAS,CACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,kBAAkB,GAAG,IAAW,EACxC,MAAM,GAAE,UAAe,QAYvB;AAED,iBAAS,SAAS,CAAC,QAAQ,EAAE,MAAM;YAzBT,kBAAkB;YAAU,UAAU;GA4B/D;AAED,iBAAS,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAOxD;AAED,iBAAS,cAAc,CAAC,QAAQ,EAAE,MAAM,UAGvC;AAoBD,iBAAS,KAAK,CAAC,QAAQ,EAAE,SAAS,QAyBjC;AAED,iBAAS,cAAc,CACtB,OAAO,EAAE,qBAAqB,EAC9B,IAAI,EAAE,qBAAqB,GAAG,SAAS,EACvC,QAAQ,EAAE,qBAAqB,GAAG,SAAS,UAW3C;AAED,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC"}
@@ -2,7 +2,5 @@ import type { NativeStackDescriptor } from "../../types/navigator";
2
2
  /**
3
3
  * Reset all stores for a given screen
4
4
  */
5
- export declare const resetStoresForScreen: (current: NativeStackDescriptor, options?: {
6
- clearActive?: boolean;
7
- }) => void;
5
+ export declare const resetStoresForScreen: (current: NativeStackDescriptor) => void;
8
6
  //# sourceMappingURL=reset-stores-for-screen.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reset-stores-for-screen.d.ts","sourceRoot":"","sources":["../../../../src/stores/utils/reset-stores-for-screen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAKnE;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAChC,SAAS,qBAAqB,EAC9B,UAAS;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,SAQvC,CAAC"}
1
+ {"version":3,"file":"reset-stores-for-screen.d.ts","sourceRoot":"","sources":["../../../../src/stores/utils/reset-stores-for-screen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAKnE;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,qBAAqB,SAIlE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-screen-transitions",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Easy screen transitions for React Native and Expo",
5
5
  "author": "Ed",
6
6
  "license": "MIT",
@@ -49,7 +49,7 @@
49
49
  "@testing-library/react-native": "^13.2.0",
50
50
  "@types/react": "~19.0.10",
51
51
  "react-native-builder-bob": "0.39.0",
52
- "typescript": "~5.8.3"
52
+ "typescript": "catalog:"
53
53
  },
54
54
  "react-native-builder-bob": {
55
55
  "source": "src",
@@ -65,5 +65,5 @@
65
65
  ]
66
66
  ]
67
67
  },
68
- "gitHead": "ea83a2b1bc369610cde35850de314c43122ba60c"
68
+ "gitHead": "888d4df9936ec0e3b8221bea7cd81115e6301693"
69
69
  }
@@ -0,0 +1,185 @@
1
+ import { beforeEach, describe, expect, it } from "bun:test";
2
+ import { resolveActiveBound } from "../stores/bounds/_utils";
3
+ import type { ScreenTransitionState } from "../types/animation";
4
+
5
+ type Dim = {
6
+ x: number;
7
+ y: number;
8
+ pageX: number;
9
+ pageY: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+
14
+ const getDimensions = (x = 0, y = 0, w = 100, h = 100): Dim => ({
15
+ x,
16
+ y,
17
+ pageX: x,
18
+ pageY: y,
19
+ width: w,
20
+ height: h,
21
+ });
22
+
23
+ const mockState = (
24
+ routeKey: string,
25
+ ids: string[] = [],
26
+ ): ScreenTransitionState => {
27
+ const bounds: Record<
28
+ string,
29
+ { bounds: Dim; styles: Record<string, unknown> }
30
+ > = {};
31
+ ids.forEach((id, i) => {
32
+ bounds[id] = { bounds: getDimensions(i * 10, i * 10), styles: {} };
33
+ });
34
+ return {
35
+ progress: 1,
36
+ closing: 0,
37
+ animating: 1,
38
+ gesture: {
39
+ x: 0,
40
+ y: 0,
41
+ normalizedX: 0,
42
+ normalizedY: 0,
43
+ isDismissing: 0,
44
+ isDragging: 0,
45
+ },
46
+ bounds,
47
+ // @ts-expect-error partial route
48
+ route: { key: routeKey },
49
+ };
50
+ };
51
+
52
+ let cache: Record<string, string>;
53
+ let lastActiveByRoute: Record<string, string>;
54
+ beforeEach(() => {
55
+ cache = {};
56
+ lastActiveByRoute = {};
57
+ });
58
+
59
+ const getPairCache = (from: string, to: string) =>
60
+ cache[`${from}|${to}`] ?? null;
61
+ const setPairCache = (from: string, to: string, id: string) => {
62
+ cache[`${from}|${to}`] = id;
63
+ };
64
+ const getRouteActive = (routeKey: string) =>
65
+ lastActiveByRoute[routeKey] ?? null;
66
+
67
+ describe("Bounds.getActiveBound - requested id priority and acceptance", () => {
68
+ it("selects requested id when present only on current (opening)", () => {
69
+ const A = "A-1";
70
+ const B = "B-1";
71
+ const current = mockState(A, ["container"]);
72
+ const previous = mockState(B, ["icon"]);
73
+
74
+ // Opening: fromKey is previous.route.key (B)
75
+ lastActiveByRoute[B] = "container";
76
+ const active = resolveActiveBound({
77
+ current,
78
+ previous,
79
+ getPairCache,
80
+ setPairCache,
81
+ getRouteActive,
82
+ });
83
+
84
+ expect(active).toBe("container");
85
+ expect(getPairCache(B, A)).toBeNull();
86
+ });
87
+
88
+ it("selects requested id when present only on other (closing)", () => {
89
+ const A = "A-2";
90
+ const B = "B-2";
91
+ const current = mockState(A, ["icon"]);
92
+ const next = mockState(B, ["container"]);
93
+
94
+ // Closing: fromKey is current.route.key (A)
95
+ lastActiveByRoute[A] = "container";
96
+ const active = resolveActiveBound({
97
+ current,
98
+ next,
99
+ getPairCache,
100
+ setPairCache,
101
+ getRouteActive,
102
+ });
103
+ expect(active).toBe("container");
104
+ expect(getPairCache(A, B)).toBeNull();
105
+ });
106
+ });
107
+
108
+ describe("Bounds.getActiveBound - hint behavior (guarded writes)", () => {
109
+ it("writes cache only when both sides have the id", () => {
110
+ const A = "A-3";
111
+ const B = "B-3";
112
+ // Both have the same id measured
113
+ const current = mockState(A, ["container"]);
114
+ const previous = mockState(B, ["container"]);
115
+
116
+ // Opening: fromKey is previous.route.key (B)
117
+ lastActiveByRoute[B] = "container";
118
+ const active = resolveActiveBound({
119
+ current,
120
+ previous,
121
+ getPairCache,
122
+ setPairCache,
123
+ getRouteActive,
124
+ });
125
+ expect(active).toBe("container");
126
+ expect(getPairCache(B, A)).toBe("container");
127
+ });
128
+
129
+ it("requested id overrides existing cache and updates it when both sides measured", () => {
130
+ const A = "A-4";
131
+ const B = "B-4";
132
+ // Both sides have icon and container
133
+ // Pre-seed a conflicting cache
134
+ setPairCache(B, A, "icon");
135
+
136
+ const current = mockState(A, ["icon", "container"]);
137
+ const previous = mockState(B, ["icon", "container"]);
138
+
139
+ // Opening: fromKey is previous.route.key (B)
140
+ lastActiveByRoute[B] = "container";
141
+ const active = resolveActiveBound({
142
+ current,
143
+ previous,
144
+ getPairCache,
145
+ setPairCache,
146
+ getRouteActive,
147
+ });
148
+ expect(active).toBe("container");
149
+ expect(getPairCache(B, A)).toBe("container");
150
+ });
151
+ });
152
+
153
+ describe("Bounds.getActiveBound - set intersection and fallbacks", () => {
154
+ it("falls back to intersection when no request or cache", () => {
155
+ const A = "A-5";
156
+ const B = "B-5";
157
+ const current = mockState(A, ["alpha", "beta"]);
158
+ const previous = mockState(B, ["beta", "gamma"]);
159
+
160
+ const active = resolveActiveBound({
161
+ current,
162
+ previous,
163
+ getPairCache,
164
+ setPairCache,
165
+ getRouteActive,
166
+ });
167
+ expect(active).toBe("beta");
168
+ });
169
+
170
+ it("when other has a single bound, selects it (no request/cache)", () => {
171
+ const A = "A-6";
172
+ const B = "B-6";
173
+ const current = mockState(A, ["alpha"]);
174
+ const previous = mockState(B, ["only"]);
175
+
176
+ const active = resolveActiveBound({
177
+ current,
178
+ previous,
179
+ getPairCache,
180
+ setPairCache,
181
+ getRouteActive,
182
+ });
183
+ expect(active).toBe("only");
184
+ });
185
+ });
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
3
+ import { useKeys } from "../providers/keys";
3
4
  import { Bounds } from "../stores/bounds";
4
5
 
5
6
  interface BoundActivatorProps {
@@ -13,15 +14,17 @@ export const BoundCapture = ({
13
14
  children,
14
15
  measure,
15
16
  }: BoundActivatorProps) => {
17
+ const { current } = useKeys();
18
+ const routeKey = current.route.key;
16
19
  const tapGesture = useMemo(() => {
17
20
  return Gesture.Tap().onStart(() => {
18
21
  "worklet";
19
22
  if (sharedBoundTag) {
20
- Bounds.setActiveBoundId(sharedBoundTag);
23
+ Bounds.setRouteActive(routeKey, sharedBoundTag);
21
24
  measure();
22
25
  }
23
26
  });
24
- }, [sharedBoundTag, measure]);
27
+ }, [sharedBoundTag, measure, routeKey]);
25
28
 
26
29
  if (!sharedBoundTag) return children;
27
30
 
@@ -24,13 +24,13 @@ export const ScreenLifecycleController = ({
24
24
 
25
25
  // Don't run e.preventDefault when the dismissal was on the local root
26
26
  if (requestedDismissOnNavigator) {
27
- resetStoresForScreen(current, { clearActive: true });
27
+ resetStoresForScreen(current);
28
28
  return;
29
29
  }
30
30
 
31
31
  // Don't run e.preventDefault when this is the first screen of the stack
32
32
  if (current.navigation.getState().index === 0) {
33
- resetStoresForScreen(current, { clearActive: true });
33
+ resetStoresForScreen(current);
34
34
  return;
35
35
  }
36
36
 
@@ -41,7 +41,7 @@ export const ScreenLifecycleController = ({
41
41
 
42
42
  // we'll ensure the dispatch is complete before resetting stores
43
43
  requestAnimationFrame(() => {
44
- resetStoresForScreen(current, { clearActive: false });
44
+ resetStoresForScreen(current);
45
45
  });
46
46
  }
47
47
  };
@@ -68,6 +68,7 @@ export function createTransitionAwareComponent<P extends object>(
68
68
 
69
69
  const { associatedStyles } = useAssociatedStyles({
70
70
  id: sharedBoundTag || styleId,
71
+ style,
71
72
  });
72
73
 
73
74
  const { measureBounds, handleLayout } = useBoundsRegistry({
@@ -13,7 +13,7 @@ type Props = {
13
13
  const EMPTY_STYLE = Object.freeze({} as StyleProps);
14
14
 
15
15
  export const RootTransitionAware = memo(({ children }: Props) => {
16
- const stylesMap = useTransitionStyles();
16
+ const { stylesMap } = useTransitionStyles();
17
17
 
18
18
  const animatedContentStyle = useAnimatedStyle(() => {
19
19
  "worklet";
@@ -1,19 +1,37 @@
1
- import { useAnimatedStyle } from "react-native-reanimated";
1
+ import {
2
+ type StyleProps,
3
+ useAnimatedStyle,
4
+ useDerivedValue,
5
+ useSharedValue,
6
+ } from "react-native-reanimated";
2
7
  import { useTransitionStyles } from "../../providers/transition-styles";
3
8
 
4
9
  type Props = {
5
10
  id?: string;
11
+ style?: StyleProps;
6
12
  };
7
13
 
8
14
  const EMPTY_STYLE = Object.freeze({});
9
15
 
10
16
  /**
11
- * This hook is used to get the associated styles for a given styleId.
12
- * It is used to get the associated styles for a given styleId.
13
- * It is used to get the associated styles for a given styleId.
17
+ * This hook is used to get the associated styles for a given styleId / boundTag.
14
18
  */
15
- export const useAssociatedStyles = ({ id }: Props = {}) => {
16
- const stylesMap = useTransitionStyles();
19
+ export const useAssociatedStyles = ({ id, style }: Props = {}) => {
20
+ const { stylesMap } = useTransitionStyles();
21
+ const showAfterFirstFrame = useSharedValue(false);
22
+
23
+ useDerivedValue(() => {
24
+ "worklet";
25
+ if (!id) {
26
+ showAfterFirstFrame.value = true;
27
+ return;
28
+ }
29
+ if (!showAfterFirstFrame.value) {
30
+ requestAnimationFrame(() => {
31
+ showAfterFirstFrame.value = true;
32
+ });
33
+ }
34
+ });
17
35
 
18
36
  const associatedStyles = useAnimatedStyle(() => {
19
37
  "worklet";
@@ -21,8 +39,25 @@ export const useAssociatedStyles = ({ id }: Props = {}) => {
21
39
  if (!id || !stylesMap) {
22
40
  return EMPTY_STYLE;
23
41
  }
42
+ const base =
43
+ (stylesMap.value[id] as Record<string, unknown>) || EMPTY_STYLE;
44
+
45
+ let opacity = 1;
46
+
47
+ if ("opacity" in base) {
48
+ opacity = base.opacity as number;
49
+ }
50
+ if (style && "opacity" in style) {
51
+ opacity = style.opacity as number;
52
+ }
53
+
54
+ // Only force opacity to 0 during the initial frame; once ready,
55
+ // return base unchanged so we never override user-provided opacity.
56
+ if (!showAfterFirstFrame.value) {
57
+ return { ...base, opacity: 0 } as Record<string, unknown>;
58
+ }
24
59
 
25
- return stylesMap.value[id] || EMPTY_STYLE;
60
+ return { ...base, opacity };
26
61
  });
27
62
 
28
63
  return { associatedStyles };
@@ -10,7 +10,6 @@ import { useKeys } from "../../providers/keys";
10
10
  import { Bounds } from "../../stores/bounds";
11
11
  import { flattenStyle } from "../../utils/bounds/_utils/flatten-styles";
12
12
  import { isBoundsEqual } from "../../utils/bounds/_utils/is-bounds-equal";
13
- import { useScreenAnimation } from "../animation/use-screen-animation";
14
13
 
15
14
  interface BoundMeasurerHookProps {
16
15
  sharedBoundTag: string;
@@ -26,7 +25,7 @@ export const useBoundsRegistry = ({
26
25
  style,
27
26
  }: BoundMeasurerHookProps) => {
28
27
  const { previous } = useKeys();
29
- const interpolatorProps = useScreenAnimation();
28
+
30
29
  const isMeasured = useSharedValue(false);
31
30
 
32
31
  const measureBounds = useCallback(() => {
@@ -36,12 +35,16 @@ export const useBoundsRegistry = ({
36
35
  if (measured) {
37
36
  const key = current.route.key;
38
37
  if (isBoundsEqual({ measured, key, sharedBoundTag })) {
39
- Bounds.setRouteActive(key, sharedBoundTag);
38
+ if (Bounds.getRouteActive(key) === sharedBoundTag) {
39
+ Bounds.setRouteActive(key, sharedBoundTag);
40
+ }
40
41
  return;
41
42
  }
42
43
 
43
44
  Bounds.setBounds(key, sharedBoundTag, measured, flattenStyle(style));
44
- Bounds.setRouteActive(key, sharedBoundTag);
45
+ if (Bounds.getRouteActive(key) === sharedBoundTag) {
46
+ Bounds.setRouteActive(key, sharedBoundTag);
47
+ }
45
48
  }
46
49
  }, [sharedBoundTag, animatedRef, current.route.key, style]);
47
50
 
@@ -56,17 +59,11 @@ export const useBoundsRegistry = ({
56
59
  const previousBounds = Bounds.getBounds(previousRouteKey);
57
60
  const hasPreviousBoundForTag = previousBounds[sharedBoundTag];
58
61
 
59
- if (interpolatorProps.value.current.animating && hasPreviousBoundForTag) {
62
+ if (hasPreviousBoundForTag) {
60
63
  measureBounds();
61
64
  isMeasured.value = true;
62
65
  }
63
- }, [
64
- measureBounds,
65
- interpolatorProps,
66
- sharedBoundTag,
67
- previous?.route.key,
68
- isMeasured,
69
- ]);
66
+ }, [measureBounds, sharedBoundTag, previous?.route.key, isMeasured]);
70
67
 
71
68
  return {
72
69
  measureBounds,
@@ -10,7 +10,9 @@ type Props = {
10
10
  const EMPTY_MAP = Object.freeze({});
11
11
 
12
12
  const TransitionStylesContext = createContext<ReturnType<
13
- typeof useDerivedValue<TransitionInterpolatedStyle>
13
+ typeof useMemo<{
14
+ stylesMap: ReturnType<typeof useDerivedValue<TransitionInterpolatedStyle>>;
15
+ }>
14
16
  > | null>(null);
15
17
 
16
18
  export function TransitionStylesProvider({ children }: Props) {
@@ -32,7 +34,11 @@ export function TransitionStylesProvider({ children }: Props) {
32
34
  : EMPTY_MAP;
33
35
  });
34
36
 
35
- const value = useMemo(() => stylesMap, [stylesMap]);
37
+ const value = useMemo(() => {
38
+ return {
39
+ stylesMap,
40
+ };
41
+ }, [stylesMap]);
36
42
 
37
43
  return (
38
44
  <TransitionStylesContext.Provider value={value}>
@@ -0,0 +1,161 @@
1
+ import type { ScreenTransitionState } from "../../types/animation";
2
+
3
+ type GetCache = (fromKey: string, toKey: string) => string | null;
4
+ type SetCache = (fromKey: string, toKey: string, id: string) => void;
5
+ type GetRouteActive = (routeKey: string) => string | null;
6
+
7
+ interface ResolveActiveBoundParams {
8
+ current: ScreenTransitionState;
9
+ next?: ScreenTransitionState;
10
+ previous?: ScreenTransitionState;
11
+ getPairCache: GetCache;
12
+ setPairCache: SetCache;
13
+ getRouteActive: GetRouteActive;
14
+ }
15
+
16
+ export function pairKey(fromKey?: string, toKey?: string) {
17
+ "worklet";
18
+ return fromKey && toKey ? `${fromKey}|${toKey}` : "";
19
+ }
20
+
21
+ const hasBound = (s: ScreenTransitionState | undefined, id?: string | null) => {
22
+ "worklet";
23
+ return !!id && !!s && !!s.bounds && !!s.bounds[id];
24
+ };
25
+
26
+ const getRoutePair = (
27
+ current: ScreenTransitionState,
28
+ next: ScreenTransitionState | undefined,
29
+ previous: ScreenTransitionState | undefined,
30
+ ) => {
31
+ "worklet";
32
+ const isClosing = !!next;
33
+ const fromKey = isClosing ? current.route.key : previous?.route.key;
34
+ const toKey = isClosing ? next?.route.key : current.route.key;
35
+ const other = next ?? previous;
36
+ return { fromKey, toKey, other } as const;
37
+ };
38
+
39
+ const resolveFromPairCache = (
40
+ fromKey: string | undefined,
41
+ toKey: string | undefined,
42
+ other: ScreenTransitionState | undefined,
43
+ getPairCache: GetCache,
44
+ ) => {
45
+ "worklet";
46
+ if (fromKey && toKey) {
47
+ const cached = getPairCache(fromKey, toKey);
48
+ if (hasBound(other, cached)) return cached as string;
49
+ }
50
+ return "";
51
+ };
52
+
53
+ const resolveFromRequested = (
54
+ reqId: string | null,
55
+ current: ScreenTransitionState,
56
+ other: ScreenTransitionState | undefined,
57
+ ) => {
58
+ "worklet";
59
+ if (hasBound(other, reqId)) return reqId as string;
60
+ if (hasBound(current, reqId)) return reqId as string;
61
+ return "";
62
+ };
63
+
64
+ const resolveFromInteresection = (
65
+ current: ScreenTransitionState,
66
+ other: ScreenTransitionState | undefined,
67
+ fromKey: string | undefined,
68
+ getRouteActive: GetRouteActive,
69
+ ) => {
70
+ "worklet";
71
+ if (!other) return "";
72
+ const a = Object.keys(current.bounds);
73
+ const b = Object.keys(other.bounds);
74
+ const inter = a.filter((k) => b.includes(k));
75
+ const otherHasAny = b.length > 0;
76
+ const routeActive = fromKey ? getRouteActive(fromKey) : null;
77
+
78
+ if (inter.length > 0) {
79
+ if (routeActive && inter.includes(routeActive)) return routeActive;
80
+ return inter[0];
81
+ }
82
+
83
+ if (routeActive && hasBound(other, routeActive)) return routeActive;
84
+ if (b.length === 1) return b[0];
85
+
86
+ if (!otherHasAny) {
87
+ if (routeActive && hasBound(current, routeActive)) return routeActive;
88
+ if (a.length === 1) return a[0];
89
+ }
90
+
91
+ return "";
92
+ };
93
+
94
+ /**
95
+ * Util function to get the active bound id for a given transition state.
96
+ *
97
+ * It will check by ( priority from highest to lowest ):
98
+ * 1. Requested id
99
+ * 2. Cache
100
+ * 3. Intersection
101
+ */
102
+ export function resolveActiveBound({
103
+ current,
104
+ next,
105
+ previous,
106
+ getPairCache,
107
+ setPairCache,
108
+ getRouteActive,
109
+ }: ResolveActiveBoundParams) {
110
+ "worklet";
111
+ const { fromKey, toKey, other } = getRoutePair(current, next, previous);
112
+
113
+ // Resolve requested from per-route most recently used (last active bound by route)
114
+ const requestedId = fromKey ? getRouteActive(fromKey) : null;
115
+ const byRequested = resolveFromRequested(requestedId, current, other);
116
+ if (byRequested) {
117
+ if (
118
+ fromKey &&
119
+ toKey &&
120
+ hasBound(current, byRequested) &&
121
+ hasBound(other, byRequested)
122
+ ) {
123
+ setPairCache(fromKey, toKey, byRequested);
124
+ }
125
+ return byRequested;
126
+ }
127
+
128
+ const byPairCache = resolveFromPairCache(fromKey, toKey, other, getPairCache);
129
+ if (byPairCache) {
130
+ if (
131
+ fromKey &&
132
+ toKey &&
133
+ hasBound(current, byPairCache) &&
134
+ hasBound(other, byPairCache)
135
+ ) {
136
+ setPairCache(fromKey, toKey, byPairCache);
137
+ }
138
+ return byPairCache;
139
+ }
140
+
141
+ const byIntersection = resolveFromInteresection(
142
+ current,
143
+ other,
144
+ fromKey,
145
+ getRouteActive,
146
+ );
147
+
148
+ if (byIntersection) {
149
+ if (
150
+ fromKey &&
151
+ toKey &&
152
+ hasBound(current, byIntersection) &&
153
+ hasBound(other, byIntersection)
154
+ ) {
155
+ setPairCache(fromKey, toKey, byIntersection);
156
+ }
157
+ return byIntersection;
158
+ }
159
+
160
+ return "";
161
+ }