@react-navigation/stack 8.0.0-alpha.51 → 8.0.0-alpha.53

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 (61) hide show
  1. package/lib/module/TransitionConfigs/CardStyleInterpolators.js +241 -83
  2. package/lib/module/TransitionConfigs/CardStyleInterpolators.js.map +1 -1
  3. package/lib/module/TransitionConfigs/HeaderStyleInterpolators.js +11 -44
  4. package/lib/module/TransitionConfigs/HeaderStyleInterpolators.js.map +1 -1
  5. package/lib/module/TransitionConfigs/TransitionEasings.js +71 -0
  6. package/lib/module/TransitionConfigs/TransitionEasings.js.map +1 -0
  7. package/lib/module/TransitionConfigs/TransitionPresets.js +50 -10
  8. package/lib/module/TransitionConfigs/TransitionPresets.js.map +1 -1
  9. package/lib/module/TransitionConfigs/TransitionSpecs.js +69 -34
  10. package/lib/module/TransitionConfigs/TransitionSpecs.js.map +1 -1
  11. package/lib/module/navigators/createStackNavigator.js +25 -16
  12. package/lib/module/navigators/createStackNavigator.js.map +1 -1
  13. package/lib/module/views/Header/Header.js +2 -0
  14. package/lib/module/views/Header/Header.js.map +1 -1
  15. package/lib/module/views/Header/HeaderContainer.js +8 -3
  16. package/lib/module/views/Header/HeaderContainer.js.map +1 -1
  17. package/lib/module/views/Header/HeaderSegment.js +3 -6
  18. package/lib/module/views/Header/HeaderSegment.js.map +1 -1
  19. package/lib/module/views/Stack/Card.js +57 -32
  20. package/lib/module/views/Stack/Card.js.map +1 -1
  21. package/lib/module/views/Stack/CardContainer.js +17 -2
  22. package/lib/module/views/Stack/CardContainer.js.map +1 -1
  23. package/lib/module/views/Stack/CardStack.js +24 -13
  24. package/lib/module/views/Stack/CardStack.js.map +1 -1
  25. package/lib/typescript/src/TransitionConfigs/CardStyleInterpolators.d.ts +19 -11
  26. package/lib/typescript/src/TransitionConfigs/CardStyleInterpolators.d.ts.map +1 -1
  27. package/lib/typescript/src/TransitionConfigs/HeaderStyleInterpolators.d.ts +3 -3
  28. package/lib/typescript/src/TransitionConfigs/HeaderStyleInterpolators.d.ts.map +1 -1
  29. package/lib/typescript/src/TransitionConfigs/TransitionEasings.d.ts +38 -0
  30. package/lib/typescript/src/TransitionConfigs/TransitionEasings.d.ts.map +1 -0
  31. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +14 -2
  32. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -1
  33. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +43 -14
  34. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -1
  35. package/lib/typescript/src/navigators/createStackNavigator.d.ts.map +1 -1
  36. package/lib/typescript/src/types.d.ts +44 -28
  37. package/lib/typescript/src/types.d.ts.map +1 -1
  38. package/lib/typescript/src/views/Header/Header.d.ts +1 -1
  39. package/lib/typescript/src/views/Header/Header.d.ts.map +1 -1
  40. package/lib/typescript/src/views/Header/HeaderContainer.d.ts.map +1 -1
  41. package/lib/typescript/src/views/Header/HeaderSegment.d.ts +1 -0
  42. package/lib/typescript/src/views/Header/HeaderSegment.d.ts.map +1 -1
  43. package/lib/typescript/src/views/Stack/Card.d.ts +9 -3
  44. package/lib/typescript/src/views/Stack/Card.d.ts.map +1 -1
  45. package/lib/typescript/src/views/Stack/CardContainer.d.ts +3 -1
  46. package/lib/typescript/src/views/Stack/CardContainer.d.ts.map +1 -1
  47. package/lib/typescript/src/views/Stack/CardStack.d.ts.map +1 -1
  48. package/package.json +5 -5
  49. package/src/TransitionConfigs/CardStyleInterpolators.tsx +351 -100
  50. package/src/TransitionConfigs/HeaderStyleInterpolators.tsx +15 -46
  51. package/src/TransitionConfigs/TransitionEasings.tsx +90 -0
  52. package/src/TransitionConfigs/TransitionPresets.tsx +56 -10
  53. package/src/TransitionConfigs/TransitionSpecs.tsx +68 -34
  54. package/src/navigators/createStackNavigator.tsx +33 -24
  55. package/src/types.tsx +46 -29
  56. package/src/views/Header/Header.tsx +2 -0
  57. package/src/views/Header/HeaderContainer.tsx +10 -4
  58. package/src/views/Header/HeaderSegment.tsx +4 -4
  59. package/src/views/Stack/Card.tsx +82 -37
  60. package/src/views/Stack/CardContainer.tsx +17 -2
  61. package/src/views/Stack/CardStack.tsx +69 -14
