react-native-screen-transitions 3.4.0-alpha.4 → 3.4.0-alpha.6

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 (96) hide show
  1. package/README.md +968 -11
  2. package/lib/commonjs/shared/components/create-boundary-component/index.js +13 -0
  3. package/lib/commonjs/shared/components/create-boundary-component/index.js.map +1 -1
  4. package/lib/commonjs/shared/components/screen-container/deferred-visibility-host.js +3 -1
  5. package/lib/commonjs/shared/components/screen-container/deferred-visibility-host.js.map +1 -1
  6. package/lib/commonjs/shared/components/screen-container/hooks/use-backdrop-pointer-events.js.map +1 -1
  7. package/lib/commonjs/shared/components/screen-container/index.js +10 -2
  8. package/lib/commonjs/shared/components/screen-container/index.js.map +1 -1
  9. package/lib/commonjs/shared/components/screen-container/layers/backdrop.js +4 -6
  10. package/lib/commonjs/shared/components/screen-container/layers/backdrop.js.map +1 -1
  11. package/lib/commonjs/shared/components/screen-container/layers/content.js +3 -6
  12. package/lib/commonjs/shared/components/screen-container/layers/content.js.map +1 -1
  13. package/lib/commonjs/shared/constants.js +2 -0
  14. package/lib/commonjs/shared/constants.js.map +1 -1
  15. package/lib/commonjs/shared/providers/gestures/helpers/gesture-physics.js +3 -2
  16. package/lib/commonjs/shared/providers/gestures/helpers/gesture-physics.js.map +1 -1
  17. package/lib/commonjs/shared/providers/gestures/helpers/gesture-reset.js +7 -5
  18. package/lib/commonjs/shared/providers/gestures/helpers/gesture-reset.js.map +1 -1
  19. package/lib/commonjs/shared/providers/gestures/helpers/gesture-targets.js +9 -6
  20. package/lib/commonjs/shared/providers/gestures/helpers/gesture-targets.js.map +1 -1
  21. package/lib/commonjs/shared/providers/screen/animation/helpers/hydrate-transition-state.js +33 -1
  22. package/lib/commonjs/shared/providers/screen/animation/helpers/hydrate-transition-state.js.map +1 -1
  23. package/lib/commonjs/shared/providers/screen/animation/helpers/pipeline.js +2 -0
  24. package/lib/commonjs/shared/providers/screen/animation/helpers/pipeline.js.map +1 -1
  25. package/lib/commonjs/shared/providers/screen/animation/helpers/use-build-transition-state.js +1 -0
  26. package/lib/commonjs/shared/providers/screen/animation/helpers/use-build-transition-state.js.map +1 -1
  27. package/lib/commonjs/shared/utils/bounds/zoom/build.js +13 -8
  28. package/lib/commonjs/shared/utils/bounds/zoom/build.js.map +1 -1
  29. package/lib/module/shared/components/create-boundary-component/index.js +13 -0
  30. package/lib/module/shared/components/create-boundary-component/index.js.map +1 -1
  31. package/lib/module/shared/components/screen-container/deferred-visibility-host.js +3 -1
  32. package/lib/module/shared/components/screen-container/deferred-visibility-host.js.map +1 -1
  33. package/lib/module/shared/components/screen-container/hooks/use-backdrop-pointer-events.js.map +1 -1
  34. package/lib/module/shared/components/screen-container/index.js +10 -2
  35. package/lib/module/shared/components/screen-container/index.js.map +1 -1
  36. package/lib/module/shared/components/screen-container/layers/backdrop.js +4 -6
  37. package/lib/module/shared/components/screen-container/layers/backdrop.js.map +1 -1
  38. package/lib/module/shared/components/screen-container/layers/content.js +3 -6
  39. package/lib/module/shared/components/screen-container/layers/content.js.map +1 -1
  40. package/lib/module/shared/constants.js +2 -0
  41. package/lib/module/shared/constants.js.map +1 -1
  42. package/lib/module/shared/providers/gestures/helpers/gesture-physics.js +3 -2
  43. package/lib/module/shared/providers/gestures/helpers/gesture-physics.js.map +1 -1
  44. package/lib/module/shared/providers/gestures/helpers/gesture-reset.js +7 -5
  45. package/lib/module/shared/providers/gestures/helpers/gesture-reset.js.map +1 -1
  46. package/lib/module/shared/providers/gestures/helpers/gesture-targets.js +9 -6
  47. package/lib/module/shared/providers/gestures/helpers/gesture-targets.js.map +1 -1
  48. package/lib/module/shared/providers/screen/animation/helpers/hydrate-transition-state.js +32 -1
  49. package/lib/module/shared/providers/screen/animation/helpers/hydrate-transition-state.js.map +1 -1
  50. package/lib/module/shared/providers/screen/animation/helpers/pipeline.js +2 -0
  51. package/lib/module/shared/providers/screen/animation/helpers/pipeline.js.map +1 -1
  52. package/lib/module/shared/providers/screen/animation/helpers/use-build-transition-state.js +1 -0
  53. package/lib/module/shared/providers/screen/animation/helpers/use-build-transition-state.js.map +1 -1
  54. package/lib/module/shared/utils/bounds/zoom/build.js +13 -8
  55. package/lib/module/shared/utils/bounds/zoom/build.js.map +1 -1
  56. package/lib/typescript/shared/components/create-boundary-component/index.d.ts.map +1 -1
  57. package/lib/typescript/shared/components/screen-container/deferred-visibility-host.d.ts +2 -1
  58. package/lib/typescript/shared/components/screen-container/deferred-visibility-host.d.ts.map +1 -1
  59. package/lib/typescript/shared/components/screen-container/hooks/use-backdrop-pointer-events.d.ts +1 -1
  60. package/lib/typescript/shared/components/screen-container/hooks/use-backdrop-pointer-events.d.ts.map +1 -1
  61. package/lib/typescript/shared/components/screen-container/index.d.ts.map +1 -1
  62. package/lib/typescript/shared/components/screen-container/layers/backdrop.d.ts +5 -1
  63. package/lib/typescript/shared/components/screen-container/layers/backdrop.d.ts.map +1 -1
  64. package/lib/typescript/shared/components/screen-container/layers/content.d.ts +3 -1
  65. package/lib/typescript/shared/components/screen-container/layers/content.d.ts.map +1 -1
  66. package/lib/typescript/shared/constants.d.ts.map +1 -1
  67. package/lib/typescript/shared/providers/gestures/helpers/gesture-physics.d.ts.map +1 -1
  68. package/lib/typescript/shared/providers/gestures/helpers/gesture-reset.d.ts.map +1 -1
  69. package/lib/typescript/shared/providers/gestures/helpers/gesture-targets.d.ts.map +1 -1
  70. package/lib/typescript/shared/providers/screen/animation/helpers/hydrate-transition-state.d.ts +16 -0
  71. package/lib/typescript/shared/providers/screen/animation/helpers/hydrate-transition-state.d.ts.map +1 -1
  72. package/lib/typescript/shared/providers/screen/animation/helpers/pipeline.d.ts.map +1 -1
  73. package/lib/typescript/shared/providers/screen/animation/helpers/use-build-transition-state.d.ts +1 -0
  74. package/lib/typescript/shared/providers/screen/animation/helpers/use-build-transition-state.d.ts.map +1 -1
  75. package/lib/typescript/shared/types/animation.types.d.ts +13 -0
  76. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  77. package/lib/typescript/shared/types/screen.types.d.ts +2 -1
  78. package/lib/typescript/shared/types/screen.types.d.ts.map +1 -1
  79. package/lib/typescript/shared/utils/bounds/zoom/build.d.ts.map +1 -1
  80. package/package.json +1 -1
  81. package/src/shared/components/create-boundary-component/index.tsx +13 -0
  82. package/src/shared/components/screen-container/deferred-visibility-host.tsx +19 -12
  83. package/src/shared/components/screen-container/hooks/use-backdrop-pointer-events.ts +1 -2
  84. package/src/shared/components/screen-container/index.tsx +13 -4
  85. package/src/shared/components/screen-container/layers/backdrop.tsx +9 -3
  86. package/src/shared/components/screen-container/layers/content.tsx +46 -42
  87. package/src/shared/constants.ts +2 -0
  88. package/src/shared/providers/gestures/helpers/gesture-physics.ts +4 -2
  89. package/src/shared/providers/gestures/helpers/gesture-reset.ts +9 -5
  90. package/src/shared/providers/gestures/helpers/gesture-targets.ts +14 -6
  91. package/src/shared/providers/screen/animation/helpers/hydrate-transition-state.ts +49 -1
  92. package/src/shared/providers/screen/animation/helpers/pipeline.ts +2 -0
  93. package/src/shared/providers/screen/animation/helpers/use-build-transition-state.ts +2 -0
  94. package/src/shared/types/animation.types.ts +15 -0
  95. package/src/shared/types/screen.types.ts +3 -1
  96. package/src/shared/utils/bounds/zoom/build.ts +33 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-screen-transitions",
