@react-navigation/stack 7.6.3 → 7.6.4

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.
@@ -11,19 +11,19 @@ import {
11
11
  type ViewStyle,
12
12
  } from 'react-native';
13
13
  import type { EdgeInsets } from 'react-native-safe-area-context';
14
+ import useLatestCallback from 'use-latest-callback';
14
15
 
15
16
  import type {
16
17
  GestureDirection,
17
18
  Layout,
18
- StackCardInterpolationProps,
19
19
  StackCardStyleInterpolator,
20
20
  TransitionSpec,
21
21
  } from '../../types';
22
22
  import { CardAnimationContext } from '../../utils/CardAnimationContext';
23
+ import { gestureActivationCriteria } from '../../utils/gestureActivationCriteria';
23
24
  import { getDistanceForDirection } from '../../utils/getDistanceForDirection';
24
25
  import { getInvertedMultiplier } from '../../utils/getInvertedMultiplier';
25
26
  import { getShadowStyle } from '../../utils/getShadowStyle';
26
- import { memoize } from '../../utils/memoize';
27
27
  import {
28
28
  GestureState,
29
29
  PanGestureHandler,
@@ -36,7 +36,7 @@ type Props = {
36
36
  interpolationIndex: number;
37
37
  opening: boolean;
38
38
  closing: boolean;
39
- next?: Animated.AnimatedInterpolation<number>;
39
+ next: Animated.AnimatedInterpolation<number> | undefined;
40
40
  current: Animated.AnimatedInterpolation<number>;
41
41
  gesture: Animated.Value;
42
42
  layout: Layout;
@@ -51,14 +51,16 @@ type Props = {
51
51
  onGestureCanceled: () => void;
52
52
  onGestureEnd: () => void;
53
53
  children: React.ReactNode;
54
- overlay: (props: {
55
- style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
56
- }) => React.ReactNode;
54
+ overlay:
55
+ | ((props: {
56
+ style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
57
+ }) => React.ReactNode)
58
+ | undefined;
57
59
  overlayEnabled: boolean;
58
- shadowEnabled: boolean;
60
+ shadowEnabled: boolean | undefined;
59
61
  gestureEnabled: boolean;
60
62
  gestureResponseDistance?: number;
61
- gestureVelocityImpact: number;
63
+ gestureVelocityImpact: number | undefined;
62
64
  transitionSpec: {
63
65
  open: TransitionSpec;
64
66
  close: TransitionSpec;
@@ -74,528 +76,524 @@ const GESTURE_VELOCITY_IMPACT = 0.3;
74
76
  const TRUE = 1;
75
77
  const FALSE = 0;
76
78
 
77
- /**
78
- * The distance of touch start from the edge of the screen where the gesture will be recognized
79
- */
80
- const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 50;
81
- const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
82
-
83
79
  const useNativeDriver = Platform.OS !== 'web';
84
80
 
85
- const hasOpacityStyle = (style: any) => {
81
+ const hasOpacityStyle = (
82
+ style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>
83
+ ) => {
86
84
  if (style) {
87
85
  const flattenedStyle = StyleSheet.flatten(style);
88
- return flattenedStyle.opacity != null;
86
+
87
+ return 'opacity' in flattenedStyle && flattenedStyle.opacity != null;
89
88
  }
90
89
 
91
90
  return false;
92
91
  };
93
92
 
94
- export class Card extends React.Component<Props> {
95
- static defaultProps = {
96
- shadowEnabled: false,
97
- gestureEnabled: true,
98
- gestureVelocityImpact: GESTURE_VELOCITY_IMPACT,
99
- overlay: ({
100
- style,
101
- }: {
102
- style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
103
- }) =>
104
- style ? (
105
- <Animated.View pointerEvents="none" style={[styles.overlay, style]} />
106
- ) : null,
107
- };
108
-
109
- componentDidMount() {
110
- if (!this.props.preloaded) {
111
- this.animate({
112
- closing: this.props.closing,
113
- });
114
- }
115
- this.isCurrentlyMounted = true;
93
+ const getAnimateToValue = ({
94
+ closing: isClosing,
95
+ layout: currentLayout,
96
+ gestureDirection: currentGestureDirection,
97
+ direction: currentDirection,
98
+ preloaded: isPreloaded,
99
+ }: {
100
+ closing?: boolean;
101
+ layout: Layout;
102
+ gestureDirection: GestureDirection;
103
+ direction: LocaleDirection;
104
+ preloaded: boolean;
105
+ }) => {
106
+ if (!isClosing && !isPreloaded) {
107
+ return 0;
116
108
  }
117
109
 
118
- componentDidUpdate(prevProps: Props) {
119
- const { gesture, direction, layout, gestureDirection, opening, closing } =
120
- this.props;
121
- const { width, height } = layout;
122
-
123
- if (width !== prevProps.layout.width) {
124
- this.layout.width.setValue(width);
125
- }
126
-
127
- if (height !== prevProps.layout.height) {
128
- this.layout.height.setValue(height);
129
- }
110
+ return getDistanceForDirection(
111
+ currentLayout,
112
+ currentGestureDirection,
113
+ currentDirection === 'rtl'
114
+ );
115
+ };
130
116
 
131
- if (gestureDirection !== prevProps.gestureDirection) {
132
- this.inverted.setValue(
117
+ const defaultOverlay = ({
118
+ style,
119
+ }: {
120
+ style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
121
+ }) =>
122
+ style ? (
123
+ <Animated.View pointerEvents="none" style={[styles.overlay, style]} />
124
+ ) : null;
125
+
126
+ function Card({
127
+ shadowEnabled = false,
128
+ gestureEnabled = true,
129
+ gestureVelocityImpact = GESTURE_VELOCITY_IMPACT,
130
+ overlay = defaultOverlay,
131
+ animated,
132
+ interpolationIndex,
133
+ opening,
134
+ closing,
135
+ next,
136
+ current,
137
+ gesture,
138
+ layout,
139
+ insets,
140
+ direction,
141
+ pageOverflowEnabled,
142
+ gestureDirection,
143
+ onOpen,
144
+ onClose,
145
+ onTransition,
146
+ onGestureBegin,
147
+ onGestureCanceled,
148
+ onGestureEnd,
149
+ children,
150
+ overlayEnabled,
151
+ gestureResponseDistance,
152
+ transitionSpec,
153
+ preloaded,
154
+ styleInterpolator,
155
+ containerStyle: customContainerStyle,
156
+ contentStyle,
157
+ }: Props) {
158
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
159
+
160
+ const didInitiallyAnimate = React.useRef(false);
161
+ const lastToValueRef = React.useRef<number | undefined>(undefined);
162
+
163
+ const interactionHandleRef = React.useRef<number | undefined>(undefined);
164
+ const animationHandleRef = React.useRef<number | undefined>(undefined);
165
+ const pendingGestureCallbackRef =
166
+ React.useRef<ReturnType<typeof setTimeout>>(undefined);
167
+
168
+ const [isClosing] = React.useState(() => new Animated.Value(FALSE));
169
+
170
+ const [inverted] = React.useState(
171
+ () =>
172
+ new Animated.Value(
133
173
  getInvertedMultiplier(gestureDirection, direction === 'rtl')
134
- );
135
- }
136
-
137
- const toValue = this.getAnimateToValue(this.props);
138
-
139
- if (
140
- this.getAnimateToValue(prevProps) !== toValue ||
141
- this.lastToValue !== toValue
142
- ) {
143
- // We need to trigger the animation when route was closed
144
- // The route might have been closed by a `POP` action or by a gesture
145
- // When route was closed due to a gesture, the animation would've happened already
146
- // It's still important to trigger the animation so that `onClose` is called
147
- // If `onClose` is not called, cleanup step won't be performed for gestures
148
- this.animate({ closing });
149
- } else if (opening && !prevProps.opening) {
150
- // This can happen when screen somewhere below in the stack comes into focus via rearranging
151
- // Also reset the animated value to make sure that the animation starts from the beginning
152
- gesture.setValue(
153
- getDistanceForDirection(layout, gestureDirection, direction === 'rtl')
154
- );
155
-
156
- this.animate({ closing });
157
- }
158
- }
159
-
160
- componentWillUnmount() {
161
- this.props.gesture?.stopAnimation();
162
- this.isCurrentlyMounted = false;
163
- this.handleEndInteraction();
164
- }
165
-
166
- private isCurrentlyMounted = false;
167
-
168
- private isClosing = new Animated.Value(FALSE);
169
-
170
- private inverted = new Animated.Value(
171
- getInvertedMultiplier(
172
- this.props.gestureDirection,
173
- this.props.direction === 'rtl'
174
- )
174
+ )
175
175
  );
176
176
 
177
- private layout = {
178
- width: new Animated.Value(this.props.layout.width),
179
- height: new Animated.Value(this.props.layout.height),
180
- };
181
-
182
- private isSwiping = new Animated.Value(FALSE);
177
+ const [layoutAnim] = React.useState(() => ({
178
+ width: new Animated.Value(layout.width),
179
+ height: new Animated.Value(layout.height),
180
+ }));
183
181
 
184
- private lastToValue: number | undefined;
182
+ const [isSwiping] = React.useState(() => new Animated.Value(FALSE));
185
183
 
186
- private interactionHandle: number | undefined;
187
- private pendingGestureCallback: number | undefined;
188
- private animationHandle: number | undefined;
184
+ const onStartInteraction = useLatestCallback(() => {
185
+ if (interactionHandleRef.current === undefined) {
186
+ interactionHandleRef.current =
187
+ InteractionManager.createInteractionHandle();
188
+ }
189
+ });
189
190
 
190
- private animate = ({
191
- closing,
192
- velocity,
193
- }: {
194
- closing: boolean;
195
- velocity?: number;
196
- }) => {
197
- const { animated, transitionSpec, onOpen, onClose, onTransition, gesture } =
198
- this.props;
191
+ const onEndInteraction = useLatestCallback(() => {
192
+ if (interactionHandleRef.current !== undefined) {
193
+ InteractionManager.clearInteractionHandle(interactionHandleRef.current);
194
+ interactionHandleRef.current = undefined;
195
+ }
196
+ });
199
197
 
200
- const toValue = this.getAnimateToValue({
201
- ...this.props,
202
- closing,
203
- });
198
+ const animate = useLatestCallback(
199
+ ({
200
+ closing: isClosingParam,
201
+ velocity,
202
+ }: {
203
+ closing: boolean;
204
+ velocity?: number;
205
+ }) => {
206
+ const toValue = getAnimateToValue({
207
+ closing: isClosingParam,
208
+ layout,
209
+ gestureDirection,
210
+ direction,
211
+ preloaded,
212
+ });
204
213
 
205
- this.lastToValue = toValue;
214
+ lastToValueRef.current = toValue;
215
+ isClosing.setValue(isClosingParam ? TRUE : FALSE);
206
216
 
207
- this.isClosing.setValue(closing ? TRUE : FALSE);
217
+ const spec = isClosingParam ? transitionSpec.close : transitionSpec.open;
218
+ const animation =
219
+ spec.animation === 'spring' ? Animated.spring : Animated.timing;
208
220
 
209
- const spec = closing ? transitionSpec.close : transitionSpec.open;
221
+ clearTimeout(pendingGestureCallbackRef.current);
210
222
 
211
- const animation =
212
- spec.animation === 'spring' ? Animated.spring : Animated.timing;
223
+ if (animationHandleRef.current !== undefined) {
224
+ cancelAnimationFrame(animationHandleRef.current);
225
+ }
213
226
 
214
- clearTimeout(this.pendingGestureCallback);
227
+ onTransition?.({
228
+ closing: isClosingParam,
229
+ gesture: velocity !== undefined,
230
+ });
215
231
 
216
- if (this.animationHandle !== undefined) {
217
- cancelAnimationFrame(this.animationHandle);
218
- }
232
+ const onFinish = () => {
233
+ if (isClosingParam) {
234
+ onClose();
235
+ } else {
236
+ onOpen();
237
+ }
219
238
 
220
- onTransition?.({ closing, gesture: velocity !== undefined });
239
+ animationHandleRef.current = requestAnimationFrame(() => {
240
+ if (didInitiallyAnimate.current) {
241
+ // Make sure to re-open screen if it wasn't removed
242
+ forceUpdate();
243
+ }
244
+ });
245
+ };
221
246
 
222
- const onFinish = () => {
223
- if (closing) {
224
- onClose();
247
+ if (animated) {
248
+ onStartInteraction();
249
+ animation(gesture, {
250
+ ...spec.config,
251
+ velocity,
252
+ toValue,
253
+ useNativeDriver,
254
+ isInteraction: false,
255
+ }).start(({ finished }) => {
256
+ onEndInteraction();
257
+ clearTimeout(pendingGestureCallbackRef.current);
258
+
259
+ if (finished) {
260
+ onFinish();
261
+ }
262
+ });
225
263
  } else {
226
- onOpen();
264
+ onFinish();
227
265
  }
266
+ }
267
+ );
228
268
 
229
- this.animationHandle = requestAnimationFrame(() => {
230
- if (this.isCurrentlyMounted) {
231
- // Make sure to re-open screen if it wasn't removed
232
- this.forceUpdate();
233
- }
234
- });
235
- };
269
+ const onGestureStateChange = useLatestCallback(
270
+ ({ nativeEvent }: PanGestureHandlerGestureEvent) => {
271
+ switch (nativeEvent.state) {
272
+ case GestureState.ACTIVE:
273
+ isSwiping.setValue(TRUE);
274
+ onStartInteraction();
275
+ onGestureBegin?.();
276
+ break;
277
+ case GestureState.CANCELLED:
278
+ case GestureState.FAILED: {
279
+ isSwiping.setValue(FALSE);
280
+ onEndInteraction();
281
+
282
+ const velocity =
283
+ gestureDirection === 'vertical' ||
284
+ gestureDirection === 'vertical-inverted'
285
+ ? nativeEvent.velocityY
286
+ : nativeEvent.velocityX;
236
287
 
237
- if (animated) {
238
- this.handleStartInteraction();
288
+ animate({
289
+ closing,
290
+ velocity,
291
+ });
239
292
 
240
- animation(gesture, {
241
- ...spec.config,
242
- velocity,
243
- toValue,
244
- useNativeDriver,
245
- isInteraction: false,
246
- }).start(({ finished }) => {
247
- this.handleEndInteraction();
293
+ onGestureCanceled?.();
294
+ break;
295
+ }
296
+ case GestureState.END: {
297
+ isSwiping.setValue(FALSE);
248
298
 
249
- clearTimeout(this.pendingGestureCallback);
299
+ let distance;
300
+ let translation;
301
+ let velocity;
250
302
 
251
- if (finished) {
252
- onFinish();
303
+ if (
304
+ gestureDirection === 'vertical' ||
305
+ gestureDirection === 'vertical-inverted'
306
+ ) {
307
+ distance = layout.height;
308
+ translation = nativeEvent.translationY;
309
+ velocity = nativeEvent.velocityY;
310
+ } else {
311
+ distance = layout.width;
312
+ translation = nativeEvent.translationX;
313
+ velocity = nativeEvent.velocityX;
314
+ }
315
+
316
+ const shouldClose =
317
+ (translation + velocity * gestureVelocityImpact) *
318
+ getInvertedMultiplier(gestureDirection, direction === 'rtl') >
319
+ distance / 2
320
+ ? velocity !== 0 || translation !== 0
321
+ : closing;
322
+
323
+ animate({ closing: shouldClose, velocity });
324
+
325
+ if (shouldClose) {
326
+ // We call onClose with a delay to make sure that the animation has already started
327
+ // This will make sure that the state update caused by this doesn't affect start of animation
328
+ pendingGestureCallbackRef.current = setTimeout(() => {
329
+ onClose();
330
+
331
+ // Trigger an update after we dispatch the action to remove the screen
332
+ // This will make sure that we check if the screen didn't get removed so we can cancel the animation
333
+ forceUpdate();
334
+ }, 32);
335
+ }
336
+
337
+ onGestureEnd?.();
338
+ break;
253
339
  }
254
- });
255
- } else {
256
- onFinish();
340
+ }
257
341
  }
258
- };
342
+ );
259
343
 
260
- private getAnimateToValue = ({
261
- closing,
262
- layout,
344
+ React.useLayoutEffect(() => {
345
+ layoutAnim.width.setValue(layout.width);
346
+ layoutAnim.height.setValue(layout.height);
347
+ inverted.setValue(
348
+ getInvertedMultiplier(gestureDirection, direction === 'rtl')
349
+ );
350
+ }, [
263
351
  gestureDirection,
264
352
  direction,
265
- preloaded,
266
- }: {
267
- closing?: boolean;
353
+ inverted,
354
+ layoutAnim.width,
355
+ layoutAnim.height,
356
+ layout.width,
357
+ layout.height,
358
+ ]);
359
+
360
+ const previousPropsRef = React.useRef<{
361
+ opening: boolean;
362
+ closing: boolean;
268
363
  layout: Layout;
269
- gestureDirection: GestureDirection;
270
364
  direction: LocaleDirection;
365
+ gestureDirection: GestureDirection;
271
366
  preloaded: boolean;
272
- }) => {
273
- if (!closing && !preloaded) {
274
- return 0;
275
- }
367
+ } | null>(null);
276
368
 
277
- return getDistanceForDirection(
278
- layout,
279
- gestureDirection,
280
- direction === 'rtl'
281
- );
282
- };
369
+ React.useEffect(() => {
370
+ return () => {
371
+ onEndInteraction();
283
372
 
284
- private handleStartInteraction = () => {
285
- if (this.interactionHandle === undefined) {
286
- this.interactionHandle = InteractionManager.createInteractionHandle();
287
- }
288
- };
373
+ if (animationHandleRef.current) {
374
+ cancelAnimationFrame(animationHandleRef.current);
375
+ }
289
376
 
290
- private handleEndInteraction = () => {
291
- if (this.interactionHandle !== undefined) {
292
- InteractionManager.clearInteractionHandle(this.interactionHandle);
293
- this.interactionHandle = undefined;
294
- }
295
- };
377
+ clearTimeout(pendingGestureCallbackRef.current);
378
+ };
296
379
 
297
- private handleGestureStateChange = ({
298
- nativeEvent,
299
- }: PanGestureHandlerGestureEvent) => {
300
- const {
301
- direction,
302
- layout,
303
- onClose,
304
- onGestureBegin,
305
- onGestureCanceled,
306
- onGestureEnd,
307
- gestureDirection,
308
- gestureVelocityImpact,
309
- } = this.props;
310
-
311
- switch (nativeEvent.state) {
312
- case GestureState.ACTIVE:
313
- this.isSwiping.setValue(TRUE);
314
- this.handleStartInteraction();
315
- onGestureBegin?.();
316
- break;
317
- case GestureState.CANCELLED:
318
- case GestureState.FAILED: {
319
- this.isSwiping.setValue(FALSE);
320
- this.handleEndInteraction();
321
-
322
- const velocity =
323
- gestureDirection === 'vertical' ||
324
- gestureDirection === 'vertical-inverted'
325
- ? nativeEvent.velocityY
326
- : nativeEvent.velocityX;
327
-
328
- this.animate({
329
- closing: this.props.closing,
330
- velocity,
331
- });
380
+ // We only want to clean up the animation on unmount
381
+ // eslint-disable-next-line react-hooks/exhaustive-deps
382
+ }, []);
332
383
 
333
- onGestureCanceled?.();
334
- break;
335
- }
336
- case GestureState.END: {
337
- this.isSwiping.setValue(FALSE);
338
-
339
- let distance;
340
- let translation;
341
- let velocity;
342
-
343
- if (
344
- gestureDirection === 'vertical' ||
345
- gestureDirection === 'vertical-inverted'
346
- ) {
347
- distance = layout.height;
348
- translation = nativeEvent.translationY;
349
- velocity = nativeEvent.velocityY;
350
- } else {
351
- distance = layout.width;
352
- translation = nativeEvent.translationX;
353
- velocity = nativeEvent.velocityX;
354
- }
384
+ const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
355
385
 
356
- const closing =
357
- (translation + velocity * gestureVelocityImpact) *
358
- getInvertedMultiplier(gestureDirection, direction === 'rtl') >
359
- distance / 2
360
- ? velocity !== 0 || translation !== 0
361
- : this.props.closing;
362
-
363
- this.animate({ closing, velocity });
364
-
365
- if (closing) {
366
- // We call onClose with a delay to make sure that the animation has already started
367
- // This will make sure that the state update caused by this doesn't affect start of animation
368
- this.pendingGestureCallback = setTimeout(() => {
369
- onClose();
370
-
371
- // Trigger an update after we dispatch the action to remove the screen
372
- // This will make sure that we check if the screen didn't get removed so we can cancel the animation
373
- this.forceUpdate();
374
- }, 32) as any as number;
375
- }
386
+ React.useEffect(() => {
387
+ if (preloaded) {
388
+ return;
389
+ }
376
390
 
377
- onGestureEnd?.();
378
- break;
391
+ if (!didInitiallyAnimate.current) {
392
+ // Animate the card in on initial mount
393
+ // Wrap in setTimeout to ensure animation starts after
394
+ // rending of the screen is done. This is especially important
395
+ // in the new architecture
396
+ // cf., https://github.com/react-navigation/react-navigation/issues/12401
397
+ if (timeoutRef.current) {
398
+ clearTimeout(timeoutRef.current);
399
+ }
400
+ timeoutRef.current = setTimeout(() => {
401
+ didInitiallyAnimate.current = true;
402
+ animate({ closing });
403
+ }, 0);
404
+ } else {
405
+ const previousOpening = previousPropsRef.current?.opening;
406
+ const previousToValue = previousPropsRef.current
407
+ ? getAnimateToValue(previousPropsRef.current)
408
+ : null;
409
+
410
+ const toValue = getAnimateToValue({
411
+ closing,
412
+ layout,
413
+ gestureDirection,
414
+ direction,
415
+ preloaded,
416
+ });
417
+
418
+ if (previousToValue !== toValue || lastToValueRef.current !== toValue) {
419
+ // We need to trigger the animation when route was closed
420
+ // The route might have been closed by a `POP` action or by a gesture
421
+ // When route was closed due to a gesture, the animation would've happened already
422
+ // It's still important to trigger the animation so that `onClose` is called
423
+ // If `onClose` is not called, cleanup step won't be performed for gestures
424
+ animate({ closing });
425
+ } else if (
426
+ typeof previousOpening === 'boolean' &&
427
+ opening &&
428
+ !previousOpening
429
+ ) {
430
+ // This can happen when screen somewhere below in the stack comes into focus via rearranging
431
+ // Also reset the animated value to make sure that the animation starts from the beginning
432
+ gesture.setValue(
433
+ getDistanceForDirection(layout, gestureDirection, direction === 'rtl')
434
+ );
435
+
436
+ animate({ closing });
379
437
  }
380
438
  }
381
- };
382
439
 
383
- // Memoize this to avoid extra work on re-render
384
- private getInterpolatedStyle = memoize(
385
- (
386
- styleInterpolator: StackCardStyleInterpolator,
387
- animation: StackCardInterpolationProps
388
- ) => styleInterpolator(animation)
389
- );
440
+ previousPropsRef.current = {
441
+ opening,
442
+ closing,
443
+ layout,
444
+ gestureDirection,
445
+ direction,
446
+ preloaded,
447
+ };
448
+ }, [
449
+ animate,
450
+ closing,
451
+ direction,
452
+ gesture,
453
+ gestureDirection,
454
+ layout,
455
+ opening,
456
+ preloaded,
457
+ ]);
390
458
 
391
- // Keep track of the animation context when deps changes.
392
- private getCardAnimation = memoize(
393
- (
394
- interpolationIndex: number,
395
- current: Animated.AnimatedInterpolation<number>,
396
- next: Animated.AnimatedInterpolation<number> | undefined,
397
- layout: Layout,
398
- insetTop: number,
399
- insetRight: number,
400
- insetBottom: number,
401
- insetLeft: number
402
- ) => ({
459
+ const interpolationProps = React.useMemo(
460
+ () => ({
403
461
  index: interpolationIndex,
404
462
  current: { progress: current },
405
463
  next: next && { progress: next },
406
- closing: this.isClosing,
407
- swiping: this.isSwiping,
408
- inverted: this.inverted,
464
+ closing: isClosing,
465
+ swiping: isSwiping,
466
+ inverted,
409
467
  layouts: {
410
468
  screen: layout,
411
469
  },
412
470
  insets: {
413
- top: insetTop,
414
- right: insetRight,
415
- bottom: insetBottom,
416
- left: insetLeft,
471
+ top: insets.top,
472
+ right: insets.right,
473
+ bottom: insets.bottom,
474
+ left: insets.left,
417
475
  },
418
- })
419
- );
420
-
421
- private gestureActivationCriteria() {
422
- const { direction, layout, gestureDirection, gestureResponseDistance } =
423
- this.props;
424
- const enableTrackpadTwoFingerGesture = true;
425
-
426
- const distance =
427
- gestureResponseDistance !== undefined
428
- ? gestureResponseDistance
429
- : gestureDirection === 'vertical' ||
430
- gestureDirection === 'vertical-inverted'
431
- ? GESTURE_RESPONSE_DISTANCE_VERTICAL
432
- : GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
433
-
434
- if (gestureDirection === 'vertical') {
435
- return {
436
- maxDeltaX: 15,
437
- minOffsetY: 5,
438
- hitSlop: { bottom: -layout.height + distance },
439
- enableTrackpadTwoFingerGesture,
440
- };
441
- } else if (gestureDirection === 'vertical-inverted') {
442
- return {
443
- maxDeltaX: 15,
444
- minOffsetY: -5,
445
- hitSlop: { top: -layout.height + distance },
446
- enableTrackpadTwoFingerGesture,
447
- };
448
- } else {
449
- const hitSlop = -layout.width + distance;
450
- const invertedMultiplier = getInvertedMultiplier(
451
- gestureDirection,
452
- direction === 'rtl'
453
- );
454
-
455
- if (invertedMultiplier === 1) {
456
- return {
457
- minOffsetX: 5,
458
- maxDeltaY: 20,
459
- hitSlop: { right: hitSlop },
460
- enableTrackpadTwoFingerGesture,
461
- };
462
- } else {
463
- return {
464
- minOffsetX: -5,
465
- maxDeltaY: 20,
466
- hitSlop: { left: hitSlop },
467
- enableTrackpadTwoFingerGesture,
468
- };
469
- }
470
- }
471
- }
472
-
473
- render() {
474
- const {
475
- styleInterpolator,
476
- interpolationIndex,
477
- current,
478
- gesture,
479
- next,
480
- layout,
481
- insets,
482
- overlay,
483
- overlayEnabled,
484
- shadowEnabled,
485
- gestureEnabled,
486
- gestureDirection,
487
- pageOverflowEnabled,
488
- children,
489
- containerStyle: customContainerStyle,
490
- contentStyle,
491
- } = this.props;
492
-
493
- const interpolationProps = this.getCardAnimation(
476
+ }),
477
+ [
494
478
  interpolationIndex,
495
479
  current,
496
480
  next,
481
+ isClosing,
482
+ isSwiping,
483
+ inverted,
497
484
  layout,
498
485
  insets.top,
499
486
  insets.right,
500
487
  insets.bottom,
501
- insets.left
502
- );
488
+ insets.left,
489
+ ]
490
+ );
503
491
 
504
- const interpolatedStyle = this.getInterpolatedStyle(
505
- styleInterpolator,
506
- interpolationProps
492
+ const { containerStyle, cardStyle, overlayStyle, shadowStyle } =
493
+ React.useMemo(
494
+ () => styleInterpolator(interpolationProps),
495
+ [styleInterpolator, interpolationProps]
507
496
  );
508
497
 
509
- const { containerStyle, cardStyle, overlayStyle, shadowStyle } =
510
- interpolatedStyle;
511
-
512
- const handleGestureEvent = gestureEnabled
513
- ? Animated.event(
514
- [
515
- {
516
- nativeEvent:
517
- gestureDirection === 'vertical' ||
518
- gestureDirection === 'vertical-inverted'
519
- ? { translationY: gesture }
520
- : { translationX: gesture },
521
- },
522
- ],
523
- { useNativeDriver }
524
- )
525
- : undefined;
526
-
527
- const { backgroundColor } = StyleSheet.flatten(contentStyle || {});
528
- const isTransparent =
529
- typeof backgroundColor === 'string'
530
- ? Color(backgroundColor).alpha() === 0
531
- : false;
532
-
533
- return (
534
- <CardAnimationContext.Provider value={interpolationProps}>
535
- {Platform.OS !== 'web' ? (
536
- <Animated.View
537
- style={{
538
- // This is a dummy style that doesn't actually change anything visually.
539
- // Animated needs the animated value to be used somewhere, otherwise things don't update properly.
540
- // If we disable animations and hide header, it could end up making the value unused.
541
- // So we have this dummy style that will always be used regardless of what else changed.
542
- opacity: current,
543
- }}
544
- // Make sure that this view isn't removed. If this view is removed, our style with animated value won't apply
545
- collapsable={false}
546
- />
547
- ) : null}
548
- {overlayEnabled ? (
549
- <View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
550
- {overlay({ style: overlayStyle })}
551
- </View>
552
- ) : null}
498
+ const onGestureEvent = React.useMemo(
499
+ () =>
500
+ gestureEnabled
501
+ ? Animated.event(
502
+ [
503
+ {
504
+ nativeEvent:
505
+ gestureDirection === 'vertical' ||
506
+ gestureDirection === 'vertical-inverted'
507
+ ? { translationY: gesture }
508
+ : { translationX: gesture },
509
+ },
510
+ ],
511
+ { useNativeDriver }
512
+ )
513
+ : undefined,
514
+ [gesture, gestureDirection, gestureEnabled]
515
+ );
516
+
517
+ const { backgroundColor } = StyleSheet.flatten(contentStyle || {});
518
+
519
+ const isTransparent =
520
+ typeof backgroundColor === 'string'
521
+ ? Color(backgroundColor).alpha() === 0
522
+ : false;
523
+
524
+ return (
525
+ <CardAnimationContext.Provider value={interpolationProps}>
526
+ {Platform.OS !== 'web' ? (
553
527
  <Animated.View
554
- style={[styles.container, containerStyle, customContainerStyle]}
555
- pointerEvents="box-none"
528
+ style={{
529
+ // This is a dummy style that doesn't actually change anything visually.
530
+ // Animated needs the animated value to be used somewhere, otherwise things don't update properly.
531
+ // If we disable animations and hide header, it could end up making the value unused.
532
+ // So we have this dummy style that will always be used regardless of what else changed.
533
+ opacity: current,
534
+ }}
535
+ // Make sure that this view isn't removed. If this view is removed, our style with animated value won't apply
536
+ collapsable={false}
537
+ />
538
+ ) : null}
539
+ {overlayEnabled ? (
540
+ <View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
541
+ {overlay({ style: overlayStyle })}
542
+ </View>
543
+ ) : null}
544
+ <Animated.View
545
+ pointerEvents="box-none"
546
+ style={[styles.container, containerStyle, customContainerStyle]}
547
+ >
548
+ <PanGestureHandler
549
+ enabled={layout.width !== 0 && gestureEnabled}
550
+ onGestureEvent={onGestureEvent}
551
+ onHandlerStateChange={onGestureStateChange}
552
+ {...gestureActivationCriteria({
553
+ layout,
554
+ direction,
555
+ gestureDirection,
556
+ gestureResponseDistance,
557
+ })}
556
558
  >
557
- <PanGestureHandler
558
- enabled={layout.width !== 0 && gestureEnabled}
559
- onGestureEvent={handleGestureEvent}
560
- onHandlerStateChange={this.handleGestureStateChange}
561
- {...this.gestureActivationCriteria()}
559
+ <Animated.View
560
+ pointerEvents="box-none"
561
+ needsOffscreenAlphaCompositing={hasOpacityStyle(cardStyle)}
562
+ style={[styles.container, cardStyle]}
562
563
  >
563
- <Animated.View
564
- needsOffscreenAlphaCompositing={hasOpacityStyle(cardStyle)}
565
- style={[styles.container, cardStyle]}
564
+ {shadowEnabled && shadowStyle && !isTransparent ? (
565
+ <Animated.View
566
+ pointerEvents="none"
567
+ style={[
568
+ styles.shadow,
569
+ gestureDirection === 'horizontal'
570
+ ? [styles.shadowHorizontal, styles.shadowStart]
571
+ : gestureDirection === 'horizontal-inverted'
572
+ ? [styles.shadowHorizontal, styles.shadowEnd]
573
+ : gestureDirection === 'vertical'
574
+ ? [styles.shadowVertical, styles.shadowTop]
575
+ : [styles.shadowVertical, styles.shadowBottom],
576
+ { backgroundColor },
577
+ shadowStyle,
578
+ ]}
579
+ />
580
+ ) : null}
581
+ <CardContent
582
+ enabled={pageOverflowEnabled}
583
+ layout={layout}
584
+ style={contentStyle}
566
585
  >
567
- {shadowEnabled && shadowStyle && !isTransparent ? (
568
- <Animated.View
569
- style={[
570
- styles.shadow,
571
- gestureDirection === 'horizontal'
572
- ? [styles.shadowHorizontal, styles.shadowStart]
573
- : gestureDirection === 'horizontal-inverted'
574
- ? [styles.shadowHorizontal, styles.shadowEnd]
575
- : gestureDirection === 'vertical'
576
- ? [styles.shadowVertical, styles.shadowTop]
577
- : [styles.shadowVertical, styles.shadowBottom],
578
- { backgroundColor },
579
- shadowStyle,
580
- ]}
581
- pointerEvents="none"
582
- />
583
- ) : null}
584
- <CardContent
585
- enabled={pageOverflowEnabled}
586
- layout={layout}
587
- style={contentStyle}
588
- >
589
- {children}
590
- </CardContent>
591
- </Animated.View>
592
- </PanGestureHandler>
593
- </Animated.View>
594
- </CardAnimationContext.Provider>
595
- );
596
- }
586
+ {children}
587
+ </CardContent>
588
+ </Animated.View>
589
+ </PanGestureHandler>
590
+ </Animated.View>
591
+ </CardAnimationContext.Provider>
592
+ );
597
593
  }
598
594
 
595
+ export { Card };
596
+
599
597
  const styles = StyleSheet.create({
600
598
  container: {
601
599
  flex: 1,