@@ -4,6 +4,7 @@ import {
4
4
  NavigationProvider,
5
5
  type Route,
6
6
  useLinkBuilder,
7
+ useLocale,
7
8
  } from '@react-navigation/native';
8
9
  import * as React from 'react';
9
10
  import {
@@ -17,10 +18,10 @@ import {
17
18
  import {
18
19
  forNoAnimation,
19
20
  forSlideLeft,
20
- forSlideRight,
21
21
  forSlideUp,
22
22
  } from '../../TransitionConfigs/HeaderStyleInterpolators';
23
23
  import type { Scene, StackHeaderMode, StackHeaderProps } from '../../types';
24
+ import { getInvertedMultiplier } from '../../utils/getInvertedMultiplier';
24
25
  import { Header } from './Header';
25
26
 
26
27
  export type HeaderHeight = {
@@ -53,6 +54,7 @@ export function HeaderContainer({
53
54
  const focusedRoute = getFocusedRoute();
54
55
  const parentHeaderBack = React.use(HeaderBackContext);
55
56
  const { buildHref } = useLinkBuilder();
57
+ const { direction } = useLocale();
56
58
 
57
59
  return (
58
60
  <View style={[styles.container, style]}>
@@ -131,6 +133,12 @@ export function HeaderContainer({
131
133
  const props: StackHeaderProps = {
132
134
  back: headerBack,
133
135
  progress: scene.progress,
136
+ inverted: getInvertedMultiplier(
137
+ isHeaderStatic && nextHeaderlessGestureDirection
138
+ ? nextHeaderlessGestureDirection
139
+ : scene.descriptor.options.gestureDirection,
140
+ direction === 'rtl'
141
+ ),
134
142
  options: scene.descriptor.options,
135
143
  route: scene.descriptor.route,
136
144
  navigation: scene.descriptor.navigation,
@@ -140,9 +148,7 @@ export function HeaderContainer({
140
148
  ? nextHeaderlessGestureDirection === 'vertical' ||
141
149
  nextHeaderlessGestureDirection === 'vertical-inverted'
142
150
  ? forSlideUp
143
- : nextHeaderlessGestureDirection === 'horizontal-inverted'
144
- ? forSlideRight
145
- : forSlideLeft
151
+ : forSlideLeft
146
152
  : headerStyleInterpolator
147
153
  : forNoAnimation,
148
154
  };
@@ -6,7 +6,6 @@ import {
6
6
  HeaderTitle,
7
7
  useFrameSize,
8
8
  } from '@react-navigation/elements';
9
- import { useLocale } from '@react-navigation/native';
10
9
  import * as React from 'react';
11
10
  import { StyleSheet, type ViewStyle } from 'react-native';
12
11
 
@@ -23,15 +22,16 @@ type Props = Omit<StackHeaderOptions, 'headerStatusBarHeight'> & {
23
22
  onGoBack?: (() => void) | undefined;
24
23
  backHref?: string | undefined;
25
24
  progress: SceneProgress;
25
+ inverted: 1 | -1;
26
26
  styleInterpolator: StackHeaderStyleInterpolator;
27
27
  };
28
28
 
29
29
  export function HeaderSegment(props: Props) {
30
- const { direction } = useLocale();
31
30
  const layout = useFrameSize((frame) => frame, true);
32
31
 
33
32
  const {
34
33
  progress,
34
+ inverted,
35
35
  modal,
36
36
  onGoBack,
37
37
  backHref,
@@ -75,7 +75,7 @@ export function HeaderSegment(props: Props) {
75
75
  styleInterpolator({
76
76
  current: { progress: progress.current },
77
77
  next: progress.next && { progress: progress.next },
78
- direction,
78
+ inverted,
79
79
  layouts: {
80
80
  header: {
81
81
  height: headerHeight,
@@ -84,7 +84,7 @@ export function HeaderSegment(props: Props) {
84
84
  screen: layout,
85
85
  },
86
86
  }),
87
- [styleInterpolator, progress, direction, headerHeight, layout]
87
+ [styleInterpolator, progress, inverted, headerHeight, layout]
88
88
  );
89
89
 
90
90
  const headerLeft: StackHeaderOptions['headerLeft'] = left
@@ -32,8 +32,16 @@ type Props = {
32
32
  interpolationIndex: number;
33
33
  opening: boolean;
34
34
  closing: boolean;
35
- next: Animated.AnimatedInterpolation<number> | undefined;
36
- current: Animated.AnimatedInterpolation<number>;
35
+ current: {
36
+ progress: Animated.AnimatedInterpolation<number>;
37
+ closing: Animated.Value;
38
+ };
39
+ next:
40
+ | {
41
+ progress: Animated.AnimatedInterpolation<number>;
42
+ closing: Animated.Value;
43
+ }
44
+ | undefined;
37
45
  gesture: Animated.Value;
38
46
  layout: Layout;
39
47
  insets: EdgeInsets;
@@ -52,7 +60,7 @@ type Props = {
52
60
  style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
53
61
  }) => React.ReactNode)
54
62
  | undefined;
55
- overlayEnabled: boolean;
63
+ overlayEnabled: boolean | undefined;
56
64
  shadowEnabled: boolean | undefined;
57
65
  gestureEnabled: boolean;
58
66
  gestureResponseDistance?: number | undefined;
@@ -121,7 +129,7 @@ const defaultOverlay = ({
121
129
  }) => (style ? <Animated.View style={[styles.overlay, style]} /> : null);
122
130
 
123
131
  function Card({
124
- shadowEnabled = false,
132
+ shadowEnabled,
125
133
  gestureEnabled = true,
126
134
  gestureVelocityImpact = GESTURE_VELOCITY_IMPACT,
127
135
  overlay = defaultOverlay,
@@ -153,15 +161,17 @@ function Card({
153
161
  contentStyle,
154
162
  }: Props) {
155
163
  const didInitiallyAnimate = React.useRef(false);
164
+ const isClosingValueLockedRef = React.useRef(false);
156
165
  const lastToValueRef = React.useRef<number | undefined>(undefined);
166
+ const isAnimatingRef = React.useRef(false);
167
+ const animationIdRef = React.useRef(0);
157
168
 
158
169
  const animationHandleRef = React.useRef<number | undefined>(undefined);
159
170
  const pendingGestureCallbackRef =
160
171
  React.useRef<ReturnType<typeof setTimeout>>(undefined);
161
172
  const pendingOnCloseCallbackRef =
162
173
  React.useRef<ReturnType<typeof setTimeout>>(undefined);
163
-
164
- const [isClosing] = React.useState(() => new Animated.Value(FALSE));
174
+ const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
165
175
 
166
176
  const [inverted] = React.useState(
167
177
  () =>
@@ -185,6 +195,8 @@ function Card({
185
195
  closing: boolean;
186
196
  velocity?: number;
187
197
  }) => {
198
+ const id = ++animationIdRef.current;
199
+
188
200
  const toValue = getAnimateToValue({
189
201
  closing: isClosingParam,
190
202
  layout,
@@ -194,7 +206,13 @@ function Card({
194
206
  });
195
207
 
196
208
  lastToValueRef.current = toValue;
197
- isClosing.setValue(isClosingParam ? TRUE : FALSE);
209
+
210
+ // Switching between open and close mid-gesture and animation can cause a visible jump.
211
+ // So we lock the closing value until the animation is finished.
212
+ if (!isClosingValueLockedRef.current) {
213
+ isClosingValueLockedRef.current = true;
214
+ current.closing.setValue(isClosingParam ? TRUE : FALSE);
215
+ }
198
216
 
199
217
  const spec = isClosingParam ? transitionSpec.close : transitionSpec.open;
200
218
  const animation =
@@ -212,6 +230,9 @@ function Card({
212
230
  });
213
231
 
214
232
  const onFinish = () => {
233
+ current.closing.setValue(isClosingParam ? TRUE : FALSE);
234
+ isClosingValueLockedRef.current = false;
235
+
215
236
  if (isClosingParam) {
216
237
  onClose();
217
238
  } else {
@@ -225,6 +246,8 @@ function Card({
225
246
  };
226
247
 
227
248
  if (animated) {
249
+ isAnimatingRef.current = true;
250
+
228
251
  animation(gesture, {
229
252
  ...spec.config,
230
253
  velocity,
@@ -232,6 +255,12 @@ function Card({
232
255
  useNativeDriver,
233
256
  isInteraction: false,
234
257
  }).start(({ finished }) => {
258
+ if (id !== animationIdRef.current) {
259
+ return;
260
+ }
261
+
262
+ isAnimatingRef.current = false;
263
+
235
264
  clearTimeout(pendingGestureCallbackRef.current);
236
265
 
237
266
  if (finished) {
@@ -239,6 +268,8 @@ function Card({
239
268
  }
240
269
  });
241
270
  } else {
271
+ isAnimatingRef.current = false;
272
+ gesture.setValue(toValue);
242
273
  onFinish();
243
274
  }
244
275
  }
@@ -270,17 +301,23 @@ function Card({
270
301
  } | null>(null);
271
302
 
272
303
  React.useEffect(() => {
304
+ const animationIdRefForCleanup = animationIdRef;
305
+
273
306
  return () => {
307
+ // Increment the animation ID when the card unmounts.
308
+ // This makes pending callbacks return because their IDs no longer match.
309
+ animationIdRefForCleanup.current++;
310
+ gesture.stopAnimation();
311
+
274
312
  if (animationHandleRef.current) {
275
313
  cancelAnimationFrame(animationHandleRef.current);
276
314
  }
277
315
 
278
316
  clearTimeout(pendingGestureCallbackRef.current);
279
317
  clearTimeout(pendingOnCloseCallbackRef.current);
318
+ clearTimeout(timeoutRef.current);
280
319
  };
281
- }, []);
282
-
283
- const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
320
+ }, [gesture]);
284
321
 
285
322
  const maybeAnimate = useLatestCallback(() => {
286
323
  clearTimeout(pendingGestureCallbackRef.current);
@@ -318,7 +355,10 @@ function Card({
318
355
  // When route was closed due to a gesture, the animation would've happened already
319
356
  // It's still important to trigger the animation so that `onClose` is called
320
357
  // If `onClose` is not called, cleanup step won't be performed for gestures
321
- animate({ closing });
358
+ // We also check if animation was already running so we don't restart it unnecessarily
359
+ if (!isAnimatingRef.current || lastToValueRef.current !== toValue) {
360
+ animate({ closing });
361
+ }
322
362
  } else if (
323
363
  typeof previousOpening === 'boolean' &&
324
364
  opening &&
@@ -365,9 +405,8 @@ function Card({
365
405
  const interpolationProps = React.useMemo(
366
406
  () => ({
367
407
  index: interpolationIndex,
368
- current: { progress: current },
369
- next: next && { progress: next },
370
- closing: isClosing,
408
+ current,
409
+ next,
371
410
  swiping: isSwiping,
372
411
  inverted,
373
412
  layouts: {
@@ -384,7 +423,6 @@ function Card({
384
423
  interpolationIndex,
385
424
  current,
386
425
  next,
387
- isClosing,
388
426
  isSwiping,
389
427
  inverted,
390
428
  layout,
@@ -405,7 +443,14 @@ function Card({
405
443
  () => {
406
444
  clearTimeout(pendingGestureCallbackRef.current);
407
445
  clearTimeout(pendingOnCloseCallbackRef.current);
446
+
408
447
  isSwiping.setValue(TRUE);
448
+
449
+ if (!isClosingValueLockedRef.current) {
450
+ isClosingValueLockedRef.current = true;
451
+ current.closing.setValue(TRUE);
452
+ }
453
+
409
454
  onGestureBegin?.();
410
455
  }
411
456
  );
@@ -538,13 +583,13 @@ function Card({
538
583
  // Animated needs the animated value to be used somewhere, otherwise things don't update properly.
539
584
  // If we disable animations and hide header, it could end up making the value unused.
540
585
  // So we have this dummy style that will always be used regardless of what else changed.
541
- opacity: current,
586
+ opacity: current.progress,
542
587
  }}
543
588
  // Make sure that this view isn't removed. If this view is removed, our style with animated value won't apply
544
589
  collapsable={false}
545
590
  />
546
591
  ) : null}
547
- {overlayEnabled ? (
592
+ {(overlayEnabled ?? overlayStyle != null) ? (
548
593
  <View style={[StyleSheet.absoluteFill, { pointerEvents: 'box-none' }]}>
549
594
  {overlay({ style: overlayStyle })}
550
595
  </View>
@@ -557,17 +602,16 @@ function Card({
557
602
  needsOffscreenAlphaCompositing={hasOpacityStyle(cardStyle)}
558
603
  style={[styles.card, cardStyle]}
559
604
  >
560
- {shadowEnabled && shadowStyle && !isTransparent ? (
605
+ {(shadowEnabled ?? shadowStyle != null) && !isTransparent ? (
561
606
  <Animated.View
562
607
  style={[
563
608
  styles.shadow,
564
- gestureDirection === 'horizontal'
565
- ? [styles.shadowHorizontal, styles.shadowStart]
566
- : gestureDirection === 'horizontal-inverted'
567
- ? [styles.shadowHorizontal, styles.shadowEnd]
568
- : gestureDirection === 'vertical'
569
- ? [styles.shadowVertical, styles.shadowTop]
570
- : [styles.shadowVertical, styles.shadowBottom],
609
+ gestureDirection === 'horizontal' ||
610
+ gestureDirection === 'horizontal-inverted'
611
+ ? styles.shadowHorizontal
612
+ : gestureDirection === 'vertical'
613
+ ? [styles.shadowVertical, styles.shadowTop]
614
+ : [styles.shadowVertical, styles.shadowBottom],
571
615
  { backgroundColor },
572
616
  shadowStyle,
573
617
  ]}
@@ -580,7 +624,9 @@ function Card({
580
624
  >
581
625
  {children}
582
626
  </CardContent>
583
- {dimStyle ? <Animated.View style={[styles.dim, dimStyle]} /> : null}
627
+ {dimStyle != null ? (
628
+ <Animated.View style={[styles.dim, dimStyle]} />
629
+ ) : null}
584
630
  </Animated.View>
585
631
  </GestureDetector>
586
632
  </Animated.View>
@@ -597,6 +643,10 @@ const styles = StyleSheet.create({
597
643
  },
598
644
  card: {
599
645
  flex: 1,
646
+ // The flip animation adds `backfaceVisibility`
647
+ // On Android, it crashes when the style property is removed
648
+ // So we always add the property with the default
649
+ backfaceVisibility: 'visible',
600
650
  // This is necessary for gestures to work
601
651
  // Without this, gestures won't work if the child view is flattened
602
652
  pointerEvents: 'auto',
@@ -617,23 +667,18 @@ const styles = StyleSheet.create({
617
667
  },
618
668
  shadowHorizontal: {
619
669
  top: 0,
670
+ start: 0,
671
+ end: 0,
620
672
  bottom: 0,
621
- width: 3,
622
673
  ...getShadowStyle({
623
674
  offset: {
624
- width: -1,
625
- height: 1,
675
+ width: 0,
676
+ height: -3,
626
677
  },
627
- radius: 5,
628
- opacity: 0.3,
678
+ radius: 12,
679
+ opacity: 0.04,
629
680
  }),
630
681
  },
631
- shadowStart: {
632
- start: 0,
633
- },
634
- shadowEnd: {
635
- end: 0,
636
- },
637
682
  shadowVertical: {
638
683
  start: 0,
639
684
  end: 0,
@@ -29,6 +29,8 @@ type Props = {
29
29
  focused: boolean;
30
30
  opening: boolean;
31
31
  closing: boolean;
32
+ closingAnimated: Animated.Value;
33
+ nextClosingAnimated: Animated.Value | undefined;
32
34
  modal: boolean;
33
35
  layout: Layout;
34
36
  gesture: Animated.Value;
@@ -66,6 +68,8 @@ function CardContainerInner({
66
68
  active,
67
69
  opening,
68
70
  closing,
71
+ closingAnimated,
72
+ nextClosingAnimated,
69
73
  gesture,
70
74
  focused,
71
75
  modal,
@@ -207,6 +211,17 @@ function CardContainerInner({
207
211
 
208
212
  const animated = animation !== 'none';
209
213
 
214
+ const { current, next } = React.useMemo(
215
+ () => ({
216
+ current: { progress: scene.progress.current, closing: closingAnimated },
217
+ next:
218
+ scene.progress.next && nextClosingAnimated
219
+ ? { progress: scene.progress.next, closing: nextClosingAnimated }
220
+ : undefined,
221
+ }),
222
+ [closingAnimated, nextClosingAnimated, scene.progress]
223
+ );
224
+
210
225
  return (
211
226
  <Card
212
227
  animated={animated}
@@ -216,8 +231,8 @@ function CardContainerInner({
216
231
  insets={insets}
217
232
  direction={direction}
218
233
  gesture={gesture}
219
- current={scene.progress.current}
220
- next={scene.progress.next}
234
+ current={current}
235
+ next={next}
221
236
  opening={opening}
222
237
  closing={closing}
223
238
  onOpen={handleOpen}
@@ -20,15 +20,24 @@ import {
20
20
  import type { EdgeInsets } from 'react-native-safe-area-context';
21
21
 
22
22
  import {
23
+ forBottomSheetAndroid,
24
+ forCrossDissolveIOS,
25
+ forDialogAndroid,
26
+ forFadeFromBottomAndroid,
27
+ forFadeFromRightAndroid,
28
+ forFlipIOS,
23
29
  forModalPresentationIOS,
24
30
  forNoAnimation as forNoAnimationCard,
31
+ forRevealFromBottomAndroid,
32
+ forScaleFromCenterAndroid,
33
+ forVerticalIOS,
25
34
  } from '../../TransitionConfigs/CardStyleInterpolators';
26
35
  import {
27
- BottomSheetAndroid,
28
36
  DefaultTransition,
29
37
  FadeFromBottomAndroid,
30
38
  FadeFromRightAndroid,
31
39
  ModalFadeTransition,
40
+ ModalFlipIOS,
32
41
  ModalSlideFromBottomIOS,
33
42
  ModalTransition,
34
43
  RevealFromBottomAndroid,
@@ -102,17 +111,33 @@ const NAMED_TRANSITIONS_PRESETS = {
102
111
  fade: ModalFadeTransition,
103
112
  fade_from_bottom: FadeFromBottomAndroid,
104
113
  fade_from_right: FadeFromRightAndroid,
114
+ flip: ModalFlipIOS,
115
+ ios_from_left: SlideFromLeftIOS,
116
+ ios_from_right: SlideFromRightIOS,
105
117
  none: DefaultTransition,
106
118
  reveal_from_bottom: RevealFromBottomAndroid,
107
119
  scale_from_center: ScaleFromCenterAndroid,
108
120
  slide_from_left: SlideFromLeftIOS,
109
121
  slide_from_right: SlideFromRightIOS,
110
- slide_from_bottom: Platform.select({
111
- ios: ModalSlideFromBottomIOS,
112
- default: BottomSheetAndroid,
113
- }),
122
+ slide_from_bottom: ModalSlideFromBottomIOS,
114
123
  } as const satisfies Record<StackAnimationName, TransitionPreset>;
115
124
 
125
+ const GESTURE_DISABLED_INTERPOLATORS = [
126
+ forCrossDissolveIOS,
127
+ forDialogAndroid,
128
+ forFadeFromBottomAndroid,
129
+ forFadeFromRightAndroid,
130
+ forFlipIOS,
131
+ forRevealFromBottomAndroid,
132
+ forScaleFromCenterAndroid,
133
+ forVerticalIOS,
134
+ ];
135
+
136
+ const SCREEN_HEADER_INTERPOLATORS = [
137
+ ...GESTURE_DISABLED_INTERPOLATORS,
138
+ forBottomSheetAndroid,
139
+ ];
140
+
116
141
  const HIDDEN_HEADER_HEIGHT = { measured: false, value: 0 };
117
142
 
118
143
  const getInterpolationIndex = (scenes: Scene[], index: number) => {
@@ -146,11 +171,26 @@ const getIsModalPresentation = (
146
171
  ) => {
147
172
  return (
148
173
  cardStyleInterpolator === forModalPresentationIOS ||
149
- // Handle custom modal presentation interpolators as well
150
- cardStyleInterpolator.name === 'forModalPresentationIOS'
174
+ cardStyleInterpolator.name === forModalPresentationIOS.name
151
175
  );
152
176
  };
153
177
 
178
+ const getIsGestureEnabledByDefault = (
179
+ cardStyleInterpolator: StackCardStyleInterpolator
180
+ ) =>
181
+ !GESTURE_DISABLED_INTERPOLATORS.some(
182
+ (interpolator) =>
183
+ cardStyleInterpolator === interpolator ||
184
+ cardStyleInterpolator.name === interpolator.name
185
+ );
186
+
187
+ const getIsScreenHeader = (cardStyleInterpolator: StackCardStyleInterpolator) =>
188
+ SCREEN_HEADER_INTERPOLATORS.some(
189
+ (interpolator) =>
190
+ cardStyleInterpolator === interpolator ||
191
+ cardStyleInterpolator.name === interpolator.name
192
+ );
193
+
154
194
  const getIsInModalPresentation = (
155
195
  scene: Scene,
156
196
  interpolationIndex: number,
@@ -323,7 +363,6 @@ export class CardStack extends React.Component<Props, State> {
323
363
  }
324
364
 
325
365
  const allRoutes = getAllRoutes(props.routes, props.state);
326
-
327
366
  const gestures = allRoutes.reduce<GestureValues>((acc, curr) => {
328
367
  const descriptor = props.descriptors[curr.key];
329
368
  const { animation } = descriptor?.options || {};
@@ -354,6 +393,13 @@ export class CardStack extends React.Component<Props, State> {
354
393
  const nextRoute = isInactive ? undefined : self[index + 1];
355
394
 
356
395
  const oldScene = state.scenes[index];
396
+ const previousScene =
397
+ oldScene?.route.key === route.key
398
+ ? oldScene
399
+ : state.scenes.find((scene) => scene.route.key === route.key);
400
+ const closing =
401
+ previousScene?.closing ??
402
+ new Animated.Value(props.closingRouteKeys.includes(route.key) ? 1 : 0);
357
403
 
358
404
  const currentGesture = gestures[route.key];
359
405
 
@@ -420,16 +466,17 @@ export class CardStack extends React.Component<Props, State> {
420
466
  : DefaultTransition;
421
467
 
422
468
  const {
423
- gestureEnabled = Platform.OS === 'ios' && isAnimationEnabled,
424
469
  gestureDirection = transitionPreset.gestureDirection,
425
470
  transitionSpec = transitionPreset.transitionSpec,
426
471
  cardStyleInterpolator = isAnimationEnabled
427
472
  ? transitionPreset.cardStyleInterpolator
428
473
  : forNoAnimationCard,
474
+ gestureEnabled = Platform.OS === 'ios' &&
475
+ isAnimationEnabled &&
476
+ getIsGestureEnabledByDefault(cardStyleInterpolator),
429
477
  headerStyleInterpolator = transitionPreset.headerStyleInterpolator,
430
- cardOverlayEnabled = (Platform.OS !== 'ios' &&
431
- optionsForTransitionConfig.presentation !== 'transparentModal') ||
432
- getIsModalPresentation(cardStyleInterpolator),
478
+ cardOverlayEnabled,
479
+ cardShadowEnabled,
433
480
  } = optionsForTransitionConfig;
434
481
 
435
482
  const headerMode: StackHeaderMode =
@@ -439,7 +486,8 @@ export class CardStack extends React.Component<Props, State> {
439
486
  optionsForTransitionConfig.presentation === 'transparentModal' ||
440
487
  nextOptions?.presentation === 'modal' ||
441
488
  nextOptions?.presentation === 'transparentModal' ||
442
- getIsModalPresentation(cardStyleInterpolator)
489
+ getIsModalPresentation(cardStyleInterpolator) ||
490
+ getIsScreenHeader(cardStyleInterpolator)
443
491
  ) &&
444
492
  Platform.OS === 'ios' &&
445
493
  descriptor.options.header === undefined
@@ -456,6 +504,7 @@ export class CardStack extends React.Component<Props, State> {
456
504
  ...descriptor.options,
457
505
  animation,
458
506
  cardOverlayEnabled,
507
+ cardShadowEnabled,
459
508
  cardStyleInterpolator,
460
509
  gestureDirection,
461
510
  gestureEnabled,
@@ -489,6 +538,7 @@ export class CardStack extends React.Component<Props, State> {
489
538
  )
490
539
  : undefined,
491
540
  },
541
+ closing,
492
542
  __memo: [
493
543
  state.layout,
494
544
  descriptor,
@@ -810,10 +860,13 @@ export class CardStack extends React.Component<Props, State> {
810
860
  // Preloaded screens should stay mounted, but remain hidden until focused
811
861
  (!isPreloaded && index >= routes.length - 2)));
812
862
 
813
- const activityMode = // Render focused and animating screens normally
863
+ const activityMode =
864
+ // Keep focused and focusing screens interactive
814
865
  focused || isFocusing
815
866
  ? 'normal'
816
867
  : inactiveBehavior === 'none' ||
868
+ // Keep effects running until the closing animation finishes
869
+ isRemoving ||
817
870
  // Unpause preloaded or retained screens so updates are visible
818
871
  // Handle retained explicitly as isInactive won't be updated until animation end
819
872
  isInactive ||
@@ -870,6 +923,8 @@ export class CardStack extends React.Component<Props, State> {
870
923
  focused={focused}
871
924
  opening={openingRouteKeys.includes(route.key)}
872
925
  closing={closingRouteKeys.includes(route.key)}
926
+ closingAnimated={scene.closing}
927
+ nextClosingAnimated={nextScene?.closing}
873
928
  layout={layout}
874
929
  gesture={gesture}
875
930
  scene={scene}