3
- "version": "3.4.0-alpha.4",
3
+ "version": "3.4.0-alpha.6",
4
4
  "description": "Easy screen transitions for React Native and Expo",
5
5
  "author": "Ed",
6
6
  "license": "MIT",
@@ -17,6 +17,7 @@ import { useAutoSourceMeasurement } from "./hooks/use-auto-source-measurement";
17
17
  import { useBoundaryMeasureAndStore } from "./hooks/use-boundary-measure-and-store";
18
18
  import { useBoundaryPresence } from "./hooks/use-boundary-presence";
19
19
  import { useGroupActiveMeasurement } from "./hooks/use-group-active-measurement";
20
+ import { useGroupActiveSourceMeasurement } from "./hooks/use-group-active-source-measurement";
20
21
  import { useInitialLayoutHandler } from "./hooks/use-initial-layout-handler";
21
22
  import { usePendingDestinationMeasurement } from "./hooks/use-pending-destination-measurement";
22
23
  import { usePendingDestinationRetryMeasurement } from "./hooks/use-pending-destination-retry-measurement";
@@ -172,6 +173,18 @@ export function createBoundaryComponent<P extends object>(
172
173
  maybeMeasureAndStore,
173
174
  });
174
175
 
176
+ // Source-side grouped retargeting: when an unfocused/source boundary
177
+ // becomes the active member, refresh its snapshot and source link so
178
+ // close transitions do not use stale pre-scroll geometry.
179
+ useGroupActiveSourceMeasurement({
180
+ enabled: runtimeEnabled,
181
+ group,
182
+ id,
183
+ hasNextScreen,
184
+ isAnimating,
185
+ maybeMeasureAndStore,
186
+ });
187
+
175
188
  // While idle on source screens, re-measure after scroll settles so a later
