@react-navigation/stack 7.6.2 → 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.
- package/lib/module/utils/gestureActivationCriteria.js +60 -0
- package/lib/module/utils/gestureActivationCriteria.js.map +1 -0
- package/lib/module/views/Header/HeaderContainer.js +3 -3
- package/lib/module/views/Header/HeaderContainer.js.map +1 -1
- package/lib/module/views/Stack/Card.js +263 -302
- package/lib/module/views/Stack/Card.js.map +1 -1
- package/lib/module/views/Stack/CardStack.js +5 -8
- package/lib/module/views/Stack/CardStack.js.map +1 -1
- package/lib/typescript/src/utils/gestureActivationCriteria.d.ts +57 -0
- package/lib/typescript/src/utils/gestureActivationCriteria.d.ts.map +1 -0
- package/lib/typescript/src/views/Header/HeaderContainer.d.ts +2 -2
- package/lib/typescript/src/views/Header/HeaderContainer.d.ts.map +1 -1
- package/lib/typescript/src/views/Stack/Card.d.ts +7 -37
- package/lib/typescript/src/views/Stack/Card.d.ts.map +1 -1
- package/lib/typescript/src/views/Stack/CardStack.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/utils/gestureActivationCriteria.tsx +70 -0
- package/src/views/Header/HeaderContainer.tsx +8 -11
- package/src/views/Stack/Card.tsx +451 -453
- package/src/views/Stack/CardStack.tsx +2 -9
- package/lib/module/utils/memoize.js +0 -29
- package/lib/module/utils/memoize.js.map +0 -1
- package/lib/typescript/src/utils/memoize.d.ts +0 -2
- package/lib/typescript/src/utils/memoize.d.ts.map +0 -1
- package/src/utils/memoize.tsx +0 -33
package/src/views/Stack/Card.tsx
CHANGED
|
@@ -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
|
|
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:
|
|
55
|
-
|
|
56
|
-
|
|
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 = (
|
|
81
|
+
const hasOpacityStyle = (
|
|
82
|
+
style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>
|
|
83
|
+
) => {
|
|
86
84
|
if (style) {
|
|
87
85
|
const flattenedStyle = StyleSheet.flatten(style);
|
|
88
|
-
|
|
86
|
+
|
|
87
|
+
return 'opacity' in flattenedStyle && flattenedStyle.opacity != null;
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
return false;
|
|
92
91
|
};
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
178
|
-
width: new Animated.Value(
|
|
179
|
-
height: new Animated.Value(
|
|
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
|
-
|
|
182
|
+
const [isSwiping] = React.useState(() => new Animated.Value(FALSE));
|
|
185
183
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
const onStartInteraction = useLatestCallback(() => {
|
|
185
|
+
if (interactionHandleRef.current === undefined) {
|
|
186
|
+
interactionHandleRef.current =
|
|
187
|
+
InteractionManager.createInteractionHandle();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
189
190
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
214
|
+
lastToValueRef.current = toValue;
|
|
215
|
+
isClosing.setValue(isClosingParam ? TRUE : FALSE);
|
|
206
216
|
|
|
207
|
-
|
|
217
|
+
const spec = isClosingParam ? transitionSpec.close : transitionSpec.open;
|
|
218
|
+
const animation =
|
|
219
|
+
spec.animation === 'spring' ? Animated.spring : Animated.timing;
|
|
208
220
|
|
|
209
|
-
|
|
221
|
+
clearTimeout(pendingGestureCallbackRef.current);
|
|
210
222
|
|
|
211
|
-
|
|
212
|
-
|
|
223
|
+
if (animationHandleRef.current !== undefined) {
|
|
224
|
+
cancelAnimationFrame(animationHandleRef.current);
|
|
225
|
+
}
|
|
213
226
|
|
|
214
|
-
|
|
227
|
+
onTransition?.({
|
|
228
|
+
closing: isClosingParam,
|
|
229
|
+
gesture: velocity !== undefined,
|
|
230
|
+
});
|
|
215
231
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
232
|
+
const onFinish = () => {
|
|
233
|
+
if (isClosingParam) {
|
|
234
|
+
onClose();
|
|
235
|
+
} else {
|
|
236
|
+
onOpen();
|
|
237
|
+
}
|
|
219
238
|
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
264
|
+
onFinish();
|
|
227
265
|
}
|
|
266
|
+
}
|
|
267
|
+
);
|
|
228
268
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
238
|
-
|
|
288
|
+
animate({
|
|
289
|
+
closing,
|
|
290
|
+
velocity,
|
|
291
|
+
});
|
|
239
292
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
299
|
+
let distance;
|
|
300
|
+
let translation;
|
|
301
|
+
let velocity;
|
|
250
302
|
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
direction === 'rtl'
|
|
281
|
-
);
|
|
282
|
-
};
|
|
369
|
+
React.useEffect(() => {
|
|
370
|
+
return () => {
|
|
371
|
+
onEndInteraction();
|
|
283
372
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
};
|
|
373
|
+
if (animationHandleRef.current) {
|
|
374
|
+
cancelAnimationFrame(animationHandleRef.current);
|
|
375
|
+
}
|
|
289
376
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
InteractionManager.clearInteractionHandle(this.interactionHandle);
|
|
293
|
-
this.interactionHandle = undefined;
|
|
294
|
-
}
|
|
295
|
-
};
|
|
377
|
+
clearTimeout(pendingGestureCallbackRef.current);
|
|
378
|
+
};
|
|
296
379
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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:
|
|
407
|
-
swiping:
|
|
408
|
-
inverted
|
|
464
|
+
closing: isClosing,
|
|
465
|
+
swiping: isSwiping,
|
|
466
|
+
inverted,
|
|
409
467
|
layouts: {
|
|
410
468
|
screen: layout,
|
|
411
469
|
},
|
|
412
470
|
insets: {
|
|
413
|
-
top:
|
|
414
|
-
right:
|
|
415
|
-
bottom:
|
|
416
|
-
left:
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
interpolationProps
|
|
492
|
+
const { containerStyle, cardStyle, overlayStyle, shadowStyle } =
|
|
493
|
+
React.useMemo(
|
|
494
|
+
() => styleInterpolator(interpolationProps),
|
|
495
|
+
[styleInterpolator, interpolationProps]
|
|
507
496
|
);
|
|
508
497
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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={
|
|
555
|
-
|
|
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
|
-
<
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
{...this.gestureActivationCriteria()}
|
|
559
|
+
<Animated.View
|
|
560
|
+
pointerEvents="box-none"
|
|
561
|
+
needsOffscreenAlphaCompositing={hasOpacityStyle(cardStyle)}
|
|
562
|
+
style={[styles.container, cardStyle]}
|
|
562
563
|
>
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
{
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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,
|