mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-02-06 22:46:01 +08:00
905 lines
24 KiB
TypeScript
Executable File
905 lines
24 KiB
TypeScript
Executable File
import * as React from 'react';
|
|
import {
|
|
View,
|
|
I18nManager,
|
|
StyleSheet,
|
|
ViewProps,
|
|
StyleProp,
|
|
ViewStyle,
|
|
Platform,
|
|
InteractionManager,
|
|
} from 'react-native';
|
|
import Animated from 'react-native-reanimated';
|
|
import {
|
|
PanGestureHandler,
|
|
State as GestureState,
|
|
} from 'react-native-gesture-handler';
|
|
import {
|
|
TransitionSpec,
|
|
CardStyleInterpolator,
|
|
Layout,
|
|
SpringConfig,
|
|
TimingConfig,
|
|
} from '../../types';
|
|
import memoize from '../../utils/memoize';
|
|
import StackGestureContext from '../../utils/StackGestureContext';
|
|
import PointerEventsView from './PointerEventsView';
|
|
|
|
type Props = ViewProps & {
|
|
index: number;
|
|
active: boolean;
|
|
closing?: boolean;
|
|
transparent?: boolean;
|
|
next?: Animated.Node<number>;
|
|
current: Animated.Value<number>;
|
|
layout: Layout;
|
|
gestureDirection: 'horizontal' | 'vertical' | 'vertical-inverted';
|
|
onOpen: (isFinished: boolean) => void;
|
|
onClose: (isFinished: boolean) => void;
|
|
onTransitionStart?: (props: { closing: boolean }) => void;
|
|
onGestureBegin?: () => void;
|
|
onGestureCanceled?: () => void;
|
|
onGestureEnd?: () => void;
|
|
children: React.ReactNode;
|
|
overlayEnabled: boolean;
|
|
shadowEnabled: boolean;
|
|
gestureEnabled: boolean;
|
|
gestureResponseDistance?: {
|
|
vertical?: number;
|
|
horizontal?: number;
|
|
};
|
|
transitionSpec: {
|
|
open: TransitionSpec;
|
|
close: TransitionSpec;
|
|
};
|
|
styleInterpolator: CardStyleInterpolator;
|
|
containerStyle?: StyleProp<ViewStyle>;
|
|
contentStyle?: StyleProp<ViewStyle>;
|
|
};
|
|
|
|
type AnimatedSpringConfig = {
|
|
damping: Animated.Value<number>;
|
|
mass: Animated.Value<number>;
|
|
stiffness: Animated.Value<number>;
|
|
restSpeedThreshold: Animated.Value<number>;
|
|
restDisplacementThreshold: Animated.Value<number>;
|
|
overshootClamping: Animated.Value<boolean>;
|
|
};
|
|
|
|
export type AnimatedTimingConfig = {
|
|
duration: Animated.Value<number>;
|
|
easing: Animated.EasingFunction;
|
|
};
|
|
|
|
type Binary = 0 | 1;
|
|
|
|
const TRUE = 1;
|
|
const TRUE_NODE = new Animated.Value(TRUE);
|
|
const FALSE = 0;
|
|
const FALSE_NODE = new Animated.Value(FALSE);
|
|
const NOOP_NODE = FALSE_NODE;
|
|
const UNSET = -1;
|
|
const UNSET_NODE = new Animated.Value(UNSET);
|
|
|
|
const MINUS_ONE_NODE = UNSET_NODE;
|
|
|
|
const TOP = -1;
|
|
const BOTTOM = 1;
|
|
|
|
const DIRECTION_VERTICAL = -1;
|
|
const DIRECTION_HORIZONTAL = 1;
|
|
|
|
const SWIPE_VELOCITY_IMPACT = 0.3;
|
|
|
|
/**
|
|
* The distance of touch start from the edge of the screen where the gesture will be recognized
|
|
*/
|
|
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 50;
|
|
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
|
|
|
|
const {
|
|
abs,
|
|
add,
|
|
block,
|
|
call,
|
|
cond,
|
|
divide,
|
|
eq,
|
|
greaterThan,
|
|
lessThan,
|
|
max,
|
|
min,
|
|
multiply,
|
|
neq,
|
|
onChange,
|
|
set,
|
|
spring,
|
|
sub,
|
|
timing,
|
|
startClock,
|
|
stopClock,
|
|
clockRunning,
|
|
Clock,
|
|
Value,
|
|
} = Animated;
|
|
|
|
// We need to be prepared for both version of reanimated. With and w/out proc
|
|
let memoizedSpring = spring;
|
|
// @ts-ignore
|
|
if (Animated.proc) {
|
|
// @ts-ignore
|
|
const springHelper = Animated.proc(
|
|
(
|
|
finished: Animated.Value<number>,
|
|
velocity: Animated.Value<number>,
|
|
position: Animated.Value<number>,
|
|
time: Animated.Value<number>,
|
|
prevPosition: Animated.Value<number>,
|
|
toValue: Animated.Adaptable<number>,
|
|
damping: Animated.Adaptable<number>,
|
|
mass: Animated.Adaptable<number>,
|
|
stiffness: Animated.Adaptable<number>,
|
|
overshootClamping: Animated.Adaptable<boolean>,
|
|
restSpeedThreshold: Animated.Adaptable<number>,
|
|
restDisplacementThreshold: Animated.Adaptable<number>,
|
|
clock: Animated.Clock
|
|
) =>
|
|
spring(
|
|
clock,
|
|
{
|
|
finished,
|
|
velocity,
|
|
position,
|
|
time,
|
|
// @ts-ignore
|
|
prevPosition,
|
|
},
|
|
{
|
|
toValue,
|
|
damping,
|
|
mass,
|
|
stiffness,
|
|
overshootClamping,
|
|
restDisplacementThreshold,
|
|
restSpeedThreshold,
|
|
}
|
|
)
|
|
);
|
|
|
|
// @ts-ignore
|
|
memoizedSpring = function(
|
|
clock: Animated.Clock,
|
|
state: {
|
|
finished: Animated.Value<number>;
|
|
velocity: Animated.Value<number>;
|
|
position: Animated.Value<number>;
|
|
time: Animated.Value<number>;
|
|
},
|
|
config: {
|
|
toValue: Animated.Adaptable<number>;
|
|
damping: Animated.Adaptable<number>;
|
|
mass: Animated.Adaptable<number>;
|
|
stiffness: Animated.Adaptable<number>;
|
|
overshootClamping: Animated.Adaptable<boolean>;
|
|
restSpeedThreshold: Animated.Adaptable<number>;
|
|
restDisplacementThreshold: Animated.Adaptable<number>;
|
|
}
|
|
) {
|
|
return springHelper(
|
|
state.finished,
|
|
state.velocity,
|
|
state.position,
|
|
state.time,
|
|
new Value(0),
|
|
config.toValue,
|
|
config.damping,
|
|
config.mass,
|
|
config.stiffness,
|
|
config.overshootClamping,
|
|
config.restSpeedThreshold,
|
|
config.restDisplacementThreshold,
|
|
clock
|
|
);
|
|
};
|
|
}
|
|
|
|
function transformSpringConfigToAnimatedValues(
|
|
config: SpringConfig
|
|
): AnimatedSpringConfig {
|
|
return {
|
|
damping: new Animated.Value(config.damping),
|
|
stiffness: new Animated.Value(config.stiffness),
|
|
mass: new Animated.Value(config.mass),
|
|
restDisplacementThreshold: new Animated.Value(
|
|
config.restDisplacementThreshold
|
|
),
|
|
restSpeedThreshold: new Animated.Value(config.restSpeedThreshold),
|
|
overshootClamping: new Animated.Value(config.overshootClamping),
|
|
};
|
|
}
|
|
|
|
function transformTimingConfigToAnimatedValues(
|
|
config: TimingConfig
|
|
): AnimatedTimingConfig {
|
|
return {
|
|
duration: new Animated.Value(config.duration),
|
|
easing: config.easing,
|
|
};
|
|
}
|
|
|
|
export default class Card extends React.Component<Props> {
|
|
static defaultProps = {
|
|
overlayEnabled: Platform.OS !== 'ios',
|
|
shadowEnabled: true,
|
|
gestureEnabled: true,
|
|
};
|
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
const { layout, gestureDirection, closing } = this.props;
|
|
const { width, height } = layout;
|
|
|
|
if (width !== prevProps.layout.width) {
|
|
this.layout.width.setValue(width);
|
|
}
|
|
|
|
if (height !== prevProps.layout.height) {
|
|
this.layout.height.setValue(height);
|
|
}
|
|
|
|
if (gestureDirection !== prevProps.gestureDirection) {
|
|
this.direction.setValue(
|
|
gestureDirection === 'vertical' ||
|
|
gestureDirection === 'vertical-inverted'
|
|
? DIRECTION_VERTICAL
|
|
: DIRECTION_HORIZONTAL
|
|
);
|
|
this.verticalGestureDirection.setValue(
|
|
gestureDirection === 'vertical-inverted' ? TOP : BOTTOM
|
|
);
|
|
}
|
|
|
|
if (closing !== prevProps.closing) {
|
|
// If the style updates during render, setting the value here doesn't work
|
|
// We need to defer it a bit so the animation starts properly
|
|
requestAnimationFrame(() =>
|
|
requestAnimationFrame(() =>
|
|
this.isClosing.setValue(closing ? TRUE : FALSE)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount(): void {
|
|
this.handleEndInteraction();
|
|
|
|
// It might sometimes happen than animation will be unmounted
|
|
// during running. However, we need to invoke listener onClose
|
|
// manually in this case
|
|
if (this.isRunningAnimation || this.noAnimationStartedSoFar) {
|
|
if (this.isVisibleValue) {
|
|
this.props.onOpen(false);
|
|
} else {
|
|
this.props.onClose(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private isVisible = new Value<Binary>(TRUE);
|
|
private isVisibleValue: Binary = TRUE;
|
|
private nextIsVisible = new Value<Binary | -1>(UNSET);
|
|
private verticalGestureDirection = new Value(
|
|
this.props.gestureDirection === 'vertical-inverted' ? TOP : BOTTOM
|
|
);
|
|
|
|
private isClosing = new Value<Binary>(FALSE);
|
|
private noAnimationStartedSoFar = true;
|
|
private isRunningAnimation = false;
|
|
|
|
private clock = new Clock();
|
|
|
|
private direction = new Value(
|
|
this.props.gestureDirection === 'vertical' ||
|
|
this.props.gestureDirection === 'vertical-inverted'
|
|
? DIRECTION_VERTICAL
|
|
: DIRECTION_HORIZONTAL
|
|
);
|
|
|
|
private layout = {
|
|
width: new Value(this.props.layout.width),
|
|
height: new Value(this.props.layout.height),
|
|
};
|
|
|
|
openingSpecConfig =
|
|
this.props.transitionSpec.open.animation === 'timing'
|
|
? transformTimingConfigToAnimatedValues(
|
|
this.props.transitionSpec.open.config
|
|
)
|
|
: transformSpringConfigToAnimatedValues(
|
|
this.props.transitionSpec.open.config
|
|
);
|
|
|
|
closingSpecConfig =
|
|
this.props.transitionSpec.close.animation === 'timing'
|
|
? transformTimingConfigToAnimatedValues(
|
|
this.props.transitionSpec.close.config
|
|
)
|
|
: transformSpringConfigToAnimatedValues(
|
|
this.props.transitionSpec.close.config
|
|
);
|
|
|
|
private distance = cond(
|
|
eq(this.direction, DIRECTION_VERTICAL),
|
|
this.layout.height,
|
|
this.layout.width
|
|
);
|
|
|
|
private gestureUntraversed = new Value(0);
|
|
private gesture = new Value(0);
|
|
private offset = new Value(0);
|
|
private velocityUntraversed = new Value(0);
|
|
private velocity = new Value(0);
|
|
|
|
private gestureState = new Value(0);
|
|
|
|
private isSwiping = new Value(FALSE);
|
|
private isSwipeCancelled = new Value(FALSE);
|
|
private isSwipeGesture = new Value(FALSE);
|
|
|
|
private toValue = new Value(0);
|
|
private frameTime = new Value(0);
|
|
|
|
private transitionVelocity = new Value(0);
|
|
|
|
private transitionState = {
|
|
position: this.props.current,
|
|
time: new Value(0),
|
|
finished: new Value(FALSE),
|
|
};
|
|
|
|
private interactionHandle: number | undefined;
|
|
|
|
private handleStartInteraction = () => {
|
|
if (this.interactionHandle === undefined) {
|
|
this.interactionHandle = InteractionManager.createInteractionHandle();
|
|
}
|
|
};
|
|
|
|
private handleEndInteraction = () => {
|
|
if (this.interactionHandle !== undefined) {
|
|
InteractionManager.clearInteractionHandle(this.interactionHandle);
|
|
this.interactionHandle = undefined;
|
|
}
|
|
};
|
|
|
|
private handleTransitionEnd = () => {
|
|
this.handleEndInteraction();
|
|
|
|
this.isRunningAnimation = false;
|
|
this.interpolatedStyle = this.getInterpolatedStyle(
|
|
this.props.styleInterpolator,
|
|
this.props.index,
|
|
this.props.current,
|
|
this.props.next,
|
|
this.props.layout
|
|
);
|
|
};
|
|
|
|
private runTransition = (isVisible: Binary | Animated.Node<number>) => {
|
|
const { open: openingSpec, close: closingSpec } = this.props.transitionSpec;
|
|
|
|
return cond(eq(this.props.current, isVisible), NOOP_NODE, [
|
|
cond(clockRunning(this.clock), NOOP_NODE, [
|
|
// Animation wasn't running before
|
|
// Set the initial values and start the clock
|
|
set(this.toValue, isVisible),
|
|
// The velocity value is ideal for translating the whole screen
|
|
// But since we have 0-1 scale, we need to adjust the velocity
|
|
set(
|
|
this.transitionVelocity,
|
|
multiply(
|
|
cond(
|
|
this.distance,
|
|
divide(this.velocity, this.distance),
|
|
FALSE_NODE
|
|
),
|
|
-1
|
|
)
|
|
),
|
|
set(this.frameTime, FALSE_NODE),
|
|
set(this.transitionState.time, FALSE_NODE),
|
|
set(this.transitionState.finished, FALSE_NODE),
|
|
set(this.isVisible, isVisible),
|
|
startClock(this.clock),
|
|
call([this.isVisible], ([value]: ReadonlyArray<Binary>) => {
|
|
this.handleStartInteraction();
|
|
|
|
const { onTransitionStart } = this.props;
|
|
this.noAnimationStartedSoFar = false;
|
|
this.isRunningAnimation = true;
|
|
onTransitionStart && onTransitionStart({ closing: !value });
|
|
}),
|
|
]),
|
|
cond(
|
|
eq(isVisible, TRUE_NODE),
|
|
openingSpec.animation === 'spring'
|
|
? memoizedSpring(
|
|
this.clock,
|
|
{ ...this.transitionState, velocity: this.transitionVelocity },
|
|
// @ts-ignore
|
|
{
|
|
...(this.openingSpecConfig as AnimatedSpringConfig),
|
|
toValue: this.toValue,
|
|
}
|
|
)
|
|
: timing(
|
|
this.clock,
|
|
{ ...this.transitionState, frameTime: this.frameTime },
|
|
{
|
|
...(this.openingSpecConfig as AnimatedTimingConfig),
|
|
toValue: this.toValue,
|
|
}
|
|
),
|
|
closingSpec.animation === 'spring'
|
|
? memoizedSpring(
|
|
this.clock,
|
|
{ ...this.transitionState, velocity: this.transitionVelocity },
|
|
// @ts-ignore
|
|
{
|
|
...(this.closingSpecConfig as AnimatedSpringConfig),
|
|
toValue: this.toValue,
|
|
}
|
|
)
|
|
: timing(
|
|
this.clock,
|
|
{ ...this.transitionState, frameTime: this.frameTime },
|
|
{
|
|
...(this.closingSpecConfig as AnimatedTimingConfig),
|
|
toValue: this.toValue,
|
|
}
|
|
)
|
|
),
|
|
cond(this.transitionState.finished, [
|
|
// Reset values
|
|
set(this.isSwipeGesture, FALSE_NODE),
|
|
set(this.gesture, FALSE_NODE),
|
|
set(this.velocity, FALSE_NODE),
|
|
// When the animation finishes, stop the clock
|
|
stopClock(this.clock),
|
|
call([this.isVisible], ([value]: ReadonlyArray<Binary>) => {
|
|
const isOpen = Boolean(value);
|
|
const { onOpen, onClose } = this.props;
|
|
|
|
this.handleTransitionEnd();
|
|
|
|
if (isOpen) {
|
|
onOpen(true);
|
|
} else {
|
|
onClose(true);
|
|
}
|
|
}),
|
|
]),
|
|
]);
|
|
};
|
|
|
|
private extrapolatedPosition = add(
|
|
this.gesture,
|
|
multiply(this.velocity, SWIPE_VELOCITY_IMPACT)
|
|
);
|
|
|
|
private exec = [
|
|
cond(
|
|
eq(this.direction, DIRECTION_HORIZONTAL),
|
|
set(
|
|
this.gesture,
|
|
multiply(
|
|
this.gestureUntraversed,
|
|
I18nManager.isRTL ? MINUS_ONE_NODE : TRUE_NODE
|
|
)
|
|
)
|
|
),
|
|
set(
|
|
this.velocity,
|
|
multiply(
|
|
this.velocityUntraversed,
|
|
I18nManager.isRTL ? MINUS_ONE_NODE : TRUE_NODE
|
|
)
|
|
),
|
|
onChange(
|
|
this.isClosing,
|
|
cond(this.isClosing, set(this.nextIsVisible, FALSE_NODE))
|
|
),
|
|
onChange(
|
|
this.nextIsVisible,
|
|
cond(neq(this.nextIsVisible, UNSET_NODE), [
|
|
// Stop any running animations
|
|
cond(clockRunning(this.clock), [
|
|
call([], this.handleTransitionEnd),
|
|
stopClock(this.clock),
|
|
]),
|
|
set(this.gesture, FALSE_NODE),
|
|
// Update the index to trigger the transition
|
|
set(this.isVisible, this.nextIsVisible),
|
|
set(this.nextIsVisible, UNSET_NODE),
|
|
])
|
|
),
|
|
onChange(
|
|
this.isVisible,
|
|
call([this.isVisible], ([isVisible]) => (this.isVisibleValue = isVisible))
|
|
),
|
|
];
|
|
|
|
private changeVisiblityExec = onChange(
|
|
this.isVisible,
|
|
call([this.isVisible], ([isVisible]) => (this.isVisibleValue = isVisible))
|
|
);
|
|
|
|
private execNoGesture = block([
|
|
...this.exec,
|
|
this.runTransition(this.isVisible),
|
|
onChange(
|
|
this.isVisible,
|
|
call([this.isVisible], ([isVisible]) => (this.isVisibleValue = isVisible))
|
|
),
|
|
this.changeVisiblityExec,
|
|
]);
|
|
|
|
private execWithGesture = block([
|
|
...this.exec,
|
|
onChange(
|
|
this.isSwiping,
|
|
call(
|
|
[this.isSwiping, this.isSwipeCancelled],
|
|
([isSwiping, isSwipeCancelled]: readonly Binary[]) => {
|
|
const {
|
|
onGestureBegin,
|
|
onGestureEnd,
|
|
onGestureCanceled,
|
|
} = this.props;
|
|
|
|
if (isSwiping === TRUE) {
|
|
this.handleStartInteraction();
|
|
|
|
onGestureBegin && onGestureBegin();
|
|
} else {
|
|
this.handleEndInteraction();
|
|
|
|
if (isSwipeCancelled === TRUE) {
|
|
onGestureCanceled && onGestureCanceled();
|
|
} else {
|
|
onGestureEnd && onGestureEnd();
|
|
}
|
|
}
|
|
}
|
|
)
|
|
),
|
|
cond(
|
|
eq(this.gestureState, GestureState.ACTIVE),
|
|
[
|
|
cond(this.isSwiping, NOOP_NODE, [
|
|
// We weren't dragging before, set it to true
|
|
set(this.isSwipeCancelled, FALSE_NODE),
|
|
set(this.isSwiping, TRUE_NODE),
|
|
set(this.isSwipeGesture, TRUE_NODE),
|
|
// Also update the drag offset to the last position
|
|
set(this.offset, this.props.current),
|
|
]),
|
|
// Update position with next offset + gesture distance
|
|
set(
|
|
this.props.current,
|
|
min(
|
|
max(
|
|
sub(
|
|
this.offset,
|
|
cond(
|
|
this.distance,
|
|
divide(
|
|
multiply(
|
|
this.gestureUntraversed,
|
|
I18nManager.isRTL ? MINUS_ONE_NODE : TRUE_NODE
|
|
),
|
|
this.distance
|
|
),
|
|
TRUE_NODE
|
|
)
|
|
),
|
|
FALSE_NODE
|
|
),
|
|
TRUE_NODE
|
|
)
|
|
),
|
|
// Stop animations while we're dragging
|
|
// and invoke proper listener
|
|
cond(
|
|
clockRunning(this.clock),
|
|
call([this.toValue], ([target]) => {
|
|
this.isRunningAnimation = false;
|
|
if (target) {
|
|
this.props.onOpen(false);
|
|
} else {
|
|
this.props.onClose(false);
|
|
}
|
|
})
|
|
),
|
|
stopClock(this.clock),
|
|
],
|
|
[
|
|
set(
|
|
this.isSwipeCancelled,
|
|
eq(this.gestureState, GestureState.CANCELLED)
|
|
),
|
|
set(this.isSwiping, FALSE_NODE),
|
|
this.runTransition(
|
|
cond(
|
|
greaterThan(
|
|
abs(this.extrapolatedPosition),
|
|
divide(this.distance, 2)
|
|
),
|
|
cond(
|
|
lessThan(
|
|
cond(
|
|
eq(this.velocity, FALSE_NODE),
|
|
this.gesture,
|
|
this.velocity
|
|
),
|
|
FALSE_NODE
|
|
),
|
|
TRUE_NODE,
|
|
FALSE_NODE
|
|
),
|
|
this.isVisible
|
|
)
|
|
),
|
|
]
|
|
),
|
|
this.changeVisiblityExec,
|
|
]);
|
|
|
|
private handleGestureEventHorizontal = Animated.event([
|
|
{
|
|
nativeEvent: {
|
|
translationX: this.gestureUntraversed,
|
|
velocityX: this.velocityUntraversed,
|
|
state: this.gestureState,
|
|
},
|
|
},
|
|
]);
|
|
|
|
private handleGestureEventVertical = Animated.event([
|
|
{
|
|
nativeEvent: {
|
|
translationY: this.gesture,
|
|
velocityY: this.velocity,
|
|
state: this.gestureState,
|
|
},
|
|
},
|
|
]);
|
|
|
|
// We need to ensure that this style doesn't change unless absolutely needs to
|
|
// Changing it too often will result in huge frame drops due to detaching and attaching
|
|
// Changing it during an animations can result in unexpected results
|
|
private getInterpolatedStyle = memoize(
|
|
(
|
|
styleInterpolator: CardStyleInterpolator,
|
|
index: number,
|
|
current: Animated.Node<number>,
|
|
next: Animated.Node<number> | undefined,
|
|
layout: Layout
|
|
) =>
|
|
styleInterpolator({
|
|
index,
|
|
current: { progress: current },
|
|
next: next && { progress: next },
|
|
closing: this.isClosing,
|
|
layouts: {
|
|
screen: layout,
|
|
},
|
|
})
|
|
);
|
|
|
|
// Keep track of the style in a property to avoid changing the animated node when deps change
|
|
// The style shouldn't change in the middle of the animation and should refer to what was there at the start of it
|
|
// Which will be the last value when just before the render which started the animation
|
|
// We need to make sure to update this when the running animation ends
|
|
private interpolatedStyle = this.getInterpolatedStyle(
|
|
this.props.styleInterpolator,
|
|
this.props.index,
|
|
this.props.current,
|
|
this.props.next,
|
|
this.props.layout
|
|
);
|
|
|
|
private gestureActivationCriteria() {
|
|
const { layout, gestureDirection, gestureResponseDistance } = this.props;
|
|
|
|
// Doesn't make sense for a response distance of 0, so this works fine
|
|
const distance =
|
|
gestureDirection === 'vertical' ||
|
|
gestureDirection === 'vertical-inverted'
|
|
? (gestureResponseDistance && gestureResponseDistance.vertical) ||
|
|
GESTURE_RESPONSE_DISTANCE_VERTICAL
|
|
: (gestureResponseDistance && gestureResponseDistance.horizontal) ||
|
|
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
|
|
|
|
if (gestureDirection === 'vertical') {
|
|
return {
|
|
maxDeltaX: 15,
|
|
minOffsetY: 5,
|
|
hitSlop: { bottom: -layout.height + distance },
|
|
};
|
|
} else if (gestureDirection === 'vertical-inverted') {
|
|
return {
|
|
maxDeltaX: 15,
|
|
minOffsetY: -5,
|
|
hitSlop: { top: -layout.height + distance },
|
|
};
|
|
} else {
|
|
const hitSlop = -layout.width + distance;
|
|
|
|
if (I18nManager.isRTL) {
|
|
return {
|
|
minOffsetX: -5,
|
|
maxDeltaY: 20,
|
|
hitSlop: { left: hitSlop },
|
|
};
|
|
} else {
|
|
return {
|
|
minOffsetX: 5,
|
|
maxDeltaY: 20,
|
|
hitSlop: { right: hitSlop },
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
private gestureRef: React.Ref<PanGestureHandler> = React.createRef();
|
|
|
|
render() {
|
|
const {
|
|
active,
|
|
transparent,
|
|
styleInterpolator,
|
|
index,
|
|
current,
|
|
next,
|
|
layout,
|
|
overlayEnabled,
|
|
shadowEnabled,
|
|
gestureEnabled,
|
|
gestureDirection,
|
|
children,
|
|
containerStyle: customContainerStyle,
|
|
contentStyle,
|
|
...rest
|
|
} = this.props;
|
|
|
|
if (!this.isRunningAnimation) {
|
|
this.interpolatedStyle = this.getInterpolatedStyle(
|
|
styleInterpolator,
|
|
index,
|
|
current,
|
|
next,
|
|
layout
|
|
);
|
|
}
|
|
|
|
const {
|
|
containerStyle,
|
|
cardStyle,
|
|
overlayStyle,
|
|
shadowStyle,
|
|
} = this.interpolatedStyle;
|
|
|
|
const handleGestureEvent = gestureEnabled
|
|
? gestureDirection === 'vertical' ||
|
|
gestureDirection === 'vertical-inverted'
|
|
? this.handleGestureEventVertical
|
|
: this.handleGestureEventHorizontal
|
|
: undefined;
|
|
|
|
let overrideFlex = null;
|
|
if (cardStyle) {
|
|
const style = StyleSheet.flatten(cardStyle) as ViewStyle;
|
|
if (style.hasOwnProperty('flex')) {
|
|
overrideFlex = { flex: style.flex };
|
|
}
|
|
}
|
|
|
|
return (
|
|
<StackGestureContext.Provider value={this.gestureRef}>
|
|
<View pointerEvents="box-none" {...rest}>
|
|
<Animated.Code
|
|
key={gestureEnabled ? 'gesture-code' : 'no-gesture-code'}
|
|
exec={gestureEnabled ? this.execWithGesture : this.execNoGesture}
|
|
/>
|
|
{overlayEnabled && overlayStyle ? (
|
|
<Animated.View
|
|
pointerEvents="none"
|
|
style={[styles.overlay, overlayStyle]}
|
|
/>
|
|
) : null}
|
|
<Animated.View
|
|
style={[styles.container, containerStyle, customContainerStyle]}
|
|
pointerEvents="box-none"
|
|
>
|
|
<PanGestureHandler
|
|
ref={this.gestureRef}
|
|
enabled={layout.width !== 0 && gestureEnabled}
|
|
onGestureEvent={handleGestureEvent}
|
|
onHandlerStateChange={handleGestureEvent}
|
|
{...this.gestureActivationCriteria()}
|
|
>
|
|
<Animated.View style={[styles.container, cardStyle]}>
|
|
{shadowEnabled && shadowStyle && !transparent ? (
|
|
<Animated.View
|
|
style={[
|
|
styles.shadow,
|
|
gestureDirection === 'horizontal'
|
|
? styles.shadowHorizontal
|
|
: styles.shadowVertical,
|
|
shadowStyle,
|
|
]}
|
|
pointerEvents="none"
|
|
/>
|
|
) : null}
|
|
<PointerEventsView
|
|
active={active}
|
|
progress={this.props.current}
|
|
style={[
|
|
styles.content,
|
|
overrideFlex,
|
|
transparent ? styles.transparent : styles.opaque,
|
|
contentStyle,
|
|
]}
|
|
>
|
|
{children}
|
|
</PointerEventsView>
|
|
</Animated.View>
|
|
</PanGestureHandler>
|
|
</Animated.View>
|
|
</View>
|
|
</StackGestureContext.Provider>
|
|
);
|
|
}
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
overflow: 'hidden',
|
|
},
|
|
overlay: {
|
|
...StyleSheet.absoluteFillObject,
|
|
backgroundColor: '#000',
|
|
},
|
|
shadow: {
|
|
position: 'absolute',
|
|
backgroundColor: '#fff',
|
|
shadowRadius: 5,
|
|
shadowColor: '#000',
|
|
shadowOpacity: 0.3,
|
|
},
|
|
shadowHorizontal: {
|
|
top: 0,
|
|
left: 0,
|
|
bottom: 0,
|
|
width: 3,
|
|
shadowOffset: { width: -1, height: 1 },
|
|
},
|
|
shadowVertical: {
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 3,
|
|
shadowOffset: { width: 1, height: -1 },
|
|
},
|
|
transparent: {
|
|
backgroundColor: 'transparent',
|
|
},
|
|
opaque: {
|
|
backgroundColor: '#eee',
|
|
},
|
|
});
|