176
189
  // close transition starts from up-to-date source geometry.
177
190
  useScrollSettledMeasurement({
@@ -6,6 +6,7 @@ import { useScreenStyles } from "../../providers/screen/styles.provider";
6
6
 
7
7
  type Props = {
8
8
  children: React.ReactNode;
9
+ pointerEvents: "box-none" | undefined;
9
10
  };
10
11
 
11
12
  /**
@@ -15,20 +16,26 @@ type Props = {
15
16
  * This sits above backdrop/content/mask/surface so a deferred transition does
16
17
  * not leak raw first-paint UI from nested layers.
17
18
  */
18
- export const DeferredVisibilityHost = memo(({ children }: Props) => {
19
- const { resolutionMode } = useScreenStyles();
19
+ export const DeferredVisibilityHost = memo(
20
+ ({ children, pointerEvents }: Props) => {
21
+ const { resolutionMode } = useScreenStyles();
20
22
 
21
- const animatedStyle = useAnimatedStyle(() => {
22
- "worklet";
23
- return resolutionMode.value === "deferred" ? HIDDEN_STYLE : VISIBLE_STYLE;
24
- });
23
+ const animatedStyle = useAnimatedStyle(() => {
24
+ "worklet";
25
+ return resolutionMode.value === "deferred" ? HIDDEN_STYLE : VISIBLE_STYLE;
26
+ });
25
27
 
26
- return (
27
- <Animated.View collapsable={false} style={[styles.host, animatedStyle]}>
28
- {children}
29
- </Animated.View>
30
- );
31
- });
28
+ return (
29
+ <Animated.View
30
+ collapsable={false}
31
+ style={[styles.host, animatedStyle]}
32
+ pointerEvents={pointerEvents}
33
+ >
34
+ {children}
35
+ </Animated.View>
36
+ );
37
+ },
38
+ );
32
39
 
33
40
  const styles = StyleSheet.create({
34
41
  host: {
@@ -1,9 +1,8 @@
1
1
  import { useDescriptors } from "../../../providers/screen/descriptors";
2
2
  import { useStackCoreContext } from "../../../providers/stack/core.provider";
3
+ import type { BackdropBehavior } from "../../../types/screen.types";
3
4
  import { StackType } from "../../../types/stack.types";
4
5
 
5
- type BackdropBehavior = "block" | "passthrough" | "dismiss" | "collapse";
6
-
7
6
  interface BackdropPointerEventsResult {
8
7
  pointerEvents: "box-none" | undefined;
9
8
  backdropBehavior: BackdropBehavior;
@@ -10,13 +10,22 @@ type Props = {
10
10
  };
11
11
 
12
12
  export const ScreenContainer = memo(({ children }: Props) => {
13
- const { pointerEvents } = useBackdropPointerEvents();
13
+ const { pointerEvents, isBackdropActive, backdropBehavior } =
14
+ useBackdropPointerEvents();
14
15
 
15
16
  return (
16
17
  <View style={styles.container} pointerEvents={pointerEvents}>
17
- <DeferredVisibilityHost>
18
- <BackdropLayer />
19
- <ContentLayer>{children}</ContentLayer>
18
+ <DeferredVisibilityHost pointerEvents={pointerEvents}>
19
+ <BackdropLayer
20
+ isBackdropActive={isBackdropActive}
21
+ backdropBehavior={backdropBehavior}
22
+ />
23
+ <ContentLayer
24
+ pointerEvents={pointerEvents}
25
+ isBackdropActive={isBackdropActive}
26
+ >
27
+ {children}
28
+ </ContentLayer>
20
29
  </DeferredVisibilityHost>
21
30
  </View>
22
31
  );
@@ -13,15 +13,20 @@ import { useScreenStyles } from "../../../providers/screen/styles.provider";
13
13
  import { AnimationStore } from "../../../stores/animation.store";
14
14
  import { GestureStore } from "../../../stores/gesture.store";
15
15
  import { SystemStore } from "../../../stores/system.store";
16
+ import type { BackdropBehavior } from "../../../types/screen.types";
16
17
  import { animateToProgress } from "../../../utils/animation/animate-to-progress";
17
18
  import { findCollapseTarget } from "../../../utils/gesture/find-collapse-target";
18
- import { useBackdropPointerEvents } from "../hooks/use-backdrop-pointer-events";
19
19
 
20
- export const BackdropLayer = memo(function BackdropLayer() {
20
+ export const BackdropLayer = memo(function BackdropLayer({
21
+ backdropBehavior,
22
+ isBackdropActive,
23
+ }: {
24
+ backdropBehavior: BackdropBehavior;
25
+ isBackdropActive: boolean;
26
+ }) {
21
27
  const { stylesMap } = useScreenStyles();
22
28
  const { current } = useDescriptors();
23
29
  const { dismissScreen } = useNavigationHelpers();
24
- const { isBackdropActive, backdropBehavior } = useBackdropPointerEvents();
25
30
 
26
31
  const BackdropComponent = current.options.backdropComponent;
27
32
  const routeKey = current.route.key;
@@ -39,6 +44,7 @@ export const BackdropLayer = memo(function BackdropLayer() {
39
44
  : null,
40
45
  [BackdropComponent],
41
46
  );
47
+
42
48
  const handleBackdropPress = useCallback(() => {
43
49
  if (backdropBehavior === "dismiss") {
44
50
  dismissScreen();
@@ -11,64 +11,68 @@ import { useGestureContext } from "../../../providers/gestures";
11
11
  import { useDescriptors } from "../../../providers/screen/descriptors";
12
12
  import { useScreenStyles } from "../../../providers/screen/styles.provider";
13
13
  import { resolveNavigationMaskEnabled } from "../../../utils/resolve-screen-transition-options";
14
- import { useBackdropPointerEvents } from "../hooks/use-backdrop-pointer-events";
15
14
  import { useContentLayout } from "../hooks/use-content-layout";
16
15
  import { MaybeMaskedNavigationContainer } from "./maybe-masked-navigation-container";
17
16
  import { SurfaceContainer } from "./surface-container";
18
17
 
19
18
  type Props = {
20
19
  children: React.ReactNode;
20
+ pointerEvents: "box-none" | undefined;
21
+ isBackdropActive: boolean;
21
22
  };
22
23
 
23
- export const ContentLayer = memo(({ children }: Props) => {
24
- const { stylesMap } = useScreenStyles();
25
- const { current } = useDescriptors();
26
- const { pointerEvents, isBackdropActive } = useBackdropPointerEvents();
24
+ export const ContentLayer = memo(
25
+ ({ children, pointerEvents, isBackdropActive }: Props) => {
26
+ const { stylesMap } = useScreenStyles();
27
+ const { current } = useDescriptors();
27
28
 
28
- const gestureContext = useGestureContext();
29
- const isNavigationMaskEnabled = resolveNavigationMaskEnabled(current.options);
30
- const contentPointerEvents = isBackdropActive ? "box-none" : pointerEvents;
29
+ const gestureContext = useGestureContext();
30
+ const isNavigationMaskEnabled = resolveNavigationMaskEnabled(
31
+ current.options,
32
+ );
33
+ const contentPointerEvents = isBackdropActive ? "box-none" : pointerEvents;
31
34
 
32
- const hasAutoSnapPoint =
33
- current.options.snapPoints?.includes("auto") ?? false;
35
+ const hasAutoSnapPoint =
36
+ current.options.snapPoints?.includes("auto") ?? false;
34
37
 
35
- const handleContentLayout = useContentLayout();
38
+ const handleContentLayout = useContentLayout();
36
39
 
37
- const animatedContentStyle = useAnimatedStyle(() => {
38
- "worklet";
39
- return stylesMap.value.content?.style || NO_STYLES;
40
- });
40
+ const animatedContentStyle = useAnimatedStyle(() => {
41
+ "worklet";
42
+ return stylesMap.value.content?.style || NO_STYLES;
43
+ });
41
44
 
42
- const animatedContentProps = useAnimatedProps(() => {
43
- "worklet";
44
- return stylesMap.value.content?.props ?? NO_PROPS;
45
- });
45
+ const animatedContentProps = useAnimatedProps(() => {
46
+ "worklet";
47
+ return stylesMap.value.content?.props ?? NO_PROPS;
48
+ });
46
49
 
47
- return (
48
- <GestureDetector gesture={gestureContext!.panGesture}>
49
- <Animated.View
50
- style={[styles.content, animatedContentStyle]}
51
- animatedProps={animatedContentProps}
52
- pointerEvents={contentPointerEvents}
53
- >
54
- <MaybeMaskedNavigationContainer
50
+ return (
51
+ <GestureDetector gesture={gestureContext!.panGesture}>
52
+ <Animated.View
53
+ style={[styles.content, animatedContentStyle]}
54
+ animatedProps={animatedContentProps}
55
55
  pointerEvents={contentPointerEvents}
56
- enabled={isNavigationMaskEnabled}
57
56
  >
58
- <SurfaceContainer pointerEvents={contentPointerEvents}>
59
- {hasAutoSnapPoint ? (
60
- <View collapsable={false} onLayout={handleContentLayout}>
61
- {children}
62
- </View>
63
- ) : (
64
- children
65
- )}
66
- </SurfaceContainer>
67
- </MaybeMaskedNavigationContainer>
68
- </Animated.View>
69
- </GestureDetector>
70
- );
71
- });
57
+ <MaybeMaskedNavigationContainer
58
+ pointerEvents={contentPointerEvents}
59
+ enabled={isNavigationMaskEnabled}
60
+ >
61
+ <SurfaceContainer pointerEvents={contentPointerEvents}>
62
+ {hasAutoSnapPoint ? (
63
+ <View collapsable={false} onLayout={handleContentLayout}>
64
+ {children}
65
+ </View>
66
+ ) : (
67
+ children
68
+ )}
69
+ </SurfaceContainer>
70
+ </MaybeMaskedNavigationContainer>
71
+ </Animated.View>
72
+ </GestureDetector>
73
+ );
74
+ },
75
+ );
72
76
 
73
77
  const styles = StyleSheet.create({
74
78
  content: {
@@ -57,6 +57,7 @@ export const createScreenTransitionState = (
57
57
  closing: 0,
58
58
  animating: 0,
59
59
  settled: 1,
60
+ logicallySettled: 1,
60
61
  entering: 0,
61
62
  gesture: { ...DEFAULT_GESTURE_VALUES },
62
63
  route,
@@ -79,6 +80,7 @@ export const DEFAULT_SCREEN_TRANSITION_STATE: ScreenTransitionState =
79
80
  closing: 0,
80
81
  animating: 0,
81
82
  settled: 1,
83
+ logicallySettled: 1,
82
84
  entering: 0,
83
85
  gesture: DEFAULT_GESTURE_VALUES,
84
86
  route: {} as RouteProp<ParamListBase>,
@@ -31,10 +31,12 @@ type GestureAxisCandidate = {
31
31
  export const normalizeVelocity = (
32
32
  velocityPixelsPerSecond: number,
33
33
  screenSize: number,
34
- maxMagnitude: number = DEFAULT_GESTURE_RELEASE_VELOCITY_MAX,
34
+ maxMagnitude?: number,
35
35
  ) => {
36
36
  "worklet";
37
- const max = Math.max(0, Math.abs(maxMagnitude));
37
+ const resolvedMaxMagnitude =
38
+ maxMagnitude ?? DEFAULT_GESTURE_RELEASE_VELOCITY_MAX;
39
+ const max = Math.max(0, Math.abs(resolvedMaxMagnitude));
38
40
  return clamp(velocityPixelsPerSecond / Math.max(1, screenSize), -max, max);
39
41
  };
40
42
 
@@ -32,14 +32,18 @@ export const resetGestureValues = ({
32
32
  shouldDismiss,
33
33
  event,
34
34
  dimensions,
35
- gestureReleaseVelocityScale = DEFAULT_GESTURE_RELEASE_VELOCITY_SCALE,
36
- gestureReleaseVelocityMax = DEFAULT_GESTURE_RELEASE_VELOCITY_MAX,
35
+ gestureReleaseVelocityScale,
36
+ gestureReleaseVelocityMax,
37
37
  }: ResetGestureValuesProps) => {
38
38
  "worklet";
39
+ const resolvedGestureReleaseVelocityScale =
40
+ gestureReleaseVelocityScale ?? DEFAULT_GESTURE_RELEASE_VELOCITY_SCALE;
41
+ const resolvedGestureReleaseVelocityMax =
42
+ gestureReleaseVelocityMax ?? DEFAULT_GESTURE_RELEASE_VELOCITY_MAX;
39
43
 
40
44
  const effectiveReleaseVelocityMax = Math.max(
41
45
  0,
42
- Math.abs(gestureReleaseVelocityMax),
46
+ Math.abs(resolvedGestureReleaseVelocityMax),
43
47
  );
44
48
 
45
49
  const vxNorm = normalizeVelocity(
@@ -67,11 +71,11 @@ export const resetGestureValues = ({
67
71
  // so the spring carries the gesture's momentum. The spec controls the
68
72
  // spring character — use an underdamped spec (e.g. FlingSpec) for orbit/fling.
69
73
  const resetVX = shouldDismiss
70
- ? vxNorm * gestureReleaseVelocityScale
74
+ ? vxNorm * resolvedGestureReleaseVelocityScale
71
75
  : vxTowardZero;
72
76
 
73
77
  const resetVY = shouldDismiss
74
- ? vyNorm * gestureReleaseVelocityScale
78
+ ? vyNorm * resolvedGestureReleaseVelocityScale
75
79
  : vyTowardZero;
76
80
 
77
81
  animateMany({
@@ -111,22 +111,30 @@ export function determineSnapTarget({
111
111
  snapPoints,
112
112
  velocity,
113
113
  dimension,
114
- velocityFactor = DEFAULT_GESTURE_SNAP_VELOCITY_IMPACT,
115
- canDismiss = true,
114
+ velocityFactor,
115
+ canDismiss,
116
116
  }: DetermineSnapTargetProps): DetermineSnapTargetResult {
117
117
  "worklet";
118
+ const resolvedVelocityFactor =
119
+ velocityFactor ?? DEFAULT_GESTURE_SNAP_VELOCITY_IMPACT;
120
+ const resolvedCanDismiss = canDismiss ?? true;
118
121
 
119
122
  // Convert velocity to progress units (positive = toward dismiss = decreasing progress)
120
- const velocityInProgress = (velocity / dimension) * velocityFactor;
123
+ const velocityInProgress = (velocity / dimension) * resolvedVelocityFactor;
121
124
 
122
125
  // Project where we'd end up with velocity
123
126
  const projectedProgress = currentProgress - velocityInProgress;
124
127
 
125
- const sanitizedSnapPoints = sanitizeSnapPoints(snapPoints, canDismiss);
128
+ const sanitizedSnapPoints = sanitizeSnapPoints(
129
+ snapPoints,
130
+ resolvedCanDismiss,
131
+ );
126
132
 
127
133
  // Build all possible targets: dismiss (0) only if allowed, plus all snap points
128
134
  const allTargets = Array.from(
129
- new Set(canDismiss ? [0, ...sanitizedSnapPoints] : sanitizedSnapPoints),
135
+ new Set(
136
+ resolvedCanDismiss ? [0, ...sanitizedSnapPoints] : sanitizedSnapPoints,
137
+ ),
130
138
  ).sort((a, b) => a - b);
131
139
 
132
140
  if (allTargets.length === 0) {
@@ -162,6 +170,6 @@ export function determineSnapTarget({
162
170
 
163
171
  return {
164
172
  targetProgress,
165
- shouldDismiss: canDismiss && targetProgress === 0,
173
+ shouldDismiss: resolvedCanDismiss && targetProgress === 0,
166
174
  };
167
175
  }
@@ -1,5 +1,10 @@
1
1
  import type { SharedValue } from "react-native-reanimated";
2
- import { EPSILON } from "../../../../constants";
2
+ import {
3
+ ANIMATION_SNAP_THRESHOLD,
4
+ EPSILON,
5
+ FALSE,
6
+ TRUE,
7
+ } from "../../../../constants";
3
8
  import type { GestureStoreMap } from "../../../../stores/gesture.store";
4
9
  import type { ScreenTransitionState } from "../../../../types/animation.types";
5
10
  import type { Layout } from "../../../../types/screen.types";
@@ -13,6 +18,7 @@ type BuiltState = {
13
18
  gesture: GestureStoreMap;
14
19
  route: BaseStackRoute;
15
20
  meta?: Record<string, unknown>;
21
+ targetProgress: SharedValue<number>;
16
22
  resolvedAutoSnapPoint: SharedValue<number>;
17
23
  measuredContentLayout: SharedValue<Layout | null>;
18
24
  hasAutoSnapPoint: boolean;
@@ -20,6 +26,13 @@ type BuiltState = {
20
26
  unwrapped: ScreenTransitionState;
21
27
  };
22
28
 
29
+ interface ComputeLogicallySettledParams {
30
+ progress: number;
31
+ targetProgress: number;
32
+ settled: number;
33
+ dragging: number;
34
+ }
35
+
23
36
  /**
24
37
  * Computes the animated snap index based on progress and snap points.
25
38
  * Returns -1 if no snap points, otherwise interpolates between indices.
@@ -41,6 +54,35 @@ const computeSnapIndex = (progress: number, snapPoints: number[]): number => {
41
54
  return snapPoints.length - 1;
42
55
  };
43
56
 
57
+ /**
58
+ * Determines whether the screen transition is logically settled.
59
+ *
60
+ * A transition is considered logically settled when:
61
+ * - The `settled` flag is explicitly set, OR
62
+ * - The screen is not being dragged AND the progress is within
63
+ * {@link ANIMATION_SNAP_THRESHOLD} of the target progress.
64
+ */
65
+ export const computeLogicallySettled = ({
66
+ progress,
67
+ targetProgress,
68
+ settled,
69
+ dragging,
70
+ }: ComputeLogicallySettledParams) => {
71
+ "worklet";
72
+
73
+ if (settled) {
74
+ return TRUE;
75
+ }
76
+
77
+ if (dragging) {
78
+ return FALSE;
79
+ }
80
+
81
+ return Math.abs(progress - targetProgress) <= ANIMATION_SNAP_THRESHOLD
82
+ ? TRUE
83
+ : FALSE;
84
+ };
85
+
44
86
  export const hydrateTransitionState = (
45
87
  s: BuiltState,
46
88
  dimensions: Layout,
@@ -72,6 +114,12 @@ export const hydrateTransitionState = (
72
114
  out.gesture.isDragging = out.gesture.dragging;
73
115
 
74
116
  out.settled = out.animating || out.gesture.dismissing || out.closing ? 0 : 1;
117
+ out.logicallySettled = computeLogicallySettled({
118
+ progress: out.progress,
119
+ targetProgress: s.targetProgress.value,
120
+ settled: out.settled,
121
+ dragging: out.gesture.dragging,
122
+ });
75
123
 
76
124
  out.meta = s.meta;
77
125
  out.layouts.screen.width = dimensions.width;
@@ -71,6 +71,7 @@ export function useScreenAnimationPipeline(): ScreenAnimationPipeline {
71
71
  progress: 0,
72
72
  stackProgress: 0,
73
73
  snapIndex: -1,
74
+ logicallySettled: 1,
74
75
  focused: true,
75
76
  active: DEFAULT_SCREEN_TRANSITION_STATE,
76
77
  navigationMaskEnabled: currentNavigationMaskEnabled,
@@ -121,6 +122,7 @@ export function useScreenAnimationPipeline(): ScreenAnimationPipeline {
121
122
  progress,
122
123
  stackProgress,
123
124
  snapIndex: current.snapIndex,
125
+ logicallySettled: helpers.active.logicallySettled,
124
126
  ...helpers,
125
127
  };
126
128
 
@@ -23,6 +23,7 @@ type BuiltState = {
23
23
  gesture: GestureStoreMap;
24
24
  route: BaseStackRoute;
25
25
  meta?: Record<string, unknown>;
26
+ targetProgress: SharedValue<number>;
26
27
  resolvedAutoSnapPoint: SharedValue<number>;
27
28
  measuredContentLayout: SharedValue<Layout | null>;
28
29
  hasAutoSnapPoint: boolean;
@@ -62,6 +63,7 @@ export const useBuildTransitionState = (
62
63
  closing: AnimationStore.getValue(key, "closing"),
63
64
  entering: AnimationStore.getValue(key, "entering"),
64
65
  animating: AnimationStore.getValue(key, "animating"),
66
+ targetProgress: SystemStore.getValue(key, "targetProgress"),
65
67
  resolvedAutoSnapPoint: SystemStore.getValue(key, "resolvedAutoSnapPoint"),
66
68
  measuredContentLayout: SystemStore.getValue(key, "measuredContentLayout"),
67
69
  hasAutoSnapPoint: snapPoints?.includes("auto") ?? false,
@@ -53,6 +53,15 @@ export type ScreenTransitionState = {
53
53
  */
54
54
  settled: number;
55
55
 
56
+ /**
57
+ * Whether this screen is logically complete for choreography purposes.
58
+ * - `0`: The screen is still meaningfully away from its animation target
59
+ * - `1`: The screen is visually close enough to its target to be treated as done
60
+ *
61
+ * Unlike `settled`, this may become `1` before the underlying spring fully stops.
62
+ */
63
+ logicallySettled: number;
64
+
56
65
  /**
57
66
  * Live gesture values for this screen.
58
67
  * Contains translation (x, y), normalized values (-1 to 1),
@@ -176,6 +185,12 @@ export interface ScreenInterpolationProps {
176
185
  */
177
186
  snapIndex: number;
178
187
 
188
+ /**
189
+ * Whether the active transition is visually close enough to its target to be
190
+ * treated as complete, even if the animation is still physically settling.
191
+ */
192
+ logicallySettled: number;
193
+
179
194
  /**
180
195
  * Function that provides access to bounds helpers for shared screen transitions.
181
196
  */
@@ -22,6 +22,8 @@ export type SheetScrollGestureBehavior =
22
22
  */
23
23
  export type SnapPoint = number | "auto";
24
24
 
25
+ export type BackdropBehavior = "block" | "passthrough" | "dismiss" | "collapse";
26
+
25
27
  export type TransitionAwareProps<T extends object> = AnimatedProps<T> & {
26
28
  /**
27
29
  * Connects this component to custom animated styles defined in screenStyleInterpolator.
@@ -282,7 +284,7 @@ export type ScreenTransitionConfig = {
282
284
  *
283
285
  * @default 'block' (or 'passthrough' for component stacks)
284
286
  */
285
- backdropBehavior?: "block" | "passthrough" | "dismiss" | "collapse";
287
+ backdropBehavior?: BackdropBehavior;
286
288
 
287
289
  /**
288
290
  * Custom component to render as the backdrop layer (between screens).
@@ -143,7 +143,10 @@ export const buildZoomStyles = ({
143
143
  ? NAVIGATION_MASK_CONTAINER_STYLE_ID
144
144
  : "content";
145
145
 
146
- if (!resolvedPair.sourceBounds) {
146
+ // To avoid initial flickering, we'll want to hide if there are no source bounds
147
+ // But to also avoid scenarios where activeId changes in dst and theres a failed measurement,
148
+ // we should only hide if entering and there is no source bounds.
149
+ if (!resolvedPair.sourceBounds && props.active.entering) {
147
150
  return {
148
151
  [focusedContainerStyleId]: HIDDEN_STYLE,
149
152
  };
@@ -281,11 +284,12 @@ export const buildZoomStyles = ({
281
284
  }
282
285
 
283
286
  const unfocusedFade = props.active?.closing
284
- ? interpolate(progress, [1.6, 2], [1, debug ? 0.5 : 0], "clamp")
285
- : interpolate(progress, [1, 1.5], [1, debug ? 0.5 : 0], "clamp");
287
+ ? interpolate(progress, [1.6, 2], [1, debug ? 1 : 0], "clamp")
288
+ : interpolate(progress, [1, 1.5], [1, debug ? 1 : 0], "clamp");
286
289
 
287
290
  const unfocusedScale = interpolate(progress, [1, 2], [1, 0.95], "clamp");
288
291
  const isUnfocusedIdle = props.active.settled === 1;
292
+ const shouldHideUnfocusedIdle = isUnfocusedIdle && !debug;
289
293
  const elementTarget =
290
294
  explicitTarget !== undefined || resolvedPair.destinationBounds
291
295
  ? getZoomContentTarget({
@@ -355,7 +359,7 @@ export const buildZoomStyles = ({
355
359
  toNumber(elementRaw.translateY) + compensatedGestureY;
356
360
  const elementScaleX = toNumber(elementRaw.scaleX, 1) * dragScale;
357
361
  const elementScaleY = toNumber(elementRaw.scaleY, 1) * dragScale;
358
- const resolvedElementStyle = isUnfocusedIdle
362
+ const resolvedElementStyle = shouldHideUnfocusedIdle
359
363
  ? {
360
364
  transform: [
361
365
  { translateX: 0 },
@@ -369,12 +373,32 @@ export const buildZoomStyles = ({
369
373
  }
370
374
  : {
371
375
  transform: [
372
- { translateX: elementTranslateX },
373
- { translateY: elementTranslateY },
374
- { scaleX: elementScaleX },
375
- { scaleY: elementScaleY },
376
+ {
377
+ translateX:
378
+ props.active.logicallySettled && !props.active.closing
379
+ ? 0
380
+ : elementTranslateX,
381
+ },
382
+ {
383
+ translateY:
384
+ props.active.logicallySettled && !props.active.closing
385
+ ? 0
386
+ : elementTranslateY,
387
+ },
388
+ {
389
+ scaleX:
390
+ props.active.logicallySettled && !props.active.closing
391
+ ? 1
392
+ : elementScaleX,
393
+ },
394
+ {
395
+ scaleY:
396
+ props.active.logicallySettled && !props.active.closing
397
+ ? 1
398
+ : elementScaleY,
399
+ },
376
400
  ],
377
- opacity: unfocusedFade,
401
+ opacity: debug ? 0.5 : unfocusedFade,
378
402
  zIndex: 9999,
379
403
  elevation: 9999,
380
404
  };