UNPKG

16.5 kBJavaScriptView Raw
1function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
2
3function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
4
5import Color from 'color';
6import * as React from 'react';
7import { Animated, InteractionManager, Platform, StyleSheet, View } from 'react-native';
8import { forModalPresentationIOS } from '../../TransitionConfigs/CardStyleInterpolators';
9import CardAnimationContext from '../../utils/CardAnimationContext';
10import getDistanceForDirection from '../../utils/getDistanceForDirection';
11import getInvertedMultiplier from '../../utils/getInvertedMultiplier';
12import memoize from '../../utils/memoize';
13import { GestureState, PanGestureHandler } from '../GestureHandler';
14import ModalStatusBarManager from '../ModalStatusBarManager';
15import CardSheet from './CardSheet';
16const GESTURE_VELOCITY_IMPACT = 0.3;
17const TRUE = 1;
18const FALSE = 0;
19/**
20 * The distance of touch start from the edge of the screen where the gesture will be recognized
21 */
22
23const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 50;
24const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
25const useNativeDriver = Platform.OS !== 'web';
26
27const hasOpacityStyle = style => {
28 if (style) {
29 const flattenedStyle = StyleSheet.flatten(style);
30 return flattenedStyle.opacity != null;
31 }
32
33 return false;
34};
35
36export default class Card extends React.Component {
37 constructor() {
38 super(...arguments);
39
40 _defineProperty(this, "isCurrentlyMounted", false);
41
42 _defineProperty(this, "isClosing", new Animated.Value(FALSE));
43
44 _defineProperty(this, "inverted", new Animated.Value(getInvertedMultiplier(this.props.gestureDirection)));
45
46 _defineProperty(this, "layout", {
47 width: new Animated.Value(this.props.layout.width),
48 height: new Animated.Value(this.props.layout.height)
49 });
50
51 _defineProperty(this, "isSwiping", new Animated.Value(FALSE));
52
53 _defineProperty(this, "interactionHandle", void 0);
54
55 _defineProperty(this, "pendingGestureCallback", void 0);
56
57 _defineProperty(this, "lastToValue", void 0);
58
59 _defineProperty(this, "animate", _ref => {
60 let {
61 closing,
62 velocity
63 } = _ref;
64 const {
65 gesture,
66 transitionSpec,
67 onOpen,
68 onClose,
69 onTransition
70 } = this.props;
71 const toValue = this.getAnimateToValue({ ...this.props,
72 closing
73 });
74 this.lastToValue = toValue;
75 this.isClosing.setValue(closing ? TRUE : FALSE);
76 const spec = closing ? transitionSpec.close : transitionSpec.open;
77 const animation = spec.animation === 'spring' ? Animated.spring : Animated.timing;
78 this.setPointerEventsEnabled(!closing);
79 this.handleStartInteraction();
80 clearTimeout(this.pendingGestureCallback);
81 onTransition === null || onTransition === void 0 ? void 0 : onTransition({
82 closing,
83 gesture: velocity !== undefined
84 });
85 animation(gesture, { ...spec.config,
86 velocity,
87 toValue,
88 useNativeDriver,
89 isInteraction: false
90 }).start(_ref2 => {
91 let {
92 finished
93 } = _ref2;
94 this.handleEndInteraction();
95 clearTimeout(this.pendingGestureCallback);
96
97 if (finished) {
98 if (closing) {
99 onClose();
100 } else {
101 onOpen();
102 }
103
104 if (this.isCurrentlyMounted) {
105 // Make sure to re-open screen if it wasn't removed
106 this.forceUpdate();
107 }
108 }
109 });
110 });
111
112 _defineProperty(this, "getAnimateToValue", _ref3 => {
113 let {
114 closing,
115 layout,
116 gestureDirection
117 } = _ref3;
118
119 if (!closing) {
120 return 0;
121 }
122
123 return getDistanceForDirection(layout, gestureDirection);
124 });
125
126 _defineProperty(this, "setPointerEventsEnabled", enabled => {
127 var _this$contentRef$curr;
128
129 const pointerEvents = enabled ? 'box-none' : 'none';
130 (_this$contentRef$curr = this.contentRef.current) === null || _this$contentRef$curr === void 0 ? void 0 : _this$contentRef$curr.setNativeProps({
131 pointerEvents
132 });
133 });
134
135 _defineProperty(this, "handleStartInteraction", () => {
136 if (this.interactionHandle === undefined) {
137 this.interactionHandle = InteractionManager.createInteractionHandle();
138 }
139 });
140
141 _defineProperty(this, "handleEndInteraction", () => {
142 if (this.interactionHandle !== undefined) {
143 InteractionManager.clearInteractionHandle(this.interactionHandle);
144 this.interactionHandle = undefined;
145 }
146 });
147
148 _defineProperty(this, "handleGestureStateChange", _ref4 => {
149 let {
150 nativeEvent
151 } = _ref4;
152 const {
153 layout,
154 onClose,
155 onGestureBegin,
156 onGestureCanceled,
157 onGestureEnd,
158 gestureDirection,
159 gestureVelocityImpact
160 } = this.props;
161
162 switch (nativeEvent.state) {
163 case GestureState.ACTIVE:
164 this.isSwiping.setValue(TRUE);
165 this.handleStartInteraction();
166 onGestureBegin === null || onGestureBegin === void 0 ? void 0 : onGestureBegin();
167 break;
168
169 case GestureState.CANCELLED:
170 {
171 this.isSwiping.setValue(FALSE);
172 this.handleEndInteraction();
173 const velocity = gestureDirection === 'vertical' || gestureDirection === 'vertical-inverted' ? nativeEvent.velocityY : nativeEvent.velocityX;
174 this.animate({
175 closing: this.props.closing,
176 velocity
177 });
178 onGestureCanceled === null || onGestureCanceled === void 0 ? void 0 : onGestureCanceled();
179 break;
180 }
181
182 case GestureState.END:
183 {
184 this.isSwiping.setValue(FALSE);
185 let distance;
186 let translation;
187 let velocity;
188
189 if (gestureDirection === 'vertical' || gestureDirection === 'vertical-inverted') {
190 distance = layout.height;
191 translation = nativeEvent.translationY;
192 velocity = nativeEvent.velocityY;
193 } else {
194 distance = layout.width;
195 translation = nativeEvent.translationX;
196 velocity = nativeEvent.velocityX;
197 }
198
199 const closing = (translation + velocity * gestureVelocityImpact) * getInvertedMultiplier(gestureDirection) > distance / 2 ? velocity !== 0 || translation !== 0 : this.props.closing;
200 this.animate({
201 closing,
202 velocity
203 });
204
205 if (closing) {
206 // We call onClose with a delay to make sure that the animation has already started
207 // This will make sure that the state update caused by this doesn't affect start of animation
208 this.pendingGestureCallback = setTimeout(() => {
209 onClose(); // Trigger an update after we dispatch the action to remove the screen
210 // This will make sure that we check if the screen didn't get removed so we can cancel the animation
211
212 this.forceUpdate();
213 }, 32);
214 }
215
216 onGestureEnd === null || onGestureEnd === void 0 ? void 0 : onGestureEnd();
217 break;
218 }
219 }
220 });
221
222 _defineProperty(this, "getInterpolatedStyle", memoize((styleInterpolator, animation) => styleInterpolator(animation)));
223
224 _defineProperty(this, "getCardAnimation", memoize((interpolationIndex, current, next, layout, insetTop, insetRight, insetBottom, insetLeft) => ({
225 index: interpolationIndex,
226 current: {
227 progress: current
228 },
229 next: next && {
230 progress: next
231 },
232 closing: this.isClosing,
233 swiping: this.isSwiping,
234 inverted: this.inverted,
235 layouts: {
236 screen: layout
237 },
238 insets: {
239 top: insetTop,
240 right: insetRight,
241 bottom: insetBottom,
242 left: insetLeft
243 }
244 })));
245
246 _defineProperty(this, "contentRef", /*#__PURE__*/React.createRef());
247 }
248
249 componentDidMount() {
250 this.animate({
251 closing: this.props.closing
252 });
253 this.isCurrentlyMounted = true;
254 }
255
256 componentDidUpdate(prevProps) {
257 const {
258 layout,
259 gestureDirection,
260 closing
261 } = this.props;
262 const {
263 width,
264 height
265 } = layout;
266
267 if (width !== prevProps.layout.width) {
268 this.layout.width.setValue(width);
269 }
270
271 if (height !== prevProps.layout.height) {
272 this.layout.height.setValue(height);
273 }
274
275 if (gestureDirection !== prevProps.gestureDirection) {
276 this.inverted.setValue(getInvertedMultiplier(gestureDirection));
277 }
278
279 const toValue = this.getAnimateToValue(this.props);
280
281 if (this.getAnimateToValue(prevProps) !== toValue || this.lastToValue !== toValue) {
282 // We need to trigger the animation when route was closed
283 // Thr route might have been closed by a `POP` action or by a gesture
284 // When route was closed due to a gesture, the animation would've happened already
285 // It's still important to trigger the animation so that `onClose` is called
286 // If `onClose` is not called, cleanup step won't be performed for gestures
287 this.animate({
288 closing
289 });
290 }
291 }
292
293 componentWillUnmount() {
294 this.props.gesture.stopAnimation();
295 this.isCurrentlyMounted = false;
296 this.handleEndInteraction();
297 }
298
299 gestureActivationCriteria() {
300 const {
301 layout,
302 gestureDirection,
303 gestureResponseDistance
304 } = this.props;
305 const enableTrackpadTwoFingerGesture = true;
306 const distance = gestureResponseDistance !== undefined ? gestureResponseDistance : gestureDirection === 'vertical' || gestureDirection === 'vertical-inverted' ? GESTURE_RESPONSE_DISTANCE_VERTICAL : GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
307
308 if (gestureDirection === 'vertical') {
309 return {
310 maxDeltaX: 15,
311 minOffsetY: 5,
312 hitSlop: {
313 bottom: -layout.height + distance
314 },
315 enableTrackpadTwoFingerGesture
316 };
317 } else if (gestureDirection === 'vertical-inverted') {
318 return {
319 maxDeltaX: 15,
320 minOffsetY: -5,
321 hitSlop: {
322 top: -layout.height + distance
323 },
324 enableTrackpadTwoFingerGesture
325 };
326 } else {
327 const hitSlop = -layout.width + distance;
328 const invertedMultiplier = getInvertedMultiplier(gestureDirection);
329
330 if (invertedMultiplier === 1) {
331 return {
332 minOffsetX: 5,
333 maxDeltaY: 20,
334 hitSlop: {
335 right: hitSlop
336 },
337 enableTrackpadTwoFingerGesture
338 };
339 } else {
340 return {
341 minOffsetX: -5,
342 maxDeltaY: 20,
343 hitSlop: {
344 left: hitSlop
345 },
346 enableTrackpadTwoFingerGesture
347 };
348 }
349 }
350 }
351
352 render() {
353 const {
354 styleInterpolator,
355 interpolationIndex,
356 current,
357 gesture,
358 next,
359 layout,
360 insets,
361 overlay,
362 overlayEnabled,
363 shadowEnabled,
364 gestureEnabled,
365 gestureDirection,
366 pageOverflowEnabled,
367 headerDarkContent,
368 children,
369 containerStyle: customContainerStyle,
370 contentStyle,
371 ...rest
372 } = this.props;
373 const interpolationProps = this.getCardAnimation(interpolationIndex, current, next, layout, insets.top, insets.right, insets.bottom, insets.left);
374 const interpolatedStyle = this.getInterpolatedStyle(styleInterpolator, interpolationProps);
375 const {
376 containerStyle,
377 cardStyle,
378 overlayStyle,
379 shadowStyle
380 } = interpolatedStyle;
381 const handleGestureEvent = gestureEnabled ? Animated.event([{
382 nativeEvent: gestureDirection === 'vertical' || gestureDirection === 'vertical-inverted' ? {
383 translationY: gesture
384 } : {
385 translationX: gesture
386 }
387 }], {
388 useNativeDriver
389 }) : undefined;
390 const {
391 backgroundColor
392 } = StyleSheet.flatten(contentStyle || {});
393 const isTransparent = typeof backgroundColor === 'string' ? Color(backgroundColor).alpha() === 0 : false;
394 return /*#__PURE__*/React.createElement(CardAnimationContext.Provider, {
395 value: interpolationProps
396 }, // StatusBar messes with translucent status bar on Android
397 // So we should only enable it on iOS
398 Platform.OS === 'ios' && overlayEnabled && next && getIsModalPresentation(styleInterpolator) ? /*#__PURE__*/React.createElement(ModalStatusBarManager, {
399 dark: headerDarkContent,
400 layout: layout,
401 insets: insets,
402 style: cardStyle
403 }) : null, /*#__PURE__*/React.createElement(Animated.View, {
404 style: {
405 // This is a dummy style that doesn't actually change anything visually.
406 // Animated needs the animated value to be used somewhere, otherwise things don't update properly.
407 // If we disable animations and hide header, it could end up making the value unused.
408 // So we have this dummy style that will always be used regardless of what else changed.
409 opacity: current
410 } // Make sure that this view isn't removed. If this view is removed, our style with animated value won't apply
411 ,
412 collapsable: false
413 }), /*#__PURE__*/React.createElement(View, _extends({
414 pointerEvents: "box-none"
415 }, rest), overlayEnabled ? /*#__PURE__*/React.createElement(View, {
416 pointerEvents: "box-none",
417 style: StyleSheet.absoluteFill
418 }, overlay({
419 style: overlayStyle
420 })) : null, /*#__PURE__*/React.createElement(Animated.View, {
421 style: [styles.container, containerStyle, customContainerStyle],
422 pointerEvents: "box-none"
423 }, /*#__PURE__*/React.createElement(PanGestureHandler, _extends({
424 enabled: layout.width !== 0 && gestureEnabled,
425 onGestureEvent: handleGestureEvent,
426 onHandlerStateChange: this.handleGestureStateChange
427 }, this.gestureActivationCriteria()), /*#__PURE__*/React.createElement(Animated.View, {
428 needsOffscreenAlphaCompositing: hasOpacityStyle(cardStyle),
429 style: [styles.container, cardStyle]
430 }, shadowEnabled && shadowStyle && !isTransparent ? /*#__PURE__*/React.createElement(Animated.View, {
431 style: [styles.shadow, gestureDirection === 'horizontal' ? [styles.shadowHorizontal, styles.shadowLeft] : gestureDirection === 'horizontal-inverted' ? [styles.shadowHorizontal, styles.shadowRight] : gestureDirection === 'vertical' ? [styles.shadowVertical, styles.shadowTop] : [styles.shadowVertical, styles.shadowBottom], {
432 backgroundColor
433 }, shadowStyle],
434 pointerEvents: "none"
435 }) : null, /*#__PURE__*/React.createElement(CardSheet, {
436 ref: this.contentRef,
437 enabled: pageOverflowEnabled,
438 layout: layout,
439 style: contentStyle
440 }, children))))));
441 }
442
443}
444
445_defineProperty(Card, "defaultProps", {
446 shadowEnabled: false,
447 gestureEnabled: true,
448 gestureVelocityImpact: GESTURE_VELOCITY_IMPACT,
449 overlay: _ref5 => {
450 let {
451 style
452 } = _ref5;
453 return style ? /*#__PURE__*/React.createElement(Animated.View, {
454 pointerEvents: "none",
455 style: [styles.overlay, style]
456 }) : null;
457 }
458});
459
460export const getIsModalPresentation = cardStyleInterpolator => {
461 return cardStyleInterpolator === forModalPresentationIOS || // Handle custom modal presentation interpolators as well
462 cardStyleInterpolator.name === 'forModalPresentationIOS';
463};
464const styles = StyleSheet.create({
465 container: {
466 flex: 1
467 },
468 overlay: {
469 flex: 1,
470 backgroundColor: '#000'
471 },
472 shadow: {
473 position: 'absolute',
474 shadowRadius: 5,
475 shadowColor: '#000',
476 shadowOpacity: 0.3
477 },
478 shadowHorizontal: {
479 top: 0,
480 bottom: 0,
481 width: 3,
482 shadowOffset: {
483 width: -1,
484 height: 1
485 }
486 },
487 shadowLeft: {
488 left: 0
489 },
490 shadowRight: {
491 right: 0
492 },
493 shadowVertical: {
494 left: 0,
495 right: 0,
496 height: 3,
497 shadowOffset: {
498 width: 1,
499 height: -1
500 }
501 },
502 shadowTop: {
503 top: 0
504 },
505 shadowBottom: {
506 bottom: 0
507 }
508});
509//# sourceMappingURL=Card.js.map
\No newline at end of file